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.AtomicBoolean;
010import java.util.concurrent.atomic.AtomicInteger;
011import java.util.concurrent.atomic.AtomicReference;
012
013import org.slf4j.Logger;
014import org.slf4j.LoggerFactory;
015
016import 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 */
026public class WebcamUpdater implements Runnable {
027
028        /**
029         * Thread factory for executors used within updater class.
030         * 
031         * @author Bartosz Firyn (sarxos)
032         */
033        private static final class UpdaterThreadFactory implements ThreadFactory {
034
035                private static final AtomicInteger number = new AtomicInteger(0);
036
037                @Override
038                public Thread newThread(Runnable r) {
039                        Thread t = new Thread(r, String.format("webcam-updater-thread-%d", number.incrementAndGet()));
040                        t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
041                        t.setDaemon(true);
042                        return t;
043                }
044
045        }
046
047        /**
048         * Class used to asynchronously notify all webcam listeners about new image
049         * available.
050         * 
051         * @author Bartosz Firyn (sarxos)
052         */
053        private static final class ImageNotification implements Runnable {
054
055                /**
056                 * Camera.
057                 */
058                private final Webcam webcam;
059
060                /**
061                 * Acquired image.
062                 */
063                private final BufferedImage image;
064
065                /**
066                 * Create new notification.
067                 * 
068                 * @param webcam the webcam from which image has been acquired
069                 * @param image the acquired image
070                 */
071                public ImageNotification(Webcam webcam, BufferedImage image) {
072                        this.webcam = webcam;
073                        this.image = image;
074                }
075
076                @Override
077                public void run() {
078                        if (image != null) {
079                                WebcamEvent we = new WebcamEvent(WebcamEventType.NEW_IMAGE, webcam, image);
080                                for (WebcamListener l : webcam.getWebcamListeners()) {
081                                        try {
082                                                l.webcamImageObtained(we);
083                                        } catch (Exception e) {
084                                                LOG.error(String.format("Notify image acquired, exception when calling listener %s", l.getClass()), e);
085                                        }
086                                }
087                        }
088                }
089        }
090
091        /**
092         * Logger.
093         */
094        private static final Logger LOG = LoggerFactory.getLogger(WebcamUpdater.class);
095
096        /**
097         * Target FPS.
098         */
099        private static final int TARGET_FPS = 50;
100
101        private static final UpdaterThreadFactory THREAD_FACTORY = new UpdaterThreadFactory();
102
103        /**
104         * Executor service.
105         */
106        private ScheduledExecutorService executor = null;
107
108        /**
109         * Executor service for image notifications.
110         */
111        private final ExecutorService notificator = Executors.newSingleThreadExecutor(THREAD_FACTORY);
112
113        /**
114         * Cached image.
115         */
116        private final AtomicReference<BufferedImage> image = new AtomicReference<BufferedImage>();
117
118        /**
119         * Webcam to which this updater is attached.
120         */
121        private Webcam webcam = null;
122
123        /**
124         * Current FPS rate.
125         */
126        private volatile double fps = 0;
127
128        /**
129         * Is updater running.
130         */
131        private AtomicBoolean running = new AtomicBoolean(false);
132
133        private volatile boolean imageNew = false;
134
135        /**
136         * Construct new webcam updater.
137         * 
138         * @param webcam the webcam to which updater shall be attached
139         */
140        protected WebcamUpdater(Webcam webcam) {
141                this.webcam = webcam;
142        }
143
144        /**
145         * Start updater.
146         */
147        public void start() {
148                if (running.compareAndSet(false, true)) {
149
150                        image.set(new WebcamReadImageTask(Webcam.getDriver(), webcam.getDevice()).getImage());
151
152                        executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY);
153                        executor.execute(this);
154
155                        LOG.debug("Webcam updater has been started");
156                } else {
157                        LOG.debug("Webcam updater is already started");
158                }
159        }
160
161        /**
162         * Stop updater.
163         */
164        public void stop() {
165                if (running.compareAndSet(true, false)) {
166
167                        executor.shutdown();
168
169                        while (!executor.isTerminated()) {
170                                try {
171                                        executor.awaitTermination(100, TimeUnit.MILLISECONDS);
172                                } catch (InterruptedException e) {
173                                        LOG.trace(e.getMessage(), e);
174                                        return;
175                                }
176                        }
177
178                        LOG.debug("Webcam updater has been stopped");
179                } else {
180                        LOG.debug("Webcam updater is already stopped");
181                }
182        }
183
184        @Override
185        public void run() {
186
187                if (!running.get()) {
188                        return;
189                }
190
191                try {
192                        tick();
193                } catch (Throwable t) {
194                        WebcamExceptionHandler.handle(t);
195                }
196
197        }
198
199        private void tick() {
200
201                long t1 = 0;
202                long t2 = 0;
203
204                // Calculate time required to fetch 1 picture.
205
206                WebcamDriver driver = Webcam.getDriver();
207                WebcamDevice device = webcam.getDevice();
208
209                assert driver != null;
210                assert device != null;
211
212                BufferedImage img = null;
213
214                t1 = System.currentTimeMillis();
215                img = webcam.transform(new WebcamReadImageTask(driver, device).getImage());
216                t2 = System.currentTimeMillis();
217
218                image.set(img);
219                imageNew = true;
220
221                // Calculate delay required to achieve target FPS. In some cases it can
222                // be less than 0 because camera is not able to serve images as fast as
223                // we would like to. In such case just run with no delay, so maximum FPS
224                // will be the one supported by camera device in the moment.
225
226                long delta = t2 - t1 + 1; // +1 to avoid division by zero
227                long delay = Math.max((1000 / TARGET_FPS) - delta, 0);
228
229                if (device instanceof WebcamDevice.FPSSource) {
230                        fps = ((WebcamDevice.FPSSource) device).getFPS();
231                } else {
232                        fps = (4 * fps + 1000 / delta) / 5;
233                }
234
235                // reschedule task
236
237                executor.schedule(this, delay, TimeUnit.MILLISECONDS);
238
239                // notify webcam listeners about the new image available
240
241                notifyWebcamImageObtained(webcam, image.get());
242        }
243
244        /**
245         * Asynchronously start new thread which will notify all webcam listeners
246         * about the new image available.
247         */
248        protected void notifyWebcamImageObtained(Webcam webcam, BufferedImage image) {
249
250                // notify webcam listeners of new image available, do that only if there
251                // are any webcam listeners available because there is no sense to start
252                // additional threads for no purpose
253
254                if (webcam.getWebcamListenersCount() > 0) {
255                        notificator.execute(new ImageNotification(webcam, image));
256                }
257        }
258
259        /**
260         * Return currently available image. This method will return immediately
261         * while it was been called after camera has been open. In case when there
262         * are parallel threads running and there is a possibility to call this
263         * method in the opening time, or before camera has been open at all, this
264         * method will block until webcam return first image. Maximum blocking time
265         * will be 10 seconds, after this time method will return null.
266         * 
267         * @return Image stored in cache
268         */
269        public BufferedImage getImage() {
270
271                int i = 0;
272                while (image.get() == null) {
273
274                        // Just in case if another thread starts calling this method before
275                        // updater has been properly started. This will loop while image is
276                        // not available.
277
278                        try {
279                                Thread.sleep(100);
280                        } catch (InterruptedException e) {
281                                throw new RuntimeException(e);
282                        }
283
284                        // Return null if more than 10 seconds passed (timeout).
285
286                        if (i++ > 100) {
287                                LOG.error("Image has not been found for more than 10 seconds");
288                                return null;
289                        }
290                }
291
292                imageNew = false;
293
294                return image.get();
295        }
296
297        protected boolean isImageNew() {
298                return imageNew;
299        }
300
301        /**
302         * Return current FPS number. It is calculated in real-time on the base of
303         * how often camera serve new image.
304         * 
305         * @return FPS number
306         */
307        public double getFPS() {
308                return fps;
309        }
310}