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    }