package oculusPrime; import developer.Ros; import org.red5.server.api.IConnection; import org.red5.server.stream.ClientBroadcastStream; import oculusPrime.State.values; import javax.imageio.ImageIO; import java.io.File; import java.io.IOException; import java.io.BufferedReader; import java.io.InputStreamReader; public class Video { private static State state = State.getReference(); private Application app = null; private String port; private int devicenum = 0; // should match lifecam cam private int adevicenum = 1; // should match lifecam mic private static final int defaultquality = 5; private static final int quality720p = 7; private static final int defaultwidth=640; private static final int defaultheight=480; public static final int lowreswidth=320; private static final int lowresheight=240; private static final String PATH="/dev/shm/avconvframes/"; private static final String EXT=".bmp"; private volatile long lastframegrab = 0; private int lastwidth=0; private int lastheight=0; private int lastfps=0; private int lastquality = 0; private Application.streamstate lastmode = Application.streamstate.stop; private long publishid = 0; static final long STREAM_RESTART = Util.ONE_MINUTE*6; static final String FFMPEG = "ffmpeg"; static final String AVCONV = "avconv"; private String avprog = AVCONV; private static long STREAM_CONNECT_DELAY = Application.STREAM_CONNECT_DELAY; private static int dumpfps = 15; private static final String STREAMSPATH="/oculusPrime/streams/"; public static final String FMTEXT = ".flv"; public static final String AUDIO = "_audio"; private static final String VIDEO = "_video"; private static final String STREAM1 = "stream1"; private static final String STREAM2 = "stream2"; private static String ubuntuVersion; public Video(Application a) { app = a; port = Settings.getReference().readRed5Setting("rtmp.port"); ubuntuVersion = Util.getUbuntuVersion(); } public void initAvconv() { state.set(State.values.stream, Application.streamstate.stop.toString()); setAudioDevice(); if (state.get(State.values.osarch).equals(Application.ARM)) { // avprog = FFMPEG; // dumpfps = 8; // STREAM_CONNECT_DELAY=3000; } File dir=new File(PATH); dir.mkdirs(); // setup shared mem folder } private void setAudioDevice() { try { String cmd[] = new String[]{"arecord", "--list-devices"}; Process proc = Runtime.getRuntime().exec(cmd); proc.waitFor(); // proc.waitFor(); String line = null; BufferedReader procReader = new BufferedReader(new InputStreamReader(proc.getInputStream())); while ((line = procReader.readLine()) != null) { if(line.startsWith("card") && line.contains("LifeCam")) { adevicenum = Integer.parseInt(line.substring(5,6)); // "card 0" Util.debug(line, this); } } } catch (Exception e) { Util.printError(e);} } public void publish (final Application.streamstate mode, final int w, final int h, final int fps) { // todo: determine video device (in constructor) // todo: disallow unsafe custom values (device can be corrupted?) if (w==lastwidth && h==lastheight && fps==lastfps && mode.equals(lastmode)) { Util.log("identical stream already running, dropped", this); return; } lastwidth = w; lastheight = h; lastfps = fps; lastmode = mode; final long id = System.currentTimeMillis(); publishid = id; lastquality = defaultquality; if (w > defaultwidth) lastquality = quality720p; final int q = lastquality; new Thread(new Runnable() { public void run() { String host = "127.0.0.1"; if (state.exists(State.values.relayserver)) host = state.get(State.values.relayserver); // nuke currently running avconv if any if (!state.get(State.values.stream).equals(Application.streamstate.stop.toString()) && !mode.equals(Application.streamstate.stop.toString())) { forceShutdownFrameGrabs(); Util.systemCallBlocking("pkill "+avprog); Util.delay(STREAM_CONNECT_DELAY); } switch (mode) { case camera: Util.systemCall(avprog+" -f video4linux2 -s " + w + "x" + h + " -r " + fps + " -i /dev/video" + devicenum + " -f flv -q " + q + " rtmp://" + host + ":" + port + "/oculusPrime/"+STREAM1); // avconv -f video4linux2 -s 640x480 -r 8 -i /dev/video0 -f flv -q 5 rtmp://127.0.0.1:1935/oculusPrime/stream1 app.driverCallServer(PlayerCommands.streammode, mode.toString()); break; case mic: Util.systemCall(avprog+" -re -f alsa -ac 1 -ar 22050 " + "-i hw:" + adevicenum + " -f flv rtmp://" + host + ":" + port + "/oculusPrime/"+STREAM1); // avconv -re -f alsa -ac 1 -ar 22050 -i hw:1 -f flv rtmp://127.0.0.1:1935/oculusPrime/stream1 app.driverCallServer(PlayerCommands.streammode, mode.toString()); break; case camandmic: Util.systemCall(avprog+" -re -f alsa -ac 1 -ar 22050 " + "-i hw:" + adevicenum + " -f flv rtmp://" + host + ":" + port + "/oculusPrime/"+STREAM2); // avconv -re -f alsa -ac 1 -ar 22050 -i hw:1 -f flv rtmp://127.0.0.1:1935/oculusPrime/stream2 Util.systemCall(avprog+" -f video4linux2 -s " + w + "x" + h + " -r " + fps + " -i /dev/video" + devicenum + " -f flv -q " + q + " rtmp://" + host + ":" + port + "/oculusPrime/"+STREAM1); app.driverCallServer(PlayerCommands.streammode, mode.toString()); break; case stop: forceShutdownFrameGrabs(); Util.systemCall("pkill "+avprog); app.driverCallServer(PlayerCommands.streammode, mode.toString()); break; } } }).start(); if (mode.equals(Application.streamstate.stop) ) return; // stream restart timer new Thread(new Runnable() { public void run() { long start = System.currentTimeMillis(); while ( id == publishid && (System.currentTimeMillis() < start + STREAM_RESTART) || state.exists(State.values.writingframegrabs) || state.getBoolean(State.values.autodocking) ) Util.delay(50); if (id == publishid) { // restart stream forceShutdownFrameGrabs(); Util.systemCall("pkill -9 "+avprog); lastmode = Application.streamstate.stop; Util.delay(STREAM_CONNECT_DELAY); publish(mode, w,h,fps); // app.driverCallServer(PlayerCommands.messageclients, "stream restart after "+(System.currentTimeMillis()-start+"ms")); } } }).start(); } private void forceShutdownFrameGrabs() { if (state.exists(State.values.writingframegrabs)) { state.delete(State.values.writingframegrabs); // Util.delay(STREAM_CONNECT_DELAY); } } public void framegrab(final String res) { state.set(State.values.framegrabbusy.name(), true); lastframegrab = System.currentTimeMillis(); new Thread(new Runnable() { public void run() { // resolution check: set to same as main stream params as default int width = lastwidth; // set lower resolution if required if (res.equals(AutoDock.LOWRES)) { width=lowreswidth; } if (!state.exists(values.writingframegrabs)) { dumpframegrabs(res); Util.delay(STREAM_CONNECT_DELAY); } else if (state.getInteger(values.writingframegrabs) != width) { // Util.log("dumpframegrabs() not using width: "+width, this); forceShutdownFrameGrabs(); Util.delay(STREAM_CONNECT_DELAY); dumpframegrabs(res); Util.delay(STREAM_CONNECT_DELAY); } // determine latest image file -- use second latest to resolve incomplete file issues int attempts = 0; while (attempts < 15) { File dir = new File(PATH); File imgfile = null; long start = System.currentTimeMillis(); while (imgfile == null && System.currentTimeMillis() - start < 10000) { int highest = 0; int secondhighest = 0; for (File file : dir.listFiles()) { int i = Integer.parseInt(file.getName().split("\\.")[0]); if (i > highest) { // imgfile = file; highest = i; } if (i > secondhighest && i < highest) { imgfile = file; secondhighest = i; } } Util.delay(1); } if (imgfile == null) { Util.log(avprog + " frame unavailable", this); break; } else { try { // 640x480 = 921654 bytes (640*480*3 + 54) // 320x240 = 230454 bytes (320*240*3 + 54) // long size = imgfile.length(); // if (size != 230454 && size != 921654) { // doesn't allow for max res, or any other res // if (size <= 54) { // image must be bigger than bitmap header size! // if (size != imgsizebytes) { // image must be correct size // Util.log("wrong size ("+size+" bytes) image file, trying again, attempt "+(attempts+1), this); // attempts++; // continue; // } app.processedImage = ImageIO.read(imgfile); break; } catch (IOException e) { Util.printError(e); attempts++; } } } state.set(values.framegrabbusy, false); } }).start(); } private void dumpframegrabs(final String res) { File dir=new File(PATH); for(File file: dir.listFiles()) file.delete(); // nuke any existing files // set to same as main stream params as default int width = lastwidth; int height = lastheight; int q = lastquality; // set lower resolution if required if (res.equals(AutoDock.LOWRES)) { width=lowreswidth; height=lowresheight; } state.set(State.values.writingframegrabs, width); String host = "127.0.0.1"; if (state.exists(State.values.relayserver)) host = state.get(State.values.relayserver); try { if ( ! Application.UBUNTU1604.equals(ubuntuVersion)) { // 14.04 and lower Runtime.getRuntime().exec(new String[]{avprog, "-analyzeduration", "0", "-i", "rtmp://" + host + ":" + port + "/oculusPrime/" + STREAM1 + " live=1", "-s", width + "x" + height, "-r", Integer.toString(dumpfps), "-q", Integer.toString(q), PATH + "%d" + EXT}); // avconv -analyzeduration 0 -i "rtmp://127.0.0.1:1935/oculusPrime/stream1 live=1" -s 640x480 -r 15 -q 5 /dev/shm/avconvframes/%d.bmp } else { Runtime.getRuntime().exec(new String[]{avprog, "-analyzeduration", "0", "-rtmp_live", "live", "-i", "rtmp://" + host + ":" + port + "/oculusPrime/" + STREAM1, "-s", width + "x" + height, "-r", Integer.toString(dumpfps), "-q", Integer.toString(q), PATH + "%d" + EXT}); // avconv -analyzeduration 0 -rtmp_live live -i rtmp://127.0.0.1:1935/oculusPrime/stream1 -s 640x480 -r 15 -q 5 /dev/shm/avconvframes/%d.bmp } }catch (Exception e) { Util.printError(e); } new Thread(new Runnable() { public void run() { Util.delay(500); // required? // continually clean all but the latest few files, prevent mem overload int i=1; while(state.exists(State.values.writingframegrabs) && System.currentTimeMillis() - lastframegrab < Util.ONE_MINUTE) { File file = new File(PATH+i+EXT); if (file.exists() && new File(PATH+(i+32)+EXT).exists()) { file.delete(); i++; } Util.delay(50); } state.delete(State.values.writingframegrabs); Util.systemCall("pkill -n " + avprog); // kills newest only File dir=new File(PATH); for(File file: dir.listFiles()) file.delete(); // clean up (gets most files) } }).start(); } public String record(String mode) { return record(mode, null); } // record to flv in webapps/oculusPrime/streams/ @SuppressWarnings("incomplete-switch") public String record(String mode, String optionalfilename) { Util.debug("record("+mode+", " + optionalfilename +"): called.. ", this); IConnection conn = app.grabber; if (conn == null) return null; if (state.get(State.values.stream) == null) return null; if (state.get(State.values.record) == null) state.set(State.values.record, Application.streamstate.stop.toString()); if (state.exists(State.values.sounddetect)) if (state.getBoolean(State.values.sounddetect)) return null; if (mode.toLowerCase().equals(Settings.TRUE)) { // TRUE, start recording if (state.get(State.values.stream).equals(Application.streamstate.stop.toString())) { app.driverCallServer(PlayerCommands.messageclients, "no stream running, unable to record"); return null; } if (!state.get(State.values.record).equals(Application.streamstate.stop.toString())) { app.driverCallServer(PlayerCommands.messageclients, "already recording, command dropped"); return null; } // Get a reference to the current broadcast stream. ClientBroadcastStream stream = (ClientBroadcastStream) app.getBroadcastStream(conn.getScope(), STREAM1); // Save the stream to disk. try { String streamName = Util.getDateStamp(); if(optionalfilename != null) streamName += "_" + optionalfilename; if(state.exists(values.roswaypoint) && state.get(State.values.navsystemstatus).equals(Ros.navsystemstate.running.toString()) ) streamName += "_" + state.get(values.roswaypoint); streamName = streamName.replaceAll(" ", "_"); // no spaces in filenames final String urlString = STREAMSPATH; state.set(State.values.record, state.get(State.values.stream)); switch((Application.streamstate.valueOf(state.get(State.values.stream)))) { case mic: app.messageplayer("recording to: " + urlString+streamName + AUDIO + FMTEXT, State.values.record.toString(), state.get(State.values.record)); stream.saveAs(streamName + AUDIO, false); break; case camandmic: if (!Settings.getReference().getBoolean(ManualSettings.useflash)) { ClientBroadcastStream audiostream = (ClientBroadcastStream) app.getBroadcastStream(conn.getScope(), STREAM2); app.messageplayer("recording to: " + urlString+streamName + AUDIO + FMTEXT, State.values.record.toString(), state.get(State.values.record)); audiostream.saveAs(streamName+AUDIO, false); } // BREAK OMITTED ON PURPOSE case camera: app.messageplayer("recording to: " + urlString+streamName + VIDEO + FMTEXT, State.values.record.toString(), state.get(State.values.record)); stream.saveAs(streamName + VIDEO, false); break; } Util.log("recording: "+streamName,this); return urlString + streamName; } catch (Exception e) { Util.printError(e); } } else { // FALSE, stop recording if (state.get(State.values.record).equals(Application.streamstate.stop.toString())) { app.driverCallServer(PlayerCommands.messageclients, "not recording, command dropped"); return null; } ClientBroadcastStream stream = (ClientBroadcastStream) app.getBroadcastStream(conn.getScope(), STREAM1); if (stream == null) return null; // if page reload state.set(State.values.record, Application.streamstate.stop.toString()); switch((Application.streamstate.valueOf(state.get(State.values.stream)))) { case camandmic: if (!Settings.getReference().getBoolean(ManualSettings.useflash)) { ClientBroadcastStream audiostream = (ClientBroadcastStream) app.getBroadcastStream(conn.getScope(), STREAM2); // app.driverCallServer(PlayerCommands.messageclients, "2nd audio recording stopped"); audiostream.stopRecording(); } // BREAK OMITTED ON PURPOSE case mic: // BREAK OMITTED ON PURPOSE case camera: stream.stopRecording(); Util.log("recording stopped", this); app.messageplayer("recording stopped", State.values.record.toString(), state.get(State.values.record)); break; } } return null; } public void sounddetect(String mode) { if (!state.exists(State.values.sounddetect)) state.set(State.values.sounddetect, false); // mode = false if (mode.toLowerCase().equals(Settings.FALSE)) { if (!state.getBoolean(State.values.sounddetect)) { app.driverCallServer(PlayerCommands.messageclients, "sound detection not running, command dropped"); } else { state.set(State.values.sounddetect, false); app.driverCallServer(PlayerCommands.messageclients, "sound detection cancelled"); } return; } // mode = true if (!state.get(State.values.stream).equals(Application.streamstate.camandmic.toString()) && !state.get(State.values.stream).equals(Application.streamstate.mic.toString()) ) { app.driverCallServer(PlayerCommands.messageclients, "no mic stream, unable to detect sound"); return; } if (state.getBoolean(State.values.sounddetect)) { app.driverCallServer(PlayerCommands.messageclients, "sound detection already running, command dropped"); return; } if (state.get(State.values.record) == null) state.set(State.values.record, Application.streamstate.stop.toString()); if (!state.get(State.values.record).equals(Application.streamstate.stop.toString())) { app.driverCallServer(PlayerCommands.messageclients, "record already running, sound detection command dropped"); return; } final String filename = "temp"; final String fullpath = Settings.streamsfolder+Util.sep+filename+AUDIO+FMTEXT; new Thread(new Runnable() { public void run() { // wait for grabber just in case video just started if (app.grabber == null) { long grabbertimeout = System.currentTimeMillis() + 2000; while (System.currentTimeMillis() < grabbertimeout) Util.delay(1); } if (app.grabber == null) { Util.log("error, grabber null", this); return; } String streamname = STREAM1; if (state.get(State.values.stream).equals(Application.streamstate.camandmic.toString())) streamname = STREAM2; ClientBroadcastStream stream = (ClientBroadcastStream) app.getBroadcastStream(app.grabber.getScope(), streamname); state.set(State.values.sounddetect, true); state.delete(State.values.streamactivity); long timeout = System.currentTimeMillis() + Util.ONE_HOUR; while (state.getBoolean(State.values.sounddetect) && System.currentTimeMillis() < timeout) { double voldB = -99; try { // start recording stream.saveAs(filename + AUDIO, false); // wait long cliplength = System.currentTimeMillis() + 5000; while (System.currentTimeMillis() < cliplength && state.getBoolean(State.values.sounddetect)) Util.delay(1); // stop recording stream.stopRecording(); if (!state.getBoolean(State.values.sounddetect)) { // cancelled during clip new File(fullpath).delete(); return; } Process proc = Runtime.getRuntime().exec("ffmpeg -i "+fullpath+" -af volumedetect -f null -"); // ffmpeg -i webapps/oculusPrime/temp_audio.flv -af volumedetect -f null - String line; BufferedReader procReader = new BufferedReader(new InputStreamReader(proc.getErrorStream())); while ((line = procReader.readLine()) != null) { if (line.contains("max_volume:")) { String[] s = line.split(" "); voldB = Double.parseDouble(s[s.length - 2]); break; } } } catch (Exception e) { Util.printError(e); state.set(State.values.sounddetect, false); new File(fullpath).delete(); return; } if (voldB > Settings.getReference().getDouble(ManualSettings.soundthresholdalt) && state.getBoolean(State.values.sounddetect)) { state.set(State.values.streamactivity, "audio " + voldB+"dB"); state.set(State.values.sounddetect, false); app.driverCallServer(PlayerCommands.messageclients, "sound detected: "+ voldB+"dB"); } new File(fullpath).delete(); } if (state.getBoolean(State.values.sounddetect)) { Util.log("sound detect timed out", this); state.set(State.values.sounddetect, false); } } }).start(); } }