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, 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 }