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 }