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