Home » Code Samples, Flash, FMS, Tutorials

Flash Media Server Streaming Speed Testing [Part 3] – Compare Multiple Server Resources, Port Connections, Detect Upload, Download, and Latency Speed

12 May 2009 | 11,572 views | 2 Comments


This tutorial is the final in a series of three. Part one of this tutorial outlined what is needed to detect a users upload, download, and latency between a Flash client and the Flash Media Server. Part two adds on the ability to detect which ports are available while connecting. In part three, we will be adding an external xml file that will allow you to compare connections to different Flash Media Servers in order to find which of them is the best connection for serving media. If you haven’t already read part one, you can find it here, and part two is available here.

Again, Bandwidth detection is most important when connecting your users to the correctly compressed media to be streamed, recorded, or delivered via a Flash Media Server. This third example will walk you through the process of being able to detect a users download bandwidth, upload bandwidth, latency speeds, and the connected port between the client computer and the host server, along with iterating through multiple Flash Media Servers in an attempt to connect to the fastest and most available server. All of the code is Actionscript 2.0, and I’ve setup a Flash Media Server 3.5 to host the server side scripting. I use Influxis as my Flash Media Server host for all of these tutorials and smaller examples.

First, lets put together our Flash Actionscript. Here our goal is to setup our connection manager, and create a BandwidthInfo object that will call to our main.asc file (this is explained next) for sending packets to the Flash Media server to the client, and vice versa. We also will be creating an instance of our NetConnManager.as (also explained further down) which will allow the flash client to detect which port is best to connect to the Flash Server.

import NetConnManager;

var bestConn:Number;
var bestBandwidth:Number;
var connectBestConn:Number;
var whichServer:String;
var hasConnected:Boolean;
var currentlyChecking:Boolean;
var nextCheck:Number;
var upstreamsAvailable:Array = new Array();
var downstreamsAvailable:Array = new Array();
var theServers:Array;
var startTime:Number = getTimer();
var ncm:NetConnManager = new NetConnManager();
var ncmListener:Object = new Object();
var theServersXML:XML = new XML();
theServersXML.ignoreWhite = true;

ncmListener.ncConnected = function(evt:Object) {
	trace("["+Math.round(getTimer()-startTime)+"ms] successfully connected using "+evt.protocol+":"+evt.port);
	conn_stat.text += "["+Math.round(getTimer()-startTime)+"ms] successfully connected using "+evt.protocol+":"+evt.port+newline;
};
ncmListener.ncFailedToConnect = function(evt:Object) {
	trace("failed to connect after "+evt.timeout+" ms");
	conn_stat.text += "failed to connect after "+evt.timeout+" ms"+newline;
};
ncm.addEventListener("ncConnected", ncmListener);
ncm.addEventListener("ncFailedToConnect", ncmListener);
ncm.connect("your.rtmphost.com", "application_name");

theServersXML.load("xml/server_config.xml");
theServersXML.onLoad = function(success) {
	var index:Number = 0;
	theServers = new Array(this.firstChild.firstChild.childNodes.length);
	for (var aNode:XMLNode = this.firstChild.firstChild.firstChild; aNode != null; aNode=aNode.nextSibling) {
		theServers[index] = new Array(aNode.firstChild.childNodes.length);
		for (var subNode:XMLNode = aNode.firstChild; subNode != null; subNode=subNode.nextSibling) {
			theServers[index][subNode.nodeName] = subNode.firstChild.nodeValue;
		}
		index++;
	}
	//setup first connection
	checkConnection(theServers[0]["host"], theServers[0]["app"], 0);
};

function checkConnection(host, app, which) {

	currentlyChecking = true;

	rec_nc = new NetConnection();
	rec_nc.onStatus = function(info) {
		trace("Level: "+info.level+" Code: "+info.code);
		if (info.code == "NetConnection.Connect.Success") {
			trace("connected to: "+this.uri);
			conn_stat.text += "connected to: "+this.uri+newline;
			startTest(rec_nc);
		} else if (info.code == "NetConnection.Connect.Failed" || info.code == "NetConnection.Connect.Closed") {
			trace("no connection to app");
			conn_stat.text += "error connection failed"+newline;
		}
	};
	rec_nc.connect("rtmp://"+host+"/"+app+"/");

	function startTest(nc) {
		_global.bwInfo = new BandwidthInfo(nc);
		_global.bwInfo.start();
	}
	for (i=0; i<1000; i++) {
		data += "C->S";
	}
	function BandwidthInfo(nc) {
		this.nc = nc;
		this.maxLength = 10;
		this.bwInHistory = new Array(this.maxLength);
		this.bwInCtr = 0;
		this.bwOutHistory = new Array(this.maxLength);
		this.bwOutCtr = 0;
		this.pingHistory = new Array(this.maxLength);
		this.headIn = 0;
		this.headOut = 0;
		this.headPing = 0;
		this.bBWOutStop = false;
		this.bBWInStop = false;
		this.onBWInTimeout = function() {
			clearInterval(this.bwInfoTimeout);
			this.bwInfoTimeout = null;
			delete this.bwInfoTimeout;
			this.bBWInStop = true;

			if (this.bwInCtr == 0) {
				conn_stat.text += "unable to receive data from server."+newline;
				this.abort("unable to receive data from server.");
				return;
			}
			this.stop();
		};
		this.onBWOutTimeout = function() {
			clearInterval(this.bwInfoTimeout);
			this.bwInfoTimeout = null;
			delete this.bwInfoTimeout;
			this.bBWOutStop = true;

			if (this.bwOutCtr == 0) {
				conn_stat.text += "unable to receive data from server\n";
				this.abort("unable to send data to server\n----------\n");
				return;
			}
			trace("testing bandwidth from server");
			conn_stat.text += "testing bandwidth from server"+newline;
			this.bwInfoTimeout = setInterval(this, "onBWInTimeout", 5*1000);
			this.serverToClient();
		};
		this.clientToServer = function() {
			this.time = getTimer();
			size = 0;
			bwinfo = this;
			this.nc.ack = function(pingVal) {
				if (!bwinfo.bBWOutStop) {
					bwinfo.bwOutHistory[bwinfo.headOut++%bwinfo.maxLength] = Math.floor(size/(getTimer()-bwinfo.time)*1000);
					bwinfo.pingHistory[bwinfo.headPing++%bwinfo.maxLength] = pingVal;
					bwinfo.nc.call("recData",0,data);
					size += 4000;
					bwinfo.bwOutCtr++;
				}
			};
			this.nc.call("recData",0,data);
			this.nc.call("recData",0,data);
		};
		this.serverToClient = function() {
			this.time = getTimer();
			size = 0;
			bwinfo = this;
			nc.onEcho = function() {
				if (!bwinfo.bBWInStop) {
					bwinfo.bwInHistory[bwinfo.headIn++%bwinfo.maxLength] = Math.floor(size/(getTimer()-bwinfo.time)*1000);
					this.call("echoData",0,0);
					size += 4000;
					bwinfo.bwInCtr++;
				}
			};
			nc.call("echoData",0,0);
			nc.call("echoData",0,0);
		};
		this.start = function() {
			conn_stat.text += "testing upload speed"+newline;
			trace("testing upload speed");
			clearInterval(this.bwInfoTimeout);
			this.bwInfoTimeout = null;
			delete this.bwInfoTimeout;
			this.bwInfoTimeout = setInterval(this, "onBWOutTimeout", 5*1000);
			this.clientToServer();
		};
		this.stop = function() {
			this.nc = null;
			var ping_rtt = 0;
			var bw_out = 0;
			var bw_in = 0;
			for (var i = 0; i<this.maxLength && i<this.bwOutCtr; i++) {
				ping_rtt = Math.max(ping_rtt, this.pingHistory[i]);
			}
			for (var i = 0; i<this.maxLength && i<this.bwOutCtr; i++) {
				bw_out += this.bwOutHistory[i];
			}
			bw_out /= Math.min(this.maxLength, this.bwOutCtr);
			bw_out = Math.round((bw_out/1024)*8);
			for (var i = 0; i<this.maxLength && i<this.bwInCtr; i++) {
				bw_in += this.bwInHistory[i];
			}
			bw_in /= Math.min(this.maxLength, this.bwInCtr);
			bw_in = Math.round((bw_in/1024)*8);

			//record
			upstreamsAvailable.push(bw_out);
			downstreamsAvailable.push(bw_in);

			var s;
			s = "bandwidth results:\n";
			s += "   upstream: "+bw_out+" kbps\n";
			s += "   downstream: "+bw_in+" kbps\n";
			s += "   latency: "+ping_rtt+" ms\n";
			if (ping_rtt>1000) {
				s += "network appears to have a very high delay\n";
			}
			if ((bw_in>500) && (bw_out>200)) {
				s += "connection supports high quality speed\n----------\n";
			} else if ((bw_in>=200) && (bw_out>=100)) {
				s += "connection supports good quality speed\n----------\n";
			} else if ((bw_in>100) && (bw_out>80)) {
				s += "connection supports mid quality speed\n----------\n";
			} else if ((bw_in>20) && (bw_out>15)) {
				s += "connection supports slow quality speed\n----------\n";
			} else {
				s += "connection supports very slow quality speed\n----------\n";
			}
			trace(s);
			conn_stat.text += s+newline;
			currentlyChecking = false;
			nextCheck = which+1;
		};
		this.abort = function(reason) {
			conn_stat.text += "failed "+reason+newline;
			trace("failed "+reason);
			currentlyChecking = false;
			nextCheck = which+1;
		};
	}

}

checkIfRunning = setInterval(this, "checkConnBusy", 1000);

function checkConnBusy() {
	if (currentlyChecking == false) {
		if (nextCheck<theServers.length) {
			checkConnection(theServers[nextCheck]["host"], theServers[nextCheck]["app"], nextCheck);
		} else {
			clearInterval(checkIfRunning);
			conn_stat.text += "comparing connection"+newline;
			compareConnections();
		}
	}
}

maxValueIndex = function (array) {
	mxm = array[0];
	for (i=0; i<array.length; i++) {
		if (array[i]>mxm) {
			mxm = array.index;
		}
	}
	return mxm;
};

Array.prototype.indexOf = function(value) {
	var i = 0;
	var l = this.length;
	var res = -1;
	for (i; i<l; i++) {
		if (this[i] == value) {
			res = i;
			break;
		}
	}
	return res;
};

function compareConnections() {
	bestConn = maxValueIndex(upstreamsAvailable);
	bestBandwidth = maxValueIndex(upstreamsAvailable);
	connectBestConn = upstreamsAvailable.indexOf(bestConn);
	setConnectionsInterval = setInterval(this, "setConnections", 500);
}
function setConnections(){
	if(theServers[connectBestConn]["host"] != undefined && theServers[connectBestConn]["app"] != undefined){
		clearInterval(setConnectionsInterval);
		conn_stat.text += "choosing server: rtmp://"+theServers[connectBestConn]["host"]+"/"+theServers[connectBestConn]["app"]+newline;
	}else{
		clearInterval(setConnectionsInterval);
		conn_stat.text += "connection to the server(s) has failed"+newline;
	}
}

In order to get the above to work correctly with your own Flash Media Server, you simply need to edit the following line.

ncm.connect("your.rtmphost.com", "application_name");

This is the path to the application which will hold our Main.asc file. You will also need to edit the xml/server_config.xml file in order to test against several different Flash Media Servers. This file serves as the que by which the flash client will verify and test each of the different connections, and finally pick the fastest one.

<servers version="1" xmlns="http://xspf.org/ns/0/">
  <serverList>
    <server>
      <host>first_host</host>
      <app>first_app</app>
    </server>
	<server>
      <host>second_host</host>
      <app>second_app</app>
    </server>
  </serverList>
</servers>

We now have our Flash file ready, and our servers setup within the server_config.xml.

Next, we want to create the server side code that will allow our final swf to call server side functions in order to return the correct upload, download, and latency values.

application.onAppStart = function (info){
	for ( i = 0; i < 500; i++ ) {
		data += "S->C";
	}
	Client.prototype.recData = function(data) {
		this.ping();
		var v = this.getStats();
		this.call("ack", 0, v.ping_rtt);
	}
	Client.prototype.echoData = function() {
		this.call("onEcho", 0, data);
	};
	Client.prototype.getBWInfo = function() {
		return this.getStats();
	};
	Client.prototype.onConnTimeout = function(){
		clearInterval( this.connTimeout );
		this.connTimeout = null;
		application.disconnect(this);
	}
}
application.onConnect = function(client_obj, id) {
	application.acceptConnection(client_obj);
}

We also need our NetConnManager.as file which allows for multiple port detections.

import mx.events.EventDispatcher;
class NetConnManager extends Object {
	//EventDispatcher needs these
	var addEventListener:Function;
	var removeEventListener:Function;
	var dispatchEvent:Function;
	var dispatchQueue:Function;
	//Constants
	private var k_DEFAULTCONNLIST = [{protocol:"rtmp", port:1935}, {protocol:"rtmp", port:443}, {protocol:"rtmpt", port:80}, {protocol:"rtmps", port:443}];
	private var k_TIMEOUT:Number = 60000;
	//Variables
	private var m_connList:Array;
	private var m_serverName:String;
	private var m_appName:String;
	private var m_streamName:String;
	private var m_connListCounter:Number;
	private var m_flashComConnectTimeOut:Number;
	private var m_validNetConnection:NetConnection;
	//Constructor
	function NetConnManager() {
		EventDispatcher.initialize(this);
	}
	//Public
	//Initiates all the connection attempts.
	//Note: If no connection list is passed, the default list is used
	function connect(p_serverName:String, p_appName:String, p_connList:Array) {
		m_serverName = p_serverName;
		m_appName = p_appName;
		m_connList = (p_connList != undefined) ? p_connList : k_DEFAULTCONNLIST;
		//Set a timeout function, just in case we never connect successfully
		clearInterval(m_flashComConnectTimeOut);
		m_flashComConnectTimeOut = setInterval(this, "onFlashComConnectTimeOut", k_TIMEOUT, k_TIMEOUT);
		//Creates a NetConnection for each of the protocols/ports listed in the m_connList list.
		//Connection attempts occur at intervals of 1.5 seconds. The first connection to succeed
		//will be used, all the others will be closed.
		for (var i = 0; i<m_connList.length; i++) {
			this["nc"+i] = new NetConnection();
			this["nc"+i].owner = this;
			this["nc"+i].connIndex = i;
			this["nc"+i].onStatus = function(info) {
				this.pending = false;
				this.owner.m_validNetConnection = this.owner["nc"+this.connIndex];
				if (info.code == "NetConnection.Connect.Success") {
					clearInterval(this.owner.m_flashComConnectTimeOut);
					this.owner.dispatchEvent({type:"ncConnected", nc:this.owner.m_validNetConnection, protocol:this.owner.m_connList[this.connIndex].protocol, port:this.owner.m_connList[this.connIndex].port});
					for (var i = 0; i<this.owner.m_connList.length; i++) {
						if (i == this.connIndex) {
							continue;
						}
						if (this.owner["nc"+i].pending) {
							clearInterval(this.owner["ncInt"+i]);
							this.owner["nc"+i].onStatus = null;
							this.owner["nc"+i].close();
							this.owner["nc"+i] = null;
							delete this.owner["nc"+i];
						}
					}
				} else {
					trace(this.owner.m_connList[this.connIndex].protocol+": "+this.owner.m_connList[this.connIndex].port+", onStatus: "+info.code+" : "+info.description);
				}
			};
			this["nc"+i].pending = true;
		}
		m_connListCounter = 0;
		nextConnect();
	}
	//Public
	//Returns the active connection or null if it does not exist.
	function getActiveConnection(Void):NetConnection {
		return m_validNetConnection == undefined ? null : m_validNetConnection;
	}
	function closeConnections() {
		m_validNetConnection.close();
	}
	//Private
	//Walks through the connection list to try every protocol.
	private function nextConnect(Void):Void {
		clearInterval(this["ncInt"+m_connListCounter]);
		this["nc"+m_connListCounter].connect(m_connList[m_connListCounter].protocol+"://"+m_serverName+":"+m_connList[m_connListCounter].port+"/"+m_appName);
		if (m_connListCounter<(m_connList.length-1)) {
			m_connListCounter++;
			this["ncInt"+m_connListCounter] = setInterval(this, "nextConnect", 1500);
		}
	}
	//Private
	//Cleans up all conenctions if none have succeeded by the timeout interval
	private function onFlashComConnectTimeOut(timeout:Number):Void {
		clearInterval(m_flashComConnectTimeOut);
		this.dispatchEvent({type:"ncFailedToConnect", timeout:timeout});
		for (var i = 0; i<m_connList.length; i++) {
			if (this["nc"+i].pending) {
				clearInterval(this["ncInt"+i]);
				this["nc"+i].onStatus = null;
				this["nc"+i].close();
				this["nc"+i] = null;
				delete this["nc"+i];
			}
		}
	}
}

Download the source

This completes the tutorial series of the various methods to detect and stream media to flash clients. All three of these tutorials have built upon each other in order to provide solid methods for dynamically streaming any type of media via Flash Media Server.

Here are the links for this tutorial in its entirety:

Flash Media Server Streaming Speed Testing [Part 1] – Detect Upload, Download, and Latency Speeds

Flash Media Server Streaming Speed Testing [Part 2] – Detect Upload, Download, and Latency Speeds, and Port Connection

Flash Media Server Streaming Speed Testing [Part 3] – Compare Multiple Server Resources, Port Connections, Detect Upload, Download, and Latency Speed