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}