001package com.github.sarxos.webcam; 002 003import java.awt.image.BufferedImage; 004import java.util.concurrent.ExecutorService; 005import java.util.concurrent.Executors; 006import java.util.concurrent.ScheduledExecutorService; 007import java.util.concurrent.ThreadFactory; 008import java.util.concurrent.TimeUnit; 009import java.util.concurrent.atomic.AtomicBoolean; 010import java.util.concurrent.atomic.AtomicInteger; 011import java.util.concurrent.atomic.AtomicReference; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import com.github.sarxos.webcam.ds.cgt.WebcamReadImageTask; 017 018 019/** 020 * The goal of webcam updater class is to update image in parallel, so all calls 021 * to fetch image invoked on webcam instance will be non-blocking (will return 022 * immediately). 023 * 024 * @author Bartosz Firyn (sarxos) 025 */ 026public class WebcamUpdater implements Runnable { 027 028 /** 029 * Thread factory for executors used within updater class. 030 * 031 * @author Bartosz Firyn (sarxos) 032 */ 033 private static final class UpdaterThreadFactory implements ThreadFactory { 034 035 private static final AtomicInteger number = new AtomicInteger(0); 036 037 @Override 038 public Thread newThread(Runnable r) { 039 Thread t = new Thread(r, String.format("webcam-updater-thread-%d", number.incrementAndGet())); 040 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 041 t.setDaemon(true); 042 return t; 043 } 044 045 } 046 047 /** 048 * Class used to asynchronously notify all webcam listeners about new image 049 * available. 050 * 051 * @author Bartosz Firyn (sarxos) 052 */ 053 private static final class ImageNotification implements Runnable { 054 055 /** 056 * Camera. 057 */ 058 private final Webcam webcam; 059 060 /** 061 * Acquired image. 062 */ 063 private final BufferedImage image; 064 065 /** 066 * Create new notification. 067 * 068 * @param webcam the webcam from which image has been acquired 069 * @param image the acquired image 070 */ 071 public ImageNotification(Webcam webcam, BufferedImage image) { 072 this.webcam = webcam; 073 this.image = image; 074 } 075 076 @Override 077 public void run() { 078 if (image != null) { 079 WebcamEvent we = new WebcamEvent(WebcamEventType.NEW_IMAGE, webcam, image); 080 for (WebcamListener l : webcam.getWebcamListeners()) { 081 try { 082 l.webcamImageObtained(we); 083 } catch (Exception e) { 084 LOG.error(String.format("Notify image acquired, exception when calling listener %s", l.getClass()), e); 085 } 086 } 087 } 088 } 089 } 090 091 /** 092 * Logger. 093 */ 094 private static final Logger LOG = LoggerFactory.getLogger(WebcamUpdater.class); 095 096 /** 097 * Target FPS. 098 */ 099 private static final int TARGET_FPS = 50; 100 101 private static final UpdaterThreadFactory THREAD_FACTORY = new UpdaterThreadFactory(); 102 103 /** 104 * Executor service. 105 */ 106 private ScheduledExecutorService executor = null; 107 108 /** 109 * Executor service for image notifications. 110 */ 111 private final ExecutorService notificator = Executors.newSingleThreadExecutor(THREAD_FACTORY); 112 113 /** 114 * Cached image. 115 */ 116 private final AtomicReference<BufferedImage> image = new AtomicReference<BufferedImage>(); 117 118 /** 119 * Webcam to which this updater is attached. 120 */ 121 private Webcam webcam = null; 122 123 /** 124 * Current FPS rate. 125 */ 126 private volatile double fps = 0; 127 128 /** 129 * Is updater running. 130 */ 131 private AtomicBoolean running = new AtomicBoolean(false); 132 133 private volatile boolean imageNew = false; 134 135 /** 136 * Construct new webcam updater. 137 * 138 * @param webcam the webcam to which updater shall be attached 139 */ 140 protected WebcamUpdater(Webcam webcam) { 141 this.webcam = webcam; 142 } 143 144 /** 145 * Start updater. 146 */ 147 public void start() { 148 if (running.compareAndSet(false, true)) { 149 150 image.set(new WebcamReadImageTask(Webcam.getDriver(), webcam.getDevice()).getImage()); 151 152 executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY); 153 executor.execute(this); 154 155 LOG.debug("Webcam updater has been started"); 156 } else { 157 LOG.debug("Webcam updater is already started"); 158 } 159 } 160 161 /** 162 * Stop updater. 163 */ 164 public void stop() { 165 if (running.compareAndSet(true, false)) { 166 167 executor.shutdown(); 168 169 while (!executor.isTerminated()) { 170 try { 171 executor.awaitTermination(100, TimeUnit.MILLISECONDS); 172 } catch (InterruptedException e) { 173 LOG.trace(e.getMessage(), e); 174 return; 175 } 176 } 177 178 LOG.debug("Webcam updater has been stopped"); 179 } else { 180 LOG.debug("Webcam updater is already stopped"); 181 } 182 } 183 184 @Override 185 public void run() { 186 187 if (!running.get()) { 188 return; 189 } 190 191 try { 192 tick(); 193 } catch (Throwable t) { 194 WebcamExceptionHandler.handle(t); 195 } 196 197 } 198 199 private void tick() { 200 201 long t1 = 0; 202 long t2 = 0; 203 204 // Calculate time required to fetch 1 picture. 205 206 WebcamDriver driver = Webcam.getDriver(); 207 WebcamDevice device = webcam.getDevice(); 208 209 assert driver != null; 210 assert device != null; 211 212 BufferedImage img = null; 213 214 t1 = System.currentTimeMillis(); 215 img = webcam.transform(new WebcamReadImageTask(driver, device).getImage()); 216 t2 = System.currentTimeMillis(); 217 218 image.set(img); 219 imageNew = true; 220 221 // Calculate delay required to achieve target FPS. In some cases it can 222 // be less than 0 because camera is not able to serve images as fast as 223 // we would like to. In such case just run with no delay, so maximum FPS 224 // will be the one supported by camera device in the moment. 225 226 long delta = t2 - t1 + 1; // +1 to avoid division by zero 227 long delay = Math.max((1000 / TARGET_FPS) - delta, 0); 228 229 if (device instanceof WebcamDevice.FPSSource) { 230 fps = ((WebcamDevice.FPSSource) device).getFPS(); 231 } else { 232 fps = (4 * fps + 1000 / delta) / 5; 233 } 234 235 // reschedule task 236 237 executor.schedule(this, delay, TimeUnit.MILLISECONDS); 238 239 // notify webcam listeners about the new image available 240 241 notifyWebcamImageObtained(webcam, image.get()); 242 } 243 244 /** 245 * Asynchronously start new thread which will notify all webcam listeners 246 * about the new image available. 247 */ 248 protected void notifyWebcamImageObtained(Webcam webcam, BufferedImage image) { 249 250 // notify webcam listeners of new image available, do that only if there 251 // are any webcam listeners available because there is no sense to start 252 // additional threads for no purpose 253 254 if (webcam.getWebcamListenersCount() > 0) { 255 notificator.execute(new ImageNotification(webcam, image)); 256 } 257 } 258 259 /** 260 * Return currently available image. This method will return immediately 261 * while it was been called after camera has been open. In case when there 262 * are parallel threads running and there is a possibility to call this 263 * method in the opening time, or before camera has been open at all, this 264 * method will block until webcam return first image. Maximum blocking time 265 * will be 10 seconds, after this time method will return null. 266 * 267 * @return Image stored in cache 268 */ 269 public BufferedImage getImage() { 270 271 int i = 0; 272 while (image.get() == null) { 273 274 // Just in case if another thread starts calling this method before 275 // updater has been properly started. This will loop while image is 276 // not available. 277 278 try { 279 Thread.sleep(100); 280 } catch (InterruptedException e) { 281 throw new RuntimeException(e); 282 } 283 284 // Return null if more than 10 seconds passed (timeout). 285 286 if (i++ > 100) { 287 LOG.error("Image has not been found for more than 10 seconds"); 288 return null; 289 } 290 } 291 292 imageNew = false; 293 294 return image.get(); 295 } 296 297 protected boolean isImageNew() { 298 return imageNew; 299 } 300 301 /** 302 * Return current FPS number. It is calculated in real-time on the base of 303 * how often camera serve new image. 304 * 305 * @return FPS number 306 */ 307 public double getFPS() { 308 return fps; 309 } 310}