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.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                }
078        }
079
080        /**
081         * RGB offsets.
082         */
083        private static final int[] BAND_OFFSETS = new int[] { 0, 1, 2 };
084
085        /**
086         * Number of bytes in each pixel.
087         */
088        private static final int[] BITS = { 8, 8, 8 };
089
090        /**
091         * Image offset.
092         */
093        private static final int[] OFFSET = new int[] { 0 };
094
095        /**
096         * Data type used in image.
097         */
098        private static final int DATA_TYPE = DataBuffer.TYPE_BYTE;
099
100        /**
101         * Image color space.
102         */
103        private static final ColorSpace COLOR_SPACE = ColorSpace.getInstance(ColorSpace.CS_sRGB);
104
105        /**
106         * Maximum image acquisition time (in milliseconds).
107         */
108        private int timeout = 5000;
109
110        private OpenIMAJGrabber grabber = null;
111        private Device device = null;
112        private Dimension size = null;
113        private ComponentSampleModel smodel = null;
114        private ColorModel cmodel = null;
115        private boolean failOnSizeMismatch = false;
116
117        private final AtomicBoolean disposed = new AtomicBoolean(false);
118        private final AtomicBoolean open = new AtomicBoolean(false);
119
120        /**
121         * When last frame was requested.
122         */
123        private final AtomicLong timestamp = new AtomicLong(-1);
124
125        private Thread refresher = null;
126
127        private String name = null;
128        private String id = null;
129        private String fullname = null;
130
131        private byte[] bytes = null;
132        private byte[][] data = null;
133
134        private long t1 = -1;
135        private long t2 = -1;
136
137        private volatile double fps = 0;
138
139        protected WebcamDefaultDevice(Device device) {
140                this.device = device;
141                this.name = device.getNameStr();
142                this.id = device.getIdentifierStr();
143                this.fullname = String.format("%s %s", this.name, this.id);
144        }
145
146        @Override
147        public String getName() {
148                return fullname;
149        }
150
151        @Override
152        public Dimension[] getResolutions() {
153                return DIMENSIONS;
154        }
155
156        @Override
157        public Dimension getResolution() {
158                if (size == null) {
159                        size = getResolutions()[0];
160                }
161                return size;
162        }
163
164        @Override
165        public void setResolution(Dimension size) {
166                if (open.get()) {
167                        throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
168                }
169                this.size = size;
170        }
171
172        @Override
173        public ByteBuffer getImageBytes() {
174
175                if (disposed.get()) {
176                        LOG.debug("Webcam is disposed, image will be null");
177                        return null;
178                }
179
180                if (!open.get()) {
181                        LOG.debug("Webcam is closed, image will be null");
182                        return null;
183                }
184
185                LOG.trace("Webcam device get image (next frame)");
186
187                // get image buffer
188
189                Pointer<Byte> image = grabber.getImage();
190                if (image == null) {
191                        LOG.warn("Null array pointer found instead of image");
192                        return null;
193                }
194
195                int length = size.width * size.height * 3;
196
197                LOG.trace("Webcam device get buffer, read {} bytes", length);
198
199                return image.getByteBuffer(length);
200        }
201
202        @Override
203        public BufferedImage getImage() {
204
205                ByteBuffer buffer = getImageBytes();
206
207                if (buffer == null) {
208                        LOG.error("Images bytes buffer is null!");
209                        return null;
210                }
211
212                buffer.get(bytes);
213
214                DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
215                WritableRaster raster = Raster.createWritableRaster(smodel, dbuf, null);
216
217                BufferedImage bi = new BufferedImage(cmodel, raster, false, null);
218                bi.flush();
219
220                return bi;
221        }
222
223        @Override
224        public void open() {
225
226                if (disposed.get()) {
227                        return;
228                }
229
230                LOG.debug("Opening webcam device {}", getName());
231
232                if (size == null) {
233                        size = getResolutions()[0];
234                }
235
236                LOG.debug("Webcam device {} starting session, size {}", device.getIdentifierStr(), size);
237
238                grabber = new OpenIMAJGrabber();
239
240                boolean started = grabber.startSession(size.width, size.height, 50, Pointer.pointerTo(device));
241                if (!started) {
242                        throw new WebcamException("Cannot start native grabber!");
243                }
244
245                LOG.debug("Webcam device session started");
246
247                Dimension size2 = new Dimension(grabber.getWidth(), grabber.getHeight());
248
249                int w1 = size.width;
250                int w2 = size2.width;
251                int h1 = size.height;
252                int h2 = size2.height;
253
254                if (w1 != w2 || h1 != h2) {
255
256                        if (failOnSizeMismatch) {
257                                throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
258                        }
259
260                        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 });
261                        size = new Dimension(w2, h2);
262                }
263
264                smodel = new ComponentSampleModel(DATA_TYPE, size.width, size.height, 3, size.width * 3, BAND_OFFSETS);
265                cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, Transparency.OPAQUE, DATA_TYPE);
266
267                LOG.debug("Initialize buffer");
268
269                int i = 0;
270                do {
271
272                        grabber.nextFrame();
273
274                        try {
275                                Thread.sleep(1000);
276                        } catch (InterruptedException e) {
277                                LOG.error("Nasty interrupted exception", e);
278                        }
279
280                } while (++i < 3);
281
282                timestamp.set(System.currentTimeMillis());
283
284                LOG.debug("Webcam device {} is now open", this);
285
286                bytes = new byte[size.width * size.height * 3];
287                data = new byte[][] { bytes };
288
289                open.set(true);
290
291                refresher = new Thread(this, String.format("frames-refresher:%s", id));
292                refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
293                refresher.setDaemon(true);
294                refresher.start();
295        }
296
297        @Override
298        public void close() {
299
300                if (!open.compareAndSet(true, false)) {
301                        return;
302                }
303
304                LOG.debug("Closing webcam device");
305
306                grabber.stopSession();
307        }
308
309        @Override
310        public void dispose() {
311
312                if (!disposed.compareAndSet(false, true)) {
313                        return;
314                }
315
316                LOG.debug("Disposing webcam device {}", getName());
317
318                close();
319        }
320
321        /**
322         * Determines if device should fail when requested image size is different
323         * than actually received.
324         * 
325         * @param fail the fail on size mismatch flag, true or false
326         */
327        public void setFailOnSizeMismatch(boolean fail) {
328                this.failOnSizeMismatch = fail;
329        }
330
331        @Override
332        public boolean isOpen() {
333                return open.get();
334        }
335
336        /**
337         * Get timeout for image acquisition.
338         * 
339         * @return Value in milliseconds
340         */
341        public int getTimeout() {
342                return timeout;
343        }
344
345        /**
346         * Set timeout for image acquisition.
347         * 
348         * @param timeout the timeout value in milliseconds
349         */
350        public void setTimeout(int timeout) {
351                this.timeout = timeout;
352        }
353
354        @Override
355        public void run() {
356
357                int result = -1;
358
359                do {
360
361                        if (Thread.interrupted()) {
362                                LOG.debug("Refresher has been interrupted");
363                                return;
364                        }
365
366                        if (!open.get()) {
367                                LOG.debug("Cancelling refresher");
368                                return;
369                        }
370
371                        LOG.trace("Next frame");
372
373                        if (t1 == -1 || t2 == -1) {
374                                t1 = System.currentTimeMillis();
375                                t2 = System.currentTimeMillis();
376                        }
377
378                        result = new NextFrameTask(this).nextFrame();
379
380                        t1 = t2;
381                        t2 = System.currentTimeMillis();
382
383                        fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5;
384
385                        if (result == -1) {
386                                LOG.error("Timeout when requesting image!");
387                        } else if (result < -1) {
388                                LOG.error("Error requesting new frame!");
389                        }
390
391                        timestamp.set(System.currentTimeMillis());
392
393                } while (open.get());
394        }
395
396        @Override
397        public double getFPS() {
398                return fps;
399        }
400}