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 }