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    }