com.github.sarxos.webcam.ds.fswebcam.FsWebcamDevice Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of webcam-capture-driver-fswebcam Show documentation
Show all versions of webcam-capture-driver-fswebcam Show documentation
This is one of the capture drivers which can be used by Webcam Capture API
to fetch image from webcam device. This specific capture driver is using
fswebcam command line tool by by Philip Heron to access the image.
The newest version!
package com.github.sarxos.webcam.ds.fswebcam;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
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.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.imageio.ImageIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.sarxos.webcam.WebcamDevice;
import com.github.sarxos.webcam.WebcamDevice.Configurable;
import com.github.sarxos.webcam.WebcamExceptionHandler;
import com.github.sarxos.webcam.WebcamResolution;
public class FsWebcamDevice implements WebcamDevice, Configurable {
public static final String PARAM_KEY_COMPRESSION = "compression";
public static final String PARAM_KEY_FORMAT = "format";
public static final String PARAM_KEY_SKIP = "skip";
public static final String PARAM_KEY_FRAMES = "frames";
public static final String PARAM_KEY_LOG = "log";
public static final String PARAM_KEY_VERBOSE = "verbose";
public static final class ExecutorThreadFactory implements ThreadFactory {
private final AtomicInteger number = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(String.format("process-reader-%d", number.incrementAndGet()));
t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
t.setDaemon(true);
return t;
}
}
public static final class StreamReader implements Runnable {
private final BufferedReader br;
private final boolean err;
public StreamReader(InputStream is, boolean err) {
LOG.debug("New stream reader");
this.br = new BufferedReader(new InputStreamReader(is));
this.err = err;
}
@Override
public void run() {
try {
try {
String line;
while ((line = br.readLine()) != null) {
LOG.debug("FsWebcam: {} {}", err ? "ERROR" : "", line);
}
} catch (IOException e) {
LOG.debug(String.format("Exception when reading %s output", err ? "STDERR" : "stdout"), e);
}
} finally {
try {
br.close();
} catch (IOException e) {
LOG.error("Exception when closing buffered reader", e);
}
}
}
}
private static final Logger LOG = LoggerFactory.getLogger(FsWebcamDevice.class);
private static final Runtime RT = Runtime.getRuntime();
private static final ExecutorThreadFactory THREAD_FACTORY = new ExecutorThreadFactory();
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(THREAD_FACTORY);
private static final Dimension[] RESOLUTIONS = new Dimension[] {
WebcamResolution.QQVGA.getSize(),
WebcamResolution.QVGA.getSize(),
WebcamResolution.VGA.getSize(),
};
private final File vfile;
private final String name;
private long counter;
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);
private String logFilePathString;
private int frames = 1;
private int skip = 0;
private String format = "jpeg";
private int compression = -1;
private boolean verbose = false;
protected FsWebcamDevice(File vfile) {
this.vfile = vfile;
this.name = vfile.getAbsolutePath();
}
@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 BufferedImage getImage() {
counter++;
if (!open.get()) {
return null;
}
BufferedImage image = null;
try {
executeFsWebcamProcess();
try {
dis = new DataInputStream(new FileInputStream(pipe));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
ByteArrayInputStream bais = new ByteArrayInputStream(readBytes());
try {
image = ImageIO.read(bais);
} catch (IOException e) {
process.destroy();
throw new RuntimeException(e);
} finally {
try {
bais.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
process.waitFor();
if (LOG.isDebugEnabled()) {
LOG.debug("Image #"+counter+" done");
}
} catch (IOException e) {
LOG.error("Process #"+counter+" IO exception", e);
} catch (InterruptedException e) {
process.destroy();
} finally {
try {
if (dis != null)
dis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
// w/a for bug in java 1.6 - waitFor requires Thread.interrupted()
// call in finally block to reset thread flags
if (Thread.interrupted()) {
throw new RuntimeException("Thread has been interrupted #"+counter);
}
}
return image;
}
private void executeFsWebcamProcess() throws IOException {
//@formatter:off
List c = new ArrayList(24);
c.add("/usr/bin/fswebcam");
c.add("--skip"); // number of skipped images
c.add(String.valueOf(skip));
c.add("--frames"); // number of images merged to the single output (default 1)
c.add(String.valueOf(frames));
c.add("--"+format); // format jpeg | png
c.add(String.valueOf(compression));
c.add("--no-banner"); // only image - no texts, banners, etc
c.add("--no-shadow");
c.add("--no-title");
c.add("--no-subtitle");
c.add("--no-timestamp");
c.add("--no-info");
c.add("--no-underlay");
c.add("--no-overlay");
c.add("--resolution"); // resolution
c.add(getResolutionString());
if (verbose) {
c.add("--verbose");
}
if (logFilePathString != null) {
c.add("--log"); // log file
c.add(logFilePathString);
}
c.add("--device"); // input video file
c.add(this.vfile.getAbsolutePath());
c.add(pipe.getAbsolutePath()); // output file (pipe)
//@formatter:on
String[] cmd = c.toArray(new String[c.size()]);
if (LOG.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
for (String cc : cmd) {
sb.append(cc).append(' ');
}
LOG.debug("Invoking command: #"+counter+" \n"+ sb.toString());
}
process = RT.exec(cmd);
// print process output
EXECUTOR.execute(new StreamReader(process.getInputStream(), false));
EXECUTOR.execute(new StreamReader(process.getErrorStream(), true));
}
@Override
public synchronized void open() {
if (disposed.get()) {
return;
}
if (!open.compareAndSet(false, true)) {
return;
}
pipe = new File("/tmp/fswebcam-pipe-" + vfile.getName() + ".mjpeg");
if (pipe.exists()) {
if (!pipe.delete()) {
throw new RuntimeException("Cannot remove streaming pipe " + pipe);
}
}
LOG.debug("Creating pipe: mkfifo {}", pipe.getAbsolutePath());
Process p = null;
try {
p = RT.exec(new String[] { "mkfifo", pipe.getAbsolutePath() });
EXECUTOR.execute(new StreamReader(p.getInputStream(), false));
EXECUTOR.execute(new StreamReader(p.getErrorStream(), true));
p.waitFor();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
return;
} finally {
p.destroy();
}
}
@Override
public synchronized void close() {
if (!open.compareAndSet(true, false)) {
return;
}
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (process != null) {
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;
}
/**
* Call this method to set device specific parameters.
* Should be called before {@link #open()} method.
* For details about config options, please see fswebcam manual.
*
* - verbose - Boolean type - If true, fswebcam command-line option --verbose is set.
*
- log - String type - If set, it's passed to fswebcam as value of command-line option --log.
*
- frames - Integer type - If set, it's passed to fswebcam as value of command-line option --frames.
*
- skip - Integer type - If set, it's passed to fswebcam as value of command-line option --skip.
*
- format - String type - Possible values are: "jpeg" (default) | "png". Passed to fswebcam as option: --[format]
*
- compression - Integer type - Passed to fswebcam together with format --[format] [compression]. Default is -1, which means automatic.
*
* All Boolean or Integer types may be also specified as String values. E.g. both "true" String or Boolean.TRUE are valid values.
*/
@Override
public void setParameters(Map parameters) {
if (parameters != null) {
Object value = null;
value = parameters.get(PARAM_KEY_VERBOSE);
if (value != null) {
verbose = Boolean.parseBoolean(String.valueOf(value));
}
value = parameters.get(PARAM_KEY_LOG);
if (value != null) {
logFilePathString = String.valueOf(value);
}
value = parameters.get(PARAM_KEY_FRAMES);
if (value != null) {
frames = Integer.parseInt(String.valueOf(value));
}
value = parameters.get(PARAM_KEY_SKIP);
if (value != null) {
skip = Integer.parseInt(String.valueOf(value));
}
value = parameters.get(PARAM_KEY_FORMAT);
if (value != null) {
format = String.valueOf(value);
}
value = parameters.get(PARAM_KEY_COMPRESSION);
if (value != null) {
compression = Integer.parseInt(String.valueOf(value));
}
}
}
}