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 byte[] bytes = null;
133        private byte[][] data = null;
134
135        private long t1 = -1;
136        private long t2 = -1;
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        @Override
153        public Dimension[] getResolutions() {
154                return DIMENSIONS;
155        }
156
157        @Override
158        public Dimension getResolution() {
159                if (size == null) {
160                        size = getResolutions()[0];
161                }
162                return size;
163        }
164
165        @Override
166        public void setResolution(Dimension size) {
167                if (open.get()) {
168                        throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
169                }
170                this.size = size;
171        }
172
173        @Override
174        public ByteBuffer getImageBytes() {
175
176                if (disposed.get()) {
177                        LOG.debug("Webcam is disposed, image will be null");
178                        return null;
179                }
180
181                if (!open.get()) {
182                        LOG.debug("Webcam is closed, image will be null");
183                        return null;
184                }
185
186                LOG.trace("Webcam device get image (next frame)");
187
188                // get image buffer
189
190                Pointer<Byte> image = grabber.getImage();
191                if (image == null) {
192                        LOG.warn("Null array pointer found instead of image");
193                        return null;
194                }
195
196                int length = size.width * size.height * 3;
197
198                LOG.trace("Webcam device get buffer, read {} bytes", length);
199
200                return image.getByteBuffer(length);
201        }
202
203        @Override
204        public BufferedImage getImage() {
205
206                ByteBuffer buffer = getImageBytes();
207
208                if (buffer == null) {
209                        LOG.error("Images bytes buffer is null!");
210                        return null;
211                }
212
213                buffer.get(bytes);
214
215                DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
216                WritableRaster raster = Raster.createWritableRaster(smodel, dbuf, null);
217
218                BufferedImage bi = new BufferedImage(cmodel, raster, false, null);
219                bi.flush();
220
221                return bi;
222        }
223
224        @Override
225        public void open() {
226
227                if (disposed.get()) {
228                        return;
229                }
230
231                LOG.debug("Opening webcam device {}", getName());
232
233                if (size == null) {
234                        size = getResolutions()[0];
235                }
236
237                LOG.debug("Webcam device {} starting session, size {}", device.getIdentifierStr(), size);
238
239                grabber = new OpenIMAJGrabber();
240
241                // NOTE!
242
243                // Following the note from OpenIMAJ code - it seams like there is some
244                // issue on 32-bit systems which prevents grabber to find devices.
245                // According to the mentioned note this for loop shall fix the problem.
246
247                DeviceList list = grabber.getVideoDevices().get();
248                for (Device d : list.asArrayList()) {
249                        d.getNameStr();
250                        d.getIdentifierStr();
251                }
252
253                boolean started = grabber.startSession(size.width, size.height, 50, Pointer.pointerTo(device));
254                if (!started) {
255                        throw new WebcamException("Cannot start native grabber!");
256                }
257
258                LOG.debug("Webcam device session started");
259
260                Dimension size2 = new Dimension(grabber.getWidth(), grabber.getHeight());
261
262                int w1 = size.width;
263                int w2 = size2.width;
264                int h1 = size.height;
265                int h2 = size2.height;
266
267                if (w1 != w2 || h1 != h2) {
268
269                        if (failOnSizeMismatch) {
270                                throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
271                        }
272
273                        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 });
274                        size = new Dimension(w2, h2);
275                }
276
277                smodel = new ComponentSampleModel(DATA_TYPE, size.width, size.height, 3, size.width * 3, BAND_OFFSETS);
278                cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, Transparency.OPAQUE, DATA_TYPE);
279
280                LOG.debug("Initialize buffer");
281
282                int i = 0;
283                do {
284
285                        grabber.nextFrame();
286
287                        try {
288                                Thread.sleep(1000);
289                        } catch (InterruptedException e) {
290                                LOG.error("Nasty interrupted exception", e);
291                        }
292
293                } while (++i < 3);
294
295                timestamp.set(System.currentTimeMillis());
296
297                LOG.debug("Webcam device {} is now open", this);
298
299                bytes = new byte[size.width * size.height * 3];
300                data = new byte[][] { bytes };
301
302                open.set(true);
303
304                refresher = new Thread(this, String.format("frames-refresher:%s", id));
305                refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
306                refresher.setDaemon(true);
307                refresher.start();
308        }
309
310        @Override
311        public void close() {
312
313                if (!open.compareAndSet(true, false)) {
314                        return;
315                }
316
317                LOG.debug("Closing webcam device");
318
319                grabber.stopSession();
320        }
321
322        @Override
323        public void dispose() {
324
325                if (!disposed.compareAndSet(false, true)) {
326                        return;
327                }
328
329                LOG.debug("Disposing webcam device {}", getName());
330
331                close();
332        }
333
334        /**
335         * Determines if device should fail when requested image size is different
336         * than actually received.
337         * 
338         * @param fail the fail on size mismatch flag, true or false
339         */
340        public void setFailOnSizeMismatch(boolean fail) {
341                this.failOnSizeMismatch = fail;
342        }
343
344        @Override
345        public boolean isOpen() {
346                return open.get();
347        }
348
349        /**
350         * Get timeout for image acquisition.
351         * 
352         * @return Value in milliseconds
353         */
354        public int getTimeout() {
355                return timeout;
356        }
357
358        /**
359         * Set timeout for image acquisition.
360         * 
361         * @param timeout the timeout value in milliseconds
362         */
363        public void setTimeout(int timeout) {
364                this.timeout = timeout;
365        }
366
367        @Override
368        public void run() {
369
370                int result = -1;
371
372                do {
373
374                        if (Thread.interrupted()) {
375                                LOG.debug("Refresher has been interrupted");
376                                return;
377                        }
378
379                        if (!open.get()) {
380                                LOG.debug("Cancelling refresher");
381                                return;
382                        }
383
384                        LOG.trace("Next frame");
385
386                        if (t1 == -1 || t2 == -1) {
387                                t1 = System.currentTimeMillis();
388                                t2 = System.currentTimeMillis();
389                        }
390
391                        result = new NextFrameTask(this).nextFrame();
392
393                        t1 = t2;
394                        t2 = System.currentTimeMillis();
395
396                        fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
397
398                        if (result == -1) {
399                                LOG.error("Timeout when requesting image!");
400                        } else if (result < -1) {
401                                LOG.error("Error requesting new frame!");
402                        }
403
404                        timestamp.set(System.currentTimeMillis());
405
406                } while (open.get());
407        }
408
409        @Override
410        public double getFPS() {
411                return fps;
412        }
413}