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