001package com.github.sarxos.webcam;
002
003import java.awt.image.BufferedImage;
004import java.util.concurrent.ExecutorService;
005import java.util.concurrent.Executors;
006import java.util.concurrent.ScheduledExecutorService;
007import java.util.concurrent.ThreadFactory;
008import java.util.concurrent.TimeUnit;
009import java.util.concurrent.atomic.AtomicInteger;
010import java.util.concurrent.atomic.AtomicReference;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import 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 */
025public 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}