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(false); 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 246 // wait initial time interval since devices has been initially 247 // discovered 248 249 Object monitor = new Object(); 250 do { 251 252 synchronized (monitor) { 253 try { 254 monitor.wait(support.getScanInterval()); 255 } catch (InterruptedException e) { 256 break; 257 } catch (Exception e) { 258 throw new RuntimeException("Problem waiting on monitor", e); 259 } 260 } 261 262 scan(); 263 264 } while (running.get()); 265 266 LOG.debug("Webcam discovery service loop has been stopped"); 267 } 268 269 private void setCurrentWebcams(List<WebcamDevice> devices) { 270 webcams = toWebcams(devices); 271 if (Webcam.isHandleTermSignal()) { 272 WebcamDeallocator.unstore(); 273 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()])); 274 } 275 } 276 277 private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) { 278 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED); 279 for (WebcamDiscoveryListener l : listeners) { 280 try { 281 l.webcamGone(event); 282 } catch (Exception e) { 283 LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e); 284 } 285 } 286 } 287 288 private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) { 289 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED); 290 for (WebcamDiscoveryListener l : listeners) { 291 try { 292 l.webcamFound(event); 293 } catch (Exception e) { 294 LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e); 295 } 296 } 297 } 298 299 /** 300 * Stop discovery service. 301 */ 302 public void stop() { 303 304 // return if not running 305 306 if (!running.compareAndSet(true, false)) { 307 return; 308 } 309 310 try { 311 runner.join(); 312 } catch (InterruptedException e) { 313 throw new WebcamException("Joint interrupted"); 314 } 315 316 LOG.debug("Discovery service has been stopped"); 317 318 runner = null; 319 } 320 321 /** 322 * Start discovery service. 323 */ 324 public void start() { 325 326 // if configured to not start, then simply return 327 328 if (!enabled.get()) { 329 LOG.info("Discovery service has been disabled and thus it will not be started"); 330 return; 331 } 332 333 // capture driver does not support discovery - nothing to do 334 335 if (support == null) { 336 LOG.info("Discovery will not run - driver {} does not support this feature", driver.getClass().getSimpleName()); 337 return; 338 } 339 340 // return if already running 341 342 if (!running.compareAndSet(false, true)) { 343 return; 344 } 345 346 // start discovery service runner 347 348 runner = new Thread(this, "webcam-discovery-service"); 349 runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 350 runner.setDaemon(true); 351 runner.start(); 352 } 353 354 /** 355 * Is discovery service running? 356 * 357 * @return True or false 358 */ 359 public boolean isRunning() { 360 return running.get(); 361 } 362 363 /** 364 * Webcam discovery service will be automatically started if it's enabled, 365 * otherwise, when set to disabled, it will never start, even when user try 366 * to run it. 367 * 368 * @param enabled the parameter controlling if discovery shall be started 369 */ 370 public void setEnabled(boolean enabled) { 371 this.enabled.set(enabled); 372 } 373 374 /** 375 * Cleanup. 376 */ 377 protected void shutdown() { 378 379 stop(); 380 381 // dispose all webcams 382 383 Iterator<Webcam> wi = webcams.iterator(); 384 while (wi.hasNext()) { 385 Webcam webcam = wi.next(); 386 webcam.dispose(); 387 } 388 389 synchronized (Webcam.class) { 390 391 // clear webcams list 392 393 webcams.clear(); 394 395 // unassign webcams from deallocator 396 397 if (Webcam.isHandleTermSignal()) { 398 WebcamDeallocator.unstore(); 399 } 400 } 401 } 402}