001 package com.github.sarxos.webcam; 002 003 import java.awt.image.BufferedImage; 004 import java.util.concurrent.ExecutorService; 005 import java.util.concurrent.Executors; 006 import java.util.concurrent.ScheduledExecutorService; 007 import java.util.concurrent.ThreadFactory; 008 import java.util.concurrent.TimeUnit; 009 import java.util.concurrent.atomic.AtomicInteger; 010 import java.util.concurrent.atomic.AtomicReference; 011 012 import org.slf4j.Logger; 013 import org.slf4j.LoggerFactory; 014 015 import 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 */ 025 public 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 long t1 = 0; 170 long t2 = 0; 171 172 // Calculate time required to fetch 1 picture. 173 174 WebcamDriver driver = Webcam.getDriver(); 175 WebcamDevice device = webcam.getDevice(); 176 177 assert driver != null; 178 assert device != null; 179 180 BufferedImage img = null; 181 182 t1 = System.currentTimeMillis(); 183 img = webcam.transform(new WebcamReadImageTask(driver, device).getImage()); 184 t2 = System.currentTimeMillis(); 185 186 image.set(img); 187 imageNew = true; 188 189 // Calculate delay required to achieve target FPS. In some cases it can 190 // be less than 0 because camera is not able to serve images as fast as 191 // we would like to. In such case just run with no delay, so maximum FPS 192 // will be the one supported by camera device in the moment. 193 194 long delta = t2 - t1 + 1; // +1 to avoid division by zero 195 long delay = Math.max((1000 / TARGET_FPS) - delta, 0); 196 197 fps = (4 * fps + 1000 / delta) / 5; 198 199 // reschedule task 200 201 executor.schedule(this, delay, TimeUnit.MILLISECONDS); 202 203 // notify webcam listeners about the new image available 204 205 notifyWebcamImageObtained(webcam, image.get()); 206 } 207 208 /** 209 * Asynchronously start new thread which will notify all webcam listeners 210 * about the new image available. 211 */ 212 protected void notifyWebcamImageObtained(Webcam webcam, BufferedImage image) { 213 214 // notify webcam listeners of new image available, do that only if there 215 // are any webcam listeners available because there is no sense to start 216 // additional threads for no purpose 217 218 if (webcam.getWebcamListenersCount() > 0) { 219 notificator.execute(new ImageNotification(webcam, image)); 220 } 221 } 222 223 /** 224 * Return currently available image. This method will return immediately 225 * while it was been called after camera has been open. In case when there 226 * are parallel threads running and there is a possibility to call this 227 * method in the opening time, or before camera has been open at all, this 228 * method will block until webcam return first image. Maximum blocking time 229 * will be 10 seconds, after this time method will return null. 230 * 231 * @return Image stored in cache 232 */ 233 public BufferedImage getImage() { 234 235 int i = 0; 236 while (image.get() == null) { 237 238 // Just in case if another thread starts calling this method before 239 // updater has been properly started. This will loop while image is 240 // not available. 241 242 try { 243 Thread.sleep(100); 244 } catch (InterruptedException e) { 245 throw new RuntimeException(e); 246 } 247 248 // Return null if more than 10 seconds passed (timeout). 249 250 if (i++ > 100) { 251 LOG.error("Image has not been found for more than 10 seconds"); 252 return null; 253 } 254 } 255 256 imageNew = false; 257 258 return image.get(); 259 } 260 261 protected boolean isImageNew() { 262 return imageNew; 263 } 264 265 /** 266 * Return current FPS number. It is calculated in real-time on the base of 267 * how often camera serve new image. 268 * 269 * @return FPS number 270 */ 271 public double getFPS() { 272 return fps; 273 } 274 }