Displaying Streamed MJPEG In Java

05/13/2012

So my day job is a Java and Informatica application developer for an insurance company. I mostly write middle ware pieces interfacing our reporting systems. Nothing exciting, but this is my main background hence the code that is written.

So I am starting to work up GUI code for Mech warfare, since most of the basic turret controls are written. What I discovered was I have a IP security camera, DLink DCS-930L, but no good way to display the video stream. So I did the usual programmer's way of solving this and started searching the web and came up with pretty much bupkisk. So plan B is write it myself. So I'm going to document so items, since it is for my own purposes anyway.

The video stream can be directly access at http:///video/mjpg.cgi once you have the camera configured. If you plug it into your browser the stream will start just fine. That just didn't feel right for the GUI, so on to reading the stream in Java.

Actual parsing of the stream isn't too bad: after the first HTTP header, you then look for a delimiter. That delimiter indicates the next HTTP header and content. The only problem I had was I wasn't reading the header correctly initially and was stripping the first magic character of the JPEG 'ff'. Once I shoved that value back into my discovered bytes for the image, it was easy to shove the resultant BufferedImage to a JPanel to draw.

Once I get more of the GUI complete, I'll get screenshots up. Anyway the code:


package net.thistleshrub.mechwarfare.mjpeg;

import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;

import javax.imageio.ImageIO;

/**
 * Given an extended JPanel and URL read and create BufferedImages to be displayed from a MJPEG stream
 * @author shrub34 Copyright 2012
 * Free for reuse, just please give me a credit if it is for a redistributed package
 */
public class MjpegRunner implements Runnable
{
	private static final String CONTENT_LENGTH = "Content-length: ";
	private static final String CONTENT_TYPE = "Content-type: image/jpeg";
	private MJpegViewer viewer;
	private InputStream urlStream;
	private StringWriter stringWriter;
	private boolean processing = true;
	
	public MjpegRunner(MJpegViewer viewer, URL url) throws IOException
	{
		this.viewer = viewer;
		URLConnection urlConn = url.openConnection();
		// change the timeout to taste, I like 1 second
		urlConn.setReadTimeout(1000);
		urlConn.connect();
		urlStream = urlConn.getInputStream();
		stringWriter = new StringWriter(128);
	}

	/**
	 * Stop the loop, and allow it to clean up
	 */
	public synchronized void stop()
	{
		processing = false;
	}
	
	/**
	 * Keeps running while process() returns true
	 * 
	 * Each loop asks for the next JPEG image and then sends it to our JPanel to draw
	 * @see java.lang.Runnable#run()
	 */
	@Override
	public void run()
	{
		while(processing)
		{
			try
			{
				byte[] imageBytes = retrieveNextImage();
				ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
				
				BufferedImage image = ImageIO.read(bais);
				viewer.setBufferedImage(image);
				
				viewer.repaint();
			}catch(SocketTimeoutException ste){
				System.err.println("failed stream read: " + ste);
				viewer.setFailedString("Lost Camera connection: " + ste);
				viewer.repaint();
				stop();
			}catch(IOException e){
				System.err.println("failed stream read: " + e);
				stop();
			}
		}
		
		// close streams
		try
		{
			urlStream.close();
		}catch(IOException ioe){
			System.err.println("Failed to close the stream: " + ioe);
		}
	}
	
	/**
	 * Using the urlStream get the next JPEG image as a byte[]
	 * @return byte[] of the JPEG
	 * @throws IOException
	 */
	private byte[] retrieveNextImage() throws IOException
	{
		boolean haveHeader = false; 
		int currByte = -1;
		
		String header = null;
		// build headers
		// the DCS-930L stops it's headers
		while((currByte = urlStream.read()) > -1 && !haveHeader)
		{
			stringWriter.write(currByte);
			
			String tempString = stringWriter.toString(); 
			int indexOf = tempString.indexOf(CONTENT_TYPE);
			if(indexOf > 0)
			{
				haveHeader = true;
				header = tempString;
			}
		}		
		
		// 255 indicates the start of the jpeg image
		while((urlStream.read()) != 255)
		{
			// just skip extras
		}
		
		// rest is the buffer
		int contentLength = contentLength(header);
		byte[] imageBytes = new byte[contentLength + 1];
		// since we ate the original 255 , shove it back in
		imageBytes[0] = (byte)255;
		int offset = 1;
		int numRead = 0;
		while (offset < imageBytes.length
			&& (numRead=urlStream.read(imageBytes, offset, imageBytes.length-offset)) >= 0) 
		{
			offset += numRead;
		}       
		
		stringWriter = new StringWriter(128);
		
		return imageBytes;
	}

	// dirty but it works content-length parsing
	private static int contentLength(String header)
	{
		int indexOfContentLength = header.indexOf(CONTENT_LENGTH);
		int valueStartPos = indexOfContentLength + CONTENT_LENGTH.length();
		int indexOfEOL = header.indexOf('\n', indexOfContentLength);
		
		String lengthValStr = header.substring(valueStartPos, indexOfEOL).trim();
		
		int retValue = Integer.parseInt(lengthValStr);
		
		return retValue;
	}
}

So there you have it, the hooks to the MJpegViewer are really simple setters and MJpegViewer is just a JPanel. Till I have more adventures worth posting.

This article was originally posted on Thistle & Shrub Studios

Top