I recently trained in our Adobe Users Group about Adobe Stratus, RTMFP, and p2p applications in flash. For now the technology is still relatively new and in beta, but it is pretty freaking awesome already, and there are some new exciting features coming in Flash 10.1. My goal here is to cover what I trained on, and to walk you through the demo application I wrote for the training called P2PChat. Check out the application.

A Short History of connecting Flash Clients

Up to this point when we’d like flash clients to communicate with each other we’ve always had to use some sort of server. When it comes to video/audio sharing, we’ve had to go through a Flash Media Server (RTMP). When you work through a flash media server you introduce additional latency to the conversation, as well as connections. For Information exchange you have many options available, LCDS, Blazeds, any http server, as well as any server that implements AMF.

With the introduction of Stratus and RTMFP we can now directly communicate and exchange data from one client to the other. But this wasn’t very scalable, much more that 10 p2p connections at any node would overwhelm the user’s connection because each client would have to be directly connected to all the other clients.

With Flash 10.1 and NetGroups we are now able to have Flash clients pass information through other flash clients. This allows such p2p networks as multicast and swarm, allowing larger networks that don’t require as much bandwidth or connections.

Terminology

RTMFP (Real Time Media Flow Protocol) - This protocol is what flash player uses to communicate between flash clients. It is built on UDP, and has some built in features for NAT traversal and rapid connection restore, and IP mobility. For now the connection between clients is managed by Adobe Stratus.

Adobe Stratus - This is an Adobe hosted server that delegates peerid’s to clients, allowing them to connect to each other. Adobe Stratus doesn’t handle getting the assigned peerid’s from one client to another. You will have to implement your own peer exchange mechanism to handle this. It has been stated that Stratus will be in beta forever, so you won’t have to worry about your applications suddently not working, or about paying for the service suddenly. It has also been said that Stratus will eventually be included in a future version of FMS (3.5.2), so you can personally host your own stratus server. You will need to get your own developer key to use in the application, you can do that here.

Peer Exchange – One thing you’ll want to consider before you write a p2p app is some sort of peer exchange mechanism. Stratus doesn’t handle this, so you’ll have to consider what requirements your application will have, and then figure out an implementation that will fulfill those requirements. The simplest form of peer exchange is to have the user enter the peerid manually, and connect that way. The other way would be to have a server manage the peerid’s.

NetConnection - In Flex this is the class you will use to connect to stratus. You will have to obtain a developer key in order to connect.

NetStream - This class is what you will use to connect to other clients, whether outgoing or ingoing.

Demo Application Walkthrough

The demo application is a simple video, audio, and chat sharing application. It relies on Stratus to obtain peerid’s, and a ZendAMF PHP Service to exchange those Id’s with other clients. First of all I’d like to walk you through the code flow that a users initiates when they log on.

  1. After the user logs on, the application initializes and connects to the stratus service and obtains a peerid.
  2. /**
     * Method called when the user attempts to "access" the application, there really is no
     * authentication or anything, but I require that the user provides a email for their name.
     *
     * @param event The MouseEvent occuring from the Login button click
     * @return void
     * @see MouseEvent
     */
    protected function loginHandler(event:Event):void
    {
    	currentUser.name = emailField.text.substring(0,emailField.text.indexOf("@"));
    	currentUser.email = emailField.text;
    	netConnection.connect(StratusAddress + DeveloperKey);
    	if(rememberField.selected)
    		loginSO.data.email = currentUser.email;
    	loginButton.label = "Connecting";
    	loginButton.enabled = false;
    }
    
  3. Once the peerid has been successfully obtained from the Stratus server, The current User and their peerid is sent to the php peer exchange service.
  4. /**
     * This method is called after you have been successfully connected to stratus.
     *
     * @param nearID A String that represents your 'PeerID' and will be given to the
     * peer exchange service.
     * @return void
     */
    protected function connectedToStratus(nearID : String) : void
    {
    	trace("Connected to Stratus... Initializing peer exchange");
    	currentUser.id = -1;
    	currentUser.peerid = nearID;
    	currentUser.ipaddress = "ipAddy";
    	currentUser.stamp = new Date();
    
    	phpRO.createPeer(currentUser);
    }
    
  5. The app then sets up the server NetStream Object. Each client has one NetStream that will feed content to any connecting NetStream from another client. I call it the serverStream, some people call it outStream. Either way, this stream is the key behind sending content out from one client to another.
  6. /**
     * This remote object handler handles the result of a user being successfully added to the
     * peer exchange service. The result contains the user, and it is stored.
     *
     * @param event ResultEvent
     * @return void
     * @see ResultEvent
     */
    protected function createPeerResultHandler(event : ResultEvent) : void
    {
    	trace("Created as Peer... Fetching All Active Peers");
    	currentUser.id = event.result.id;
    	phpRO.getAllActivePeers();
    	this.currentState = 'Use';
    	timer.start();
    
    	serverStream = new NetStream(netConnection, NetStream.DIRECT_CONNECTIONS);
    	serverStream.addEventListener(NetStatusEvent.NET_STATUS,netStatusHandler);
    	serverStream.publish("media");
    
    	var mic : Microphone = Microphone.getMicrophone();
    	if(mic != null)
    	{
    		mic.setUseEchoSuppression(true);
    		serverStream.attachAudio(mic);
    	}
    	var camera : Camera = Camera.getCamera();
    	if(camera != null)
    	{
    
    		camera.setQuality(65000,90);
    		camera.setMotionLevel(45,2000);
    		camera.setMode(160,120,15,true);
    		serverStream.attachCamera(camera);
    	}
    	serverStream.client = new NetStreamClient();
    
    	video = new Video();
    	video.width = 160;
    	video.height = 120;
    	video.attachCamera(camera);
    	myVideoPlayer.addChild(video);
    }
    
  7. A request is made to receive all the other active clients and their peerid’s.
  8. An object is made for each other active client.
    private function addPeer(peer : Peer) : void
    {
    	currentPeers.addItem(peer);
    	var pv : PeerView = new PeerView();
    	pv.peer = peer;
    	pv.netConnection = netConnection;
    	viewPeers.addElement(pv);
    }
    
    1. This object contains a reference to the netConnection, and the other peerid.
    2. It makes a connection to another peer using the NetStream constructor.
    3. It plays the incoming feed to an associated video.
    4. [Bindable]public var peer : Peer = new Peer();
      public var netConnection : NetConnection;
      private var netStream : NetStream;
      
      protected function creationCompleteHandler(event:FlexEvent):void
      {
      	netStream = new NetStream(netConnection,peer.peerid);
      	netStream.client = new NetStreamClient();
      	netStream.play("media");
      	var video : Video = new Video(160,120);
      	video.attachNetStream(netStream);
      	videoDisp.addChild(video);
      }
      

One other important consideration is the inclusion of a NetStream client object. This object contains methods that can be called from other clients. For instance if you were writing a p2p game application, you would want to use the client object as a way to pass game information from one client to another. In Flash 10.1, you will be able to set the reliability of the data to unreliable. (And also the Audio and Video) This is good news for multiplayer game developers, using UDP, and its lossy qualities, and also Flash not worrying about packet order, you will get the information from one client to the other in the fastest way possible.

In my demo app I use this feature for two things, to send messages and to send files. Here is the netstreamclient object I use

package
{
	import flash.net.FileReference;
	import flash.net.NetStream;
	import flash.utils.ByteArray;

	import mx.controls.Alert;
	import mx.events.CloseEvent;

	public class NetStreamClient
	{
		public var acceptConnections : Boolean = true;

		protected var incomingFileName : String;
		protected var incomingFileData : ByteArray;

		public function NetStreamClient()
		{
		}

		public function onPeerConnect(subscriberStream : NetStream) : Boolean
		{
			trace("onPeerConnect");
			return acceptConnections;
		}

		public function recieveMessage(from : String, message : String) : void
		{
			AppModel.getInstance().chatText += from + ": " + message + "\n";
		}

		public function recieveFile(from : String, fileName : String, fileData : ByteArray) : void
		{
			Alert.show("Save recieved file " + fileName + " from " + from + "?", "File Recieved", (Alert.YES | Alert.NO), null, saveFileHandler);
			incomingFileName = fileName;
			incomingFileData = fileData;
		}

		protected function saveFileHandler(event : CloseEvent) : void
		{
			if(event.detail == Alert.NO)
				return;
			var file : FileReference = new FileReference();
			file.save(incomingFileData,incomingFileName);
		}
	}
}

I’ll only concentrate on the sending of messages between clients. You can dig into the provided sample code to learn more about the file sending. The method I call is recieveMessage, which accepts two parameters, the person sending the message (from) and the message.

In my application, when the user clicks send, or presses the enter key, I call the method on all the other clients. (You can specifically send it to only one client if you wish, using the array on the netconnection of activepeers) Here is the call…

/**
 * Called when the user chooses to send a message to the other users. This method will essentially
 * show the users message to the user as sent from him, and then will send the message out to any
 * connected peers. The peers will then display the message in their messagebox as being from this user.
 * This method requires two users to be viewing each other in order to be able to see the messages from each
 * other, because of the nature of the server/client model i've set up. So each user must be the server of the
 * other user, and also, more importantly (since you can only be the client of one server at a time) each
 * user must be the client of the other user.
 *
 * @param event A general Event, this is so that this method can be called from both the button, or the Enter key press.
 * @return void
 */
protected function send_clickHandler(event : Event) : void
{
	AppModel.getInstance().chatText += currentUser.name + ": " + chatInput.text + "\n";
	serverStream.send("recieveMessage", currentUser.name, chatInput.text);
	chatInput.text = "";
}

As you can see all i am doing is using the .send method with the name of the method I wish to execute on the other peer, as well as the parameters to the method. In my case that ends up being the method in the netstreamclient.
Well that about wraps it up, if you have any questions feel free to contact me via 
email
,

or chat.

Source Code available here.

By downloading my source you are simply agreeing to give me credit if you use my code or solutions. And as always, please leave a comment with feedback, your creations, or even just to say thanks.