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