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 }