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;
017
018import org.bridj.Pointer;
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022import com.github.sarxos.webcam.WebcamDevice;
023import com.github.sarxos.webcam.WebcamDevice.BufferAccess;
024import com.github.sarxos.webcam.WebcamException;
025import com.github.sarxos.webcam.WebcamExceptionHandler;
026import com.github.sarxos.webcam.WebcamResolution;
027import com.github.sarxos.webcam.WebcamTask;
028import com.github.sarxos.webcam.ds.buildin.natives.Device;
029import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
030import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
031
032
033public class WebcamDefaultDevice implements WebcamDevice, BufferAccess, Runnable, WebcamDevice.FPSSource {
034
035        /**
036         * Logger.
037         */
038        private static final Logger LOG = LoggerFactory.getLogger(WebcamDefaultDevice.class);
039
040        /**
041         * Artificial view sizes. I'm really not sure if will fit into other webcams
042         * but hope that OpenIMAJ can handle this.
043         */
044        private final static Dimension[] DIMENSIONS = new Dimension[] {
045                WebcamResolution.QQVGA.getSize(),
046                WebcamResolution.QVGA.getSize(),
047                WebcamResolution.VGA.getSize(),
048        };
049
050        private class NextFrameTask extends WebcamTask {
051
052                private final AtomicInteger result = new AtomicInteger(0);
053
054                public NextFrameTask(WebcamDevice device) {
055                        super(device);
056                }
057
058                public int nextFrame() {
059                        try {
060                                process();
061                        } catch (InterruptedException e) {
062                                LOG.debug("Image buffer request interrupted", e);
063                        }
064                        return result.get();
065                }
066
067                @Override
068                protected void handle() {
069
070                        WebcamDefaultDevice device = (WebcamDefaultDevice) getDevice();
071                        if (!device.isOpen()) {
072                                return;
073                        }
074
075                        grabber.setTimeout(timeout);
076                        result.set(grabber.nextFrame());
077                        fresh.set(true);
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         * Is the last image fresh one.
123         */
124        private final AtomicBoolean fresh = new AtomicBoolean(false);
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        /**
136         * Current FPS.
137         */
138        private volatile double fps = 0;
139
140        protected WebcamDefaultDevice(Device device) {
141                this.device = device;
142                this.name = device.getNameStr();
143                this.id = device.getIdentifierStr();
144                this.fullname = String.format("%s %s", this.name, this.id);
145        }
146
147        @Override
148        public String getName() {
149                return fullname;
150        }
151
152        public String getDeviceName() {
153                return name;
154        }
155
156        public String getDeviceId() {
157                return id;
158        }
159
160        public Device getDeviceRef() {
161                return device;
162        }
163
164        @Override
165        public Dimension[] getResolutions() {
166                return DIMENSIONS;
167        }
168
169        @Override
170        public Dimension getResolution() {
171                if (size == null) {
172                        size = getResolutions()[0];
173                }
174                return size;
175        }
176
177        @Override
178        public void setResolution(Dimension size) {
179
180                if (size == null) {
181                        throw new IllegalArgumentException("Size cannot be null");
182                }
183
184                if (open.get()) {
185                        throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
186                }
187
188                this.size = size;
189        }
190
191        @Override
192        public ByteBuffer getImageBytes() {
193
194                if (disposed.get()) {
195                        LOG.debug("Webcam is disposed, image will be null");
196                        return null;
197                }
198                if (!open.get()) {
199                        LOG.debug("Webcam is closed, image will be null");
200                        return null;
201                }
202
203                // if image is not fresh, update it
204
205                if (fresh.compareAndSet(false, true)) {
206                        updateFrameBuffer();
207                }
208
209                // get image buffer
210
211                LOG.trace("Webcam grabber get image pointer");
212
213                Pointer<Byte> image = grabber.getImage();
214                fresh.set(false);
215
216                if (image == null) {
217                        LOG.warn("Null array pointer found instead of image");
218                        return null;
219                }
220
221                int length = size.width * size.height * 3;
222
223                LOG.trace("Webcam device get buffer, read {} bytes", length);
224
225                return image.getByteBuffer(length);
226        }
227
228        @Override
229        public void getImageBytes(ByteBuffer target) {
230
231                if (disposed.get()) {
232                        LOG.debug("Webcam is disposed, image will be null");
233                        return;
234                }
235                if (!open.get()) {
236                        LOG.debug("Webcam is closed, image will be null");
237                        return;
238                }
239
240                int minSize = size.width * size.height * 3;
241                int curSize = target.remaining();
242
243                if (minSize < curSize) {
244                        throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
245                }
246
247                // if image is not fresh, update it
248
249                if (fresh.compareAndSet(false, true)) {
250                        updateFrameBuffer();
251                }
252
253                // get image buffer
254
255                LOG.trace("Webcam grabber get image pointer");
256
257                Pointer<Byte> image = grabber.getImage();
258                fresh.set(false);
259
260                if (image == null) {
261                        LOG.warn("Null array pointer found instead of image");
262                        return;
263                }
264
265                LOG.trace("Webcam device read buffer {} bytes", minSize);
266
267                image = image.validBytes(minSize);
268                image.getBytes(target);
269
270        }
271
272        @Override
273        public BufferedImage getImage() {
274
275                ByteBuffer buffer = getImageBytes();
276
277                if (buffer == null) {
278                        LOG.error("Images bytes buffer is null!");
279                        return null;
280                }
281
282                byte[] bytes = new byte[size.width * size.height * 3];
283                byte[][] data = new byte[][] { bytes };
284
285                buffer.get(bytes);
286
287                DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
288                WritableRaster raster = Raster.createWritableRaster(smodel, dbuf, null);
289
290                BufferedImage bi = new BufferedImage(cmodel, raster, false, null);
291                bi.flush();
292
293                return bi;
294        }
295
296        @Override
297        public void open() {
298
299                if (disposed.get()) {
300                        return;
301                }
302
303                LOG.debug("Opening webcam device {}", getName());
304
305                if (size == null) {
306                        size = getResolutions()[0];
307                }
308                if (size == null) {
309                        throw new RuntimeException("The resolution size cannot be null");
310                }
311
312                LOG.debug("Webcam device {} starting session, size {}", device.getIdentifierStr(), size);
313
314                grabber = new OpenIMAJGrabber();
315
316                // NOTE!
317
318                // Following the note from OpenIMAJ code - it seams like there is some
319                // issue on 32-bit systems which prevents grabber to find devices.
320                // According to the mentioned note this for loop shall fix the problem.
321
322                DeviceList list = grabber.getVideoDevices().get();
323                for (Device d : list.asArrayList()) {
324                        d.getNameStr();
325                        d.getIdentifierStr();
326                }
327
328                boolean started = grabber.startSession(size.width, size.height, 50, Pointer.pointerTo(device));
329                if (!started) {
330                        throw new WebcamException("Cannot start native grabber!");
331                }
332
333                LOG.debug("Webcam device session started");
334
335                Dimension size2 = new Dimension(grabber.getWidth(), grabber.getHeight());
336
337                int w1 = size.width;
338                int w2 = size2.width;
339                int h1 = size.height;
340                int h2 = size2.height;
341
342                if (w1 != w2 || h1 != h2) {
343
344                        if (failOnSizeMismatch) {
345                                throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
346                        }
347
348                        Object[] args = new Object[] { w1, h1, w2, h2, w2, h2 };
349                        LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
350
351                        size = new Dimension(w2, h2);
352                }
353
354                smodel = new ComponentSampleModel(DATA_TYPE, size.width, size.height, 3, size.width * 3, BAND_OFFSETS);
355                cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, Transparency.OPAQUE, DATA_TYPE);
356
357                LOG.debug("Initialize buffer");
358
359                int i = 0;
360                do {
361
362                        grabber.nextFrame();
363
364                        try {
365                                Thread.sleep(1000);
366                        } catch (InterruptedException e) {
367                                LOG.error("Nasty interrupted exception", e);
368                        }
369
370                } while (++i < 3);
371
372                LOG.debug("Webcam device {} is now open", this);
373
374                open.set(true);
375
376                refresher = new Thread(this, String.format("frames-refresher-[%s]", id));
377                refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
378                refresher.setDaemon(true);
379                refresher.start();
380        }
381
382        @Override
383        public void close() {
384
385                if (!open.compareAndSet(true, false)) {
386                        return;
387                }
388
389                LOG.debug("Closing webcam device");
390
391                grabber.stopSession();
392        }
393
394        @Override
395        public void dispose() {
396
397                if (!disposed.compareAndSet(false, true)) {
398                        return;
399                }
400
401                LOG.debug("Disposing webcam device {}", getName());
402
403                close();
404        }
405
406        /**
407         * Determines if device should fail when requested image size is different
408         * than actually received.
409         * 
410         * @param fail the fail on size mismatch flag, true or false
411         */
412        public void setFailOnSizeMismatch(boolean fail) {
413                this.failOnSizeMismatch = fail;
414        }
415
416        @Override
417        public boolean isOpen() {
418                return open.get();
419        }
420
421        /**
422         * Get timeout for image acquisition.
423         * 
424         * @return Value in milliseconds
425         */
426        public int getTimeout() {
427                return timeout;
428        }
429
430        /**
431         * Set timeout for image acquisition.
432         * 
433         * @param timeout the timeout value in milliseconds
434         */
435        public void setTimeout(int timeout) {
436                this.timeout = timeout;
437        }
438
439        /**
440         * Update underlying memory buffer and fetch new frame.
441         */
442        private void updateFrameBuffer() {
443
444                LOG.trace("Next frame");
445
446                if (t1 == -1 || t2 == -1) {
447                        t1 = System.currentTimeMillis();
448                        t2 = System.currentTimeMillis();
449                }
450
451                int result = new NextFrameTask(this).nextFrame();
452
453                t1 = t2;
454                t2 = System.currentTimeMillis();
455
456                fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
457
458                if (result == -1) {
459                        LOG.error("Timeout when requesting image!");
460                } else if (result < -1) {
461                        LOG.error("Error requesting new frame!");
462                }
463        }
464
465        @Override
466        public void run() {
467
468                do {
469
470                        if (Thread.interrupted()) {
471                                LOG.debug("Refresher has been interrupted");
472                                return;
473                        }
474
475                        if (!open.get()) {
476                                LOG.debug("Cancelling refresher");
477                                return;
478                        }
479
480                        updateFrameBuffer();
481
482                } while (open.get());
483        }
484
485        @Override
486        public double getFPS() {
487                return fps;
488        }
489}