Accessibility

Streaming Through Firewalls and Proxies: From the Client's Perspective

Will Law

Chief Technical Officer
HostCast
www.hostcast.com

A common set of requirements arise repeatedly when deploying Macromedia Flash Video on web pages. How often have you required Macromedia Flash Player to:

  1. Negotiate its way through the various firewalls and proxy servers that stand between it and Macromedia Flash Communication Server?
  2. Detect the bandwidth of the client so that the appropriate video may be delivered?
  3. Display the first frame of the video as quickly as possible?

These requirements are quite difficult to satisfy simultaneously. Detecting bandwidth can take several seconds to measure. Allowing the default rtmp connection to fail-over and attempt to tunnel on rtmpt:80 can take up to a minute. So how can you bypass the firewall and proxy obstacles and prevent the empty box from appearing in your video window while Flash Player is waiting for content? In short, how do you maintain the usability of Flash Player? The Media class components will not help you—in fact they exacerbate this situation. You can only pass them a single protocol/port combination and they don't detect bandwidth natively.

A good solution would enable you to specify exactly which set of port and protocol combinations you wanted to use in your connection attempts. Such a solution would also measure bandwidth and detect stream length, and it would do all this while delivering the first frame of the video in as short a time as possible.

This tutorial demonstrates how you can create such a solution, using only some relatively simple client- and server-side ActionScript and no media or communication components. I've included a working example, which you are welcome to pick apart, copy, and extend to suit your own needs.

Requirements

To complete this tutorial you will need to install the following software and files:

Macromedia Flash Communication Server

Macromedia Flash MX 2004

Prerequisite knowledge:

A working knowledge of ActionScript, including an understanding of client- and server-side NetConnection and client-side NetStream functions.

Tutorials and sample files:

Recommended articles and resources:

Developing a Strategy

The good news is that negotiating firewalls is not as mysterious or as sophisticated as it sounds. In fact, your approach can be rather simple:

  1. Try a series of port and protocol combinations and take the first one that works. While doing so, be careful not to try them too quickly in succession and to clean up those that fail. Leave at least 1,500 milliseconds between connection attempts.
  2. When you have a good connection, immediately play just the first frame of the video. This enables you to get a representation of the video in place on the web page, without having to store a separate JPEG file as a placeholder. Use the best quality video that you have, because even modem users can receive one frame of high-quality video relatively quickly.
  3. As soon as the first frame is in place, detect the bandwidth. Bandwidth detection can take up to 5 or more seconds, depending on the client connection. While the user is still watching the web page load and you have a frame grab to hide behind, perform this time-intensive task. By the time the user figures out that this is a video and that they can play it, the bandwidth detection job will be done.
  4. After bandwidth is detected, read the length of the file on the server. You should do this because in most instances you will need to know the length to build a scrub bar, and you cannot rely on duration information to be written correctly into the header of the Flash Video (FLV) file. It is much more reliable (and very quick) to get the server to read it.
  5. After you have the stream length, expose (or enable) the playback controls. This prevents users from attempting to play back the video before you know the speed at which they are connected. When they click the Play button, you can direct them to the appropriate stream for their bandwidth on the server. This may be an entirely different stream from the one you used to generate the first frame.

The NCManager class

Most of the functionality you may want in a NetConnection class can be wrapped up in a custom class, which makes it convenient to use in multiple projects. Examine the contents of the NCManager.as file in a text editor and use it as the basis for writing your own custom class. (This file, which is part of the sample files of this tutorial, is based on the NCManager class contained inside the FLVPlayer, by Giacomo "Peldi" Guilizzoni). The NCManager class starts by importing the EventDispatcher class and defining some default functions that this class requires. You'll use events to disseminate information about the progress, results, and errors associated with the connection.

import mx.events.EventDispatcher;
class NCManager extends Object {
    var addEventListener:Function;
    var removeEventListener:Function;
    var dispatchEvent:Function;
    var dispatchQueue:Function;

You then define a default connection list. This list consists of an array of objects. Each object possesses attributes that define the protocol and port on which you want to attempt a connection. You can adjust this array to suit the characteristics of your Flash Communication Server installation, which may not have all protocol/port combinations enabled. The protocols and ports will be attempted in the order in which they are defined in this array, so place the most common combinations first.

    private var k_DEFAULTCONNLIST = [
           {protocol:"rtmp", port:1935}, 
           {protocol:"rtmp", port:443}, 
           {protocol:"rtmpt", port:80}
    ];

The public function connect is called to initiate the connection attempts. It takes the FCS server name, FCS application name, and a connection list as parameters. If the connection list is not passed, then the default connection list is used.

function connect(p_serverName:String,p_appName:String,p_connList:Array) {

The connect function cycles through the connection list and creates a connection attempt for each protocol/port combination. Notice that there are two functions that are called by the server-side code. The first function is onBWCheck, whose job is to increment and then echo back a counter. This counter serves as the payload during the bandwidth test. The second function is onBWDone, which is called after the bandwidth test is complete to dispatch an event communicating the measured bandwidth value to all listeners.

this["nc"+i].onBWDone = function(p_bw) {
    this.owner.dispatchEvent({type:"ncBandWidth", kbps:p_bw});
};
this["nc"+i].onBWCheck = function(counter) {
    return ++counter;
};    

When a valid NetConnection is found, an event is dispatched that contains a reference to that NetConnection instance as well as the port and protocol that were successful in penetrating the firewall. The code then loops through all the open connections and shuts down the connections that were unsuccessful.

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];
        }
    }
}

The nextConnect function walks through the connection list, trying each protocol and port combination. Notice that the interval at which it does this is 1,500 ms. It is important that you do not attempt subsequent connections too quickly. Very fast, multiple connections from clients could place undue load on the FCS server. In fact, Macromedia strongly recommends that you do not attempt four or more connection attempts with the interval set to 0 (that is, execute the connection attempts in parallel).

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);
    }
}

If no connections are successful within the default timeout period (initially set to 60 seconds), then the timeout interval will trigger the onFlashComConnectTimeOut function. This function issues an event and cleans up the open NetConnection instances.

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];
        }
    }
}

A series of public utility functions are defined. getStreamLength accepts a stream name and calls a matching server-side function that reads the duration of the stream. The response triggers an event, with the event object containg the streamlength and the name of the stream.

function getStreamLength(streamName:String) {
    var res = new Object();
    res.owner = this;
    res.name = streamName;
    res.onResult = function(p_length) {
         this.owner.dispatchEvent({type:"ncStreamLength", length:p_length, name:res.name});
};
    m_validNetConnection.call("getStreamLength", res, streamName);
}

getBandWidth calls a server-side function that initiates the bandwidth check. When complete, the onBWDone function of the valid NetConnection instance is called.

function getBandWidth(Void):Void {
    m_validNetConnection.call("checkBandwidth", null);
}
getActiveConnection is a utility function which simply returns a reference to the valid NetConnection, or null if it does not exist.
function getActiveConnection(Void):NetConnection {
    return m_validNetConnection == undefined ? null : m_validNetConnection;
}

The Server-Side Code

The focus of this tutorial is firewall negotiation, rather than bandwidth detection. For this reason, you should use the bandwidth detection code already explained in the Macromedia Developer Center article Delivering Flash Video: Dynamic Bandwidth Detection with Macromedia Flash Communication Server by Stefan Richter. In this note, Richter uses code that was developed by Pritham Shetty, director of engineering for Flash Communication Server, and published by Guilizzoni on his blog.

In order to integrate this code with your NCManager class, you will need to add two additional functions to the client class. The getStreamLength function accepts a stream name (without the .flv extension) and returns its duration in seconds.

Client.prototype.getStreamLength = function(p_streamName) {
    trace("getStreamlength called for " + p_streamName);
    return Stream.length(p_streamName);
};

The checkBandwidth function calls the calculateClientBw function on the application object. Once the bandwidth check is complete, the onBWDone function on the client side of the NetConnection instance is called. You may recall that you've handled that response in the previous section.

Client.prototype.checkBandwidth = function() {
    trace("CheckBandwidth called");
    application.calculateClientBw(this);
};

Calling the Class from Within Your FLA File

In a full ActionScript 2.0, class-driven implementation, the NCManager class would be called by a parent class. Since establishing that full hierarchy would make for a long tutorial, you can instead refer to the test.fla file (part of this tutorial's sample files download), which calls the class directly and in an easy-to-understand manner.

Rather than go through the code line by line, just examine the main elements of the program flow:

  1. The NCManager class is imported and instantiated, and then the connect method is called. Note that the library contains a movie clip symbol, which holds a reference to this class. The linkage is NCManager, the ActionScript 2.0 class is NCManager and it is set to export for ActionScript in first frame. Note also that the optional connection list is not being passed (it would be the third parameter), meaning that the default connection list within the class will be used.

    import NCManager;
    var ncm:NCManager = new NCManager();
    ncm.connect(k_SERVERNAME, k_APPNAME);
    
  2. The first good connection made by the class will be captured by the ncmListener event listener object. The first thing it does is play the first frame of the video. This is a departure from most other workflows, which usally try to detect the bandwidth immediately.

    ncmListener.ncConnected = function(evt:Object) {
        trace("["+Math.round(getTimer()-k_STARTTIME)+"ms] Successfully connected using "+evt.protocol+":"+evt.port);
        playFirstFrame(evt.nc);
    };
  3. The first frame has finished displaying once the stream onStatus function catches the "NetStream.Play.Stop" event. The getBandWidth method is then called on the NCManager class instance. firstFramePending is used as a flag so that getBandWidth is called only this first time and not every time that the stream stops.

    ns.onStatus = function(info) {
        if (info.code == "NetStream.Play.Stop" && firstFramePending) {
            firstFramePending = false;
            ncm.getBandWidth();
        }
    };
  4. A successful bandwidth measurement is captured by the ncmListener event listener object. The program then decides whether to deliver the high or low bandwidth file to the user, based on a preconfigured bandwidth threshold. The getStreamlength method is then called on the NCManager class instance, to retrieve the length of this file.

    ncmListener.ncBandWidth = function(evt:Object) {
        fileName = (evt.kbps>300) ? k_FILENAME_HIGH : k_FILENAME_LOW;
        ncm.getStreamLength(fileName);
    };
  5. The streamlength response is trapped again by ncmListener. With this in hand, the system is finally ready to allow the user to play the file and the play button is exposed.
    ncmListener.ncStreamLength = function(evt:Object) { 
        addPlayButton();
    };
    

Putting It All Together

All the files necessary to create a working example are included in the sample files ZIP file, which you can download from the first page of this tutorial. Download and unzip the contents and then follow these deployment instructions:

  1. In the <applications> directory of your Flash Communication Server, create a new folder called test.
  2. Into this folder copy the file main.asc and then create another folder called streams.
  3. Inside the streams folder, create a folder called _definst_.
  4. Into the _definst_ folder, copy the files high_sample.flv and low_sample.flv.
  5. On the client, copy the test.fla and NCManager.as files into the same directory.
  6. Open the file test.fla inside Flash MX 2004 and make sure that k_SERVERNAME points at the address of your FCS server.
  7. Press Control+Enter to run the movie.
  8. View the trace statements, which provide information about the progress of the code.

Figure 1 shows a screen shot of the FLA file running. In this example, the successful connection occurred after 133 ms using rtmp on port 1935 and the first frame was loaded at 390 ms. The bandwidth was then measured, the streamlength obtained and the play control exposed 750 ms after the connections was initiated. The user interface in this example is exceedingly plain; of course, you could dress it up into a full-fledged control interface with a scrub bar and multiple control buttons.

The test.fla file in operation

Figure 1: The test.fla file in operation

Summary

To make the most of the sample code provided with this tutorial, you may want to extend the NCManager class to wrap functions that you use repeatedly when making a connection, such as authentication routines, quality-of-service checks, or data handling functions. For a great example on how to write a scalable, class-based, skinnable video player, refer to the source code of the Streaming FLVplayer. If you build anything close to Guilizzoni's solution, you'll be doing fine!

Here are the main points you should take away from this article:

About the author

Will Law is CTO of HostCast, a consulting and development firm in San Francisco. He focuses on Flash video and any bright, shiny thing you hang over his crib.