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}