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.methods.HttpHead;
023import org.apache.http.client.protocol.ClientContext;
024import org.apache.http.impl.auth.BasicScheme;
025import org.apache.http.impl.client.BasicAuthCache;
026import org.apache.http.protocol.BasicHttpContext;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import com.github.sarxos.webcam.WebcamDevice;
031import com.github.sarxos.webcam.WebcamException;
032import com.github.sarxos.webcam.ds.ipcam.impl.IpCamHttpClient;
033import com.github.sarxos.webcam.ds.ipcam.impl.IpCamMJPEGStream;
034
035
036/**
037 * IP camera device.
038 * 
039 * @author Bartosz Firyn (SarXos)
040 */
041public class IpCamDevice implements WebcamDevice {
042
043        /**
044         * Logger.
045         */
046        private static final Logger LOG = LoggerFactory.getLogger(IpCamDevice.class);
047
048        private final class PushImageReader implements Runnable {
049
050                private final Object lock = new Object();
051                private IpCamMJPEGStream stream = null;
052                private BufferedImage image = null;
053                private boolean running = true;
054                private WebcamException exception = null;
055                private HttpGet get = null;
056                private URI uri = null;
057
058                public PushImageReader(URI uri) {
059                        this.uri = uri;
060                        stream = new IpCamMJPEGStream(requestStream(uri));
061                }
062
063                private InputStream requestStream(URI uri) {
064
065                        BasicHttpContext context = new BasicHttpContext();
066
067                        IpCamAuth auth = getAuth();
068                        if (auth != null) {
069                                AuthCache cache = new BasicAuthCache();
070                                cache.put(new HttpHost(uri.getHost()), new BasicScheme());
071                                context.setAttribute(ClientContext.AUTH_CACHE, cache);
072                        }
073
074                        try {
075                                get = new HttpGet(uri);
076
077                                HttpResponse respone = client.execute(get, context);
078                                HttpEntity entity = respone.getEntity();
079
080                                Header ct = entity.getContentType();
081                                if (ct == null) {
082                                        throw new WebcamException("Content Type header is missing");
083                                }
084
085                                if (ct.getValue().startsWith("image/")) {
086                                        throw new WebcamException("Cannot read images in PUSH mode, change mode to PULL");
087                                }
088
089                                return entity.getContent();
090
091                        } catch (Exception e) {
092                                throw new WebcamException("Cannot download image", e);
093                        }
094                }
095
096                @Override
097                public void run() {
098
099                        while (running) {
100
101                                if (stream.isClosed()) {
102                                        break;
103                                }
104
105                                try {
106
107                                        LOG.trace("Reading MJPEG frame");
108
109                                        BufferedImage image = stream.readFrame();
110
111                                        if (image != null) {
112                                                this.image = image;
113                                                synchronized (lock) {
114                                                        lock.notifyAll();
115                                                }
116                                        }
117
118                                } catch (IOException e) {
119
120                                        // case when someone manually closed stream, do not log
121                                        // exception, this is normal behavior
122
123                                        if (stream.isClosed()) {
124                                                LOG.debug("Stream already closed, returning");
125                                                return;
126                                        }
127
128                                        if (e instanceof EOFException) {
129
130                                                LOG.debug("EOF detected, recreating MJPEG stream");
131
132                                                get.releaseConnection();
133
134                                                try {
135                                                        stream.close();
136                                                } catch (IOException ioe) {
137                                                        throw new WebcamException(ioe);
138                                                }
139
140                                                stream = new IpCamMJPEGStream(requestStream(uri));
141
142                                                continue;
143                                        }
144
145                                        LOG.error("Cannot read MJPEG frame", e);
146
147                                        if (failOnError) {
148                                                exception = new WebcamException("Cannot read MJPEG frame", e);
149                                                throw exception;
150                                        }
151                                }
152                        }
153
154                        try {
155                                stream.close();
156                        } catch (IOException e) {
157                                LOG.debug("Some nasty exception when closing MJPEG stream", e);
158                        }
159
160                }
161
162                public BufferedImage getImage() {
163                        if (exception != null) {
164                                throw exception;
165                        }
166                        if (image == null) {
167                                try {
168                                        synchronized (lock) {
169                                                lock.wait();
170                                        }
171                                } catch (InterruptedException e) {
172                                        throw new WebcamException("Reader thread interrupted", e);
173                                } catch (Exception e) {
174                                        throw new RuntimeException("Problem waiting on lock", e);
175                                }
176                        }
177                        return image;
178                }
179
180                public void stop() {
181                        running = false;
182                }
183        }
184
185        private String name = null;
186        private URL url = null;
187        private IpCamMode mode = null;
188        private IpCamAuth auth = null;
189        private IpCamHttpClient client = new IpCamHttpClient();
190        private PushImageReader pushReader = null;
191        private boolean failOnError = false;
192
193        private volatile boolean open = false;
194        private volatile boolean disposed = false;
195
196        private Dimension[] sizes = null;
197        private Dimension size = null;
198
199        public IpCamDevice(String name, String url, IpCamMode mode) throws MalformedURLException {
200                this(name, new URL(url), mode, null);
201        }
202
203        public IpCamDevice(String name, URL url, IpCamMode mode) {
204                this(name, url, mode, null);
205        }
206
207        public IpCamDevice(String name, String url, IpCamMode mode, IpCamAuth auth) throws MalformedURLException {
208                this(name, new URL(url), mode, auth);
209        }
210
211        public IpCamDevice(String name, URL url, IpCamMode mode, IpCamAuth auth) {
212
213                if (name == null) {
214                        throw new IllegalArgumentException("Name cannot be null");
215                }
216
217                this.name = name;
218                this.url = url;
219                this.mode = mode;
220                this.auth = auth;
221
222                if (auth != null) {
223                        AuthScope scope = new AuthScope(new HttpHost(url.getHost().toString()));
224                        client.getCredentialsProvider().setCredentials(scope, auth);
225                }
226        }
227
228        protected static final URL toURL(String url) {
229
230                String base = null;
231                if (url.startsWith("http://")) {
232                        base = url;
233                } else {
234                        base = String.format("http://%s", url);
235                }
236
237                try {
238                        return new URL(base);
239                } catch (MalformedURLException e) {
240                        throw new WebcamException(String.format("Incorrect URL '%s'", url), e);
241                }
242        }
243
244        @Override
245        public String getName() {
246                return name;
247        }
248
249        @Override
250        public Dimension[] getResolutions() {
251
252                if (sizes != null) {
253                        return sizes;
254                }
255
256                if (!open) {
257                        open();
258                }
259
260                int attempts = 0;
261                do {
262                        BufferedImage img = getImage();
263                        if (img != null) {
264                                sizes = new Dimension[] { new Dimension(img.getWidth(), img.getHeight()) };
265                                break;
266                        }
267                } while (attempts++ < 5);
268
269                close();
270
271                if (sizes == null) {
272                        throw new WebcamException("Cannot get initial image from IP camera device " + getName());
273                }
274
275                return sizes;
276        }
277
278        protected void setSizes(Dimension[] sizes) {
279                this.sizes = sizes;
280        }
281
282        @Override
283        public Dimension getResolution() {
284                if (size == null) {
285                        size = getResolutions()[0];
286                }
287                return size;
288        }
289
290        @Override
291        public void setResolution(Dimension size) {
292                this.size = size;
293        }
294
295        @Override
296        public synchronized BufferedImage getImage() {
297
298                if (!open) {
299                        throw new WebcamException("IpCam device not open");
300                }
301
302                switch (mode) {
303                        case PULL:
304                                return getImagePullMode();
305                        case PUSH:
306                                return getImagePushMode();
307                }
308
309                throw new WebcamException(String.format("Unsupported mode %s", mode));
310        }
311
312        private BufferedImage getImagePushMode() {
313
314                if (pushReader == null) {
315
316                        URI uri = null;
317                        try {
318                                uri = getURL().toURI();
319                        } catch (URISyntaxException e) {
320                                throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e);
321                        }
322
323                        pushReader = new PushImageReader(uri);
324
325                        // TODO: change to executor
326
327                        Thread thread = new Thread(pushReader, String.format("%s-reader", getName()));
328                        thread.setDaemon(true);
329                        thread.start();
330                }
331
332                return pushReader.getImage();
333        }
334
335        private BufferedImage getImagePullMode() {
336
337                synchronized (this) {
338
339                        HttpGet get = null;
340                        URI uri = null;
341
342                        try {
343                                uri = getURL().toURI();
344                        } catch (URISyntaxException e) {
345                                throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e);
346                        }
347
348                        BasicHttpContext context = new BasicHttpContext();
349
350                        IpCamAuth auth = getAuth();
351                        if (auth != null) {
352                                AuthCache cache = new BasicAuthCache();
353                                cache.put(new HttpHost(uri.getHost()), new BasicScheme());
354                                context.setAttribute(ClientContext.AUTH_CACHE, cache);
355                        }
356
357                        try {
358                                get = new HttpGet(uri);
359
360                                HttpResponse respone = client.execute(get, context);
361                                HttpEntity entity = respone.getEntity();
362
363                                Header ct = entity.getContentType();
364                                if (ct == null) {
365                                        throw new WebcamException("Content Type header is missing");
366                                }
367
368                                if (ct.getValue().startsWith("multipart/")) {
369                                        throw new WebcamException("Cannot read MJPEG stream in PULL mode, change mode to PUSH");
370                                }
371
372                                InputStream is = entity.getContent();
373                                if (is == null) {
374                                        return null;
375                                }
376
377                                return ImageIO.read(is);
378
379                        } catch (IOException e) {
380
381                                // fall thru, it means we closed stream
382                                if (e.getMessage().equals("closed")) {
383                                        return null;
384                                }
385
386                                throw new WebcamException("Cannot download image", e);
387
388                        } catch (Exception e) {
389                                throw new WebcamException("Cannot download image", e);
390                        } finally {
391                                if (get != null) {
392                                        get.releaseConnection();
393                                }
394                        }
395                }
396        }
397
398        /**
399         * This method will send HTTP HEAD request to the camera URL to check
400         * whether it's online or offline. It's online when this request succeed and
401         * it's offline if any exception occurs or response code is 404 Not Found.
402         * 
403         * @return True if camera is online, false otherwise
404         */
405        public boolean isOnline() {
406
407                LOG.debug("Checking online status for {} at {}", getName(), getURL());
408
409                URI uri = null;
410                try {
411                        uri = getURL().toURI();
412                } catch (URISyntaxException e) {
413                        throw new WebcamException(String.format("Incorrect URI syntax '%s'", uri), e);
414                }
415
416                HttpHead head = new HttpHead(uri);
417
418                HttpResponse response = null;
419                try {
420                        response = client.execute(head);
421                } catch (Exception e) {
422                        return false;
423                } finally {
424                        if (head != null) {
425                                head.releaseConnection();
426                        }
427                }
428
429                return response.getStatusLine().getStatusCode() != 404;
430        }
431
432        @Override
433        public void open() {
434                if (disposed) {
435                        LOG.warn("Device cannopt be open because it's already disposed");
436                        return;
437                }
438                open = true;
439        }
440
441        @Override
442        public void close() {
443
444                if (!open) {
445                        return;
446                }
447
448                if (pushReader != null) {
449                        pushReader.stop();
450                        pushReader = null;
451                }
452
453                open = false;
454        }
455
456        public URL getURL() {
457                return url;
458        }
459
460        public IpCamMode getMode() {
461                return mode;
462        }
463
464        public IpCamAuth getAuth() {
465                return auth;
466        }
467
468        public void setAuth(IpCamAuth auth) {
469                if (auth != null) {
470                        URL url = getURL();
471                        AuthScope scope = new AuthScope(url.getHost(), url.getPort());
472                        client.getCredentialsProvider().setCredentials(scope, auth);
473                }
474        }
475
476        public void resetAuth() {
477                client.getCredentialsProvider().clear();
478        }
479
480        public void setFailOnError(boolean failOnError) {
481                this.failOnError = failOnError;
482        }
483
484        @Override
485        public void dispose() {
486                disposed = true;
487        }
488
489        @Override
490        public boolean isOpen() {
491                return open;
492        }
493}