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.protocol.ClientContext;
023import org.apache.http.impl.auth.BasicScheme;
024import org.apache.http.impl.client.BasicAuthCache;
025import org.apache.http.protocol.BasicHttpContext;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import com.github.sarxos.webcam.WebcamDevice;
030import com.github.sarxos.webcam.WebcamException;
031import com.github.sarxos.webcam.ds.ipcam.impl.IpCamHttpClient;
032import com.github.sarxos.webcam.ds.ipcam.impl.IpCamMJPEGStream;
033
034
035/**
036 * IP camera device.
037 * 
038 * @author Bartosz Firyn (SarXos)
039 */
040public 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}