Author: vanevery
Android App: Picture Tweeter
P2P video streaming with Adobe Stratus
It works! (I guess you already knew that since it is how Chatroulette works.
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 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(); }
The problem with delicious
I recently heard (well, read on a listserv) from a couple of folks I know that were looking for an alternative to Delicious. Â These people have a lot invested in their bookmarks but are finding it difficult to re-find them for various reasons and have therefore decided to move on.
I too have been having that problem as of late. Â Perhaps I am not careful enough to put every tag in that I should for every entry, perhaps I am not consistent enough in my tagging and so forth. Â While I could blame myself, it seems there are a couple of things that could be done to help me out, for instance, if I bookmark something, perhaps allow me to search not only the tags I have entered but also the top tags as many people do a better job of tagging than one. Â That’s the power of the crowd, no?
I know, I know, Delicious gives me the opportunity to use the top tags when bookmarking. Â Unfortunately, the main way that Delicious has been failing me is not recognizing when I am linking to something that already exists because the URL is slightly different therefore not giving me the opportunity to use those tags.
I spend a lot of time reading articles in the times, some of them directly from the site, some of them via RSS and many of them via email (from various news alerts that I have setup). Â Each of these methods, visiting the same story on the times site yields a slightly different URL ending:
?partner=rss&emc=rss
?sudsredirect=true
?emc=eta1
?hpw
?th&emc=th
Here is the main URL for an article which is the base but could contain any of the above at the end:
http://www.nytimes.com/2010/01/04/business/media/04click.html
It seems that I generally come upon NY Times articles in a different way than most other Delicious users as they never seem to be previously bookmarked. Â Strange.. Â
Doing a tag search on Delicious for something obvious in the arlticle: “nytimes” and “seeclickfix” illustrates the problem:
Since mine will be the 12th version, I won’t have the benefit of having any tag suggestions from previous bookmarkers. This makes me sad and it probably means that I’ll never find the article again.
ITP Show in full swing
It’s that time again.. The ITP Winter Show is in full swing.
Great projects everywhere you turn!
(If you come by tomorrow, be sure to bring your mobile phone and use the handy dandy ITP Guestbook app (for iPhone, Android and other Webkit based browsers) developed by Juri Imamura.)
Live iPhone Video
I recently read about an app for the iPhone called Knocking Video. It is apparently the first app that allows live streams from an iPhone (any iPhone model) that has been approved by Apple. The story I read went on to describe the saga of it’s struggle for approval and it seems was given the thumbs up from none other than Steve Jobs.
A great story and I love the concept of the app. Unfortunately I think it is doomed to failure. There are just too many barriers in it that are needlessly going to turn off potential users.
The first problem has to do with the sign-up portion of the app. It asks for first name, last name and email. The problem is that it’s error checking is just too aggressive and bug filled. For instance my last name is two words and that wasn’t allowed. Good luck people who want to find me on the app, you won’t be able to because I had to use a last name that isn’t correct. Perhaps you could try to find me via my email address? Guess again, it didn’t allow a dash in my domain name so again I had to use an alternate.
Second, once you join you have to figure out somehow if any of your friends are already using it. There is no way to test the app (as far as I can tell) without a friend “knocking”. They should at least have an echo or testing user that people could try it with.
Since I have no way to evaluate the app, I am not going to send emails to my friends asking them to join..
Ooh yeah, I went to the help and about screens to figure out how to let the company know my issues but the email address they list doesn’t exist.. Guess this blog post will have to suffice, perhaps they’ll read it.
Simple Flash Video By The Pixel
package { import flash.display.Sprite; import flash.media.Camera; import flash.media.Video; import flash.display.Bitmap; import flash.display.BitmapData; import flash.utils.ByteArray; import flash.events.Event; import flash.utils.Timer; import flash.events.TimerEvent; public class VideoProcessing extends Sprite { // Camera private var camera:Camera; // My Video private var videoOut:Video; // My Bitmapdata private var bmpd:BitmapData; // Bitmap private var bmp:Bitmap; private var timer:Timer; public function VideoProcessing() { trace("Starting"); // Setup the camera camera = Camera.getCamera(); // Video components videoOut = new Video(); // Set positions videoOut.x = 0; videoOut.y = 0; // Attach camera to our video videoOut.attachCamera(camera); addChild(videoOut); // Create the bitmapdata object bmpd = new BitmapData(320,240); // Create the bitmap image bmp = new Bitmap(bmpd); // Add it to the stage bmp.x = 0; bmp.y = 240; addChild(bmp); // Create timer timer = new Timer(10,0); timer.addEventListener(TimerEvent.TIMER, grabFrame); timer.start(); trace("done starting"); } private function grabFrame(e:TimerEvent):void { //trace("timer"); // Save the frame to the bitmapdata object bmpd.draw(videoOut); // Modify the bmpd //http://itp.nyu.edu/~dbo3/cgi-bin/ClassWiki.cgi?ICMVideo#removeStillBg for (var row:int=0; row<bmpd.height; row=row+1) { //for each row for(var col:int=0; col<bmpd.width; col=col+1) { //for each column //get the color of this pixels var pix:uint = bmpd.getPixel(col,row); var red:int = pix >> 16; var green:int = pix >> 8 & 0xff; var blue:int = pix & 0xff if (red > 50 && green > 50 && blue > 50) { bmpd.setPixel(col,row,0); } } } } } }
Flash Media Server Sending Images
One of my students in my live web class is developing an interesting application that sends screenshots to other people. I put together some sample code to help him along and thought this would be of general interest.
Using the Flash Media Server with Remote Shared Objects this can be built. Here is a walk through of the code:
First of all, this uses the JPGEncoder class from the AS3CoreLib so you probably want to grab that and import it.
import com.adobe.images.JPGEncoder;
This example uses a SharedObject, a NetConnection and NetStreams for sending the video as well as the screen shots. Once the NetConnection is established, the SharedObject can be setup:
// Listener for connection private function connectionHandler (e:NetStatusEvent):void { // If we are connected if (e.info.code == "NetConnection.Connect.Success") { // Set up the shared object // We'll call it SimpleSO, pass in the app url and not make it persistent sharedObject = SharedObject.getRemote("SimpleSO",nc.uri,false); // Tell the shared object to call methods in this class if requested sharedObject.client = this; // Add a listener for when shared object is changed sharedObject.addEventListener (SyncEvent.SYNC,syncEventCallBack); // Connect the shared object to our netConnection sharedObject.connect(nc); // All of the video streaming setup doEverything(); // Register mouseclicks, how we'll determine when to send a frame stage.addEventListener(MouseEvent.MOUSE_DOWN, saveFrame); } }
Here is the method that is called when the mouse is clicked. It creates a bitmapdata object, encodes as a JPEG and sends that as a bytearray through the shared object:
private function saveFrame(e:MouseEvent):void { // Save the frame to the bitmapdata object var bmpd:BitmapData = new BitmapData(320,240); bmpd.draw(videoIn1); // First encode as JPEG so it is smaller var jpgEncoder:JPGEncoder = new JPGEncoder(100); var jpgByteArray:ByteArray = jpgEncoder.encode(bmpd); // Send it via the shared object sharedObject.send("newBitmap",jpgByteArray); }
The SharedObject.send method calls the function “newBitmap” on everyone who is connected and passes in the jpgByteArray. The newBitmap function uses the Loader to uncompress the JPG and when it is done calls “gotBitmapData”:
public function newBitmap(jpgByteArray:ByteArray):void { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, gotBitmapData) loader.loadBytes(jpgByteArray); }
gotBitmap data just creates a regular Bitmap and displays it:
private function gotBitmapData(e:Event):void { var decodedBitmapData:BitmapData = Bitmap(e.target.content).bitmapData // Create the bitmap image var bmp:Bitmap = new Bitmap(decodedBitmapData); // Add it to the stage bmp.x = 0; bmp.y = 240; addChild(bmp); }
Here is the full AS3 class (it could be improved but it works):
package { // Import JPEGEncoder Class from: http://code.google.com/p/as3corelib/ import com.adobe.images.JPGEncoder; import flash.display.Sprite; import flash.display.MovieClip; import flash.events.NetStatusEvent; import flash.net.NetConnection; import flash.net.NetStream; import flash.media.Camera; import flash.media.Microphone; import flash.media.Video; import flash.display.Bitmap; import flash.display.BitmapData; import flash.events.MouseEvent; import flash.utils.ByteArray; import flash.net.SharedObject; import flash.events.SyncEvent; import flash.events.Event; import flash.display.Loader; public class VideoCapture extends Sprite { // Shared Object for communication private var sharedObject:SharedObject; // Overall NetConnection for communicating with FMS private var nc:NetConnection; // RTMP URL, same as directory on FMS private var rtmpURL:String = "rtmp://xxxx/webcam"; // NetStreams for each stream private var netStreamOut:NetStream; private var netStreamIn1:NetStream; // Camera private var camera:Camera; // Microphone private var microphone:Microphone; // My Video private var videoOut:Video; // Video Components private var videoIn1:Video; // Stream Names private var outStream:String; private var inStream1:String; public function VideoCapture() { // Construct NetConnection and connect to server nc = new NetConnection(); nc.connect(rtmpURL); // Add a listener for connection nc.addEventListener (NetStatusEvent.NET_STATUS,connectionHandler); } // Listener for connection private function connectionHandler (e:NetStatusEvent):void { // If we are connected if (e.info.code == "NetConnection.Connect.Success") { // Set up the shared object // We'll call it SimpleSO, pass in the app url and not make it persistent sharedObject = SharedObject.getRemote("SimpleSO",nc.uri,false); // Tell the shared object to call methods in this class if requested sharedObject.client = this; // Connect the shared object to our netConnection sharedObject.connect(nc); // All of the video streaming doEverything(); // Register mouseclicks, how we'll determine when to send a frame stage.addEventListener(MouseEvent.MOUSE_DOWN, saveFrame); } } // Gets the results from the server private function doEverything():void { // Name of streams outStream="one"; inStream1="one"; // Setup the camera camera = Camera.getCamera(); // setup the microphone microphone = Microphone.getMicrophone(); // Video components videoOut = new Video(); videoIn1 = new Video(); // Set positions videoOut.x = 0; videoOut.y = 0; videoIn1.x = 320; videoIn1.y = 0; // Add them to the screen addChild(videoOut); addChild(videoIn1); // Publish our stream netStreamOut = new NetStream(nc); netStreamOut.attachAudio(microphone); netStreamOut.attachCamera(camera); netStreamOut.publish(outStream, "live"); // Attach camera to our video videoOut.attachCamera(camera); //Play incoming streamed video netStreamIn1 = new NetStream(nc); videoIn1.attachNetStream(netStreamIn1); netStreamIn1.play(inStream1); } private function saveFrame(e:MouseEvent):void { // Save the frame to the bitmapdata object var bmpd:BitmapData = new BitmapData(320,240); bmpd.draw(videoIn1); // First encode as JPEG so it is smaller var jpgEncoder:JPGEncoder = new JPGEncoder(100); var jpgByteArray:ByteArray = jpgEncoder.encode(bmpd); // Send it via the shared object sharedObject.send("newBitmap",jpgByteArray); } public function newBitmap(jpgByteArray:ByteArray):void { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, gotBitmapData) loader.loadBytes(jpgByteArray); } private function gotBitmapData(e:Event):void { var decodedBitmapData:BitmapData = Bitmap(e.target.content).bitmapData // Create the bitmap image var bmp:Bitmap = new Bitmap(decodedBitmapData); // Add it to the stage bmp.x = 0; bmp.y = 240; addChild(bmp); } } }