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; 017 018import org.bridj.Pointer; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022import com.github.sarxos.webcam.WebcamDevice; 023import com.github.sarxos.webcam.WebcamDevice.BufferAccess; 024import com.github.sarxos.webcam.WebcamException; 025import com.github.sarxos.webcam.WebcamExceptionHandler; 026import com.github.sarxos.webcam.WebcamResolution; 027import com.github.sarxos.webcam.WebcamTask; 028import com.github.sarxos.webcam.ds.buildin.natives.Device; 029import com.github.sarxos.webcam.ds.buildin.natives.DeviceList; 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 fresh.set(true); 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 * Is the last image fresh one. 123 */ 124 private final AtomicBoolean fresh = new AtomicBoolean(false); 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 /** 136 * Current FPS. 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 public String getDeviceName() { 153 return name; 154 } 155 156 public String getDeviceId() { 157 return id; 158 } 159 160 public Device getDeviceRef() { 161 return device; 162 } 163 164 @Override 165 public Dimension[] getResolutions() { 166 return DIMENSIONS; 167 } 168 169 @Override 170 public Dimension getResolution() { 171 if (size == null) { 172 size = getResolutions()[0]; 173 } 174 return size; 175 } 176 177 @Override 178 public void setResolution(Dimension size) { 179 180 if (size == null) { 181 throw new IllegalArgumentException("Size cannot be null"); 182 } 183 184 if (open.get()) { 185 throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first"); 186 } 187 188 this.size = size; 189 } 190 191 @Override 192 public ByteBuffer getImageBytes() { 193 194 if (disposed.get()) { 195 LOG.debug("Webcam is disposed, image will be null"); 196 return null; 197 } 198 if (!open.get()) { 199 LOG.debug("Webcam is closed, image will be null"); 200 return null; 201 } 202 203 // if image is not fresh, update it 204 205 if (fresh.compareAndSet(false, true)) { 206 updateFrameBuffer(); 207 } 208 209 // get image buffer 210 211 LOG.trace("Webcam grabber get image pointer"); 212 213 Pointer<Byte> image = grabber.getImage(); 214 fresh.set(false); 215 216 if (image == null) { 217 LOG.warn("Null array pointer found instead of image"); 218 return null; 219 } 220 221 int length = size.width * size.height * 3; 222 223 LOG.trace("Webcam device get buffer, read {} bytes", length); 224 225 return image.getByteBuffer(length); 226 } 227 228 @Override 229 public void getImageBytes(ByteBuffer target) { 230 231 if (disposed.get()) { 232 LOG.debug("Webcam is disposed, image will be null"); 233 return; 234 } 235 if (!open.get()) { 236 LOG.debug("Webcam is closed, image will be null"); 237 return; 238 } 239 240 int minSize = size.width * size.height * 3; 241 int curSize = target.remaining(); 242 243 if (minSize < curSize) { 244 throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize)); 245 } 246 247 // if image is not fresh, update it 248 249 if (fresh.compareAndSet(false, true)) { 250 updateFrameBuffer(); 251 } 252 253 // get image buffer 254 255 LOG.trace("Webcam grabber get image pointer"); 256 257 Pointer<Byte> image = grabber.getImage(); 258 fresh.set(false); 259 260 if (image == null) { 261 LOG.warn("Null array pointer found instead of image"); 262 return; 263 } 264 265 LOG.trace("Webcam device read buffer {} bytes", minSize); 266 267 image = image.validBytes(minSize); 268 image.getBytes(target); 269 270 } 271 272 @Override 273 public BufferedImage getImage() { 274 275 ByteBuffer buffer = getImageBytes(); 276 277 if (buffer == null) { 278 LOG.error("Images bytes buffer is null!"); 279 return null; 280 } 281 282 byte[] bytes = new byte[size.width * size.height * 3]; 283 byte[][] data = new byte[][] { bytes }; 284 285 buffer.get(bytes); 286 287 DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET); 288 WritableRaster raster = Raster.createWritableRaster(smodel, dbuf, null); 289 290 BufferedImage bi = new BufferedImage(cmodel, raster, false, null); 291 bi.flush(); 292 293 return bi; 294 } 295 296 @Override 297 public void open() { 298 299 if (disposed.get()) { 300 return; 301 } 302 303 LOG.debug("Opening webcam device {}", getName()); 304 305 if (size == null) { 306 size = getResolutions()[0]; 307 } 308 if (size == null) { 309 throw new RuntimeException("The resolution size cannot be null"); 310 } 311 312 LOG.debug("Webcam device {} starting session, size {}", device.getIdentifierStr(), size); 313 314 grabber = new OpenIMAJGrabber(); 315 316 // NOTE! 317 318 // Following the note from OpenIMAJ code - it seams like there is some 319 // issue on 32-bit systems which prevents grabber to find devices. 320 // According to the mentioned note this for loop shall fix the problem. 321 322 DeviceList list = grabber.getVideoDevices().get(); 323 for (Device d : list.asArrayList()) { 324 d.getNameStr(); 325 d.getIdentifierStr(); 326 } 327 328 boolean started = grabber.startSession(size.width, size.height, 50, Pointer.pointerTo(device)); 329 if (!started) { 330 throw new WebcamException("Cannot start native grabber!"); 331 } 332 333 LOG.debug("Webcam device session started"); 334 335 Dimension size2 = new Dimension(grabber.getWidth(), grabber.getHeight()); 336 337 int w1 = size.width; 338 int w2 = size2.width; 339 int h1 = size.height; 340 int h2 = size2.height; 341 342 if (w1 != w2 || h1 != h2) { 343 344 if (failOnSizeMismatch) { 345 throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2)); 346 } 347 348 Object[] args = new Object[] { w1, h1, w2, h2, w2, h2 }; 349 LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args); 350 351 size = new Dimension(w2, h2); 352 } 353 354 smodel = new ComponentSampleModel(DATA_TYPE, size.width, size.height, 3, size.width * 3, BAND_OFFSETS); 355 cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, Transparency.OPAQUE, DATA_TYPE); 356 357 LOG.debug("Initialize buffer"); 358 359 int i = 0; 360 do { 361 362 grabber.nextFrame(); 363 364 try { 365 Thread.sleep(1000); 366 } catch (InterruptedException e) { 367 LOG.error("Nasty interrupted exception", e); 368 } 369 370 } while (++i < 3); 371 372 LOG.debug("Webcam device {} is now open", this); 373 374 open.set(true); 375 376 refresher = new Thread(this, String.format("frames-refresher-[%s]", id)); 377 refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 378 refresher.setDaemon(true); 379 refresher.start(); 380 } 381 382 @Override 383 public void close() { 384 385 if (!open.compareAndSet(true, false)) { 386 return; 387 } 388 389 LOG.debug("Closing webcam device"); 390 391 grabber.stopSession(); 392 } 393 394 @Override 395 public void dispose() { 396 397 if (!disposed.compareAndSet(false, true)) { 398 return; 399 } 400 401 LOG.debug("Disposing webcam device {}", getName()); 402 403 close(); 404 } 405 406 /** 407 * Determines if device should fail when requested image size is different 408 * than actually received. 409 * 410 * @param fail the fail on size mismatch flag, true or false 411 */ 412 public void setFailOnSizeMismatch(boolean fail) { 413 this.failOnSizeMismatch = fail; 414 } 415 416 @Override 417 public boolean isOpen() { 418 return open.get(); 419 } 420 421 /** 422 * Get timeout for image acquisition. 423 * 424 * @return Value in milliseconds 425 */ 426 public int getTimeout() { 427 return timeout; 428 } 429 430 /** 431 * Set timeout for image acquisition. 432 * 433 * @param timeout the timeout value in milliseconds 434 */ 435 public void setTimeout(int timeout) { 436 this.timeout = timeout; 437 } 438 439 /** 440 * Update underlying memory buffer and fetch new frame. 441 */ 442 private void updateFrameBuffer() { 443 444 LOG.trace("Next frame"); 445 446 if (t1 == -1 || t2 == -1) { 447 t1 = System.currentTimeMillis(); 448 t2 = System.currentTimeMillis(); 449 } 450 451 int result = new NextFrameTask(this).nextFrame(); 452 453 t1 = t2; 454 t2 = System.currentTimeMillis(); 455 456 fps = (4 * fps + 1000 / (t2 - t1 + 1)) / 5; 457 458 if (result == -1) { 459 LOG.error("Timeout when requesting image!"); 460 } else if (result < -1) { 461 LOG.error("Error requesting new frame!"); 462 } 463 } 464 465 @Override 466 public void run() { 467 468 do { 469 470 if (Thread.interrupted()) { 471 LOG.debug("Refresher has been interrupted"); 472 return; 473 } 474 475 if (!open.get()) { 476 LOG.debug("Cancelling refresher"); 477 return; 478 } 479 480 updateFrameBuffer(); 481 482 } while (open.get()); 483 } 484 485 @Override 486 public double getFPS() { 487 return fps; 488 } 489}