001 package com.github.sarxos.webcam.ds.ipcam; 002 003 import java.awt.Dimension; 004 import java.awt.image.BufferedImage; 005 import java.io.EOFException; 006 import java.io.IOException; 007 import java.io.InputStream; 008 import java.net.MalformedURLException; 009 import java.net.URI; 010 import java.net.URISyntaxException; 011 import java.net.URL; 012 013 import javax.imageio.ImageIO; 014 015 import org.apache.http.Header; 016 import org.apache.http.HttpEntity; 017 import org.apache.http.HttpHost; 018 import org.apache.http.HttpResponse; 019 import org.apache.http.auth.AuthScope; 020 import org.apache.http.client.AuthCache; 021 import org.apache.http.client.methods.HttpGet; 022 import org.apache.http.client.protocol.ClientContext; 023 import org.apache.http.impl.auth.BasicScheme; 024 import org.apache.http.impl.client.BasicAuthCache; 025 import org.apache.http.protocol.BasicHttpContext; 026 import org.slf4j.Logger; 027 import org.slf4j.LoggerFactory; 028 029 import com.github.sarxos.webcam.WebcamDevice; 030 import com.github.sarxos.webcam.WebcamException; 031 import com.github.sarxos.webcam.ds.ipcam.impl.IpCamHttpClient; 032 import com.github.sarxos.webcam.ds.ipcam.impl.IpCamMJPEGStream; 033 034 035 /** 036 * IP camera device. 037 * 038 * @author Bartosz Firyn (SarXos) 039 */ 040 public class IpCamDevice implements WebcamDevice { 041 042 /** 043 * Logger. 044 */ 045 private static final Logger LOG = LoggerFactory.getLogger(IpCamDevice.class); 046 047 private final class PushImageReader implements Runnable { 048 049 private final Object lock = new Object(); 050 private IpCamMJPEGStream stream = null; 051 private BufferedImage image = null; 052 private boolean running = true; 053 private WebcamException exception = null; 054 private HttpGet get = null; 055 private URI uri = null; 056 057 public PushImageReader(URI uri) { 058 this.uri = uri; 059 stream = new IpCamMJPEGStream(requestStream(uri)); 060 } 061 062 private InputStream requestStream(URI uri) { 063 064 BasicHttpContext context = new BasicHttpContext(); 065 066 IpCamAuth auth = getAuth(); 067 if (auth != null) { 068 AuthCache cache = new BasicAuthCache(); 069 cache.put(new HttpHost(uri.getHost()), new BasicScheme()); 070 context.setAttribute(ClientContext.AUTH_CACHE, cache); 071 } 072 073 try { 074 get = new HttpGet(uri); 075 076 HttpResponse respone = client.execute(get, context); 077 HttpEntity entity = respone.getEntity(); 078 079 Header ct = entity.getContentType(); 080 if (ct == null) { 081 throw new WebcamException("Content Type header is missing"); 082 } 083 084 if (ct.getValue().startsWith("image/")) { 085 throw new WebcamException("Cannot read images in PUSH mode, change mode to PULL"); 086 } 087 088 return entity.getContent(); 089 090 } catch (Exception e) { 091 throw new WebcamException("Cannot download image", e); 092 } 093 } 094 095 @Override 096 public void run() { 097 098 while (running) { 099 100 if (stream.isClosed()) { 101 break; 102 } 103 104 try { 105 106 LOG.trace("Reading MJPEG frame"); 107 108 BufferedImage image = stream.readFrame(); 109 110 if (image != null) { 111 this.image = image; 112 synchronized (lock) { 113 lock.notifyAll(); 114 } 115 } 116 117 } catch (IOException e) { 118 119 // case when someone manually closed stream, do not log 120 // exception, this is normal behavior 121 if (stream.isClosed()) { 122 LOG.debug("Stream already closed, returning"); 123 return; 124 } 125 126 if (e instanceof EOFException) { 127 128 LOG.debug("EOF detected, recreating MJPEG stream"); 129 130 get.releaseConnection(); 131 132 try { 133 stream.close(); 134 } catch (IOException ioe) { 135 throw new WebcamException(ioe); 136 } 137 138 stream = new IpCamMJPEGStream(requestStream(uri)); 139 140 continue; 141 } 142 143 LOG.error("Cannot read MJPEG frame", e); 144 145 if (failOnError) { 146 exception = new WebcamException("Cannot read MJPEG frame", e); 147 throw exception; 148 } 149 } 150 } 151 152 try { 153 stream.close(); 154 } catch (IOException e) { 155 LOG.debug("Some nasty exception when closing MJPEG stream", e); 156 } 157 158 } 159 160 public BufferedImage getImage() { 161 if (exception != null) { 162 throw exception; 163 } 164 if (image == null) { 165 try { 166 synchronized (lock) { 167 lock.wait(); 168 } 169 } catch (InterruptedException e) { 170 throw new WebcamException("Reader thread interrupted", e); 171 } catch (Exception e) { 172 throw new RuntimeException("Problem waiting on lock", e); 173 } 174 } 175 return image; 176 } 177 178 public void stop() { 179 running = false; 180 } 181 } 182 183 private String name = null; 184 private URL url = null; 185 private IpCamMode mode = null; 186 private IpCamAuth auth = null; 187 private IpCamHttpClient client = new IpCamHttpClient(); 188 private PushImageReader pushReader = null; 189 private boolean failOnError = false; 190 191 private volatile boolean open = false; 192 private volatile boolean disposed = false; 193 194 private Dimension[] sizes = null; 195 private Dimension size = null; 196 197 public IpCamDevice(String name, URL url, IpCamMode mode) { 198 this(name, url, mode, null); 199 } 200 201 public IpCamDevice(String name, URL url, IpCamMode mode, IpCamAuth auth) { 202 203 if (name == null) { 204 throw new IllegalArgumentException("Name cannot be null"); 205 } 206 207 this.name = name; 208 this.url = url; 209 this.mode = mode; 210 this.auth = auth; 211 212 if (auth != null) { 213 AuthScope scope = new AuthScope(new HttpHost(url.toString())); 214 client.getCredentialsProvider().setCredentials(scope, auth); 215 } 216 } 217 218 protected static final URL toURL(String url) { 219 220 String base = null; 221 if (url.startsWith("http://")) { 222 base = url; 223 } else { 224 base = String.format("http://%s", url); 225 } 226 227 try { 228 return new URL(base); 229 } catch (MalformedURLException e) { 230 throw new WebcamException(String.format("Incorrect URL '%s'", url), e); 231 } 232 } 233 234 @Override 235 public String getName() { 236 return name; 237 } 238 239 @Override 240 public Dimension[] getResolutions() { 241 242 if (sizes != null) { 243 return sizes; 244 } 245 246 if (!open) { 247 open(); 248 } 249 250 int attempts = 0; 251 do { 252 BufferedImage img = getImage(); 253 if (img != null) { 254 sizes = new Dimension[] { new Dimension(img.getWidth(), img.getHeight()) }; 255 break; 256 } 257 } while (attempts++ < 5); 258 259 close(); 260 261 if (sizes == null) { 262 throw new WebcamException("Cannot get initial image from IP camera device " + getName()); 263 } 264 265 return sizes; 266 } 267 268 protected void setSizes(Dimension[] sizes) { 269 this.sizes = sizes; 270 } 271 272 @Override 273 public Dimension getResolution() { 274 if (size == null) { 275 size = getResolutions()[0]; 276 } 277 return size; 278 } 279 280 @Override 281 public void setResolution(Dimension size) { 282 this.size = size; 283 } 284 285 @Override 286 public BufferedImage getImage() { 287 288 if (!open) { 289 throw new WebcamException("IpCam device not open"); 290 } 291 292 switch (mode) { 293 case PULL: 294 return getImagePullMode(); 295 case PUSH: 296 return getImagePushMode(); 297 } 298 299 throw new WebcamException(String.format("Unsupported mode %s", mode)); 300 } 301 302 private BufferedImage getImagePushMode() { 303 304 if (pushReader == null) { 305 306 synchronized (this) { 307 308 URI uri = null; 309 try { 310 uri = getURL().toURI(); 311 } catch (URISyntaxException e) { 312 throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e); 313 } 314 315 pushReader = new PushImageReader(uri); 316 317 // TODO: change to executor 318 319 Thread thread = new Thread(pushReader, String.format("%s-reader", getName())); 320 thread.setDaemon(true); 321 thread.start(); 322 } 323 } 324 325 return pushReader.getImage(); 326 } 327 328 private BufferedImage getImagePullMode() { 329 synchronized (this) { 330 331 HttpGet get = null; 332 URI uri = null; 333 334 try { 335 uri = getURL().toURI(); 336 } catch (URISyntaxException e) { 337 throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e); 338 } 339 340 BasicHttpContext context = new BasicHttpContext(); 341 342 IpCamAuth auth = getAuth(); 343 if (auth != null) { 344 AuthCache cache = new BasicAuthCache(); 345 cache.put(new HttpHost(uri.getHost()), new BasicScheme()); 346 context.setAttribute(ClientContext.AUTH_CACHE, cache); 347 } 348 349 try { 350 get = new HttpGet(uri); 351 352 HttpResponse respone = client.execute(get, context); 353 HttpEntity entity = respone.getEntity(); 354 355 Header ct = entity.getContentType(); 356 if (ct == null) { 357 throw new WebcamException("Content Type header is missing"); 358 } 359 360 if (ct.getValue().startsWith("multipart/")) { 361 throw new WebcamException("Cannot read MJPEG stream in PULL mode, change mode to PUSH"); 362 } 363 364 InputStream is = entity.getContent(); 365 if (is == null) { 366 return null; 367 } 368 369 return ImageIO.read(is); 370 371 } catch (IOException e) { 372 373 // fall thru, it means we closed stream 374 if (e.getMessage().equals("closed")) { 375 return null; 376 } 377 378 throw new WebcamException("Cannot download image", e); 379 380 } catch (Exception e) { 381 throw new WebcamException("Cannot download image", e); 382 } finally { 383 if (get != null) { 384 get.releaseConnection(); 385 } 386 } 387 } 388 } 389 390 @Override 391 public void open() { 392 if (disposed) { 393 LOG.warn("Device cannopt be open because it's already disposed"); 394 return; 395 } 396 open = true; 397 } 398 399 @Override 400 public void close() { 401 402 if (!open) { 403 return; 404 } 405 406 if (pushReader != null) { 407 pushReader.stop(); 408 pushReader = null; 409 } 410 411 open = false; 412 } 413 414 public URL getURL() { 415 return url; 416 } 417 418 public IpCamMode getMode() { 419 return mode; 420 } 421 422 public IpCamAuth getAuth() { 423 return auth; 424 } 425 426 public void setAuth(IpCamAuth auth) { 427 if (auth != null) { 428 URL url = getURL(); 429 AuthScope scope = new AuthScope(url.getHost(), url.getPort()); 430 client.getCredentialsProvider().setCredentials(scope, auth); 431 } 432 } 433 434 public void resetAuth() { 435 client.getCredentialsProvider().clear(); 436 } 437 438 public void setFailOnError(boolean failOnError) { 439 this.failOnError = failOnError; 440 } 441 442 @Override 443 public void dispose() { 444 disposed = true; 445 } 446 447 @Override 448 public boolean isOpen() { 449 return open; 450 } 451 }