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