package com.github.sarxos.webcam.ds.ffmpegcli; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import javax.imageio.ImageIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.sarxos.webcam.WebcamDevice; public class FFmpegCliDevice implements WebcamDevice, WebcamDevice.BufferAccess { private static final Logger LOG = LoggerFactory.getLogger(FFmpegCliDevice.class); private static final Runtime RT = Runtime.getRuntime(); private File vfile = null; private String name = null; private Dimension[] resolutions = null; private Dimension resolution = null; private Process process = null; private File pipe = null; private ByteArrayOutputStream baos = new ByteArrayOutputStream(); private DataInputStream dis = null; private AtomicBoolean open = new AtomicBoolean(false); private AtomicBoolean disposed = new AtomicBoolean(false); protected FFmpegCliDevice(File vfile, String vinfo) { String[] parts = vinfo.split(" : "); this.vfile = vfile; this.name = vfile.getAbsolutePath(); this.resolutions = readResolutions(parts[3].trim()); } private Dimension[] readResolutions(String res) { List<Dimension> resolutions = new ArrayList<Dimension>(); String[] parts = res.split(" "); for (String part : parts) { String[] xy = part.split("x"); resolutions.add(new Dimension(Integer.parseInt(xy[0]), Integer.parseInt(xy[1]))); } return resolutions.toArray(new Dimension[resolutions.size()]); } @Override public String getName() { return name; } @Override public Dimension[] getResolutions() { return resolutions; } @Override public Dimension getResolution() { if (resolution == null) { resolution = getResolutions()[0]; } return resolution; } private String getResolutionString() { Dimension d = getResolution(); return String.format("%dx%d", d.width, d.height); } @Override public void setResolution(Dimension resolution) { this.resolution = resolution; } private synchronized byte[] readBytes() { if (!open.get()) { return null; } baos.reset(); int b, c; try { // search for SOI while (true) { if ((b = dis.readUnsignedByte()) == 0xFF) { if ((c = dis.readUnsignedByte()) == 0xD8) { baos.write(b); baos.write(c); break; // SOI found } } } // read until EOI do { baos.write(c = dis.readUnsignedByte()); if (c == 0xFF) { baos.write(c = dis.readUnsignedByte()); if (c == 0xD9) { break; // EOI found } } } while (true); } catch (IOException e) { throw new RuntimeException(e); } return baos.toByteArray(); } @Override public synchronized ByteBuffer getImageBytes() { if (!open.get()) { return null; } return ByteBuffer.wrap(readBytes()); } @Override public void getImageBytes(ByteBuffer buffer) { if (!open.get()) { return; } buffer.put(readBytes()); } @Override public BufferedImage getImage() { if (!open.get()) { return null; } ByteArrayInputStream bais = new ByteArrayInputStream(readBytes()); try { return ImageIO.read(bais); } catch (IOException e) { throw new RuntimeException(e); } finally { try { bais.close(); } catch (IOException e) { throw new RuntimeException(e); } } } @Override public synchronized void open() { if (disposed.get()) { return; } if (!open.compareAndSet(false, true)) { return; } pipe = new File("/tmp/" + vfile.getName() + ".mjpeg"); //@formatter:off String[] cmd = new String[] { "ffmpeg", "-y", // overwrite output file "-f", "video4linux2", // format "-input_format", "mjpeg", // input format "-r", "50", // requested FPS "-s", getResolutionString(), // frame dimension "-i", vfile.getAbsolutePath(), // input file "-vcodec", "copy", // video codec to be used pipe.getAbsolutePath(), // output file }; //@formatter:on if (LOG.isDebugEnabled()) { StringBuilder sb = new StringBuilder(); for (String c : cmd) { sb.append(c).append(' '); } LOG.debug("Executing command: {}", sb.toString()); } try { RT.exec(new String[] { "mkfifo", pipe.getAbsolutePath() }).waitFor(); process = RT.exec(cmd); dis = new DataInputStream(new FileInputStream(pipe)); } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public synchronized void close() { if (!open.compareAndSet(true, false)) { return; } try { dis.close(); } catch (IOException e) { throw new RuntimeException(e); } process.destroy(); try { process.waitFor(); } catch (InterruptedException e) { throw new RuntimeException(e); } if (!pipe.delete()) { pipe.deleteOnExit(); } } @Override public void dispose() { if (disposed.compareAndSet(false, true) && open.get()) { close(); } } @Override public boolean isOpen() { return open.get(); } @Override public String toString() { return "video device " + name; } }