001package com.github.sarxos.webcam.ds.buildin;
002
003import java.awt.Dimension;
004import java.awt.Transparency;
005import java.awt.color.ColorSpace;
006import java.awt.image.BufferedImage;
007import java.awt.image.ColorModel;
008import java.awt.image.ComponentColorModel;
009import java.awt.image.ComponentSampleModel;
010import java.awt.image.DataBuffer;
011import java.awt.image.DataBufferByte;
012import java.awt.image.Raster;
013import java.awt.image.WritableRaster;
014import java.nio.ByteBuffer;
015import java.util.concurrent.atomic.AtomicBoolean;
016import java.util.concurrent.atomic.AtomicInteger;
017import java.util.concurrent.atomic.AtomicLong;
018
019import org.bridj.Pointer;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023import com.github.sarxos.webcam.WebcamDevice;
024import com.github.sarxos.webcam.WebcamDevice.BufferAccess;
025import com.github.sarxos.webcam.WebcamException;
026import com.github.sarxos.webcam.WebcamExceptionHandler;
027import com.github.sarxos.webcam.WebcamResolution;
028import com.github.sarxos.webcam.WebcamTask;
029import com.github.sarxos.webcam.ds.buildin.natives.Device;
030import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
031import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
032
033
034public class WebcamDefaultDevice implements WebcamDevice, BufferAccess, Runnable, WebcamDevice.FPSSource {
035
036        /**
037         * Logger.
038         */
039        private static final Logger LOG = LoggerFactory.getLogger(WebcamDefaultDevice.class);
040
041        /**
042         * Artificial view sizes. I'm really not sure if will fit into other webcams
043         * but hope that OpenIMAJ can handle this.
044         */
045        private final static Dimension[] DIMENSIONS = new Dimension[] {
046                WebcamResolution.QQVGA.getSize(),
047                WebcamResolution.QVGA.getSize(),
048                WebcamResolution.VGA.getSize(),
049        };
050
051        private class NextFrameTask extends WebcamTask {
052
053                private final AtomicInteger result = new AtomicInteger(0);
054
055                public NextFrameTask(WebcamDevice device) {
056                        super(device);
057                }
058
059                public int nextFrame() {
060                        try {
061                                process();
062                        } catch (InterruptedException e) {
063                                LOG.debug("Image buffer request interrupted", e);
064                        }
065                        return result.get();
066                }
067
068                @Override
069                protected void handle() {
070
071                        WebcamDefaultDevice device = (WebcamDefaultDevice) getDevice();
072                        if (!device.isOpen()) {
073                                return;
074                        }
075
076                        grabber.setTimeout(timeout);
077                        result.set(grabber.nextFrame());
078                }
079        }
080
081        /**
082         * RGB offsets.
083         */
084        private static final int[] BAND_OFFSETS = new int[] { 0, 1, 2 };
085
086        /**
087         * Number of bytes in each pixel.
088         */
089        private static final int[] BITS = { 8, 8, 8 };
090
091        /**
092         * Image offset.
093         */
094        private static final int[] OFFSET = new int[] { 0 };
095
096        /**
097         * Data type used in image.
098         */
099        private static final int DATA_TYPE = DataBuffer.TYPE_BYTE;
100
101        /**
102         * Image color space.
103         */
104        private static final ColorSpace COLOR_SPACE = ColorSpace.getInstance(ColorSpace.CS_sRGB);
105
106        /**
107         * Maximum image acquisition time (in milliseconds).
108         */
109        private int timeout = 5000;
110
111        private OpenIMAJGrabber grabber = null;
112        private Device device = null;
113        private Dimension size = null;
114        private ComponentSampleModel smodel = null;
115        private ColorModel cmodel = null;
116        private boolean failOnSizeMismatch = false;
117
118        private final AtomicBoolean disposed = new AtomicBoolean(false);
119        private final AtomicBoolean open = new AtomicBoolean(false);
120
121        /**
122         * When last frame was requested.
123         */
124        private final AtomicLong timestamp = new AtomicLong(-1);
125
126        private Thread refresher = null;
127
128        private String name = null;
129        private String id = null;
130        private String fullname = null;
131
132        private long t1 = -1;
133        private long t2 = -1;
134
135        private volatile double fps = 0;
136
137        protected WebcamDefaultDevice(Device device) {
138                this.device = device;
139                this.name = device.getNameStr();
140                this.id = device.getIdentifierStr();
141                this.fullname = String.format("%s %s", this.name, this.id);
142        }
143
144        @Override
145        public String getName() {
146                return fullname;
147        }
148
149        @Override
150        public Dimension[] getResolutions() {
151                return DIMENSIONS;
152        }
153
154        @Override
155        public Dimension getResolution() {
156                if (size == null) {
157                        size = getResolutions()[0];
158                }
159                return size;
160        }
161
162        @Override
163        public void setResolution(Dimension size) {
164
165                if (size == null) {
166                        throw new IllegalArgumentException("Size cannot be null");
167                }
168
169                if (open.get()) {
170                        throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
171                }
172
173                this.size = size;
174        }
175
176        @Override
177        public ByteBuffer getImageBytes() {
178
179                if (disposed.get()) {
180                        LOG.debug("Webcam is disposed, image will be null");
181                        return null;
182                }
183
184                if (!open.get()) {
185                        LOG.debug("Webcam is closed, image will be null");
186                        return null;
187                }
188
189                LOG.trace("Webcam device get image (next frame)");
190
191                // get image buffer
192
193                Pointer<Byte> image = grabber.getImage();
194                if (image == null) {
195                        LOG.warn("Null array pointer found instead of image");
196                        return null;
197                }
198
199                int length = size.width * size.height * 3;
200
201                LOG.trace("Webcam device get buffer, read {} bytes", length);
202
203                return image.getByteBuffer(length);
204        }
205
206        @Override
207        public BufferedImage getImage() {
208
209                ByteBuffer buffer = getImageBytes();
210
211                if (buffer == null) {
212                        LOG.error("Images bytes buffer is null!");
213                        return null;
214                }
215
216                byte[] bytes = new byte[size.width * size.height * 3];
217                byte[][] data = new byte[][] { bytes };
218
219                buffer.get(bytes);
220
221                DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
222                WritableRaster raster = Raster.createWritableRaster(smodel, dbuf, null);
223
224                BufferedImage bi = new BufferedImage(cmodel, raster, false, null);
225                bi.flush();
226
227                return bi;
228        }
229
230        @Override
231        public void open() {
232
233                if (disposed.get()) {
234                        return;
235                }
236
237                LOG.debug("Opening webcam device {}", getName());
238
239                if (size == null) {
240                        size = getResolutions()[0];
241                }
242                if (size == null) {
243                        throw new RuntimeException("The resolution size cannot be null");
244                }
245
246                LOG.debug("Webcam device {} starting session, size {}", device.getIdentifierStr(), size);
247
248                grabber = new OpenIMAJGrabber();
249
250                // NOTE!
251
252                // Following the note from OpenIMAJ code - it seams like there is some
253                // issue on 32-bit systems which prevents grabber to find devices.
254                // According to the mentioned note this for loop shall fix the problem.
255
256                DeviceList list = grabber.getVideoDevices().get();
257                for (Device d : list.asArrayList()) {
258                        d.getNameStr();
259                        d.getIdentifierStr();
260                }
261
262                boolean started = grabber.startSession(size.width, size.height, 50, Pointer.pointerTo(device));
263                if (!started) {
264                        throw new WebcamException("Cannot start native grabber!");
265                }
266
267                LOG.debug("Webcam device session started");
268
269                Dimension size2 = new Dimension(grabber.getWidth(), grabber.getHeight());
270
271                int w1 = size.width;
272                int w2 = size2.width;
273                int h1 = size.height;
274                int h2 = size2.height;
275
276                if (w1 != w2 || h1 != h2) {
277
278                        if (failOnSizeMismatch) {
279                                throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
280                        }
281
282                        LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", new Object[] { w1, h1, w2, h2, w2, h2 });
283                        size = new Dimension(w2, h2);
284                }
285
286                smodel = new ComponentSampleModel(DATA_TYPE, size.width, size.height, 3, size.width * 3, BAND_OFFSETS);
287                cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, Transparency.OPAQUE, DATA_TYPE);
288
289                LOG.debug("Initialize buffer");
290
291                int i = 0;
292                do {
293
294                        grabber.nextFrame();
295
296                        try {
297                                Thread.sleep(1000);
298                        } catch (InterruptedException e) {
299                                LOG.error("Nasty interrupted exception", e);
300                        }
301
302                } while (++i < 3);
303
304                timestamp.set(System.currentTimeMillis());
305
306                LOG.debug("Webcam device {} is now open", this);
307
308                open.set(true);
309
310                refresher = new Thread(this, String.format("frames-refresher-[%s]", id));
311                refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
312                refresher.setDaemon(true);
313                refresher.start();
314        }
315
316        @Override
317        public void close() {
318
319                if (!open.compareAndSet(true, false)) {
320                        return;
321                }
322
323                LOG.debug("Closing webcam device");
324
325                grabber.stopSession();
326        }
327
328        @Override
329        public void dispose() {
330
331                if (!disposed.compareAndSet(false, true)) {
332                        return;
333                }
334
335                LOG.debug("Disposing webcam device {}", getName());
336
337                close();
338        }
339
340        /**
341         * Determines if device should fail when requested image size is different
342         * than actually received.
343         * 
344         * @param fail the fail on size mismatch flag, true or false
345         */
346        public void setFailOnSizeMismatch(boolean fail) {
347                this.failOnSizeMismatch = fail;
348        }
349
350        @Override
351        public boolean isOpen() {
352                return open.get();
353        }
354
355        /**
356         * Get timeout for image acquisition.
357         * 
358         * @return Value in milliseconds
359         */
360        public int getTimeout() {
361                return timeout;
362        }
363
364        /**
365         * Set timeout for image acquisition.
366         * 
367         * @param timeout the timeout value in milliseconds
368         */
369        public void setTimeout(int timeout) {
370                this.timeout = timeout;
371        }
372
373        @Override
374        public void run() {
375
376                int result = -1;
377
378                do {
379
380                        if (Thread.interrupted()) {
381                                LOG.debug("Refresher has been interrupted");
382                                return;
383                        }
384
385                        if (!open.get()) {
386                                LOG.debug("Cancelling refresher");
387                                return;
388                        }
389
390                        LOG.trace("Next frame");
391
392                        if (t1 == -1 || t2 == -1) {
393                                t1 = System.currentTimeMillis();
394                                t2 = System.currentTimeMillis();
395                        }
396
397                        result = new NextFrameTask(this).nextFrame();
398
399                        t1 = t2;
400                        t2 = System.currentTimeMillis();
401
402                        fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
403
404                        if (result == -1) {
405                                LOG.error("Timeout when requesting image!");
406                        } else if (result < -1) {
407                                LOG.error("Error requesting new frame!");
408                        }
409
410                        timestamp.set(System.currentTimeMillis());
411
412                } while (open.get());
413        }
414
415        @Override
416        public double getFPS() {
417                return fps;
418        }
419}