I recently had the opportunity to develop a solution in both Java and Flash for pulling Motion JPEG streams from IP cameras and thought it might be nice to document a bit.
Motion JPEG is generally served via HTTP from IP cameras as a single file. Meaning, the connection stays open and the camera just keeps sending individual JPEG images down the pipe. The images should start with a MIME boundary message such as:
--myboundary
Content-Type: image/jpeg
Content-Length: 22517
or
--randomstring
Content-Type: image/jpeg
Content-Length: 22598
The key in the development is to find the boundary and save the bytes between each and treat that as a JPEG image. Neither of these snippets are great or even complete but they should give you a bit of a start.
Java:
package com.mobvcasting.mjpegparser; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Authenticator; import java.net.PasswordAuthentication; import java.net.URL; public class MJPEGParser { /** * @param args */ public static void main(String[] args) { MJPEGParser mp = new MJPEGParser("http://192.168.1.10/mjpg/video.mjpg", "username", "password"); } public MJPEGParser(String mjpeg_url) { this(mjpeg_url,null,null); } public MJPEGParser(String mjpeg_url, String username, String password) { int imageCount = 0; try { if (username != null && password != null) { Authenticator.setDefault(new HTTPAuthenticator(username, password)); } URL url = new URL(mjpeg_url); BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream())); String inputLine; int lineCount = 0; boolean lineCountStart = false; boolean saveImage = false; while ((inputLine = in.readLine()) != null) { // Should be checking just for "--" probably if (inputLine.lastIndexOf("--myboundary") > -1) { // Got an image boundary, stop last image // Start counting lines to get past: // Content-Type: image/jpeg // Content-Length: 22517 saveImage = false; lineCountStart = true; System.out.println("Got a new boundary"); System.out.println(inputLine); } else if (lineCountStart) { lineCount++; if (lineCount >= 2) { lineCount = 0; lineCountStart = false; imageCount++; saveImage = true; System.out.println("Starting a new image"); } } else if (saveImage) { System.out.println("Saving an image line"); } else { System.out.println("What's this:"); System.out.println(inputLine); } } in.close(); } catch (IOException e) { e.printStackTrace(); } } static class HTTPAuthenticator extends Authenticator { private String username, password; public HTTPAuthenticator(String user, String pass) { username = user; password = pass; } protected PasswordAuthentication getPasswordAuthentication() { System.out.println("Requesting Host : " + getRequestingHost()); System.out.println("Requesting Port : " + getRequestingPort()); System.out.println("Requesting Prompt : " + getRequestingPrompt()); System.out.println("Requesting Protocol: " + getRequestingProtocol()); System.out.println("Requesting Scheme : " + getRequestingScheme()); System.out.println("Requesting Site : " + getRequestingSite()); return new PasswordAuthentication(username, password.toCharArray()); } } }
ActionScript 3
import flash.display.Sprite; import flash.errors.*; import flash.events.*; import flash.net.URLRequest; import flash.net.URLStream; import flash.utils.ByteArray; var stream:URLStream; var mjpegBuffer:ByteArray = new ByteArray(); // The actual image var imageBytes:ByteArray; // = new ByteArray(); // The chars at the end of the image var endPos:String = "\n--myboundary"; // Started to find, finished finding var done:Boolean = false; var started:Boolean = false; // Don't know why I have to save these to a ByteArray to do the comparisons but it seems I do var startBytes:ByteArray = new ByteArray(); var startByte:int = 0xFF; var secondByte:int = 0xD8; startBytes.writeByte(0xFF); startBytes.writeByte(0xD8); trace(startBytes.length); var startNum:int = startBytes[0]; trace(startNum); var nextNum:int = startBytes[1]; trace(nextNum); // Open the stream stream = new URLStream(); var request:URLRequest = new URLRequest("http://192.168.1.10/mjpg/video.mjpg?resolution=160x90&fps=1"); configureListeners(stream); try { stream.load(request); } catch (error:Error) { trace("Unable to load requested URL."); } function configureListeners(dispatcher:EventDispatcher):void { dispatcher.addEventListener(ProgressEvent.PROGRESS, progressHandler); } function progressHandler(event:Event):void { trace("Running"); stream.readBytes(mjpegBuffer,mjpegBuffer.length,stream.bytesAvailable); for (var i:int = 0; i < mjpegBuffer.length; i++) { var currentByte:int = mjpegBuffer[i]; var nextByte:int = mjpegBuffer[i+1]; var thirdByte:int = mjpegBuffer[i+2]; var fourthByte:int = mjpegBuffer[i+3]; //var randNum:Number = Math.random(); //if (randNum > .5 && randNum < .6) { trace(currentByte); } if (!started) { if (currentByte == startNum && nextByte == nextNum) { trace("Started"); started = true; imageBytes = new ByteArray(); imageBytes.writeByte(currentByte); //imageBytes.writeByte(0xD8); // Gets written in the else } } else { if (currentByte == endPos.charCodeAt(0) && nextByte == endPos.charCodeAt(1) && thirdByte == endPos.charCodeAt(2) && fourthByte == endPos.charCodeAt(3)) { trace("done"); trace(imageBytes); done = true; started = false; var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onByteArrayLoaded) loader.loadBytes(imageBytes); //stream.close(); } else { imageBytes.writeByte(currentByte); } } } } function onByteArrayLoaded(e:Event):void { var loader:Loader = Loader(e.target.loader); loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, onByteArrayLoaded); var bitmapData:BitmapData = Bitmap(e.target.content).bitmapData; //sprLoaded.graphics.clear(); graphics.beginBitmapFill(bitmapData); graphics.drawRect(0,0,bitmapData.width, bitmapData.height); graphics.endFill(); }