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