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 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 }