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