001package com.github.sarxos.webcam; 002 003import java.awt.image.BufferedImage; 004import java.util.concurrent.Executors; 005import java.util.concurrent.RejectedExecutionException; 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.WebcamGetImageTask; 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 * Logger. 049 */ 050 private static final Logger LOG = LoggerFactory.getLogger(WebcamUpdater.class); 051 052 /** 053 * Target FPS. 054 */ 055 private static final int TARGET_FPS = 50; 056 057 private static final UpdaterThreadFactory THREAD_FACTORY = new UpdaterThreadFactory(); 058 059 /** 060 * Executor service. 061 */ 062 private ScheduledExecutorService executor = null; 063 064 /** 065 * Cached image. 066 */ 067 private final AtomicReference<BufferedImage> image = new AtomicReference<BufferedImage>(); 068 069 /** 070 * Webcam to which this updater is attached. 071 */ 072 private Webcam webcam = null; 073 074 /** 075 * Current FPS rate. 076 */ 077 private volatile double fps = 0; 078 079 /** 080 * Is updater running. 081 */ 082 private AtomicBoolean running = new AtomicBoolean(false); 083 084 private volatile boolean imageNew = false; 085 086 /** 087 * Construct new webcam updater. 088 * 089 * @param webcam the webcam to which updater shall be attached 090 */ 091 protected WebcamUpdater(Webcam webcam) { 092 this.webcam = webcam; 093 } 094 095 /** 096 * Start updater. 097 */ 098 public void start() { 099 100 if (running.compareAndSet(false, true)) { 101 102 image.set(new WebcamGetImageTask(Webcam.getDriver(), webcam.getDevice()).getImage()); 103 104 executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY); 105 executor.execute(this); 106 107 LOG.debug("Webcam updater has been started"); 108 } else { 109 LOG.debug("Webcam updater is already started"); 110 } 111 } 112 113 /** 114 * Stop updater. 115 */ 116 public void stop() { 117 if (running.compareAndSet(true, false)) { 118 119 executor.shutdown(); 120 while (!executor.isTerminated()) { 121 try { 122 executor.awaitTermination(100, TimeUnit.MILLISECONDS); 123 } catch (InterruptedException e) { 124 return; 125 } 126 } 127 128 LOG.debug("Webcam updater has been stopped"); 129 } else { 130 LOG.debug("Webcam updater is already stopped"); 131 } 132 } 133 134 @Override 135 public void run() { 136 137 if (!running.get()) { 138 return; 139 } 140 141 try { 142 tick(); 143 } catch (Throwable t) { 144 WebcamExceptionHandler.handle(t); 145 } 146 147 } 148 149 private void tick() { 150 151 if (!webcam.isOpen()) { 152 return; 153 } 154 155 long t1 = 0; 156 long t2 = 0; 157 158 // Calculate time required to fetch 1 picture. 159 160 WebcamDriver driver = Webcam.getDriver(); 161 WebcamDevice device = webcam.getDevice(); 162 163 assert driver != null; 164 assert device != null; 165 166 BufferedImage img = null; 167 168 t1 = System.currentTimeMillis(); 169 img = webcam.transform(new WebcamGetImageTask(driver, device).getImage()); 170 t2 = System.currentTimeMillis(); 171 172 image.set(img); 173 imageNew = true; 174 175 // Calculate delay required to achieve target FPS. In some cases it can 176 // be less than 0 because camera is not able to serve images as fast as 177 // we would like to. In such case just run with no delay, so maximum FPS 178 // will be the one supported by camera device in the moment. 179 180 long delta = t2 - t1 + 1; // +1 to avoid division by zero 181 long delay = Math.max((1000 / TARGET_FPS) - delta, 0); 182 183 if (device instanceof WebcamDevice.FPSSource) { 184 fps = ((WebcamDevice.FPSSource) device).getFPS(); 185 } else { 186 fps = (4 * fps + 1000 / delta) / 5; 187 } 188 189 // reschedule task 190 191 if (webcam.isOpen()) { 192 try { 193 executor.schedule(this, delay, TimeUnit.MILLISECONDS); 194 } catch (RejectedExecutionException e) { 195 LOG.trace("Webcam update has been rejected", e); 196 } 197 } 198 199 // notify webcam listeners about the new image available 200 201 webcam.notifyWebcamImageAcquired(image.get()); 202 } 203 204 /** 205 * Return currently available image. This method will return immediately 206 * while it was been called after camera has been open. In case when there 207 * are parallel threads running and there is a possibility to call this 208 * method in the opening time, or before camera has been open at all, this 209 * method will block until webcam return first image. Maximum blocking time 210 * will be 10 seconds, after this time method will return null. 211 * 212 * @return Image stored in cache 213 */ 214 public BufferedImage getImage() { 215 216 int i = 0; 217 while (image.get() == null) { 218 219 // Just in case if another thread starts calling this method before 220 // updater has been properly started. This will loop while image is 221 // not available. 222 223 try { 224 Thread.sleep(100); 225 } catch (InterruptedException e) { 226 throw new RuntimeException(e); 227 } 228 229 // Return null if more than 10 seconds passed (timeout). 230 231 if (i++ > 100) { 232 LOG.error("Image has not been found for more than 10 seconds"); 233 return null; 234 } 235 } 236 237 imageNew = false; 238 239 return image.get(); 240 } 241 242 protected boolean isImageNew() { 243 return imageNew; 244 } 245 246 /** 247 * Return current FPS number. It is calculated in real-time on the base of 248 * how often camera serve new image. 249 * 250 * @return FPS number 251 */ 252 public double getFPS() { 253 return fps; 254 } 255}