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 /** 139 * Scan for newly added or already removed webcams. 140 */ 141 public void scan() { 142 143 WebcamDiscoveryListener[] listeners = Webcam.getDiscoveryListeners(); 144 145 List<WebcamDevice> tmpnew = driver.getDevices(); 146 List<WebcamDevice> tmpold = null; 147 148 try { 149 tmpold = getDevices(getWebcams(Long.MAX_VALUE, TimeUnit.MILLISECONDS)); 150 } catch (TimeoutException e) { 151 throw new WebcamException(e); 152 } 153 154 // convert to linked list due to O(1) on remove operation on 155 // iterator versus O(n) for the same operation in array list 156 157 List<WebcamDevice> oldones = new LinkedList<WebcamDevice>(tmpold); 158 List<WebcamDevice> newones = new LinkedList<WebcamDevice>(tmpnew); 159 160 Iterator<WebcamDevice> oi = oldones.iterator(); 161 Iterator<WebcamDevice> ni = null; 162 163 WebcamDevice od = null; // old device 164 WebcamDevice nd = null; // new device 165 166 // reduce lists 167 168 while (oi.hasNext()) { 169 170 od = oi.next(); 171 ni = newones.iterator(); 172 173 while (ni.hasNext()) { 174 175 nd = ni.next(); 176 177 // remove both elements, if device name is the same, which 178 // actually means that device is exactly the same 179 180 if (nd.getName().equals(od.getName())) { 181 ni.remove(); 182 oi.remove(); 183 break; 184 } 185 } 186 } 187 188 // if any left in old ones it means that devices has been removed 189 if (oldones.size() > 0) { 190 191 List<Webcam> notified = new ArrayList<Webcam>(); 192 193 for (WebcamDevice device : oldones) { 194 for (Webcam webcam : webcams) { 195 if (webcam.getDevice().getName().equals(device.getName())) { 196 notified.add(webcam); 197 break; 198 } 199 } 200 } 201 202 setCurrentWebcams(tmpnew); 203 204 for (Webcam webcam : notified) { 205 notifyWebcamGone(webcam, listeners); 206 webcam.dispose(); 207 } 208 } 209 210 // if any left in new ones it means that devices has been added 211 if (newones.size() > 0) { 212 213 setCurrentWebcams(tmpnew); 214 215 for (WebcamDevice device : newones) { 216 for (Webcam webcam : webcams) { 217 if (webcam.getDevice().getName().equals(device.getName())) { 218 notifyWebcamFound(webcam, listeners); 219 break; 220 } 221 } 222 } 223 } 224 } 225 226 @Override 227 public void run() { 228 229 // do not run if driver does not support discovery 230 231 if (support == null) { 232 return; 233 } 234 235 running = true; 236 237 // wait initial time interval since devices has been initially 238 // discovered 239 240 Object monitor = new Object(); 241 242 do { 243 244 synchronized (monitor) { 245 try { 246 monitor.wait(support.getScanInterval()); 247 } catch (InterruptedException e) { 248 if (LOG.isTraceEnabled()) { 249 LOG.error("Interrupted", e); 250 } 251 break; 252 } catch (Exception e) { 253 throw new RuntimeException("Problem waiting on monitor", e); 254 } 255 } 256 257 scan(); 258 259 } while (running); 260 } 261 262 private void setCurrentWebcams(List<WebcamDevice> devices) { 263 webcams = toWebcams(devices); 264 if (Webcam.isHandleTermSignal()) { 265 WebcamDeallocator.unstore(); 266 WebcamDeallocator.store(webcams.toArray(new Webcam[webcams.size()])); 267 } 268 } 269 270 private static void notifyWebcamGone(Webcam webcam, WebcamDiscoveryListener[] listeners) { 271 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.REMOVED); 272 for (WebcamDiscoveryListener l : listeners) { 273 try { 274 l.webcamGone(event); 275 } catch (Exception e) { 276 LOG.error(String.format("Webcam gone, exception when calling listener %s", l.getClass()), e); 277 } 278 } 279 } 280 281 private static void notifyWebcamFound(Webcam webcam, WebcamDiscoveryListener[] listeners) { 282 WebcamDiscoveryEvent event = new WebcamDiscoveryEvent(webcam, WebcamDiscoveryEvent.ADDED); 283 for (WebcamDiscoveryListener l : listeners) { 284 try { 285 l.webcamFound(event); 286 } catch (Exception e) { 287 LOG.error(String.format("Webcam found, exception when calling listener %s", l.getClass()), e); 288 } 289 } 290 } 291 292 /** 293 * Stop discovery service. 294 */ 295 public synchronized void stop() { 296 297 running = false; 298 299 if (runner == null) { 300 return; 301 } 302 303 try { 304 runner.join(); 305 } catch (InterruptedException e) { 306 throw new WebcamException("Joint interrupted"); 307 } 308 309 runner = null; 310 } 311 312 /** 313 * Start discovery service. 314 */ 315 public synchronized void start() { 316 317 // discovery service has been already started 318 319 if (runner != null) { 320 return; 321 } 322 323 // capture driver does not support discovery - nothing to do 324 325 if (support == null) { 326 return; 327 } 328 329 // start discovery service runner 330 331 runner = new Thread(this, "webcam-discovery-service"); 332 runner.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance()); 333 runner.setDaemon(true); 334 runner.start(); 335 } 336 337 /** 338 * Is discovery service running? 339 * 340 * @return True or false 341 */ 342 public boolean isRunning() { 343 return running; 344 } 345 346 /** 347 * Cleanup. 348 */ 349 protected synchronized void shutdown() { 350 351 stop(); 352 353 // dispose all webcams 354 355 for (Webcam webcam : webcams) { 356 webcam.dispose(); 357 } 358 359 webcams.clear(); 360 361 // unassign webcams from deallocator 362 363 if (Webcam.isHandleTermSignal()) { 364 WebcamDeallocator.unstore(); 365 } 366 } 367 }