001 package com.github.sarxos.webcam;
002
003 import java.util.ArrayList;
004 import java.util.Collections;
005 import java.util.Iterator;
006 import java.util.LinkedList;
007 import java.util.List;
008 import java.util.concurrent.Callable;
009 import java.util.concurrent.ExecutionException;
010 import java.util.concurrent.ExecutorService;
011 import java.util.concurrent.Executors;
012 import java.util.concurrent.Future;
013 import java.util.concurrent.ThreadFactory;
014 import java.util.concurrent.TimeUnit;
015 import java.util.concurrent.TimeoutException;
016
017 import org.slf4j.Logger;
018 import org.slf4j.LoggerFactory;
019
020
021 public class WebcamDiscoveryService implements Runnable {
022
023 private static final Logger LOG = LoggerFactory.getLogger(WebcamDiscoveryService.class);
024
025 private static final class WebcamsDiscovery implements Callable<List<Webcam>>, ThreadFactory {
026
027 private final WebcamDriver driver;
028
029 public WebcamsDiscovery(WebcamDriver driver) {
030 this.driver = driver;
031 }
032
033 @Override
034 public List<Webcam> call() throws Exception {
035 return toWebcams(driver.getDevices());
036 }
037
038 @Override
039 public Thread newThread(Runnable r) {
040 Thread t = new Thread(r, "webcam-discovery-service");
041 t.setDaemon(true);
042 t.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
043 return t;
044 }
045 }
046
047 private final WebcamDriver driver;
048 private final WebcamDiscoverySupport support;
049
050 private volatile List<Webcam> webcams = null;
051
052 private volatile boolean running = false;
053
054 private Thread runner = null;
055
056 protected WebcamDiscoveryService(WebcamDriver driver) {
057
058 if (driver == null) {
059 throw new IllegalArgumentException("Driver cannot be null!");
060 }
061
062 this.driver = driver;
063 this.support = (WebcamDiscoverySupport) (driver instanceof WebcamDiscoverySupport ? driver : null);
064 }
065
066 private static List<Webcam> toWebcams(List<WebcamDevice> devices) {
067 List<Webcam> webcams = new ArrayList<Webcam>();
068 for (WebcamDevice device : devices) {
069 webcams.add(new Webcam(device));
070 }
071 return webcams;
072 }
073
074 /**
075 * Get list of devices used by webcams.
076 *
077 * @return List of webcam devices
078 */
079 private static List<WebcamDevice> getDevices(List<Webcam> webcams) {
080 List<WebcamDevice> devices = new ArrayList<WebcamDevice>();
081 for (Webcam webcam : webcams) {
082 devices.add(webcam.getDevice());
083 }
084 return devices;
085 }
086
087 public synchronized List<Webcam> getWebcams(long timeout, TimeUnit tunit) throws TimeoutException {
088
089 if (timeout < 0) {
090 throw new IllegalArgumentException("Timeout cannot be negative");
091 }
092
093 if (tunit == null) {
094 throw new IllegalArgumentException("Time unit cannot be null!");
095 }
096
097 if (webcams == null) {
098
099 WebcamsDiscovery discovery = new WebcamsDiscovery(driver);
100 ExecutorService executor = Executors.newSingleThreadExecutor(discovery);
101 Future<List<Webcam>> future = executor.submit(discovery);
102
103 executor.shutdown();
104
105 try {
106
107 executor.awaitTermination(timeout, tunit);
108
109 if (future.isDone()) {
110 webcams = future.get();
111 } else {
112 future.cancel(true);
113 }
114
115 } catch (InterruptedException e) {
116 throw new WebcamException(e);
117 } catch (ExecutionException e) {
118 throw new WebcamException(e);
119 }
120
121 if (webcams == null) {
122 throw new TimeoutException(String.format("Webcams discovery timeout (%d ms) has been exceeded", timeout));
123 }
124
125 WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
126 for (Webcam webcam : webcams) {
127 notifyWebcamFound(webcam, listeners);
128 }
129
130 if (Webcam.isHandleTermSignal()) {
131 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
132 }
133 }
134
135 return Collections.unmodifiableList(webcams);
136 }
137
138 @Override
139 public void run() {
140
141 // do not run if driver does not support discovery
142
143 if (support == null) {
144 return;
145 }
146
147 running = true;
148
149 // wait initial time interval since devices has been initially
150 // discovered
151
152 Object monitor = new Object();
153
154 do {
155
156 synchronized (monitor) {
157 try {
158 monitor.wait(support.getScanInterval());
159 } catch (InterruptedException e) {
160 if (LOG.isTraceEnabled()) {
161 LOG.error("Interrupted", e);
162 }
163 break;
164 } catch (Exception e) {
165 throw new RuntimeException("Problem waiting on monitor", e);
166 }
167 }
168
169 WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners();
170
171 // do nothing when there are no listeners to be notified
172
173 if (listeners.length == 0) {
174 continue;
175 }
176
177 List<WebcamDevice> tmpnew = driver.getDevices();
178 List<WebcamDevice> tmpold = null;
179
180 try {
181 tmpold = getDevices(getWebcams(Long.MAX_VALUE, TimeUnit.MILLISECONDS));
182 } catch (TimeoutException e) {
183 throw new WebcamException(e);
184 }
185
186 // convert to linked list due to O(1) on remove operation on
187 // iterator versus O(n) for the same operation in array list
188
189 List<WebcamDevice> oldones = new LinkedList<WebcamDevice>(tmpold);
190 List<WebcamDevice> newones = new LinkedList<WebcamDevice>(tmpnew);
191
192 Iterator<WebcamDevice> oi = oldones.iterator();
193 Iterator<WebcamDevice> ni = null;
194
195 WebcamDevice od = null; // old device
196 WebcamDevice nd = null; // new device
197
198 // reduce lists
199
200 while (oi.hasNext()) {
201
202 od = oi.next();
203 ni = newones.iterator();
204
205 while (ni.hasNext()) {
206
207 nd = ni.next();
208
209 // remove both elements, if device name is the same, which
210 // actually means that device is exactly the same
211
212 if (nd.getName().equals(od.getName())) {
213 ni.remove();
214 oi.remove();
215 break;
216 }
217 }
218 }
219
220 // if any left in old ones it means that devices has been removed
221 if (oldones.size() > 0) {
222
223 List<Webcam> notified = new ArrayList<Webcam>();
224
225 for (WebcamDevice device : oldones) {
226 for (Webcam webcam : webcams) {
227 if (webcam.getDevice().getName().equals(device.getName())) {
228 notified.add(webcam);
229 break;
230 }
231 }
232 }
233
234 setCurrentWebcams(tmpnew);
235
236 for (Webcam webcam : notified) {
237 notifyWebcamGone(webcam, listeners);
238 webcam.dispose();
239 }
240 }
241
242 // if any left in new ones it means that devices has been added
243 if (newones.size() > 0) {
244
245 setCurrentWebcams(tmpnew);
246
247 for (WebcamDevice device : newones) {
248 for (Webcam webcam : webcams) {
249 if (webcam.getDevice().getName().equals(device.getName())) {
250 notifyWebcamFound(webcam, listeners);
251 break;
252 }
253 }
254 }
255 }
256
257 } while (running);
258 }
259
260 private void setCurrentWebcams(List<WebcamDevice> devices) {
261 webcams = toWebcams(devices);
262 if (Webcam.isHandleTermSignal()) {
263 WebcamDeallocator.unstore();
264 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()]));
265 }
266 }
267
268 private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) {
269 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED);
270 for (WebcamDiscoveryListener l : listeners) {
271 try {
272 l.webcamGone(event);
273 } catch (Exception e) {
274 LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e);
275 }
276 }
277 }
278
279 private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) {
280 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED);
281 for (WebcamDiscoveryListener l : listeners) {
282 try {
283 l.webcamFound(event);
284 } catch (Exception e) {
285 LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e);
286 }
287 }
288 }
289
290 /**
291 * Stop discovery service.
292 */
293 public synchronized void stop() {
294
295 running = false;
296
297 if (runner == null) {
298 return;
299 }
300
301 try {
302 runner.join();
303 } catch (InterruptedException e) {
304 throw new WebcamException("Joint interrupted");
305 }
306
307 runner = null;
308 }
309
310 /**
311 * Start discovery service.
312 */
313 public synchronized void start() {
314
315 // discovery service has been already started
316
317 if (runner != null) {
318 return;
319 }
320
321 // capture driver does not support discovery - nothing to do
322
323 if (support == null) {
324 return;
325 }
326
327 // start discovery service runner
328
329 runner = new Thread(this, "webcam-discovery-service");
330 runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
331 runner.setDaemon(true);
332 runner.start();
333 }
334
335 /**
336 * Is discovery service running?
337 *
338 * @return True or false
339 */
340 public boolean isRunning() {
341 return running;
342 }
343
344 /**
345 * Cleanup.
346 */
347 protected synchronized void shutdown() {
348
349 stop();
350
351 // dispose all webcams
352
353 for (Webcam webcam : webcams) {
354 webcam.dispose();
355 }
356
357 webcams.clear();
358
359 // unassign webcams from deallocator
360
361 if (Webcam.isHandleTermSignal()) {
362 WebcamDeallocator.unstore();
363 }
364 }
365 }