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