Accessibility

Table of Contents

Creating a video player using the state design pattern and ActionScript 3.0

Creating a Flash Media Server 2 application

Now that the structure can support a simple FLV playback system, the next step is to add two more states and see if the state machine can be adapted to a Flash Media Server 2 application. To keep the focus on the design pattern, only two new states will be added to the Play, Stop, and Pause states: Record and Append.

Making the change from a Flash application to a Flash Media Server (FMS) application requires key changes in the FLA script to include a connection to the server, as well as adding Camera and Microphone objects. Other than that, adding the Record and Append states is relatively simple.

The first task when working with state machine models is to update the model. Figure 7 shows the addition of Append and Record. The original three states are pretty much the same as before. Note that the Stop state is the central one for all transitions except for the Play-Pause toggle. To change from any state except Pause, the transition must go first to the Stop state.

Added states for the FMS state machine

Figure 7. Added states for the FMS state machine

As noted at the outset, statecharts make it easy to see required program changes. By adding the new states—Pause, Append, and Record—all of the other states and context need to be changed as well. However, you don't need to change a huge number of conditional statements. The testing application in the Actions panel needs changes as well, but because that code is more like a user of the state machine rather than an actual part of the state machine, it will be handled separately.

Examining the state machine code

The single big script for the entire FMS 2 state machine will still be saved as VideoWorks.as, so that's the file you'll be changing. However, the number of states has grown. I provide each with a comment and number next to it to help you keep track. (If you're more comfortable with smaller scripts, you can break this one down into individual classes.)

Note: Because the Stop state does more than just stop the play—it stops the recording and appending as well—I changed the name of the methods from stopPlay() to stopAll().

Save the following code as State.as:

package
{
  import flash.net.NetStream;
  interface State
  {
  function startPlay(ns:NetStream, flv:String):void;
  function startRecord(ns:NetStream, flv:String):void;
  function startAppend(ns:NetStream, flv:String):void;
  function stopAll(ns:NetStream):void;
  function doPause(ns:NetStream):void;
  }
}

Save the following code as PlayState.as:

package 
{
   //Play State #3
   import flash.net.NetStream;

   class PlayState implements State
   {
      var videoWorks:VideoWorks;
      public function PlayState(videoWorks:VideoWorks)
      {
         trace("--Play State--");
         this.videoWorks=videoWorks;
      }
      public function startPlay(ns:NetStream,flv:String):void
      {
         trace("You're already playing");
      }
      public function stopAll(ns:NetStream):void
      {
         ns.close();
         trace("Stop playing.");
         videoWorks.setState(videoWorks.getStopState());
      }
      public function startRecord(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function startAppend(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function doPause(ns:NetStream):void
      {
         ns.togglePause();
         trace("Start pausing.");
         videoWorks.setState(videoWorks.getPauseState());
      }
   }
}

Save the following code as StopState.as:

package 
{
   //Stop State #2
   import flash.net.NetStream;

   class StopState implements State
   {
      var videoWorks:VideoWorks;
      public function StopState(videoWorks:VideoWorks)
      {
         trace("--Stop State--");
         this.videoWorks=videoWorks;
      }
      public function startPlay(ns:NetStream,flv:String):void
      {
         //Note: the second paramater - 0 - specifies an FLV file
         //the NetStream method is from Client Side
         //Communication ActionScript but works with AS 3.0
         //because ObjectEncoding is imported. 
         ns.play(flv,0);
         trace("Begin playing");
         videoWorks.setState(videoWorks.getPlayState());
      }
      public function startRecord(ns:NetStream,flv:String):void
      {
         ns.publish(flv,"record");
         trace("Begin recording");
         videoWorks.setState(videoWorks.getRecordState());
      }
      public function startAppend(ns:NetStream,flv:String):void
      {
         ns.publish(flv,"append");
         trace("Begin appending");
         videoWorks.setState(videoWorks.getAppendState());
      }
      public function stopAll(ns:NetStream):void
      {
         trace("You're already stopped");
      }
      public function doPause(ns:NetStream):void
      {
         trace("Must be playing to pause.");
      }
   }
}

Save the following code as RecordState.as:

package 
{
   //Record State #5
   import flash.net.NetStream;
   
   class RecordState implements State
   {
      var videoWorks:VideoWorks;
      public function RecordState(videoWorks:VideoWorks)
      {
         trace("--Record State--");
         this.videoWorks=videoWorks;
      }
      public function startPlay(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function stopAll(ns:NetStream):void
      {
         ns.close();
         trace("Stop recording.");
         videoWorks.setState(videoWorks.getStopState());
      }
      public function startRecord(ns:NetStream,flv:String):void
      {
         trace("You're already recording");
      }
      public function startAppend(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function doPause(ns:NetStream):void
      {
         trace("Must be playing to pause.");
      }
   }
}

Save the following code as AppendState.as:

package 
{
   //Append State #6
   import flash.net.NetStream;
   

   class AppendState implements State
   {
      var videoWorks:VideoWorks;
      public function AppendState(videoWorks:VideoWorks)
      {
         trace("--Append State--");
         this.videoWorks=videoWorks;
      }
      public function startPlay(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function stopAll(ns:NetStream):void
      {
         ns.close();
         trace("Stop appending.");
         videoWorks.setState(videoWorks.getStopState());
      }
      public function startRecord(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function startAppend(ns:NetStream,flv:String):void
      {
         trace("You're already appending");
      }
      public function doPause(ns:NetStream):void
      {
         trace("Must be playing to pause.");
      }
   }
}

Save the following code as PauseState.as:

package 
{
   //Pause State #4
   import flash.net.NetStream;

   class PauseState implements State
   {
      var videoWorks:VideoWorks;
      public function PauseState(videoWorks:VideoWorks)
      {
         trace("--Pause State--");
         this.videoWorks=videoWorks;
      }
      public function startPlay(ns:NetStream,flv:String):void
      {
         trace("You have to go to unpause");
      }
      public function stopAll(ns:NetStream):void
      {
         trace("Don't go to Stop from Pause");
      }
      public function startRecord(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function startAppend(ns:NetStream,flv:String):void
      {
         trace("You have to stop first.");
      }
      public function doPause(ns:NetStream):void
      {
         ns.togglePause();
         trace("Quit pausing.");
         videoWorks.setState(videoWorks.getPlayState());
      }
   }

}

Save the following code as VideoWorks.as:

package 
{
   //Context Class #7
   import flash.net.NetStream;
   public class VideoWorks
   {
      var playState:State;
      var stopState:State;
      var recordState:State;
      var appendState:State;
      var pauseState:State;
      var state:State;
      public function VideoWorks ()
      {
         trace ("Video Player is on");
         playState = new PlayState(this);
         stopState = new StopState(this);
         recordState = new RecordState(this);
         appendState = new AppendState(this);
         pauseState=new PauseState(this);
         state=stopState;
      }
      public function startPlay (ns:NetStream,flv:String):void
      {
         state.startPlay (ns,flv);
      }
      public function startRecord (ns:NetStream,flv:String):void
      {
         state.startRecord (ns,flv);
      }
      public function startAppend (ns:NetStream,flv:String):void
      {
         state.startAppend (ns,flv);
      }
      public function stopAll (ns:NetStream):void
      {
         state.stopAll (ns);
      }
      public function doPause (ns:NetStream):void
      {
         state.doPause (ns);
      }
      public function setState (state:State):void
      {
         trace ("A new state is set");
         this.state=state;
      }
      public function getState ():State
      {
         return state;
      }
      public function getPlayState ():State
      {
         return this.playState;
      }
      public function getRecordState ():State
      {
         return this.recordState;
      }
      public function getAppendState ():State
      {
         return this.appendState;
      }
      public function getPauseState ():State
      {
         return this.pauseState;
      }
      public function getStopState () :State
      {
         return this.stopState;
      }
   }
}

That's a lot of code to create something as simple as a video application with record, append, play, pause, and stop functionality. However, this design follows every principle of good OOP. You could expand it further by adding more states to the current system.

Implementing the state machine

To implement the FMS 2 state machine, you need to write an ActionScript file that will instantiate an instance of the VideoWorks class and then add the name of the class in the Document Class window of an FLA file . In the folder where you keep your FMS 2 server-side folders, add a folder named flvstate. After your first recording, FMS 2 automatically generates a folder named flv within the flvstate folder. That's where you need to place all of your FLV files that were not recorded with this application.

Besides connecting to the server, the code must also deal with a cautionary user interface issue. When using a NetStream.togglePause(), you do not want the user to click the Stop button while paused. To fix that problem, the Stop button's visibility is set to false when the pause is toggled on. (The button conveniently disappears, along with the temptation to click it.)

Before you add more code to the script for the Actions panel, you need to add more buttons to the Stage along with giving them instance names. Figure 8 shows the final configuration with the assigned instance names to the objects on the Stage.

Final Flash Media Server player/recorder

Figure 8. Final Flash Media Server player/recorder

The first step is to create a class that will have all of the right packages you need. In an ActionScript file, write the following script and save it as TestVidFMS.as:

package 
{
   //Test Module #8
   import flash.display.Sprite;
   import flash.net.NetConnection;
   import flash.net.NetStream;
   import flash.net.ObjectEncoding;
   import flash.media.Video;
   import flash.media.Camera;
   import flash.media.Microphone;
   import flash.text.TextField;
   import flash.text.TextFieldType;
   import flash.events.MouseEvent;
   import flash.events.NetStatusEvent;

   public class TestVidFMS extends Sprite
   {
      private var nc:NetConnection;
      private var ns:NetStream;
      private var dummy:Object;
      private var flv_txt:TextField;
      private var cam:Camera;
      private var mic:Microphone;
      private var stateVid:VideoWorks;
      private var playCheck:Boolean;
      private var pauseCheck:Boolean;
      private var playBtn:NetBtn;
      private var stopBtn:NetBtn;
      private var pauseBtn:NetBtn;
      private var recordBtn:NetBtn;
      private var appendBtn:NetBtn;

      public function TestVidFMS ()
      {
         //************
         //Add the text field
         //************
         flv_txt= new TextField();
         flv_txt.border=true;
         flv_txt.background=true;
         flv_txt.backgroundColor=0xfab383;
         flv_txt.type=TextFieldType.INPUT;
         flv_txt.x=(550/2)-45;
         flv_txt.y=15;
         flv_txt.width=90;
         flv_txt.height=18;
         addChild (flv_txt);
         //FMS State Machine
         NetConnection.defaultObjectEncoding=flash.net.ObjectEncoding.AMF0;
         nc = new NetConnection();
         nc.objectEncoding = flash.net.ObjectEncoding.AMF0;
         nc.addEventListener (NetStatusEvent.NET_STATUS,checkHookupStatus);
         //Use your own domain/IP address on RTMP
         nc.connect ("rtmp://192.168.0.11/flvstate/flv");
         //OR set up a local connection
         //nc.connect("rtmp:/flvstate/flv");
         //nc.connect(null);

         //Camera & Microphone Settings
         cam = Camera.getCamera();
         cam.setMode (320,240,15);
         cam.setKeyFrameInterval (30);
         cam.setQuality (0,80);
         mic = Microphone.getMicrophone();
         mic.rate=11;

         //Add video object
         vid=new Video(320,240);

         addChild (vid);
         vid.x=(550/2)-(320/2);
         vid.y=40;
         setLocal ();

         //Instantiate State Machine
         stateVid=new VideoWorks;

         //Play, Stop, Record, Append and Pause Buttons
         playBtn=new NetBtn("Play");
         addChild (playBtn);
         playBtn.x=(550/2)-(320/2);
         playBtn.y=300;
         var playCheck:Boolean=false;

         recordBtn=new NetBtn("Record");
         addChild (recordBtn);
         recordBtn.x=(550/2)+((320/2)-60);
         recordBtn.y=300;

         appendBtn=new NetBtn("Append");
         addChild (appendBtn);
         appendBtn.x=(550/2)+((320/2)-60);
         appendBtn.y=330;

         stopBtn=new NetBtn("Stop");
         addChild (stopBtn);
         stopBtn.x=(550/2)-25;
         stopBtn.y=300;

         pauseBtn=new NetBtn("Pause");
         addChild (pauseBtn);
         pauseBtn.x=(550/2)-(320/2);
         pauseBtn.y=330;
         pauseCheck=true;

         //Add Event Listeners
         playBtn.addEventListener (MouseEvent.CLICK,doPlay);
         stopBtn.addEventListener (MouseEvent.CLICK,doStop);
         recordBtn.addEventListener (MouseEvent.CLICK,doRecord);
         appendBtn.addEventListener (MouseEvent.CLICK,doAppend);
         pauseBtn.addEventListener (MouseEvent.CLICK,doPause);

      }
      //Add Control Functions
      function setNet ()
      {
         vid.attachNetStream (ns);
      }
      function setLocal ()
      {
         vid.attachCamera (cam);
      }
      var flv:String;
      function doPlay (e:MouseEvent):void
      {
         if (flv_txt.text != "" && flv_txt.text != "Provide file name")
         {
            setNet ();
            flv_txt.textColor=0x000000;
            flv=flv_txt.text;
            stateVid.startPlay (ns,flv);
            if (! playCheck)
            {
               playCheck=true;
            }
         }
         else
         {
            flv_txt.textColor=0xcc0000;
            flv_txt.text="Provide file name";
         }
      }
      function doRecord (e:MouseEvent):void
      {
         if (flv_txt.text != "" && flv_txt.text != "Provide file name")
         {
            ns.attachAudio (mic);
            ns.attachCamera (cam);
            flv_txt.textColor=0x000000;
            flv=flv_txt.text;
            stateVid.startRecord (ns,flv);
            if (! playCheck)
            {
               playCheck=true;
            }
         }
         else
         {
            flv_txt.textColor=0xcc0000;
            flv_txt.text="Provide file name";
         }
      }
      function doAppend (e:MouseEvent):void
      {
         if (flv_txt.text != "" && flv_txt.text != "Provide file name")
         {
            ns.attachAudio (mic);
            ns.attachCamera (cam);
            flv_txt.textColor=0x000000;
            flv=flv_txt.text;
            stateVid.startAppend (ns,flv);
            if (! playCheck)
            {
               playCheck=true;
            }
         }
         else
         {
            flv_txt.textColor=0xcc0000;
            flv_txt.text="Provide file name";
         }
      }
      function doPause (e:MouseEvent):void
      {
         if (pauseCheck)
         {
            pauseCheck=false;
            if (playCheck)
            {
               stopBtn.visible=false;
            }
            stateVid.doPause (ns);
         }
         else
         {
            pauseCheck=true;
            stopBtn.visible=true;
            stateVid.doPause (ns);
         }
      }
      function doStop (e:MouseEvent):void
      {
         playCheck=false;
         stateVid.stopAll (ns);
         vid.clear ();
         setLocal ();
      }
      //Check connection, instantiate stream, 
      //and set up metadata event handler
      function checkHookupStatus (event:NetStatusEvent):void
      {
         if (event.info.code == "NetConnection.Connect.Success")
         {
            ns = new NetStream(nc);
            dummy=new Object();
            ns.client=dummy;
            dummy.onMetaData=getMeta;
            ns.addEventListener (NetStatusEvent.NET_STATUS,flvCheck);
         }
      }
      //MetaData
      function getMeta (mdata:Object):void
      {
         trace (mdata.duration);
      }
      //Handle flv
      private function flvCheck (event:NetStatusEvent):void
      {
         switch (event.info.code)
         {
            case "NetStream.Play.Stop" :
               stateVid.stopAll(ns);
               setLocal();
               break;
            case "NetStream.Play.StreamNotFound" :
               stateVid.stopAll(ns);
               flv_txt.text="File not found";
               setLocal();
               break;
         }
      }
   }
}

Once you save the TestVidFMS.as file, open a new FLA file and, in the Document Class window, type TestVidFMS. Give it a test ride and you're all set.

As Brian Lesser and Stefan Richter helpfully pointed out to me, one of the critical new pieces of code when working with ActionScript 3.0 is the following line:

NetConnection.defaultObjectEncoding=flash.net.ObjectEncoding.AMF0;

Flash Media Server 2 uses Action Message Format 0, (AMF0) encoding. Flex 2 now uses AMF3. For more information on getting started with ActionScript 3.0 and Flash Media Server 2, see Brian Lesser's article, Building a Simple Flex 2/FMS 2 Test Application.

The bulk of the code stored in the test file deals with setting up the button instances, attaching listeners to the buttons, and creating the button callbacks. The VideoWorks class is instantiated in the stateVid object and the button callbacks simply use the state engine's implementation of the different methods.

Note that if you click Record or Append while playing a video, the Output window lets you know what's going on in the state engine. It tells you what has to be done before you can either record or append a file. The user doesn't end up making a mistake. The concept of a state machine is not only one to optimize your application, but it can include many helpful features that keep the user from having a bad experience.

Where to go from here

For more information about ActionScript 3.0 and design patterns, see ActionScript 3.0 Design Patterns (by Bill Sanders and Chandima Cumaranatunge: O'Reilly, 2007). For some good examples of design patterns used with Flash Communication Server, see Programming Flash Communication Server (by Brian Lesser, Giacomo Guilizzoni, Joey Lott, Robert Reinhardt, and Justin Watkins; O'Reilly).

The ultimate work on Flash and state machines can be found in How to Construct and Use Device Simulations: Flash MX for Interactive Simulation (Jonathan Kaye and David Castillo; Delmar Learning). It provides an illuminating discussion of state machines and statecharts.