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    }