001package com.github.sarxos.webcam;
002
003import java.awt.image.BufferedImage;
004import java.util.concurrent.Executors;
005import java.util.concurrent.RejectedExecutionException;
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.WebcamGetImageTask;
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         * Logger.
049         */
050        private static final Logger LOG = LoggerFactory.getLogger(WebcamUpdater.class);
051
052        /**
053         * Target FPS.
054         */
055        private static final int TARGET_FPS = 50;
056
057        private static final UpdaterThreadFactory THREAD_FACTORY = new UpdaterThreadFactory();
058
059        /**
060         * Executor service.
061         */
062        private ScheduledExecutorService executor = null;
063
064        /**
065         * Cached image.
066         */
067        private final AtomicReference<BufferedImage> image = new AtomicReference<BufferedImage>();
068
069        /**
070         * Webcam to which this updater is attached.
071         */
072        private Webcam webcam = null;
073
074        /**
075         * Current FPS rate.
076         */
077        private volatile double fps = 0;
078
079        /**
080         * Is updater running.
081         */
082        private AtomicBoolean running = new AtomicBoolean(false);
083
084        private volatile boolean imageNew = false;
085
086        /**
087         * Construct new webcam updater.
088         * 
089         * @param webcam the webcam to which updater shall be attached
090         */
091        protected WebcamUpdater(Webcam webcam) {
092                this.webcam = webcam;
093        }
094
095        /**
096         * Start updater.
097         */
098        public void start() {
099
100                if (running.compareAndSet(false, true)) {
101
102                        image.set(new WebcamGetImageTask(Webcam.getDriver(), webcam.getDevice()).getImage());
103
104                        executor = Executors.newSingleThreadScheduledExecutor(THREAD_FACTORY);
105                        executor.execute(this);
106
107                        LOG.debug("Webcam updater has been started");
108                } else {
109                        LOG.debug("Webcam updater is already started");
110                }
111        }
112
113        /**
114         * Stop updater.
115         */
116        public void stop() {
117                if (running.compareAndSet(true, false)) {
118
119                        executor.shutdown();
120                        while (!executor.isTerminated()) {
121                                try {
122                                        executor.awaitTermination(100, TimeUnit.MILLISECONDS);
123                                } catch (InterruptedException e) {
124                                        return;
125                                }
126                        }
127
128                        LOG.debug("Webcam updater has been stopped");
129                } else {
130                        LOG.debug("Webcam updater is already stopped");
131                }
132        }
133
134        @Override
135        public void run() {
136
137                if (!running.get()) {
138                        return;
139                }
140
141                try {
142                        tick();
143                } catch (Throwable t) {
144                        WebcamExceptionHandler.handle(t);
145                }
146
147        }
148
149        private void tick() {
150
151                if (!webcam.isOpen()) {
152                        return;
153                }
154
155                long t1 = 0;
156                long t2 = 0;
157
158                // Calculate time required to fetch 1 picture.
159
160                WebcamDriver driver = Webcam.getDriver();
161                WebcamDevice device = webcam.getDevice();
162
163                assert driver != null;
164                assert device != null;
165
166                BufferedImage img = null;
167
168                t1 = System.currentTimeMillis();
169                img = webcam.transform(new WebcamGetImageTask(driver, device).getImage());
170                t2 = System.currentTimeMillis();
171
172                image.set(img);
173                imageNew = true;
174
175                // Calculate delay required to achieve target FPS. In some cases it can
176                // be less than 0 because camera is not able to serve images as fast as
177                // we would like to. In such case just run with no delay, so maximum FPS
178                // will be the one supported by camera device in the moment.
179
180                long delta = t2 - t1 + 1; // +1 to avoid division by zero
181                long delay = Math.max((1000 / TARGET_FPS) - delta, 0);
182
183                if (device instanceof WebcamDevice.FPSSource) {
184                        fps = ((WebcamDevice.FPSSource) device).getFPS();
185                } else {
186                        fps = (4 * fps + 1000 / delta) / 5;
187                }
188
189                // reschedule task
190
191                if (webcam.isOpen()) {
192                        try {
193                                executor.schedule(this, delay, TimeUnit.MILLISECONDS);
194                        } catch (RejectedExecutionException e) {
195                                LOG.trace("Webcam update has been rejected", e);
196                        }
197                }
198
199                // notify webcam listeners about the new image available
200
201                webcam.notifyWebcamImageAcquired(image.get());
202        }
203
204        /**
205         * Return currently available image. This method will return immediately
206         * while it was been called after camera has been open. In case when there
207         * are parallel threads running and there is a possibility to call this
208         * method in the opening time, or before camera has been open at all, this
209         * method will block until webcam return first image. Maximum blocking time
210         * will be 10 seconds, after this time method will return null.
211         * 
212         * @return Image stored in cache
213         */
214        public BufferedImage getImage() {
215
216                int i = 0;
217                while (image.get() == null) {
218
219                        // Just in case if another thread starts calling this method before
220                        // updater has been properly started. This will loop while image is
221                        // not available.
222
223                        try {
224                                Thread.sleep(100);
225                        } catch (InterruptedException e) {
226                                throw new RuntimeException(e);
227                        }
228
229                        // Return null if more than 10 seconds passed (timeout).
230
231                        if (i++ > 100) {
232                                LOG.error("Image has not been found for more than 10 seconds");
233                                return null;
234                        }
235                }
236
237                imageNew = false;
238
239                return image.get();
240        }
241
242        protected boolean isImageNew() {
243                return imageNew;
244        }
245
246        /**
247         * Return current FPS number. It is calculated in real-time on the base of
248         * how often camera serve new image.
249         * 
250         * @return FPS number
251         */
252        public double getFPS() {
253                return fps;
254        }
255}