Thursday 16 May 2013

Motion JPG / MJPG

MJPG is a format which is used by some webcams.  This format is just a series of JPG images as bytes separated by a boundary and some headers.  For example

    --myboundary
    Content-Type: image/jpg
    Content-Length: 1234

    afhaoiughwer< 1234 bytes >
    --myboundary

    Content-Type: image/jpg
    Content-Length: 2345

    poipoijngiuh< 2345 bytes >

The blank line after the headers is also used in http requests to separate headers from content - this is pretty standard. This is pretty simple to parse and get the jpg files from.  Below is a sample java class which does this.  This allows a number of images to be skipped so that not every image has to be processed if that isn't required.  This currently just bins the headers but could easily be extended to return the header properties as well.


/**
 * Class to parse a MJPG stream and generate jpg files
 */
public class MjpgParser
{
    /**
     * The content length header.
     */
    private static final String CONTENT_LENGTH_HEADER = "Content-Length: ";
    
    /**
     * The size of the content length string.  Used to chop of the head name and calculate the integer.
     */
    private static final int CONTENT_LENGTH_HEADER_SIZE = CONTENT_LENGTH_HEADER.length();

    /**
     * The input stream.
     */
    private BufferedInputStream mjpgStream;
    
    /**
     * The number of images to skip.
     */
    private int imagesToSkip;

    /**
     * Constructs a new MjpgParser with the given parameters.
     *
     * @param urlResource The url mjpg resource
     * @throws IOException an exception from opening the url
     */
    public MjpgParser(final String urlResource) throws IOException
    {
        this(urlResource, 0);
    }

    /**
     * Constructs a new MjpgParser with the given parameters.
     *
     * @param urlResource The url mjpg resource
     * @param imagesToSkip The number of images to skip.
     * @throws IOException an exception from opening the url
     */
    public MjpgParser(final String urlResource, final int imagesToSkip) throws IOException
    {
        final URL url = new URL(urlResource);
        initialise(url.openStream(), imagesToSkip);
    }
    
    /**
     * Initialise.
     * 
     * @param inputStream The Input Stream
     * @param imagesToSkipNo The number of images to skip.
     */
    private void initialise(final InputStream inputStream, final int imagesToSkipNo)
    {
        this.mjpgStream = new BufferedInputStream(inputStream);
        this.imagesToSkip = imagesToSkipNo;
    }
    
    /**
     * Get the next jpg as a JavaFX Image.
     *
     * @return an Image object
     */
    public Image nextAsImage() throws IOException
    {
        return new Image(new ByteArrayInputStream(nextWithSkip()));
    }
    
    /**
     * Get the next jpg as an array of bytes.
     *
     * @return the bytes of the next jpg
     */
    public byte[] nextAsBytes() throws IOException
    {
        return nextWithSkip();
    }
    
    /**
     * Close the streams.
     */
    public void close() throws IOException
    {
        mjpgStream.close();
    }
    
    /**
     * This method calls next but takes account of the imagesToSkip value.
     * 
     * @return the jpg file after skipping the correct number
     * @throws IOException Thrown by reading from the stream
     */
    private byte[] nextWithSkip() throws IOException
    {
        for (int i = 0; i < imagesToSkip - 1; i++)
        {
            next();
        }
        return next();
    }
    
    /**
     * Get the next jpg as bytes.  This reads the headers and then reads the jpg from the 
     * stream using the Content-Length value to know when to stop.
     * 
     * @return the next jpg file
     * @throws IOException 
     */
    private byte[] next() throws IOException
    {
        // Find the boundary line
        String lineStr = readHeaderLine();
        while (!lineStr.startsWith("--"))
        {
            lineStr = readHeaderLine();
        }
        
        // Read the headers.
        int contentLength = 0;
        lineStr = readHeaderLine();
        while (!lineStr.isEmpty())
        {
            // If this is the content length then process it.
            if (lineStr.startsWith(CONTENT_LENGTH_HEADER))
            {
                contentLength = parseContentLength(lineStr);
            }

            lineStr = readHeaderLine();
        }
        
        return readJpgBytes(contentLength);
    }
    
    /**
     * Read a line from the input stream. This is a header line so it will be a readable string 
     * and can be trimmed to remove the end of line characters.
     * 
     * @return the line as bytes
     * @throws IOException 
     */
    private String readHeaderLine() throws IOException
    {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int entry = mjpgStream.read();
        while (entry != '\n')
        {
            baos.write(entry);
            entry = mjpgStream.read();
        }
        return new String(baos.toByteArray()).trim();
    }
    
    /**
     * Parse the content length line.
     *
     * @param contentLengthLine The line to parse
     * @return The content length
     */
    private int parseContentLength(final String contentLengthLine)
    {
        return Integer.parseInt(contentLengthLine.substring(CONTENT_LENGTH_HEADER_SIZE));
    }
    
    /**
     * Read the jpg.
     * 
     * @param contentLength the length of the jpg
     * @return the jpg as an {@link Image}
     * @throws IOException 
     */
    private byte[] readJpgBytes(final int contentLength) throws IOException
    {
        final byte [] jpgBytes = new byte[contentLength];
        for (int i = 0; i < contentLength; i++)
        {
            jpgBytes[i] = (byte) mjpgStream.read();
        }
        return jpgBytes;
    }
}






Maven Sources Plugin

This useful plugin will generate and install a jar of the sources of the project.  Most of the time a really simple configuration works.  However, there is one thing worth noting.  If this is used with the wrong goal the sources will be regenerated.  If this is used in conjunction with the Maven Replacer Plugin then the replacer will not run as the goal for this is usually prepare-package.

So, the configuration which will not frig with the sources is


    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.2.1</version>
        <executions>
            <execution>
                <id>attach-sources</id>
                <phase>verify</phase>
                <goals>
                    <goal>jar-no-fork</goal>
                </goals>
            </execution>
        </executions>
    </plugin>

The jar-no-fork will force this plugin to use the sources which already exist.  If this is only jar then the sources will be regenerated.