Live Experimental Interactive Television

The students from my class (Live Experimental Interactive Television) at ITP are on again tonight. This week it is Unitv:
Unitv Logo

UnItv is an exciting new venture in live television. Performed as a local news broadcast, the ‘news team’ is given real-time user submitted content (images, questions, lyrics, and youtube videos) on which they improv the news, weather, a celebrity interview and a performance with a special musical guest. The home television audience is provided with a two-screen setup where they use our website to become part of the show. In the spirit of true improvisation, our cast is dependent upon user suggestions to make up the show as they string one joke along to the next. UnItv was created through the Interactive Telecommunications Program (ITP) at NYU in collaboration with Manhattan Neighborhood Network and airs on the Manhattan Neighborhood Network Tuesday April 20th at 8:30pm. For more information, check out http://unitv.me.

If you are in Manhattan, you can watch on MNN at 8:30 PM:
Time Warner Cable channel 34
RCN channel 82
Verizon FiOS channel 33
(all in Manhattan only)

If you aren’t in Manhattan, there *should be* a live stream of the program on http://unitv.me

Hope you catch it!

Motion JPEG in Flash and Java

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();
			
			
			
		}