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}