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