Advertisement

Creating a Bandwidth Tester for Loading Video in Flash With AS3

by

In web development and media content serving you often need a way to make sure the user has a smooth viewing experience. For users with little bandwidth and slow internet connections you need to serve a smaller video than for somebody on a T1 connection. This tutorial will teach you how to detect a user's bandwidth and serve him the appropriate Flash video. We'll be building an interface similar to the YouTube quality selector, while learning a bit about using custom events and reusable classes.


Step 1: BandwidthTester.as

We're going to create a class called BandwidthTester that will take care of measuring the bandwidth. We'll then use that class in the player to load a certain movie. The class is not that complicated; we will load a test file and measure how many bytes have been loaded every second, then use this data to get the average speed.

Let's get started. First create a folder in which we'll put all the sources. I have provided, in the final source file download, a folder called videos in which there are three FLV files.

In the main folder, create a file named BandwidthTester.as and enter the following:

package {
	import flash.net.*;
	import flash.utils.*;
	import flash.events.*;
	
	public class BandwidthTester extends EventDispatcher {
		public static const BAND_TESTED = 'tested';
		public static const TEST = 'test';
		
		private var bandwidth = 0;			//final average bandwidth
		private var peak_bandwidth = 0;		//peak bandwidth
		private var curr_bandwidth = 0;		//current take bandwidth
		
		private var testfile = 'videos/video_hq.flv';
		private var l;						//loader
		private var tm;						//timer
		private var last_bytes = 0;			//bytes loaded last time
		private var bands;					//recorded byte speeds
		private var _latency = 1;		//network utilization approximation
		
		
		public function BandwidthTester( latency=0 ){
			trace('bandwidth tester loaded.');
		}
		
	}	
}

Here I have created the package, imported the required packages and created the main BandwidthTester class. You'll notice the class extends EventDispatcher. We need this in order to dispatch events to classes that subscribe. I have created a couple of variables to hold the various elements: the two constants will hold two types of events; the bandwidth variable will hold the final average speed; the peak_bandwidth will hold the peaking speed (the highest measured value); and the curr_bandwidth will be used in the TIMER event to hold the current measured speed.

I have defined a testfile variable which points to the video with the largest size. Ideally you would want to set this to a file over 1MB on the internet that doesn't change, such as a google cdn library or something. I have also defined variables to hold the URLLoader, the timer, a variable that holds the last_bytes loaded, the bands variable which will hold all the measurements and a property called _latency that will be used as an approximation of network overhead.


Step 2: Constructor

Let's make the main functionality of the bandwidth tester. Replace the constructor function's contents with the following:

public function BandwidthTester( latency=0 ){
	tm = new Timer(1000, 3);
	tm.addEventListener( TimerEvent.TIMER, get_band );
	tm.addEventListener( TimerEvent.TIMER_COMPLETE, timer_complete );
	bands = new Array();
	_latency = 1-latency;
}
	
public function start(){
	l = new URLLoader();
	l.addEventListener( Event.OPEN, start_timer );
	l.addEventListener( Event.COMPLETE, end_download );
	l.load( new URLRequest( testfile ) );
}
	
public function start_timer( e:Event ){
	tm.start();
}

I am setting up the timer with a delay of 1 second and 3 repeats. This ensures that we'll have three timer events at a one second distance in time. We'll use the get_band() function to save the bytes loaded. I am also initializing the bands array and setting the latency from the constructor arguments. The whole testing doesn't start until I call the start() method, which creates a URLLoader and loads the test file, after adding event listeners to the OPEN and COMPLETE events. When the file has begun downloading, we start the timer.


Step 3: Get the Bandwidth

We need to make the get_band() function:

private function get_band( e:TimerEvent ){
	curr_bandwidth = Math.floor(((l.bytesLoaded-last_bytes) /125) * _latency);
	bands.push( curr_bandwidth );
	last_bytes = l.bytesLoaded;
		
	dispatchEvent( new Event( BandwidthTester.TEST ) );
}

This is the heart of the measurement code. We calculate the current speed after the first second by subtracting the last_bytes from the bytesLoaded property of the urlloader object, multiplied by the latency. The latency is a number between 0 and 1 so when we set, for example, network overhead approximation to 0.2, the speed will be multiplied by 0.8 -- that is, 80 percent of the speed.

I also divide the subtracted bytes by 125 to get the number of kilobits/second. In networking the speed is measured as number of kilobits per second ( kB/s ) and 1 kilobit equals 125 bytes. When networks advertise 1 Mb/s, they usually mean 1 megabit/second, which means the maximum download speed would be 125 kilobytes/s. I use Math.floor() to get a integer from the calculations.

Next, we push the current speed ( or curr_bandwidth ) into the bands array, we set the last_bytes to the current bytesLoaded and dispatch a new Event of type BandwidthTester.TEST. This function will be called every second for three seconds. At the end of those three seconds, we have a 3-element array that contains the speed values


Step 4: Remaining Functions

Let's create the remaining functions to calculate the final bandwidth:

private function timer_complete( e:TimerEvent ){
	l.close();
	bands.sort( Array.NUMERIC | Array.DESCENDING );
	
	peak_bandwidth = bands[0];
	bandwidth = calc_avg_bandwidth();
			
	dispatchEvent( new Event( BandwidthTester.BAND_TESTED ) );
}

private function end_download( e ){
	tm.stop();
	l.close();
	bands.sort( Array.NUMERIC | Array.DESCENDING );
	bandwidth = 10000;
	peak_bandwidth = (bands[0])? bands[0] : bandwidth;
			
	dispatchEvent( new Event( BandwidthTester.BAND_TESTED ) );
}

The timer_complete() function is called after the timer completes its three timer events. Here, we stop the loading of the URLLoader by calling close(), and we sort the bands array. The sort() function accepts some values for the behavior of the sorting. I have combined Array.NUMERIC, which sorts based on numbers and Array.DESCENDING which sets the sorting to be from highest to lowest numbers. This will sort the bands array so that in bands[0] we'll have the highest measured speed, which we use to set the peak_bandwidth. I also set the bandwidth to the result of calc_avg_bandwidth(), which we'll code shortly. I also dispatch a BAND_TESTED event.

The end_download() function is almost the same, with the exception that if the bands array has not been filled, I set the peak_bandwidth and the bandwidth to 10000. This would happen very rarely -- only if you are on a very fast connection, and the file downloads in less than a second -- in which case, the bandwidth is not an issue.


Step 5: Final Bandwidth

Let's calculate the final bandwidth by creating an average of the 3 values in the bands[] array:

private function calc_avg_bandwidth(){
	var total = 0;
	var len = bands.length;
	while( len-- ){
		total += bands[len];
	}
	return Math.round( total / bands.length );
}

This function is a typical averaging function: I add all the values in the bands[] array and divide the result by the number of elements in the array, and then return it rounded to an integer with Math.round(). This averaging function ensures that if there are irregularities between the speeds, we get a better approximation of the bandwidth.


Step 6: Utility Functions

There are a few more utility functions we need to create:

public function set latency( prc ){
	this._latency = 1-prc;
}
		
public function getBandwidth(){
	return bandwidth;
}
		
public function getPeak(){
	return peak_bandwidth;
}
public function last_speed(){
    return curr_bandwidth;
}

I have defined a setter function for the _latency property, in case you need to set it after the instantiation. It's not necessary, but it is cool :). I also have defined a getBandwidth() function which we'll use to get the final bandwidth and a getPeak() function which returns the peak_bandwidth; again I could have made the bandwidth and peak_bandwidth public but this is more standard. The last_speed() function returns the curr_bandwidth variable, so this will hold the last measured speed.

Now, the getBandwidth() and getPeak() should only be called after the BAND_TESTED event has fired, but we'll see how to use this class in a moment. Here is the final code for this class:

package {
	import flash.net.*;
	import flash.utils.*;
	import flash.events.*;
	
	public class BandwidthTester extends EventDispatcher {
		public static const BAND_TESTED = 'tested';
		public static const TEST = 'test';
		
		private var bandwidth = 0;			//final average bandwidth
		private var peak_bandwidth = 0;		//peak bandwidth
		private var curr_bandwidth = 0;		//current take bandwidth
		
		private var testfile = 'videos/video_hq.flv';
		private var l;						//loader
		private var tm;						//timer
		private var last_bytes = 0;			//bytes loaded last time
		private var bands;					//recorded byte speeds
		private var _latency = 1;		//network utilization approximation
		
		
		public function BandwidthTester( latency=0 ){
	        tm = new Timer(1000, 3);
		    tm.addEventListener( TimerEvent.TIMER, get_band );
		    tm.addEventListener( TimerEvent.TIMER_COMPLETE, timer_complete );
		    bands = new Array();
		    _latency = 1-latency;
	    }
		public function start(){
		    l = new URLLoader();
		    l.addEventListener( Event.OPEN, start_timer );
		    l.addEventListener( Event.COMPLETE, end_download );
		    l.load( new URLRequest( testfile ) );
	    }
		
        public function start_timer( e:Event ){
		    tm.start();
	    }
        
        private function get_band( e:TimerEvent ){
	        curr_bandwidth = Math.floor(((l.bytesLoaded-last_bytes) /125) * _latency);
	        bands.push( curr_bandwidth );
	        last_bytes = l.bytesLoaded;
	        dispatchEvent( new Event( BandwidthTester.TEST ) );
        }
        
        private function timer_complete( e:TimerEvent ){
	        l.close();
	        bands.sort( Array.NUMERIC | Array.DESCENDING );
		    peak_bandwidth = bands[0];
	        bandwidth = calc_avg_bandwidth();
			dispatchEvent( new Event( BandwidthTester.BAND_TESTED ) );
        }
        
        private function end_download( e ){
        	tm.stop();
	        l.close();
	        bands.sort( Array.NUMERIC | Array.DESCENDING );
	        bandwidth = 10000;
	        peak_bandwidth = (bands[0])? bands[0] : bandwidth;
			dispatchEvent( new Event( BandwidthTester.BAND_TESTED ) );
        }
        
        private function calc_avg_bandwidth(){
	        var total = 0;
	        var len = bands.length;
	        while( len-- ){
		        total += bands[len];
	        }
	        return Math.round( total / bands.length );
        }
        
        public function set latency( prc ){
        	this._latency = 1-prc;
        }
        
        public function getBandwidth(){
        	return bandwidth;
        }
		
        public function getPeak(){
	        return peak_bandwidth;
        }  
		
	}	
}

Step 7: Testing

Well, that was a lot of code, and we still haven't tested this whole testing class! We'll use this class in a small player that will load three different FLVs based on speed. In Flash, create a new Flash Document with dimensions of 640x480px and set the frame rate to 30fps.

Flash bandwidth streaming video player

I have set the background color of the movie to black (#000000). Let's create the document class. Create a new Actionscript file and name it BandwidthPlayerTest.as and enter the following inside:

package {
	
	import flash.display.*;
	import flash.events.*;
	import flash.media.*;
	import flash.xml.*;
	import flash.text.*;
	import flash.net.*;
	
	public class BandwidthPlayerTest extends Sprite {
		
		var bt;     	 	//bandwidth tester
		var finalvideo = '';	//url path
		var video_id = false;
		var videos = new Array('videos/video_lq.flv', 'videos/video_mq.flv', 'videos/video_hq.flv'); //video array
		var nc		  	 	//net connection
		var stream			//netstream
		var t;
		
		public function BandwidthPlayerTest(){
			t = new TextField();
			t.defaultTextFormat = new TextFormat('Arial',12,0xffffff);
			t.width = 200;
			t.autoSize = TextFieldAutoSize.LEFT;
			t.x = 10;
			t.y = 10;
			t.selectable = false;
			t.text = 'Calculating speed ...';
			addChild( t );
			
			this.bt = new BandwidthTester(0);
			this.bt.addEventListener( BandwidthTester.BAND_TESTED, play_video );
			this.bt.addEventListener( BandwidthTester.TEST, band_test );
			this.bt.start();
		}
		
		
		
	}	
}

This is the main document class for the Flash Document. I have imported the flash.media and flash.net packages, as we will use the Video class to play the video. Let's go through the variables I have set for this class:

bt will hold the bandwidth tester instance, finalvideo will hold the chosen path to the video, video_id will hold the chosen index (we'll need this later), nc and stream will hold the NetConnection and NetStream classes, and videos is an array that holds the paths to the videos.

In the constructor, we create a new textfield (for testing) and we create an instance of the BandwidthTester class. I have provided a value of 0 for latency as we don't need that right now.


Step 8: band_test() and play_video()

Let's create the band_test() and play_video() functions:

    private function band_test( e ){
	    t.text += '\ntest: '+e.target.last_speed()+' kb/s';
    }
		
	private function play_video( e ){
		var bw = e.target.getBandwidth();
			
		t.text += '\n\nFinal bandwidth: '+bw+' kb/s';
		t.text += '\nPeak bandwidth: '+e.target.getPeak()+' kb/s';

		if( bw > 400 ){
			video_id = 2;
		} else if( bw > 128 ){
			video_id = 1;
		} else {
			video_id = 0;
		}
		finalvideo = videos[video_id];
			
		nc = new NetConnection();
		nc.addEventListener( NetStatusEvent.NET_STATUS, nc_status );
		nc.connect( null );
	}

You can see I have added a listener to BandwidthTester.TEST that will trigger band_test() every second, so we get the measured speed and append it to the textfield. Obviously, on a live application, this would be hidden and you would probably use this only in debugging.

The next function, play_video(), is where we select the proper FLV. I get the average bandwidth in the bw variable, append some results to the textfield, and begin comparing the speeds. If we get a speed higher than 400kb/s we play the highest version, otherwise we play the lower versions. The video_id variable stores the index in the videos[] array.


Step 9: Loading the FLV

Let's create the helper functions to load the FLV:

private function nc_status( e ){
	switch( e.info.code ){
		case "NetConnection.Connect.Success":
            connect_stream();
            break;
        case "NetStream.Play.StreamNotFound":
            t.text = 'Could not find video.';
            break;
	}
}
		
private function connect_stream(){
	stream = new NetStream( nc );
	stream.addEventListener( NetStatusEvent.NET_STATUS, nc_status );
	stream.addEventListener( AsyncErrorEvent.ASYNC_ERROR, function(e){ } );
			
	var video = new Video( stage.stageWidth, stage.stageHeight );
	addChildAt( video, 0 );
	video.attachNetStream( stream );
	video.smoothing = true;
	stream.play( finalvideo );
	t.text += '\nVideo '+finalvideo;
}

the nc_status() function is pretty standard in FLV loading, we call the connect_stream() function if we get NetConnection.Connect.Success, or else we show an error in the textfield (if the movie cannot be loaded).

The connect_stream() function is where we attach the netconnection to the NetStream. I have added an event listener for AsyncErrorEvent.ASYNC_ERROR to which I attached an empty function to stop the tracing of async errors in the trace window, as this is not really necesary.

So next, I have defined the video object, I have attached the stream and I called play(). Pretty simple, eh? I have also set smoothing to true so the FLV looks better.

Back in the main Flash document, set the document class to BandwidthPlayerTest. If you test the movie now, you should get a three second measuring time, after which the FLV will load. At this moment, we can't exactly simulate all the speeds so you can export the SWF and put it on a server (along with the videos, of course) and test the speed. You should get the correct movie depending on the downloading speed.


Step 9: Quality Selector

As an extra, I will show you how to create a quality selector for the player. In the flash document, select the Rectangle Tool (R), set the rectangle roundness to 100, and draw a 90x25px rectangle with a fill color of #333333, like in the picture:

Flash bandwidth streaming video player

Step 10: video_selector

Press F8 to create a new movieclip from the rounded rectangle. Name this clip video_selector and click Advanced. Check Export for Actionscript and in the Identifier field, enter video_sel.

Flash bandwidth streaming video player

Step 11: Add Layers

Double-click the movie clip to edit it, and, in the timeline, create four more layers and name them as in the picture:

Flash bandwidth streaming video player

Step 12: Buttons Rectangle

On the buttons layer, create a 30x25px rectangle with a fill color of #CC0000. With the rectangle selected, press F8 and create a new button with the name button, like in the picture:

Flash bandwidth streaming video player

Step 13: Buttons States

Double click the newly created button and move the first frame to the Over frame; then, duplicate the frame (by holding Alt and dragging the frame) to the Down and Hit states. If you want, you can use a darker shade of red on the Down state, to make it more interesting.

Flash bandwidth streaming video player

Step 14: Buttons Instances

Back in the "video_selector" movieclip, duplicate the button twice more so that you have three buttons and position them at 0, 30 and 60px respectively. Name the three buttons b1, b2 and b3.

Flash bandwidth streaming video player

Step 15: Buttons Hover

Create another 30x25px rectangle with a fill color of #CC0000, press F8, and turn it into a movieclip called hover. Go back to the video_selector movie clip, add an instance of this hover movie clip, give it an instance name of hover, and position it at -30, 0px.


Step 16: Buttons Mask

Select the rounded rectangle from the bg layer and press Copy (Ctrl+C) and Paste (Ctrl+V) on the mask layer and set the mask layer as a mask, like in the picture. Set the buttons and the hover layer as masked layers, too. If you test the movie now, you'll get a nice bar with three buttons. Let's create a label for the buttons.

Flash bandwidth streaming video player

Step 17: Buttons Label

On the text layer, create a new static textfield and enter LQ, MQ and HQ. Leave some spaces to align every label above its corresponding button, or make three separate textfields; either way, it should look like this picture:

Flash bandwidth streaming video player

Step 18: Buttons Functionality

Let's create the functionality. We want to be able to click on a quality button and load a certain movie. Add this code to BandwidthPlayerTest.as, after the connect_stream() function:

private function load_video_lq( e ){
	t.text = this.videos[0];
	stream.play( this.videos[0] );
	video_selector.hover.x = 0;
}
		
private function load_video_mq( e ){
	t.text = this.videos[1];
	stream.play( this.videos[1] );
	video_selector.hover.x = 30;
}
		
private function load_video_hq( e ){
	t.text = this.videos[2];
	stream.play( this.videos[2] );
	video_selector.hover.x = 60;
}

Basically, I have created three more functions to load the three different movies, and have just hardcoded the index, for the purpose of this tutorial. Now, we need to attach these functions to the buttons in the video_selector movie clip.


Step 19: Correct FLV

In the constructor, add this line:

video_selector.alpha = 0;

Add this line to the play_video() function:

create_video_selector(video_id);

And finally let's create the create_video_selector() function:

private function create_video_selector( hover ){
	video_selector.hover.x = hover*30;
	video_selector.alpha = 1;
	video_selector.b1.addEventListener( MouseEvent.CLICK, load_video_lq );
	video_selector.b2.addEventListener( MouseEvent.CLICK, load_video_mq );
	video_selector.b3.addEventListener( MouseEvent.CLICK, load_video_hq );
}

We pass the video_id to this function; it contains either 0, 1 or 2. We then set the hover rectangle to the appropriate position using hover*30. We finally show the video_selector and add the three event listeners to the b1, b2 and b3 buttons.

If you test the movie now, you'll see how the hover clip goes in the right position, depending on the loaded movie. If you click on a different button, the player loads the other FLV. Of course, this is by no means a "complete solution" to accomplish this, especially since we're hardcoding the positions in the videos array and number of movies.


Conclusion

This is the end of the tutorial, I hope you liked it! Leave a comment if you have created improved functionality for the player, I'll be interested to hear what you did!

Advertisement