it.lilik.capturemjpeg.CaptureMJPEG.java Source code

Java tutorial

Introduction

Here is the source code for it.lilik.capturemjpeg.CaptureMJPEG.java

Source

/*
   This file is part of CaptureMJPEG.
    
CaptureMJPEG is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
CaptureMJPEG is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser Public License for more details.
    
You should have received a copy of the GNU Lesser Public License
along with CaptureMJPEG.  If not, see <http://www.gnu.org/licenses/>.
    
Copyright (c) 2008-09 - Alessio Caiazza, Cosimo Cecchi
 */
package it.lilik.capturemjpeg;

import it.zm.interfaces.VideoPanel;
import it.zm.util.ImageUtils;

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.io.IOUtils;

// CV some cleaning is needed, check sync, reimplement with java Scanner class:
// http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Scanner.html

/**
 * This class produces JPEG images from Motion JPEG stream.<br/>
 * It searches for a callback function called <code>void captureMJPEGEvent(PImage img)</code>
 * into the parent {@link processing.core.PApplet}<br/>
 * <p>
 * 
 * <b>Example</b><br/>
 * <pre>
import it.lilik.capturemjpeg.*;
    
private CaptureMJPEG capture;
private PImage next_img = null;
    
void setup() {
  size(400, 300);
  background(0);
  capture = new CaptureMJPEG(this,
     "http://mynetworkcamera/image?speed=20",
     "admin",
     "password");
  capture.startCapture();
  frameRate(20);
}
       
void draw() {
  if (next_img != null) {
image(next_img, 0, 0);
  }
}
       
void captureMJPEGEvent(PImage img) {
  next_img = img;
}
</pre>
 *
 *
 * @author Alessio Caiazza 
 * @author Cosimo Cecchi
 *
 */
public class CaptureMJPEG extends Thread {

    /** timeout for HTTP request    */
    private static final int HTTP_TIMEOUT = 5000;
    /** client */
    private HttpClient client;
    /** method */
    private HttpMethod method;
    /** used for stopping this thread */
    private boolean shouldStop;
    /** <code>true</code> if there are pending changes in <code>method</code> */
    private boolean isChangePending;
    /** circular buffer for images */
    private CircularBuffer buffer;

    /** the parent <code>PApplet</code> **/
    private VideoPanel parent;
    /** callback method **/
    private Method captureEventMethod;
    /** Last processed image **/
    private Image lastImage;

    // If true change the image size according to the frame size, if unset do the contrary, resize the frame according to the image size
    private boolean changeImageSize;

    /**
     * Checks if the running thread is in stopping state.
     * If <code>true</code> the thread is stopped or it will
     * stop after the next image was captured. 
     * 
     * @return the internal status
     */
    public boolean isStopping() {
        return shouldStop;
    }

    /**
     * Starts the capture cycle.
     */
    public void startCapture() {
        this.start();
    }

    /**
     * Stops this thread when the current image if finished
     * 
     */
    public void stopCapture() {
        this.shouldStop = true;
    }

    /**
     * Changes the URI.<br>
     * A new connection will be performed after a complete
     * image reading.
     * @param url the url of the MJPEG stream
     * 
     */
    public void setURL(String url) {
        synchronized (this.method) {
            this.method.releaseConnection();
            try {
                this.method.setURI(new URI(url, false));
            } catch (URIException e) {
                e.printStackTrace();
            }
            this.isChangePending = true;
            this.method.setFollowRedirects(true);
        }
    }

    /**
     * Sets username and password for HTTP Auth.
     * 
     * @param username the username
     * @param password the password
     */
    public void setCredential(String username, String password) {
        UsernamePasswordCredentials creds = new UsernamePasswordCredentials(username, password);
        client.getState().setCredentials(AuthScope.ANY, creds);
    }

    // Set resizable, if set the image is resized before displaying, if unset the image is shown as it is
    public void setResizable(boolean res) {
        changeImageSize = res;
    }

    /**
     * Creates a <code>CaptureMJPEG</code> without HTTP Auth credential
     */
    public CaptureMJPEG(VideoPanel parent, String url) {
        this(parent, url, null, null);
    }

    /**
     * Creates a <code>CaptureMJPEG</code> with HTTP Auth credential
     * 
     * @param parent the <code>ImageWindow</code> which uses this object
     * @param url the MJPEG stream URI
     * @param username HTTP AUTH username
     * @param password HTTP AUTH password
     */
    public CaptureMJPEG(VideoPanel parent, String url, String username, String password) {
        this.lastImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
        //this.lastImage.init(parent.width, parent.height, PImage.RGB);
        this.method = new GetMethod(url);
        this.shouldStop = false;
        this.changeImageSize = false;
        buffer = new CircularBuffer();
        // create a singular HttpClient object
        this.client = new HttpClient();

        // establish a connection within 5 seconds
        this.client.getHttpConnectionManager().getParams().setConnectionTimeout(HTTP_TIMEOUT);

        if (username != null && password != null) {
            this.setCredential(username, password);
        }
        setURL(url);
        //set the ThreadName useful for debug
        setName("CaptureMJPEG");
        setPriority(Thread.MAX_PRIORITY);

        this.parent = parent;

        //System.out.println("Capture initialized");

        //parent.registerDispose(this);       
        //register callback
        try {
            captureEventMethod = parent.getClass().getMethod("captureMJPEGEvent", new Class[] { Image.class });
        } catch (Exception e) {
            System.out.println("Cannot set callback " + e.getMessage());
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Thread#run()
     */
    public void run() {
        BufferedInputStream is = null;
        InputStream responseBody = null;
        String boundary = "";

        while (!this.shouldStop) {

            this.isChangePending = false;

            if (!parent.isFocused() && parent.bwSaverActive()) { // Not focused -> wait one second
                try {
                    java.lang.Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                continue;
            }

            // Open HTTP client
            try {
                this.client.executeMethod(this.method);
                responseBody = this.method.getResponseBodyAsStream();
                if (this.method.getStatusCode() == 404) {
                    TextImage error = new TextImage("Unable to find '" + this.method.getPath() + "' on host '"
                            + this.method.getURI().getHost() + "'");
                    setErrorImage(error);

                    // Try again 
                    System.out.println("Unable to find endpoint");
                    continue;
                }
                is = new BufferedInputStream(responseBody);
            } catch (Exception e) {
                TextImage error;
                try {
                    error = new TextImage("Unable to connect to '" + this.method.getURI().getHost() + "' ("
                            + e.getLocalizedMessage() + ")");
                    setErrorImage(error);
                    System.out.println("Unable to connect");
                } catch (URIException e1) {
                    e1.printStackTrace();
                }

                // Try again
                continue;
            }

            try {

                // automagically guess the boundary
                Header contentType = this.method.getResponseHeader("Content-Type");
                String contentTypeS = contentType.toString();
                int startIndex = contentTypeS.indexOf("boundary=");
                int endIndex = contentTypeS.indexOf(';', startIndex);
                if (endIndex == -1) {//boundary is the last option
                    /* some servers, like mjpeg-streamer puts
                     * a '\r' character at the end of each line.
                     */
                    if ((endIndex = contentTypeS.indexOf('\r', startIndex)) == -1)
                        endIndex = contentTypeS.length();
                }
                boundary = contentTypeS.substring(startIndex + 9, endIndex);
                //some cameras put -- on boundary, some not
                if (boundary.charAt(0) != '-' && boundary.charAt(1) != '-')
                    boundary = "--" + boundary;

            } catch (Exception e) {
                // Who cares? Running the next cycle will solve the issue, whatever...
            }

            while (!this.shouldStop) {

                synchronized (this.method) {
                    this.buffer.clear();

                } //end synchronized         

                byte[] img;
                MJPEGInputStream mis = new MJPEGInputStream(is, boundary);
                //System.out.println("Created");
                try {
                    synchronized (method) {
                        img = mis.readImage();
                        //System.out.println("Read");
                    }
                    //synchronized (lastImage) {
                    if (captureEventMethod != null) {
                        try {
                            //System.out.println("Call invoker");
                            Image tmp = getImage(new ByteArrayInputStream(img));
                            captureEventMethod.invoke(parent, new Object[] { this.assign(tmp) });
                        } catch (Exception e) {
                            System.err.println(
                                    "Disabling captureEvent() for " + parent.getName() + " because of an error.");
                            e.printStackTrace();
                            captureEventMethod = null;
                        }
                    } else {
                        this.buffer.push(new ByteArrayInputStream(img));
                    }
                    //}
                } catch (IOException e) {
                    TextImage error = new TextImage(e.getLocalizedMessage());

                    setErrorImage(error);

                    break;

                }

                if ((!parent.isFocused() && parent.bwSaverActive()) || this.isChangePending)
                    break;
            }

            // Reading from stream failed -> let's close the stream, in the next occurrency it'll be opened again...
            this.method.releaseConnection();
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

    /*
     * Bypass Default PImage costructur forcing
     * a load like in Processing < 1.0
     *
     */
    private Image getImage(ByteArrayInputStream bais) throws IOException {
        Image tmp = null;
        BufferedImage bi = (BufferedImage) ImageIO.read(bais);

        tmp = bi;
        return tmp;

    }

    private void setErrorImage(TextImage error) {

        try {
            captureEventMethod.invoke(parent, new Object[] { this.assign(error) });
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Callback method. It's invoked by processing on stop.
     */
    public void dispose() {
        this.stopCapture();
    }

    /**
     * Provides the oldest image not yet provided.
     * If there's no such image, provides the last provided.
     * 
     * @return a <code>PImage</code>
     */
    public Image getImage() {
        synchronized (lastImage) {
            if (!isImageAvailable())
                return lastImage;
            try {
                Image tmp = getImage(buffer.pop());
                return assign(tmp);
            } catch (IOException e1) {
                e1.printStackTrace();
                return lastImage;
            }
        }
    }

    /**
     * Return <code>true</code> if there is at least one image available into
     * the internal buffer.
     * 
     * @return the availability status
     */
    public boolean isImageAvailable() {
        return !this.buffer.isEmpty();
    }

    /**
     * Assigns <code>tmp</tmp> to <code>lastImage</code> without creating a new <code>PImage</code> 
     * @param tmp the new <code>PImage</code>
     * @return a reference to <code>lastImage</code>
     */
    private Image assign(Image tmp) {

        lastImage = tmp;

        BufferedImage img;
        if (changeImageSize) {
            // The image is shown into a mosaic, resize it
            img = (BufferedImage) ImageUtils.scaleImage(parent.getWidth(), parent.getHeight(), (BufferedImage) tmp);
        } else {
            // The image is not part of a mosaic, let's show it as it is
            img = (BufferedImage) tmp;

            // Let's resize frame and image window according to the image
            parent.setSize(img.getWidth(), img.getHeight());
            parent.setFrameSize(img.getWidth(), img.getHeight());
        }

        return img;
    }

}