Accessibility

Creating a video sharing web application using Flex 2 and Flash Media Server 2

Jens Loeffler

Adobe

Companies like YouTube, MySpace, and Photobucket have made a significant impact on the way we consume and socialize video. These sites enable anyone to be the director of his or her own videos, upload them to the web, and become a new online video celebrity. The web is the video distribution channel of the twenty-first century.

This article demonstrates how to create your own social media application in just a few steps. This project provides you with the foundation and will perhaps inspire you to build your own video sharing site.

The goal of this article is to demonstrate how to create a video sharing application as follows:

Figure 1 provides an architectural diagram of the application that I demonstrate in this article.

Video sharing application architecture

Figure 1. Video sharing application architecture

In this article I use Flex 2 to create the visual client interface. I chose this technology because you can leverage its prebuilt UI components and reduce the amount of code required for building the application. If you are an advanced Flash developer, you can leverage the Flex 2 AS 3.0 code and create your own Flash-based version. Adobe Flash CS3 Professional offers the same core functionality, including the upload feature, the NetConnection and NetStream objects to connect and communicate with Flash Media Server, as well as the FLVPlayback component for video playback.

I use Flash Media Server for its streaming and data communication features. It streams the video files and video thumbnails to the user and provides the data required for building the video thumbnail list. It eliminates the need for a database in this case and streamlines the video, audio, and data communication.

Requirements

In order to make the most of this article, you need the following software and files. Please make sure that this software is installed before continuing.

Flex 2

Flash Media Server 2

PHP-enabled web server

Rhozet Carbon Coder (on the server)

Note: Although I chose Rhozet Carbon Coder as the server-side video encoding solution for this article, alternative encoding solutions are available from vendors like On2, Telestream, and Anystream. FFmpeg is a free solution using the lower-quality Sorenson Spark codec. You can easily exchange the server-side encoder in this tutorial with another solution.

Sample files:

Prerequisite knowledge

This article targets intermediate to experienced web developers. Knowledge of Flex and server configuration is recommended.

Creating the video upload component

Flex and Flash enable you to upload files directly to a web server by leveraging the APIs in Adobe Flash Player 9. The technique is similar to HTML forms and requires a server component to receive and save the file. This can be based on a variety of technologies; in this case, it is PHP running on a Windows Apache web server. You could also build this in Adobe ColdFusion.

Building the upload interface in Flex 2

First you need to create a Flex-based upload interface using Flex UI components and the built-in upload feature of the FileReference class. Open your Flex 2 authoring environment and create a new project; call it myTube. Keeping in mind that more elements need to be added to the interface, create an Hbox layout with a text box, input and status fields, and Browse and Upload buttons:

<mx:HBox height="330" id="hbox"   
    <mx:Form>
       <mx:FormItem label="Choose your file:">
          <mx:TextInput id="videofile" editable="false"/>
       </mx:FormItem>
       <mx:FormItem>
          <mx:Button label="Browse" click="onBrowse()"/>
          <mx:Button id="upload" label="Upload" click="onUpload()" enabled="false"/>
       </mx:FormItem>
       <mx:FormItem>
          <mx:Text id="status"/>
       </mx:FormItem>
    </mx:Form>
 </mx:HBox>

This Flex XML structure displays the user interface to enable the user to browse for a video (see Figure 2). When selected, the video filename appears in the text box, and when it's clicked, the Upload button starts uploading the video.

User interface for browsing for a local video file

Figure 2. User interface for browsing for a local video file

To enable this functionality, the onBrowse() and onUpload() functions handling the click events need to be defined in the script:

<?xml version="1.0" encoding="utf-8"?>

<mx:Application xmlns:mx="/2006/mxml" layout="vertical" creationComplete="init()" xmlns:local="*"> <mx:Script>     <![CDATA[        import flash.net.URLRequest;        import flash.net.URLVariables;        private var fileRef:FileReference;        private var nc:NetConnection;        private var server:String;        // Initializes the interface and connects to the Flash Media Server private function init() : void {       server = "localhost";   }        // Browses for a local file and creates a FileReference        private function onBrowse() : void {      fileRef = new FileReference(); fileRef.addEventListener(Event.SELECT, selectHandler);             try {          var success:Boolean = fileRef.browse();         }          catch (error:Error) {          trace("Unable to browse for files.");             }            function selectHandler(event:Event):void {                 videofile.text = fileRef.name;                 status.text = "Ready for upload";      upload.enabled = true;             }         }         // Sends the file to the server     private function onUpload():void {              var request:URLRequest = new URLRequest("http://" + server + "/mytube/upload.php")      fileRef.addEventListener(Event.COMPLETE, completeHandler);              try {                  fileRef.upload(request);                  status.text = "Uploading " + fileRef.name + " ... ";              }              catch (error:Error) {                  status.text = "Upload error";              }              // Upload complete, initializs the video conversion              function completeHandler(event:Event):void {                  status.text = "Upload complete";             }           }     ]]> </mx:Script> <!--the layout definition from above --> </mx:Application>

In the ActionScript code above, the onBrowse() function creates a FileReference object (fileRef) that provides the filename and an upload method, sending the file to the PHP script. Once the upload is completed, the fileReference object triggers the event handler, completeHandler. The handler then displays a confirmation message in the status text field.

Writing a server-side PHP script

To save the video files on a web server, you'll need a server-side PHP script. Create an upload.php file and save it at the path http://[MY_SERVER_DNS]/mytube/upload.php. Add the following script to the file:

<?php
    if(!is_dir("c:/mytube/uploaded")) mkdir("c:/mytube/uploaded", 0755);  
    move_uploaded_file($_FILES['Filedata']['tmp_name'], "c:/mytube/uploaded/".$_FILES['Filedata']['name']);
?>

The objective is to move all uploaded files to the path C:\mytube\uploaded. The script checks if the folder already exists and, if not, creates it. Move_uploaded_file is a PHP function that moves the video file to the target folder once the upload is complete.

Setting up video encoding

This part is specific for Rhozet Carbon Coder. The encoder enables multi-threaded On2 VP6 FLV encoding, which makes it ideal for this application. The current trial version requires a USB dongle, which you can request on their website. Alternative server-side encoding solutions can be configured in a similar fashion using the same command-line approach.

Rhozet Carbon Coder

Rhozet enables you to create encoding profiles and use them with a command-line encoding API (see Figure 3). In this case, a new configuration is created and configured to encode 640 x 480, 30 fps VP6 Flash 8 video, and 128 kbps MBp3 audio. The video bit rate is 800 kbps and the audio bit rate 128 kbps. This certainly requires a high-bandwidth connection.

Configuring encoding settings in Rhozet Carbon Coder

Figure 3. Configuring encoding settings in Rhozet Carbon Coder

To reach a broader audience with less required bandwidth, you can lower these settings to 320 x 240 with 220 kbps video and 96 kbps audio, or encode both qualities and switch to the appropriate video stream based on the available client bandwidth. Flash Media Server 2 automatically detects the client's bandwidth upon connection.

To build the command-line script for the server-side encoding process, you need to know your encoding profile ID. The profile IDs are located in the folder C:\Program Files\Common Files\Rhozet\Carbon Coder\System Presets. A preset is represented by a XML file containing a PresetGUID. This identifier is required later on for the API command-line setup.

Encoding the PHP script

The command-line program that executes the encoding process is the rzcp.exe file, which is located in the installation folder (C:\Program Files\Rhozet\Carbon Coder\rzcp.exe). To execute the batch, create a new PHP file, name it convert.php, and place it on the web server so it can be accessed via http://[MY_SERVER_DNS]/mytube/convert.php:

<?

$filename = $_REQUEST['filename']; $output = runExternal( "c:\\mytube\\carbon\\rzcp.exe -source:\"c:\\mytube\\uploaded\\$filename\" -target:{EA8EFAB9-A292-436D-90F1-0D7CFD6DE3B4} -targetdir=\"C:\\Program Files\\Macromedia\\Flash Media Server 2\\applications\\mytube\\streams\\_definst_\" -prf_ft", &$code); if( $code ) { echo "bad transcoding"; } else { echo "looks good"; } print $output; function runExternal( $cmd, &$code ) { echo "command" . $cmd; $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 2 => array("pipe", "w") // stderr is a file to write to ); $pipes= array(); $process = proc_open($cmd, $descriptorspec, $pipes); $output= ""; if (!is_resource($process)) return false;   #close child's input   fclose($pipes[0]);   stream_set_blocking($pipes[1],false);   stream_set_blocking($pipes[2],false);   $todo=array($pipes[1],$pipes[2]);   while(true) {     $read= array();     if(!feof($pipes[1]) ) $read[]= $pipes[1];     if(!feof($pipes[2]) ) $read[]= $pipes[2];     if(!$read) break;     $ready= stream_select($read, $write=NULL, $ex= NULL, 2);     if ($ready === false) {       echo "died";       break; #should never happen - something died    }    foreach($read as $r) {    $s=fread($r,1024);     $output.=$s;    }   }   fclose($pipes[1]);   fclose($pipes[2]);   $code=proc_close($process);   return $output; } ?>

In this script above, this line configures and starts the encoding process:

$output = runExternal( "c:\\mytube\\carbon\\rzcp.exe -source:\"c:\\mytube\\uploaded\\$filename\"
    -target:{EA8EFAB9-A292-436D-90F1-0D7CFD6DE3B4} -targetdir=\"C:\\Program
    Files\\Macromedia\\Flash Media Server 2\\applications\\mytube\\streams\\_definst_\" -prf_ft", &$code);

For each backslash, an additional backslash is required for proper encoding. RZCP.exe requires the source file, your PresetGUID, and the target directory—which in this case is located in the Flash Media Server applications directory. When this script executes, it starts to encode the video file; once it's finished, it moves the encoded FLV file to Flash Media Server. If you wish to use another encoding solution, you can replace this line with another encoding script.

Adding encoding to the Flex application

The PHP script is complete. Now it's time to add the encoding functionality to the Flex application by adding the following code to the upload completeHandler:

  // Upload complete, initializes the video conversion

function completeHandler(event:Event):void {       status.text = "Upload complete";      videofile.text = "";   var variables:URLVariables = new URLVariables();     variables.filename = fileRef.name;     request.url = "http://" + server + "/mytube/convert.php";     request.method = URLRequestMethod.POST;     request.data = variables;    var loader:URLLoader = new URLLoader();     loader.dataFormat = URLLoaderDataFormat.VARIABLES;  loader.addEventListener(Event.COMPLETE, completeHandler);  loader.addEventListener(flash.events.HTTPStatusEvent.HTTP_STATUS,onStatus);    try {         status.text = "Encoding video..."; loader.load(request); }    catch (error:Error) {   status.text = "Unable to load URL"; }    function onStatus(event:HTTPStatusEvent):void {     if (event.status != 0) {             status.text = "Encoding Error";   } }    function completeHandler(event:Event):void {          status.text = "Encoding Complete";     } }

This script creates the URLVariables object containing the filename, and passes the name to the convert.php file using the URLLoader object. Once the encoding process is completed, it calls the completeHandler() function. This function then creates and updates a video thumbnail list. To enable this, you need to add your own custom script to Flash Media Server.

Configuring Flash Media Server

Now you need to set up and configure Flash Media Server to serve the video thumbnails and stream the video content.

Flash Media Server 2 is Adobe's solution to enabling one-way and multi-way communication for video, audio, and data in a streamlined, efficient way. This application uses multiple features of Flash Media Server:

A limited Developer Edition of Flash Media Server is available for free for you to test and use for application development. There is also a Flash Media Server Professional edition with different profiles and an Origin/Edge architecture for large-scale deployments. The communication is based on Adobe's proprietary RTMP protocol, which also enables server- and client-side function calls. Flash Media Server provides a management console to monitor and control applications. If you are getting started with Flash Media Server, you should familiarize yourself with the console because it's an important component for Flash Media Server development.

Setting the application's internal structure

Flash Media Server enables you to host several applications with multiple instances per application. An example would be a chat application with multiple rooms. The logic of the application could be located in the folder of the application, while users would be able to create different, independent rooms of the chat application. In case there is no instance specified, it will automatically default to _definst_, the default instance of the application. Streams need to be located in the folder \[MYAPPLICATION]\streams\[MYINSTANCENAME]\.

In this application example, the application logic is located in the mytube application folder:

C:\Program Files\Macromedia\Flash Media Server 2\applications\mytube\

Because you don't need different instances of this application, the encoded FLV files are located in the default instance folder of the application:

C:\Program Files\Macromedia\Flash Media Server 2\applications\mytube\streams\_definst_\

Writing the application script

Flash Media Server scripts are located in the main.asc file in the application folder. Create an empty text file and place it here:

C:\Program Files\Macromedia\Flash Media Server 2\applications\mytube\main.asc

A Flash Media Server application does not necessarily require a main.asc file. The mytube application does require it because you want to be able to return a list of all FLV files located in the stream folder. This will be possible with the following script:

application.onConnect = function(clObj) { 
this.acceptConnection(clObj);
}

Client.prototype.getFiles = function() {   var fileList = new File("/streams/_defInst_/");   var temp = fileList.list();   return temp; }

The first function automatically accepts incoming connections. The getFiles() function reads all the filenames of the /streams/_defInst_/ folder and returns it to the client as an array. This allows the Flex application to display thumbnails of the video files.

Building the thumbnail browser

There are several ways to display thumbnails of video files. In this case the application connects to Flash Media Server, starts playing the video, seeks to the beginning of the video, and then pauses. This feature utilizes the rich feature set of Flash Media Server.

As an alternative, Flash and Flex enable you to capture bitmap data of visual elements. You could send this data to a server script and create a JPEG file on the server side. Another option would be to configure the encoder to create an additional JPEG file of the encoded video file. Of the many options available, the MyTube application uses the first solution.

Building the Flex application: data

Now you will create a dynamic data structure in Flex and connect it to the Flash Media Server data source. The video grid updates at the start up of the application as well as once the server has finished encoding a file. Both events trigger the refreshVideos() function:

  // Initializes the interface and connects to the Flash Media Server

private function init() : void { server = "localhost"; nc = new NetConnection(); nc.connect("rtmp://"+server+"/mytube/");    nc.addEventListener(NetStatusEvent.NET_STATUS,onConnect);        } // Video thumbs refresh upon connect private function onConnect(status:Object) : void { refreshVideos(); } // Retrieves the video file list from the FMS private function refreshVideos() : void { nc.call("getFiles", new Responder(onThumbnailListReturned)); } // Creates an ArrayCollection based the video file paths private function onThumbnailListReturned(thumblist:Array):void {  movieThumbnailList = new ArrayCollection();    movieThumbnailList.source = thumblist;   } private function onUpload():void {    <...existing code...>    function completeHandler(event:Event):void {     status.text = "Encoding Complete";
refreshVideos(); }    <...existing code...> }

In the script above, the init() function has been updated to establish a connection to Flash Media Server. Once the NetConnection is established, the onConnect() function is triggered, which then calls the refreshVideos() function. The same function is getting called by the completeHandler() function after a successful upload. The refreshVideos() function calls the getFiles() function on Flash Media Server using the existing NetConnection object and then populates the movieThumbnailList ArrayCollection once the data returns.

Building the Flex application: user interface

Your application needs to connect the data source to the visual user interface components. Along the way, I'll demonstrate how to build a new component to display the video thumbnail.

Each time the function refreshVideos() is called, the list movieThumbnailList is updated with the latest video paths. You could display this data in a datagrid, but in this case you want to create and populate a grid of movie thumbnails.

To show the thumbnails in the grid, you need to add new visual elements to the interface:

<...existing code...>



private
function onThumbClicked(event:Event):void {     }  <...existing code...> </mx:Form> </mx:HBox> <mx:Tile autoLayout="true" id="tile1">    <mx:Repeater id="rp1" dataProvider="{movieThumbnailList}">        <local:ThumbnailViewer connection="{nc}" moviesource="{rp1.currentItem}" thumbClicked="onThumbClicked(event)" id="thumbnailviewer1"/>    </mx:Repeater> </mx:Tile>

The application uses the Tile element of Flex and a new custom component called ThumbnailViewer, which requires the active NetConnection and the video path as arguments. It fires a thumbsClicked event, which then triggers onThumbsClicked(event) to display the selected video in the not-yet-defined video player.

Create a new file called ThumbnailViewer.mxml to build this component:

<?xml version="1.0" encoding="utf-8"?>

<mx:Canvas xmlns:mx="/2006/mxml" width="200" height="175" click="onClick()" creationComplete="buildUI()" xmlns:local="*"> <mx:Metadata>    [Event(name="thumbClicked", type="flash.events.Event")] </mx:Metadata> <mx:Script>    <![CDATA[     import flash.events.*;         import flash.media.Video;         import flash.net.NetConnection;         import flash.net.NetStream;         import flash.display.Sprite;        [Bindable]         public var moviesource:Object;        [Bindable]         public var connection:NetConnection;         private var stream:NetStream;          // Dispatches the click event     private function onClick():void {        dispatchEvent(new Event("thumbClicked"));     }            // Connects to the main NetConnection    public function buildUI() : void {               connectStream();
}        // Creates the NetStream and builds to user interface            private function connectStream():void {  stream = new NetStream(connection);          var videofile:String =  moviesource.name.split("/streams/_defInst_/")[1].split(".flv")[0];          var video:Video = new Video();             video.attachNetStream(stream); stream.play(videofile);      stream.pause(); setTimeout(showPreview,100); description.addChild(video); video.width = 180; video.height = 135;        var tf:TextField = new TextField(); tf.text = videofile.split("_FLASH")[0]; tf.y = -20 tf.autoSize = "left";        var format:TextFormat = new TextFormat(); format.font = "Verdana"; format.color = 0x000000; tf.setTextFormat(format); description.addChild(tf); }        // Seeks to the beginning of the video        private function showPreview():void {      stream.seek(0);         }      ]]> </mx:Script> <mx:Panel width="200" height="175">    <mx:Label id="description"/> </mx:Panel> </mx:Canvas>

The component has a standard behavior pattern once it initializes. The event handler creationComplete() builds the user interface by connecting a new NetStream object to the existing NetConnection object. You need to attach the NetStream to a new Video object to display the video. It then pauses and seeks to the first frame after a short delay. This delay is required to make sure the video playback is initialized before it seeks to the position 0.

Additionally create a text object to display the name of the video file. The onClick() function is defined to fire an event to the main application to display the actual video.

And there you go: video thumbnails appear below the upload component (see Figure 4). The main component is missing, of course: the actual video player.

New visual UI added to the application showing video thumbnails

Figure 4. New visual UI added to the application showing video thumbnails

Adding the video player

For simplicity's sake, the application uses the Flex-based VideoDisplay component. The advantage of this is much simpler code, but a major drawback is the fact that it establishes a second connection to the server. Please feel free to create your own custom video player based on the ThumbnailViewer component and add the shared NetConnection functionality.

You need to enhance the layout one more time:

<mx:HBox height="330" id="hbox">

<mx:Panel width="340" height="306" layout="absolute" scroll="false" id="panel1">    <mx:VideoDisplay id="mainVideo" x="0" y="-1" width="320" height="240"/>    <mx:Button label="Play" id="controls" click="PlayPause()" y="242" x="3"/>    <mx:Button label="Zoom" id="zoombutton" click="zoom()" y="242" x="259"/> </mx:Panel> <mx:Form>    <mx:FormItem label="Choose your file:">             <mx:TextInput id="videofile" editable="false" x="10" y="358"/> </mx:FormItem>    <mx:FormItem> <mx:Button label="Browse" click="onBrowse()" x="180" y="359"/>         <mx:Button id="upload" label="Upload" click="onUpload()" enabled="false" x="256" y="360"/>    </mx:FormItem>    <mx:FormItem>      <mx:Text id="status"/> </mx:FormItem> </mx:Form> </mx:HBox>

This code adds another panel to the HBox with a VideoDisplay object and a Play/Pause and Zoom button. It also updates the x/y positions of some existing components for more accurate positioning. Two more functions are being triggered by the new buttons and need to be defined:

private var playbackstatus:String;



// Toggles between play and pause public function PlayPause() : void {
if
(playbackstatus == "stop") { controls.label = "Stop"; playbackstatus = "play"; mainVideo.play();  } else if (playbackstatus == "play") {     controls.label = "Play";   playbackstatus = "stop";   mainVideo.pause(); } } // Zoom in function. Rearranges the interface to display the video in full resolution. private function zoom() : void { // Code coming soon }

The PlayPause() function toggles the playback. The VideoDisplay component offers play() and pause() methods and also has a source property with the RTMP video path. The video is being set by the onThumbClicked() function:

// Select a video thumbnail
public
function onThumbClicked(event:Event):void {
 var thumb:ThumbnailViewer = event.target as ThumbnailViewer; mainVideo.source = "rtmp://" + server + "/mytube/"+thumb.moviesource.name.split("/streams/_defInst_/")[1].split(".flv")[0]; controls.label = "Stop"; playbackstatus = "play"; mainVideo.play(); }

Voilà! Here is your new video player (see Figure 5).

The video player is now added to the UI.

Figure 5. The video player is now added to the UI.

As I mentioned above, the MyTube application offers high-quality video encoding and playback with a 640 x 480 resolution. Therefore, you need a zoom function to experience the full quality.

The zoom button has already been added to the interface; now it needs to be implemented. To switch between different layouts, the application uses the powerful concept of states in Flex. Flex Builder enables you to manage the states easily within the eclipse interface. To transfer to another state, the currentState property simply needs to be set to another state value:

<mx:states>

<!-- default view --> <mx:State name="standard"/> <!-- fullscreen view -->    <mx:State name="fullscreen" >
<!-- Resize the video -->    <mx:SetProperty target="{mainVideo}" name="width" value="640"/>    <mx:SetProperty target="{mainVideo}" name="height" value="480"/>    <mx:SetProperty target="{mainVideo}" name="y"/>    <mx:SetProperty target="{mainVideo}" name="x"/>    <!-- Remove the upload forms-->  <mx:RemoveChild target="{videofile}"/>    <mx:RemoveChild target="{upload}"/>    <mx:RemoveChild target="{browse}"/>     <!-- Rearrange the interface-->
<mx:RemoveChild
target="{mainVideo}"/>    <mx:AddChild position="firstChild" target="{mainVideo}"/>    <mx:RemoveChild target="{panel1}"/>    <mx:RemoveChild target="{hbox}"/> <!-- Add Zoom out button --> <mx:AddChild relativeTo="{tile1}" position="before">        <mx:Button label="Zoom Out" click="zoomOut()"/>    </mx:AddChild>  </mx:State> </mx:states>

<...existing code...> // Zoom in function. Rearranges the interface to display the video in full resolution. private function zoom() : void { currentState = "fullscreen"; } // Zoom out function. Reverses the interface to the regular view. private function zoomOut() : void {   currentState = "standard"; }

The "fullscreen" state resizes the video player to a full 640 x 480 resolution, removes the upload form, rearranges the interface, and adds a Zoom Out button to be able to return to the original state (see Figure 6).

The UI in "fullscreen" state

Figure 6. The UI in "fullscreen" state

Where to go from here

This sample application shows how easy it is to create your own social media video-sharing application using the latest techniques and efficient server technologies like Flash Media Server, a rich user experience built in Flex 2, and a high-quality video encoding engine.

But this is just the beginning; you can easily enhance this application by improving the interface, adding commenting features, categories, the new hardware-accelerated, full-screen feature of Flash Player or maybe even video annotation or live chats! Endless possibilities. Who knows: maybe your video-based application will be the next Web 2.0 superstar.

About the author

Jens Loeffler is a technical evangelist for Adobe Systems, responsible for driving awareness, adoption, and loyalty of Adobe Flash streaming video products and services within the media and entertainment industry. In his role, he provides consultative leadership for technical and sales activities and supports the core product management and marketing team. Having a sophisticated understanding of Flash technologies and strategy, as well as broad engineering, multimedia and visual skills, Jens is a passionate Flash and Flash Media Server expert. He has worked with Flash and related Adobe products since 1999 and has received several industry awards for his work. Prior to joining Adobe, Jens worked as a senior Flash developer for R/GA and Creative Bubble. He has spoken at such industry events as Streaming Media East, Flash in the Can, and Flashbelt.