001package com.github.sarxos.webcam.ds.fswebcam; 002 003import java.awt.Dimension; 004import java.awt.image.BufferedImage; 005import java.io.BufferedReader; 006import java.io.ByteArrayInputStream; 007import java.io.ByteArrayOutputStream; 008import java.io.DataInputStream; 009import java.io.File; 010import java.io.FileInputStream; 011import java.io.FileNotFoundException; 012import java.io.IOException; 013import java.io.InputStream; 014import java.io.InputStreamReader; 015import java.nio.ByteBuffer; 016import java.util.concurrent.ExecutorService; 017import java.util.concurrent.Executors; 018import java.util.concurrent.ThreadFactory; 019import java.util.concurrent.atomic.AtomicBoolean; 020import java.util.concurrent.atomic.AtomicInteger; 021 022import javax.imageio.ImageIO; 023 024import org.slf4j.Logger; 025import org.slf4j.LoggerFactory; 026 027import com.github.sarxos.webcam.WebcamDevice; 028import com.github.sarxos.webcam.WebcamExceptionHandler; 029import com.github.sarxos.webcam.WebcamResolution; 030 031 032public class FsWebcamDevice implements WebcamDevice, WebcamDevice.BufferAccess { 033 034 public static final class ExecutorThreadFactory implements ThreadFactory { 035 036 private final AtomicInteger number = new AtomicInteger(0); 037 038 @Override 039 public Thread newThread(Runnable r) { 040 Thread t = new Thread(r); 041 t.setName(String.format("process-reader-%d", number.incrementAndGet())); 042 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 043 t.setDaemon(true); 044 return t; 045 } 046 } 047 048 public static final class StreamReader implements Runnable { 049 050 private final BufferedReader br; 051 private final boolean err; 052 053 public StreamReader(InputStream is, boolean err) { 054 LOG.debug("New stream reader"); 055 this.br = new BufferedReader(new InputStreamReader(is)); 056 this.err = err; 057 } 058 059 @Override 060 public void run() { 061 try { 062 try { 063 String line; 064 while ((line = br.readLine()) != null) { 065 LOG.debug("FsWebcam: {} {}", err ? "ERROR" : "", line); 066 } 067 } catch (IOException e) { 068 LOG.debug(String.format("Exception when reading %s output", err ? "STDERR" : "stdout"), e); 069 } 070 } finally { 071 try { 072 br.close(); 073 } catch (IOException e) { 074 LOG.error("Exception when closing buffered reader", e); 075 } 076 } 077 } 078 } 079 080 private static final Logger LOG = LoggerFactory.getLogger(FsWebcamDevice.class); 081 private static final Runtime RT = Runtime.getRuntime(); 082 private static final ExecutorThreadFactory THREAD_FACTORY = new ExecutorThreadFactory(); 083 private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(THREAD_FACTORY); 084 085 private static final Dimension[] RESOLUTIONS = new Dimension[] { 086 WebcamResolution.QQVGA.getSize(), 087 WebcamResolution.QVGA.getSize(), 088 WebcamResolution.VGA.getSize(), 089 }; 090 091 private final File vfile; 092 private final String name; 093 094 private Dimension resolution = null; 095 private Process process = null; 096 private File pipe = null; 097 private ByteArrayOutputStream baos = new ByteArrayOutputStream(); 098 private DataInputStream dis = null; 099 100 private AtomicBoolean open = new AtomicBoolean(false); 101 private AtomicBoolean disposed = new AtomicBoolean(false); 102 103 protected FsWebcamDevice(File vfile) { 104 this.vfile = vfile; 105 this.name = vfile.getAbsolutePath(); 106 } 107 108 @Override 109 public String getName() { 110 return name; 111 } 112 113 @Override 114 public Dimension[] getResolutions() { 115 return RESOLUTIONS; 116 } 117 118 @Override 119 public Dimension getResolution() { 120 if (resolution == null) { 121 resolution = getResolutions()[0]; 122 } 123 return resolution; 124 } 125 126 private String getResolutionString() { 127 Dimension d = getResolution(); 128 return String.format("%dx%d", d.width, d.height); 129 } 130 131 @Override 132 public void setResolution(Dimension resolution) { 133 this.resolution = resolution; 134 } 135 136 private synchronized byte[] readBytes() { 137 138 if (!open.get()) { 139 return null; 140 } 141 142 baos.reset(); 143 144 int b, c; 145 try { 146 147 // search for SOI 148 while (true) { 149 if ((b = dis.readUnsignedByte()) == 0xFF) { 150 if ((c = dis.readUnsignedByte()) == 0xD8) { 151 baos.write(b); 152 baos.write(c); 153 break; // SOI found 154 } 155 } 156 } 157 158 // read until EOI 159 do { 160 baos.write(c = dis.readUnsignedByte()); 161 if (c == 0xFF) { 162 baos.write(c = dis.readUnsignedByte()); 163 if (c == 0xD9) { 164 break; // EOI found 165 } 166 } 167 } while (true); 168 169 } catch (IOException e) { 170 throw new RuntimeException(e); 171 } 172 173 return baos.toByteArray(); 174 175 } 176 177 @Override 178 public synchronized ByteBuffer getImageBytes() { 179 if (!open.get()) { 180 return null; 181 } 182 return ByteBuffer.wrap(readBytes()); 183 } 184 185 @Override 186 public BufferedImage getImage() { 187 188 if (!open.get()) { 189 return null; 190 } 191 192 //@formatter:off 193 String[] cmd = new String[] { 194 "/usr/bin/fswebcam", 195 "--no-banner", // only image - no texts, banners, etc 196 "--no-shadow", 197 "--no-title", 198 "--no-subtitle", 199 "--no-timestamp", 200 "--no-info", 201 "--no-underlay", 202 "--no-overlay", 203 "-d", vfile.getAbsolutePath(), // input video file 204 "-r", getResolutionString(), // resolution 205 pipe.getAbsolutePath(), // output file (pipe) 206 LOG.isDebugEnabled() ? "-v" : "", // enable verbosity if debug mode is enabled 207 }; 208 //@formatter:on 209 210 if (LOG.isDebugEnabled()) { 211 StringBuilder sb = new StringBuilder(); 212 for (String c : cmd) { 213 sb.append(c).append(' '); 214 } 215 LOG.debug("Invoking command: {}", sb.toString()); 216 } 217 218 BufferedImage image = null; 219 220 try { 221 222 process = RT.exec(cmd); 223 224 // print process output 225 EXECUTOR.execute(new StreamReader(process.getInputStream(), false)); 226 EXECUTOR.execute(new StreamReader(process.getErrorStream(), true)); 227 228 try { 229 dis = new DataInputStream(new FileInputStream(pipe)); 230 } catch (FileNotFoundException e) { 231 throw new RuntimeException(e); 232 } 233 234 ByteArrayInputStream bais = new ByteArrayInputStream(readBytes()); 235 try { 236 image = ImageIO.read(bais); 237 } catch (IOException e) { 238 process.destroy(); 239 throw new RuntimeException(e); 240 } finally { 241 try { 242 bais.close(); 243 } catch (IOException e) { 244 throw new RuntimeException(e); 245 } 246 } 247 248 process.waitFor(); 249 250 } catch (IOException e) { 251 LOG.error("Process IO exception", e); 252 } catch (InterruptedException e) { 253 process.destroy(); 254 } finally { 255 256 try { 257 dis.close(); 258 } catch (IOException e) { 259 throw new RuntimeException(e); 260 } 261 262 // w/a for bug in java 1.6 - waitFor requires Thread.interrupted() 263 // call in finally block to reset thread flags 264 265 if (Thread.interrupted()) { 266 throw new RuntimeException("Thread has been interrupted"); 267 } 268 } 269 270 return image; 271 } 272 273 @Override 274 public synchronized void open() { 275 276 if (disposed.get()) { 277 return; 278 } 279 280 if (!open.compareAndSet(false, true)) { 281 return; 282 } 283 284 pipe = new File("/tmp/fswebcam-pipe-" + vfile.getName() + ".mjpeg"); 285 286 if (pipe.exists()) { 287 if (!pipe.delete()) { 288 throw new RuntimeException("Cannot remove streaming pipe " + pipe); 289 } 290 } 291 292 LOG.debug("Creating pipe: mkfifo {}", pipe.getAbsolutePath()); 293 294 Process p = null; 295 try { 296 p = RT.exec(new String[] { "mkfifo", pipe.getAbsolutePath() }); 297 298 EXECUTOR.execute(new StreamReader(p.getInputStream(), false)); 299 EXECUTOR.execute(new StreamReader(p.getErrorStream(), true)); 300 301 p.waitFor(); 302 303 } catch (IOException e) { 304 throw new RuntimeException(e); 305 } catch (InterruptedException e) { 306 return; 307 } finally { 308 p.destroy(); 309 } 310 } 311 312 @Override 313 public synchronized void close() { 314 315 if (!open.compareAndSet(true, false)) { 316 return; 317 } 318 319 if (dis != null) { 320 try { 321 dis.close(); 322 } catch (IOException e) { 323 throw new RuntimeException(e); 324 } 325 } 326 327 if (process != null) { 328 process.destroy(); 329 } 330 331 try { 332 process.waitFor(); 333 } catch (InterruptedException e) { 334 throw new RuntimeException(e); 335 } 336 337 if (!pipe.delete()) { 338 pipe.deleteOnExit(); 339 } 340 } 341 342 @Override 343 public void dispose() { 344 if (disposed.compareAndSet(false, true) && open.get()) { 345 close(); 346 } 347 } 348 349 @Override 350 public boolean isOpen() { 351 return open.get(); 352 } 353 354 @Override 355 public String toString() { 356 return "video device " + name; 357 } 358}