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    }