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