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