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 }