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    }