Articles

multiuser as3 framework for flash media server and red5

Multiuser programming is a hard thing to do. Not only does it involve a different way of approaching a problem, it also means you have to learn to program the server and communicate with it and all of it’s connected clients.
The flash media server and it’s opensource variant red5 give us two perfect platforms to do complex multiuser interaction. While they differ in the way you write server code, they are the same when it comes to writing client side code for flash/flex.

Because there are no client side abstractions to multiuser programming out there, we wrote our own and share it with you here. In our nl.dpdk.services.fms package you can find a number of classes that abstract the gory details of interacting with either the flash media server or red5, making it easier to code, easier to maintain, easier to understand cleaner and less error prone. It still allows you to use advanced features and hack away at a low level, but makes it a breeze to setup a complex environment in a short time. The classes we present in our package focus only on data exchange and not on doing streams.

Since multiuser programming is extremely cool, gives you loads of possibilities to do fun stuff and does not need to be hard, we’ll try to get you up and running with fms in this post.

In an earlier post we already wrote something about coding for the flash media server (fms).
Now, we have also provided a mini flash media server framework, that gives you lots of functionality out of the box. It is written so you can easily extend it, works with some good design patterns, to let you add functionality to the framework without it getting in your way. It features extensive logging on the server, flash remoting functionality directly from the flash media server to a webserver of choice, a default shared object for all connected users, tracking user id’s to uniquely identify user’s to the flashmedia server (which was not possible before fms version 3, see the earlier post) and integrates perfectly with our client side package. It features a user abstraction on the fms, to easily have all your connected user’s data available on a domain object. It does automatic validation based on ip or domains, to see if clients are allowed to connect, it has a built in connect and disconnect functionality and cleans up al objects when neccessary. Furthermore, it has a ping functionality built in to check (in an interval) if clients are still connected, thereby overcoming a known fms bug.

Oh, and it is heavily documented so you can find your way around the code and adjust it to your needs. It can be found in /src/fms in the repository.

Let’s see some code:

private var service : FMSService;
 
public function connect() {
	//the service we use to talk to the fms
	service = new FMSService("rtmp://example.com/application");
	/*
	 * add event listeners for the most important events (listen to all events, since fms is hard and we NEED to know if something goes wrong)
	 * 
	 */
	//when we connect, the normal flow
	service.addEventListener(FMSEvent.CONNECTED, onConnected);
	//bad network or forcefully disconnected by fms (fms logic)
	service.addEventListener(FMSEvent.ERROR_DISCONNECTED, onError);
	//attempt to connect was denied (fms logic)
	service.addEventListener(FMSEvent.ERROR_REJECTED, onError);
	//a call to an fms method failed (non existent call. bad spelling?)
	service.addEventListener(FMSEvent.ERROR_CALL_FAILED, onError); 
	//all other errors (these are of lesser importance)
	service.addEventListener(FMSEvent.ERROR_GENERIC, onError);
	//the remoteId, this is the means by which we identify ourselves (everyone has a unique remoteId)
	service.addEventListener(RemoteIdEvent.REMOTE_ID, onRemoteId);
	//now connect
	service.connect();
}

That’s all there is to connecting to a multiuser server!
You would need to implement your event listeners to correctly handle what is going on, but as you can see, in most cases, the error events can be routed to one listener, as they will be fatal most of the times.
No more listening to nasty NetStatusEvents, of which there is only 1 type and loads of different (mostly unimportant) codes. The FMSService abstraction allows you low level access to the underlying NetConnection object to see if you’re connected, or to do direct calls to the multiuser server. We recommend you subclass the FMSService to have a strongly typed interface when doing calls on the fms server, with methods implemented on the subclass doing calls via the underlying NetConnection object. The NetConnection’s client property is set to the concrete subclass of FMSService, so any method defined on the subclass is reachable from the multiuser server, we use the convention of prefixing the public methods with ‘receive’ so as to differentiate them from the public api of FMSService.

Let’s see how to use a remote shared object. The server side framework comes with a predefined ‘users’ shared object. It gives you easy access to a shared object in an OOP fashion (the default way to get a shared object in as3 is via a Static class. It also saves you writing very nasty update code in the SyncEvent handler for shared object, while making the code to use a shared object usable for every shared object you wish to talk to, something that was not possible before (due to the SyncEvent handling, which is now abstracted away).

private var users : FMSSharedObject;
public function connectToUsers():void{
	//create a reference to a remote shared object.
	//at the moment, we are connected to the fms and we use that connection to connect
	users = new FMSSharedObject("users/users", service.getConnection());
 
	//connection error, probably because of bad connection to fms or because of nonexistent shared object and no fms write permmssions
	users.addEventListener(FMSSharedObjectEvent.ERROR_CONNECT, onUserError);
	//asynch error
	users.addEventListener(FMSSharedObjectEvent.ERROR_ASYNC, onUserError);
	//generic errors
	users.addEventListener(FMSSharedObjectEvent.ERROR_NETSTATUS, onUserError);
	//whenever a new user (object) is added to the remote shared object
	users.addEventListener(FMSSharedObjectDataEvent.NEW, onUserNew);
 
	//when the shared object is fully retrieved for the first time and ready to be used
	users.addEventListener(FMSSharedObjectDataEvent.SYNCHED, onUserSynched);
	//whenever a user is removed from the shared object
	users.addEventListener(FMSSharedObjectDataEvent.DELETED, onUserDeleted);
	//whenever a user is changed on the shared object
	users.addEventListener(FMSSharedObjectDataEvent.CHANGED, onUserChanged);
	//client side write to the shared object failed (no write permission by fms)
	users.addEventListener(FMSSharedObjectDataEvent.WRITE_REJECTED, onWriteRejected);
	//client side write to the shared object succeeded, this is called only for the client that did the write.
	//that specific client will NOT get a CHANGED event, only WRITE_SUCCES
	users.addEventListener(FMSSharedObjectDataEvent.WRITE_SUCCESS, onWriteSucces);
 
	//connect to it
	users.connect();
}

As you can see, there are two types of events here: the FMSSharedObjectEvent, which deals with data about the shared object and the FMSSharedObjectDataEvent which deals specifically with data updates from the shared object. Basically, once you’re up and running, you’ll only have to deal with the data updates from a shared object, which allows you to easily setup communication between multiple users.
You wouldn’t need to implement all the event listeners in all circumstances. The WRITE types are only necessary when directly writing to a shared object from the flash client itself. In the case that the server handles this, they are not needed. The most important ones are basically FMSSharedObjectDataEvent types NEW, CHANGED and DELETED which inform you of what happened to the data on the the shared object.

When you connect to multiple shared objects, the code will remain clean as you can work with the Event based paradigm of flash, no need to do low level stuff.

Now, how would you consume the data from the Shared object?
simple, use your event listeners defined on the RSO (remote shared object) and do stuff with the data.

private function onUserChanged(event : FMSSharedObjectDataEvent) : void {
	//get the id, the property name of the shared object that is changed.
	var id : int = event.getData().getId();
	//trace the id and the name (name is the 'n' property, defined on the shared object)
	trace("id: " + id + ", name: " + event.getData().getData().n);
}

We implemented the onUserChanged event handler, got out the property name of the property that was changed and traced the id and the user’s name (part of our default user shared object). The ‘n’ property, for name, is kept short so we don’t waste bandwidth with long property names.

The code examples above works with the fms framework out of the box. Of course, when you implement the other handlers, you can do much more fun stuff.

The above code was taken from a class on creative programming, in which we were able to create a sample application that let connected user’s share their mouse cursors on screen. The complete example follows at the end of this post. Before we show that let’s have a quick look at how you would communicate directly with the server by calling methods on it.

There’s the quick and dirty way by getting the netconnection instance out of the fmsservice and calling a method on it:

//call a method, no need for a responder (null) and pass a message and id as arguments
service.getConnection().call("doChat", null, message, id);

You see it’s very easy to communicate with the server either by talking with remote shared object, or by calling methods on the server itself. In the above instance the code on the server would look like this

Client.prototype.doChat = function(message, id){
	var to = RemoteId.getClientByRemoteId(id);
	to.call("receiveChat", null, message);
}

With the above snippets you can thus target a specific client and send a chat message to it (provided that you have their id, which you can get via a shared object for instance). The ‘RemoteId.getClientByRemoteId’ is an abstraction on the server side, but as you can see, the code is minimal.

On implementing the methods that are called from fms to flash and vice versa: we prefer subclassing FMSService and implementing a method that takes a domain object (like Chat or User) as it’s parameter and have the method parse it (via a factory) for passing it to the netconnection like in the example above when calling ‘doChat’. This allows you to have your application talk to the FMSService in it’s domain specific language and does not burden your application but instead the FMSService to parse the data and to control what and how it is sent. It does force you to spit out events from the FMSService’s subclass that contains data whenever data is sent back from the fms, but we feel that overall it is cleaner to code this way. On top of that, in the above example, you would have to implement the ‘receiveChat’ method anyway on a subclass of FMSService.

The whole reason to do this kind of subclassing is because of the paradigm of ‘dataservices’ as a proxy being a way in to a remote service. It is the boundary between your local code and between the remote code. Therefore, responsibilities should be seperated and your local code should talk in it’s language to the service and the service should be the bridge between local code and remote code and is the only class that should have knowledge of both. It also allows for better testing of code. See this post for the lowdown on asynchronous testing with fms.

If it doesn’t make sense, ask and we will elaborate a bit.

Here’s the code for the full example as presented in the class.

package nl.avans.lessen.les4 {
	import nl.avans.lessen.Base;
	import nl.dpdk.collections.dictionary.HashMap;
	import nl.dpdk.log.Log;
	import nl.dpdk.services.fms.FMSEvent;
	import nl.dpdk.services.fms.FMSService;
	import nl.dpdk.services.fms.rso.FMSSharedObject;
	import nl.dpdk.services.fms.rso.FMSSharedObjectDataEvent;
	import nl.dpdk.services.fms.rso.FMSSharedObjectEvent;
	import nl.dpdk.services.fms.user.RemoteIdEvent;
	import flash.events.Event;
 
	/**
	 * All mice on all screens!
	 * 
	 * We will connect to the flash media server.
	 * After the connection is established, we will connect to a remote shared object that contains the name and remoteId for all users connected to the fms.
	 * We will then make sure we can write our mouse coordinates to the remote shared object.
	 * Since everybody will get all those updates, we can draw every connected user's mouse to our screen.
	 * 
	 * all TODO items are useful for practicing purposes and students should do these before the next lession, including the stuff on the last slide of the presentation
 
	 * 
	 * icons: www.famfamfam.com
	 * 
	 * @author rolf
	 */
	public class Les4 extends Base {
		//a reference to the service we will use to connect to the flash media server (FMS)
		private var service : FMSService;
		//a reference to a remote shared object
		private var users : FMSSharedObject;
		//a reference to the id that represents the current user
		private var me : int;
		//a reference to a map that holds references to all the cursors we will create
		private var cursors : HashMap = new HashMap();
 
 
		/**
		 * constructor
		 */
		public function Les4() {
			//the service we use to talk to the fms (USE YOUR OWN SERVICE HERE
			service = new FMSService("rtmp://example.com/application");
			/*
			 * add event listeners for the most important events (listen to all events, since fms is hard and we NEED to know if something goes wrong)
			 * 
			 */
 
 
 
			//when we connect, the normal flow
			service.addEventListener(FMSEvent.CONNECTED, onConnected);
			//bad network or forcefully disconnected by fms (fms logic)
			service.addEventListener(FMSEvent.ERROR_DISCONNECTED, onError);
			//attempt to connect was denied (fms logic)
			service.addEventListener(FMSEvent.ERROR_REJECTED, onError);
			//a call to an fms method failed (non existent call. bad spelling?)
			service.addEventListener(FMSEvent.ERROR_CALL_FAILED, onError); 
			//all other errors (these are of lesser importance)
			service.addEventListener(FMSEvent.ERROR_GENERIC, onError);
			//the remoteId, this is the means by which we identify ourselves (everyone has a unique remoteId)
			service.addEventListener(RemoteIdEvent.REMOTE_ID, onRemoteId);
 
			service.connect();
		}
 
 
 
		private function onRemoteId(event : RemoteIdEvent) : void {
			Log.debug("OnRremoteId: " + event.getRemoteId().getId(), toString());
			me = event.getRemoteId().getId();
		}
 
 
		/**
		 * the event handler for the connection event from fms.
		 * normally, you would set a 'green light' (a visual) so we know we are connected
		 */
		private function onConnected(event : FMSEvent) : void {
			connectToUsers();
		}
 
 
 
		private function connectToUsers() : void {
			//create a reference to a remote shared object.
			//at the moment, we are connected to the fms and we use that connection to connect
			users = new FMSSharedObject("users/users", service.getConnection());
 
 
			//connection error, probably because of bad connection to fms or because of nonexistent shared object and no fms write permmssions
			users.addEventListener(FMSSharedObjectEvent.ERROR_CONNECT, onUserError);
			//asynch error
			users.addEventListener(FMSSharedObjectEvent.ERROR_ASYNC, onUserError);
			//generic errors
			users.addEventListener(FMSSharedObjectEvent.ERROR_NETSTATUS, onUserError);
			//whenever a new user (object) is added to the remote shared object
			users.addEventListener(FMSSharedObjectDataEvent.NEW, onUserNew);
 
			//when the shared object is fully retrieved for the first time and ready to be used
			users.addEventListener(FMSSharedObjectDataEvent.SYNCHED, onUserSynched);
			//whenever a user is removed from the shared object
			users.addEventListener(FMSSharedObjectDataEvent.DELETED, onUserDeleted);
			//whenever a user is changed on the shared object
			users.addEventListener(FMSSharedObjectDataEvent.CHANGED, onUserChanged);
			//client side write to the shared object failed (no write permission by fms)
			users.addEventListener(FMSSharedObjectDataEvent.WRITE_REJECTED, onWriteRejected);
			//client side write to the shared object succeeded, this is called only for the client that did the write.
			//that specific client will NOT get a CHANGED event, only WRITE_SUCCES
			users.addEventListener(FMSSharedObjectDataEvent.WRITE_SUCCESS, onWriteSucces);
 
			//connect to it
			users.connect();
		}
 
 
 
		private function onWriteSucces(event : FMSSharedObjectDataEvent) : void {
			//Log.debug("Les4.onWriteSucces(event)", toString());
			var id : int = event.getData().getId();
 
			//since we can only get this 'write succes' for our own user, update the position here too.
			var cursor : Cursor = cursors.search('cursor' + id);
			cursor.x = event.getData().getData().x;
			cursor.y = event.getData().getData().y;
		}
 
 
		/**
		 * when the user shared object is fully retrieved right after connecting, this method is called
		 * therefore, it is a perfect hook to start manipulating the shared object.
		 */
		private function onUserSynched(event : FMSSharedObjectDataEvent) : void {
			Log.debug("OnUserSynched", toString());
			//start listening to the on enter frame event
			addEventListener(Event.ENTER_FRAME, oEF);
		}
 
		private function oEF(event : Event) : void {
			/**
			 * write your mouse position each on enter frame.
			 * keep in mind that this is performance heavy to send an update each enterframe when using a high framerate.
			 * directly writing to the remote shared object from the flash client makes it possible to easily hack the application as we can write our 'own' hacked version.
			 * this is for demo purposes.
			 * The preferred way is by calling a method on the fms (via the netconnection) and have the fms validate and update the shared object.
			 * 
			 * Also, when we manipulate a shared object ourselves, a shared object property that represents the current user is not automatically removed.
			 * Only because we are using an fms code framework here does everything work properly
			 */
 
			//TODO, optimize, only update/write when the mouse position has changed.
			//TODO, insert your name here by getting it as input from the user
			//TODO, call a method (you create yourself) on the fms to set the position on the shared object.
			var o : Object = new Object();
			o.n = "rolf";
			o.x = mouseX;
			o.y = mouseY;
			o.rId = me;
			users.write(me, o);
		}
 
 
		/**
		 * whenever the remote shared object is changed (because there was a write to it)
		 */
		private function onUserChanged(event : FMSSharedObjectDataEvent) : void {
			var id : int = event.getData().getId();
			//Log.debug('onUserChanged: ' + id, toString());
			//get a reference to the cursor of this user and set it's position
			try {
				//we use a try/catch, since other students here might not write correct code and this might cause errors.
				//for instance, when someone forgets to set the x property or y property.
				var cursor : Cursor = cursors.search('cursor' + id);
				cursor.x = event.getData().getData().x;
				cursor.y = event.getData().getData().y;
				cursor.setName(event.getData().getData().n);
			}catch(e : Error) {
				Log.error(e.message, toString());
			}
		}
 
 
		/**
		 * a user was removed from the shared object (handled automatically by dpdk code on the fms)
		 */
		private function onUserDeleted(event : FMSSharedObjectDataEvent) : void {
			var id : int = event.getData().getId();
			Log.debug('onUserDeleted: ' + id, toString());
			//remove the cursor and remove from displaylist
			var cursor : Cursor = cursors.search("cursor" + id);
			cursors.remove("cursor" + id);
			removeChild(cursor);
		}
 
		private function onUserNew(event : FMSSharedObjectDataEvent) : void {
			var id : int = event.getData().getId();
			Log.debug('onUserNew: ' + event.getData().getId() + ", " + event.getData().getData().n, toString());
			//TODO, only create cursors for other users, not for yourself (this also means not updating your own properties when they are changed)
			//TODO, give users a random icon for their mouse (see the icons in the library of 'les4.fla'
 
			//create a cursor and store it for later reference, also add it to the display list
			var cursor : Cursor = new Cursor(id);
			cursors.insert("cursor" + id, cursor);
			addChild(cursor);
 
 
			//check to see if the new user has the same id as the id we have received previously in 'onRemoteId'.
			//if so, this is my data and we will give our cursor a slightly different look.
			if(id == me) {
				Log.debug("this is me!", toString());
				cursor.graphics.beginFill(0xff9900);
				cursor.graphics.drawCircle(0, 0, 5);
				cursor.graphics.endFill();
			}
		}
 
 
		/**
		 * ERROR HANDLERS BELOW
		 * TODO, create a decent error message or do some decent error handling instead of only a trace.
		 * always let your users know something is wrong (feedback)
		 */
 
		private function onUserError(event : FMSSharedObjectEvent) : void {
			Log.debug(event.getMessage(), toString());
		}
 
		private function onError(event : FMSEvent) : void {
			Log.debug(event.getMessage(), toString());
		}
 
		private function onWriteRejected(event : FMSSharedObjectDataEvent) : void {
			Log.debug("Les4.onWriteRejected(event)", toString());
		}
	}
}

We also used a supporting “Cursor” class, that was linked to a library item in the .fla file with an icon of a mouse cursor from the excellent site www.famfamfam.com

package nl.avans.lessen.les4 {
	import flash.display.MovieClip;
	import flash.text.TextField;
 
	/**
	 * This class is empty at the moment.
	 * It serves as a class to create a link to a library item in the les4.fla file
	 * In this way, we can create a visual cursor programmatically
	 * 
	 * @author rolf
	 */
	public class Cursor extends MovieClip {
		private var id : int;
		private var tf : TextField;
 
 
		public function Cursor(id: int = 0) {
			this.id = id;
			//TODO, add a textfield via the flash library (public var tf: TextField) and show the name of the connected user attached to the mouse
			tf = new TextField();
			tf.text = id.toString();
			tf.textColor = 0xFFFFFF;
			tf.x = 10;
			tf.y = 10;
 
			addChild(tf);
 
 
		}
 
		public function setName(name: String):void{
			tf.text  = id + " " + name;
		}
	}
}

So try out some multiuser programming and go crazy!
This post obviously touched only a littlebit of the possibilities, but feel free to ask some questions and we’ll try to answer them.

0 Responses to “multiuser as3 framework for flash media server and red5”


  1. No Comments

Leave a Reply