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