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