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