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