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.protocol.ClientContext; 023import org.apache.http.impl.auth.BasicScheme; 024import org.apache.http.impl.client.BasicAuthCache; 025import org.apache.http.protocol.BasicHttpContext; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029import com.github.sarxos.webcam.WebcamDevice; 030import com.github.sarxos.webcam.WebcamException; 031import com.github.sarxos.webcam.ds.ipcam.impl.IpCamHttpClient; 032import com.github.sarxos.webcam.ds.ipcam.impl.IpCamMJPEGStream; 033 034 035/** 036 * IP camera device. 037 * 038 * @author Bartosz Firyn (SarXos) 039 */ 040public 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, String url, IpCamMode mode) throws MalformedURLException { 198 this(name, new URL(url), mode, null); 199 } 200 201 public IpCamDevice(String name, URL url, IpCamMode mode) { 202 this(name, url, mode, null); 203 } 204 205 public IpCamDevice(String name, String url, IpCamMode mode, IpCamAuth auth) throws MalformedURLException { 206 this(name, new URL(url), mode, auth); 207 } 208 209 public IpCamDevice(String name, URL url, IpCamMode mode, IpCamAuth auth) { 210 211 if (name == null) { 212 throw new IllegalArgumentException("Name cannot be null"); 213 } 214 215 this.name = name; 216 this.url = url; 217 this.mode = mode; 218 this.auth = auth; 219 220 if (auth != null) { 221 AuthScope scope = new AuthScope(new HttpHost(url.toString())); 222 client.getCredentialsProvider().setCredentials(scope, auth); 223 } 224 } 225 226 protected static final URL toURL(String url) { 227 228 String base = null; 229 if (url.startsWith("http://")) { 230 base = url; 231 } else { 232 base = String.format("http://%s", url); 233 } 234 235 try { 236 return new URL(base); 237 } catch (MalformedURLException e) { 238 throw new WebcamException(String.format("Incorrect URL '%s'", url), e); 239 } 240 } 241 242 @Override 243 public String getName() { 244 return name; 245 } 246 247 @Override 248 public Dimension[] getResolutions() { 249 250 if (sizes != null) { 251 return sizes; 252 } 253 254 if (!open) { 255 open(); 256 } 257 258 int attempts = 0; 259 do { 260 BufferedImage img = getImage(); 261 if (img != null) { 262 sizes = new Dimension[] { new Dimension(img.getWidth(), img.getHeight()) }; 263 break; 264 } 265 } while (attempts++ < 5); 266 267 close(); 268 269 if (sizes == null) { 270 throw new WebcamException("Cannot get initial image from IP camera device " + getName()); 271 } 272 273 return sizes; 274 } 275 276 protected void setSizes(Dimension[] sizes) { 277 this.sizes = sizes; 278 } 279 280 @Override 281 public Dimension getResolution() { 282 if (size == null) { 283 size = getResolutions()[0]; 284 } 285 return size; 286 } 287 288 @Override 289 public void setResolution(Dimension size) { 290 this.size = size; 291 } 292 293 @Override 294 public synchronized BufferedImage getImage() { 295 296 if (!open) { 297 throw new WebcamException("IpCam device not open"); 298 } 299 300 switch (mode) { 301 case PULL: 302 return getImagePullMode(); 303 case PUSH: 304 return getImagePushMode(); 305 } 306 307 throw new WebcamException(String.format("Unsupported mode %s", mode)); 308 } 309 310 private BufferedImage getImagePushMode() { 311 312 if (pushReader == null) { 313 314 URI uri = null; 315 try { 316 uri = getURL().toURI(); 317 } catch (URISyntaxException e) { 318 throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e); 319 } 320 321 pushReader = new PushImageReader(uri); 322 323 // TODO: change to executor 324 325 Thread thread = new Thread(pushReader, String.format("%s-reader", getName())); 326 thread.setDaemon(true); 327 thread.start(); 328 } 329 330 return pushReader.getImage(); 331 } 332 333 private BufferedImage getImagePullMode() { 334 335 synchronized (this) { 336 337 HttpGet get = null; 338 URI uri = null; 339 340 try { 341 uri = getURL().toURI(); 342 } catch (URISyntaxException e) { 343 throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e); 344 } 345 346 BasicHttpContext context = new BasicHttpContext(); 347 348 IpCamAuth auth = getAuth(); 349 if (auth != null) { 350 AuthCache cache = new BasicAuthCache(); 351 cache.put(new HttpHost(uri.getHost()), new BasicScheme()); 352 context.setAttribute(ClientContext.AUTH_CACHE, cache); 353 } 354 355 try { 356 get = new HttpGet(uri); 357 358 HttpResponse respone = client.execute(get, context); 359 HttpEntity entity = respone.getEntity(); 360 361 Header ct = entity.getContentType(); 362 if (ct == null) { 363 throw new WebcamException("Content Type header is missing"); 364 } 365 366 if (ct.getValue().startsWith("multipart/")) { 367 throw new WebcamException("Cannot read MJPEG stream in PULL mode, change mode to PUSH"); 368 } 369 370 InputStream is = entity.getContent(); 371 if (is == null) { 372 return null; 373 } 374 375 return ImageIO.read(is); 376 377 } catch (IOException e) { 378 379 // fall thru, it means we closed stream 380 if (e.getMessage().equals("closed")) { 381 return null; 382 } 383 384 throw new WebcamException("Cannot download image", e); 385 386 } catch (Exception e) { 387 throw new WebcamException("Cannot download image", e); 388 } finally { 389 if (get != null) { 390 get.releaseConnection(); 391 } 392 } 393 } 394 } 395 396 @Override 397 public void open() { 398 if (disposed) { 399 LOG.warn("Device cannopt be open because it's already disposed"); 400 return; 401 } 402 open = true; 403 } 404 405 @Override 406 public void close() { 407 408 if (!open) { 409 return; 410 } 411 412 if (pushReader != null) { 413 pushReader.stop(); 414 pushReader = null; 415 } 416 417 open = false; 418 } 419 420 public URL getURL() { 421 return url; 422 } 423 424 public IpCamMode getMode() { 425 return mode; 426 } 427 428 public IpCamAuth getAuth() { 429 return auth; 430 } 431 432 public void setAuth(IpCamAuth auth) { 433 if (auth != null) { 434 URL url = getURL(); 435 AuthScope scope = new AuthScope(url.getHost(), url.getPort()); 436 client.getCredentialsProvider().setCredentials(scope, auth); 437 } 438 } 439 440 public void resetAuth() { 441 client.getCredentialsProvider().clear(); 442 } 443 444 public void setFailOnError(boolean failOnError) { 445 this.failOnError = failOnError; 446 } 447 448 @Override 449 public void dispose() { 450 disposed = true; 451 } 452 453 @Override 454 public boolean isOpen() { 455 return open; 456 } 457}