Displaying Streamed MJPEG In Java
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://
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