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 }