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 }