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    }