Requirements

Prerequisite knowledge
Because ActionScript 3.0 is so new, any experience you have with Flex 2 will be helpful, but a good grounding in ActionScript 2.0 should be sufficient to understand the structures.


User level
Advanced

Required products

Adobe Animate CC
Flash Media Server (Download trial)
Flash Player

Sample files
state_machine_as3.zip (2359 KB)


When you create a Flash Media Server 2 application, you typically place emphasis on optimizing the quality of the communications. That is certainly as it should be. Likewise, quality object-oriented programming (OOP) is another priority. One standard in OOP is design patterns—abstract concepts for solving recurring problems using designs that optimize OOP when you apply them appropriately. The seminal work in design patterns is Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm Ralph Johnson, and John Vlissides, affectionately known as "The Gang of Four" or simply GoF.

One design pattern in particular, the state design pattern (or SDP), focuses on the different states in an application, transitions between states, and the different behaviors within a state. A simple Flash video player application, for example, has two states: Stop and Play. In the Stop state, the video is not playing; in the Play state, a video is playing. Furthermore, the player transitions from the Stop state to the Play state using a method that changes the application's state. Likewise, it transitions from Play to Stop using a different transition and method to make it happen.

An interface holds the transitions, and each state implements the transitions as methods that are unique to that state. Each method is implemented differently depending on the context of its use. For example, the startPlay() method would do one thing in the Stop state and something entirely different the Play state, even though startPlay() is part of both states. To understand and appreciate the value of the SDP, it helps to know something about state machines.

This article begins with a simple two-state application that plays and stops playing an FLV file. It requires only Flash CS3 Professional and Flash Player 9, which you can download from the links below. The initial application introduces the basics of a state machine and the state design pattern. (With a few changes, the applications in this article will work with Adobe Flex 2 as well.)

The application is then expanded into a more robust one using the same state structure and incorporating Flash Media Server 2. This illustrates both the expandability of an application using a design pattern and the process of incorporating Flash Media Server 2 into that design using ActionScript 3.0.

State machines and statecharts

A state machine is the conceptual model of states you would be using in an application with the SDP; the actual application is the state engine. So if my video player application is designed around key states, that design represents the state machine. However, one need not worry about which is which because they're used differently and interchangeably in the literature on state machines. The important point to keep in mind is the idea of states and their differing contextual behavior.

Rather than beginning with the usual diagrams associated with design patterns, I'll start with a statechart. At its most basic level, a statechart is an illustration of an application's states and transitions. As such, it is a model for the state machine and engine. Taking a simple video player application, you can see the Play and Stop states. When the application is first run, the application enters the Stop state and can transition only to the Play state (see Figure 1).

Statechart showing two states
Figure 1. Statechart showing two states

The line going from the black dot to the Stop state shows the Application Not Running state. For all intents and purposes, however, assume that the starting point is the Stop state. This could be illustrated in a hierarchical state with Application Running and Application Not Running states, or you could even place the whole hierarchy into Computer On and Computer Off states, but that's not too useful because you aren't coding to those states.

Before I discuss getting from one state to another, consider what each state can actually do. In the Stop state, I can initiate only the Play state. That is, I cannot stop in the Stop state because I'm already stopped. By the same token, if I'm in the Play state, the only thing I can do is transition to the Stop state.

Transitions

The transitions in a state machine are the actions that change states. In the simple statechart, the line from Stop to Play would be a startPlay() method of some sort; and from Play to Stop, it would be a stopPlay() method. As more states are added, you might find that you cannot transition directly from one state to another. Rather, you have to go through a series of states to get where you want to go. As you will see further on, if you're in the Stop state, you cannot go directly to the Pause state. You have to go first to the Play state before going to the Pause state.

Triggers

Finally, to initiate a transition, you need some kind of trigger. A trigger is any event that initiates a transition from one state to another. Usually we think of some kind of user action as a trigger, such as a mouse movement or button click. However, in simulations certain states can be triggered by ongoing conditions, such as an FLV ending play, draining a simulated battery, or a collision with an object. Likewise, triggers are subject to contexts and should work only in the appropriate contexts to initiate a state. So while you might use a Play button to initiate the Play state from the Stop state, it should not trigger a Play state from the Play state.

Often triggers are placed along with the transitions on the statecharts. This helps identify the trigger events and the transitions they trigger. Figure 2 shows the statechart updated to include both the triggers and transitions they initiate.

Triggers in a statechart
Figure 2. Triggers in a statechart

If you're interested in more information about using state engines, statecharts, and the more general aspects of working with Flash and states, see Flash MX for Interactive Simulation by Jonathan Kaye and David Castillo (Thomson, 2003). Although it goes back a couple generations of Flash, the book is timeless in its concepts and shows some very smooth device simulations.

State design pattern

Fortunately, one of the original design patterns that GoF described is the State pattern. Closely resembling the Strategy pattern, the State pattern is used when an application's behavior depends on changing states at runtime or has complex conditional statements that branch depending on a current state (see Figure 3). When the internal states change, an object alters its behavior when designed using the State pattern.

State design structure
Figure 3. State design structure

Using the SDP, all of the behaviors (methods) for a single state are placed into single objects (concrete states), and all transition behaviors for the application (state machine) are placed into a single interface. Each state object implements the interface in a fashion appropriate for the state. Because of this structure, no conditional statements are required to branch differentially depending on the current state. Rather than writing complex conditional statements, the individual state objects define how the methods are to behave for that state.

For example, with a two-state machine (Play and Stop) the following pseudo code could direct the state behavior to start playing the video depending on the state machine's current state:

function doPlay():void{ if(state == Play) { trace("You're already playing."); } else if (state == Stop) { trace("Go to the Play state."); } }

Note: By the way, an important but small difference between ActionScript 2.0 and ActionScript 3.0 is that all void special types are in lowercase. In ActionScript 2.0 the first letter was in caps: Void . Watch out for that!

With a couple of states, that's not too difficult. As you add states, however, things get more complicated and you're swimming in a sea of conditional statements that all have to work in sync.

The alternative is to set up "contextual" behavior using a State pattern. For example, the following code has two different objects with different implementations of behaviors from an interface:

//Interface interface State { function startPlay():void; function stopPlay():void; } //Play State object class PlayState implements State { public function startPlay():void { trace("You're already playing"); } public function stopPlay():void { trace("Go to the Stop state."); } } //Stop State object class StopState implements State { public function startPlay():void { trace("Go to the Play state."); } public function stopPlay():void { trace("You're already stopped"); } }

As you can see, the behaviors (methods) have different implementations in the different states. When you add more states, all you need to do is add their transitional behaviors to the interface and create a new concrete state (class) that implements them. Each new behavior needs to be added to the existing state classes.

Context manager in a state design pattern

To manage the states and their transitions, you need some kind of management object—something to keep track of everything in the state machine. In Figure 3 the Context box is the abstraction of the state engine. The context manages the different states that make up the state machine and contain the different states. Figure 4 shows a more concrete representation of what needs to be transformed.

Video player state diagram
Figure 4. Video player state diagram

Creating a context class

In looking at the example of creating a simple video player, we need a context that will serve to get and set the different states. So the next phase will be to look at a class (object) that does just that. Let's first take a look at the class, and then you'll see what's going on:

01 package 02 { 03 //Context class 04 class VideoWorks 05 { 06 var playState:State; 07 var stopState:State; 08 var state:State; 09 public function VideoWorks() 10 { 11 trace("Video Player is On"); 12 playState = new PlayState(this); 13 stopState = new StopState(this); 14 state=stopState; 15 } 16 public function startPlay():void 17 { 18 state.startPlay(); 19 } 20 public function stopPlay():void 21 { 22 state.stopPlay(); 23 } 24 public function setState(state:State):void 25 { 26 trace("A new state is set"); 27 this.state=state; 28 } 29 public function getState():State 30 { 31 return state; 32 } 33 public function getPlayState():State 34 { 35 return this.playState; 36 } 37 public function getStopState():State 38 { 39 return this.stopState; 40 } 41 } 42 }

Initially, in lines 6–14, the script instantiates three State objects—one of each of the two you designed (PlayState and StopState), and one (state) that acts as a variable to hold the current state. Because the state machine begins in the Stop state, the state variable is assigned the Stop state. (This works just like your car in the morning before you change it from the Off state to the On state.)

Next, the two behaviors from the State interface are specified in terms of the current state's context (lines 16–23). Although you're going to have to add some code to the two state classes for it to work with the context class, for now think of what will happen in the two different states when those behaviors are executed. For example, in the Play state the startPlay() method doesn't do anything but in the Stop state it switches to the Play state.

Finally, add the getter and setter methods (lines 24–40). You need a total of six methods—a set and get function for each of the three state instances. The setters return nothing and the getters return a State object.

Completing a state class

To get everything working, you need to revise the state classes to include the reference to the context: VideoWorks. Also, because you're working with ActionScript 3.0, all of the classes and interface need to be in a package container.

Note: All files are grouped into a single unreferenced folder for purposes of simplicity. However, typically you would be using multiple folders for organizing your files. Each package would import the appropriate related files. But by using a single folder, this application reduces a layer of complexity.

Save the following code as State.as:

package { //State Machine Interface interface State { function startPlay():void; function stopPlay():void; } }

Save the following code as PlayState.as:

package { //Play State class PlayState implements State { var videoWorks:VideoWorks; public function PlayState(videoWorks:VideoWorks) { trace("--Play State--"); this.videoWorks=videoWorks; } public function startPlay():void { trace("You're already playing"); } public function stopPlay():void { trace("Stop playing."); videoWorks.setState(videoWorks.getStopState()); } }

Save the following code as StopState.as:

package { //Stop State; class StopState implements State { var videoWorks:VideoWorks; public function StopState(videoWorks:VideoWorks) { trace("--Stop State--"); this.videoWorks=videoWorks; } public function startPlay():void { trace("Begin playing"); videoWorks.setState(videoWorks.getPlayState()); } public function stopPlay():void { trace("You're already stopped"); } } }

Save the following code as VideoWorks.as:

package { //Context class class VideoWorks { var playState:State; var stopState:State; var state:State; public function VideoWorks() { trace("Video Player is On"); playState = new PlayState(this); stopState = new StopState(this); state=stopState; } public function startPlay():void { state.startPlay(); } public function stopPlay():void { state.stopPlay(); } 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 getStopState():State { return this.stopState; } } }

To test the state engine completely, you need to test each state. By calling each state from a different state as well as from within itself, you can see the contextual nature of the state machine. The following steps show you how:

  1. In order to use the Stage and execute a test of the program in ActionScript 3.0, you need to use the Display class. In general you can use either a sprite or a movie clip, but because you're not using a Timeline here, use a sprite. Open a new ActionScript file and enter the following code as TestState.as:
package { //Test states import flash.display.Sprite; public class TestState extends Sprite { public function TestState():void { var test:VideoWorks = new VideoWorks(); test.startPlay(); test.startPlay(); test.stopPlay(); test.stopPlay(); } } }
  1. Open a new Flash document and display the Properties panel. Click on the Stage and, in the Properties panel, type TestState in the Document Class text window.
  2. Test the movie by pressing Control+Enter (Command+Return) just as you would for any application test. In the Output window, you should see the following:
Video Player is On --Play State-- --Stop State-- Begin playing A new state is set You're already playing Stop playing. A new state is set You're already stopped

Both the VideoWorks class and the PlayState and StopState classes include trace()  a statement to indicate their instantiation, and appear as soon as you test the script. Because the initial state is Stop, when the script calls the first startPlay() method, you will see "Begin playing" in the output window, along with the message, "A new state is set."

However, note that when the same state is called a second time, the state responds with the message, "You're already playing," and no state transition occurs. However, as soon as the script calls the first stopPlay() method, it notes both that the playing has stopped and a new state is set. The second call of stopPlay(), though, responds with "You're already stopped."

The advantage of making each state self-aware becomes clear when you consider the alternative. For example, if I knock together an application that I want to use for playing FLV files, what happens if I click the Play button when it's already playing? It starts playing the video all over again because it has no idea what state it's in. However, if I click Play and have a state machine design, the application knows it's in a Play state and will require the user to transition first to the Stop state before starting the Play over again. Of course, you can design the state machine to restart playing if that's what you want to happen on all Play commands.

The point is that you can design the states to do exactly what you want—not what they will do automatically if left unconsidered.

From the conceptual to the functional

All you've seen so far has been the output of trace() statements to help understand how a state design pattern and state machine works. To add something useful, you need to include a reference to both a NetStream() object and a string for referencing a FLV file. However, you need only a string reference for playing the video because you can stop it simply by closing the NetStream() instance.

Below is an update to the VideoWorks.as file. Importantly, I've added the NetConnection and NetStream classes from the flash.net package because both classes have been included in the user package created. While the new script has set up the state machine to actually play and stop a video, all of the trace() statements have been left in place:

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

Save the following code as PlayState.as:

package { import flash.net.NetStream; //Play State 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 stopPlay(ns:NetStream):void { ns.close(); trace("Stop playing."); videoWorks.setState(videoWorks.getStopState()); } } }

Save the following code as StopState.as:

package { 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 { ns.play(flv); trace("Begin playing"); videoWorks.setState(videoWorks.getPlayState()); } public function stopPlay(ns:NetStream):void { trace("You're already stopped"); } } }

Save the following code as VideoWorks.as:

package { import flash.net.NetStream; class VideoWorks { var playState:State; var stopState:State; var state:State; public function VideoWorks() { trace("Video Player is on"); playState = new PlayState(this); stopState = new StopState(this); state=stopState; } public function startPlay(ns,flv):void { state.startPlay(ns,flv); } public function stopPlay(ns):void { state.stopPlay(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 getStopState():State { return this.stopState; } } }

The FLA that tests the state engine uses the new ActionScript 3.0 SimpleButton class to create buttons for starting and stopping the video play. Like most classes, this one is set up for reuse whenever an application requires a button labeled with text. A second class file, BtnState.as, provides the necessary styling context. Used together, the two classes can easily create and style different buttons. Thus, before writing the test code in the Flash authoring environment, create the following classes in an ActionScript file and save as BtnState.as and NetBtn.as, respectively, in the same folder as the VideoWorks.as file:

package { //Create a button that will display states #5 import flash.display.Sprite; import flash.display.Shape; import flash.text.TextFormat; import flash.text.TextField; import flash.text.TextFieldAutoSize; class BtnState extends Sprite { private var btnLabel:TextField; private var btnWidth:Number; private var bkground:Shape public function BtnState (color:uint,color2:uint,btnLabelText:String) { btnLabel=new TextField ; btnLabel.text=btnLabelText; btnLabel.x=5; btnLabel.autoSize=TextFieldAutoSize.LEFT; var format:TextFormat=new TextFormat("Verdana"); format.size=12; btnLabel.setTextFormat (format); btnWidth=btnLabel.textWidth + 10; bkground=new Shape; bkground.graphics.beginFill (color); bkground.graphics.lineStyle (2,color2); bkground.graphics.drawRect (0,0,btnWidth,18); addChild (bkground); addChild (btnLabel); } } } package { //Button for transition triggers #6 import flash.display.SimpleButton; public class NetBtn extends SimpleButton { public function NetBtn (txt:String) { upState = new BtnState(0xfab383, 0x9e0039,txt); downState = new BtnState(0xffffff,0x9e0039, txt); overState= new BtnState (0x9e0039,0xfab383,txt); hitTestState=upState; } } }

Keep BtnState.as and NetBtn.as handy; they will be used to provide buttons in all of the developing examples.

Staging and coding the application

The final step is to create a script for bringing everything to the Stage. The first step is to build a testing class as you did with the abstract state machine. Open up a new ActionScript file and add the following code (saving it as TestVid.as):

package { //Test State Machine #7 import flash.display.Sprite; import flash.net.NetConnection; import flash.net.NetStream; import flash.media.Video; import flash.text.TextField; import flash.text.TextFieldType; import flash.events.MouseEvent; import flash.events.NetStatusEvent; public class TestVid extends Sprite { private var nc:NetConnection=new NetConnection(); private var ns:NetStream; private var vid:Video=new Video(320,240); private var vidTest:VideoWorks; private var playBtn:NetBtn; private var stopBtn:NetBtn; private var flv:String; private var flv_txt:TextField; private var dummy:Object; public function TestVid () { nc.connect (null); ns=new NetStream(nc); addChild (vid); vid.x=(stage.stageWidth/2)-(vid.width/2); vid.y=(stage.stageHeight/2)-(vid.height/2); //Instantiate State Machine vidTest=new VideoWorks(); //Play and Stop Buttons playBtn=new NetBtn("Play"); addChild (playBtn); playBtn.x=(stage.stageWidth/2)-50; playBtn.y=350; stopBtn=new NetBtn("Stop"); addChild (stopBtn); stopBtn.x=(stage.stageWidth/2)+50; stopBtn.y=350; //Add Event Listeners playBtn.addEventListener (MouseEvent.CLICK,doPlay); stopBtn.addEventListener (MouseEvent.CLICK,doStop); //Add the text field flv_txt= new TextField(); flv_txt.border=true; flv_txt.borderColor=0x9e0039; flv_txt.background=true; flv_txt.backgroundColor=0xfab383; flv_txt.type=TextFieldType.INPUT; flv_txt.x=(stage.stageWidth/2)-45; flv_txt.y=10; flv_txt.width=90; flv_txt.height=16; addChild (flv_txt); //This prevents a MetaData error being thrown dummy=new Object(); ns.client=dummy; dummy.onMetaData=getMeta; //NetStream ns.addEventListener (NetStatusEvent.NET_STATUS, flvCheck); } //MetaData private function getMeta (mdata:Object):void { trace (mdata.duration); } //Handle flv private function flvCheck (event:NetStatusEvent):void { switch (event.info.code) { case "NetStream.Play.Stop" : vidTest.stopPlay (ns); vid.clear (); break; case "NetStream.Play.StreamNotFound" : vidTest.stopPlay (ns); flv_txt.text="File not found"; break; } } //Start play private function doPlay (e:MouseEvent):void { if (flv_txt.text != "" && flv_txt.text != "Provide file name") { flv_txt.textColor=0x000000; flv=flv_txt.text + ".flv"; vidTest.startPlay (ns,flv); vid.attachNetStream (ns); } else { flv_txt.textColor=0xcc0000; flv_txt.text="Provide file name"; } } //Stop play private function doStop (e:MouseEvent):void { vidTest.stopPlay (ns); vid.clear (); } } }

As you can see, the code includes importing several different packages. In using ActionScript 3.0, you will find that it is crucial to import the right packages for your applications. By avoiding the use of the wildcard character (*), the application imports only exactly what you need and no more, thereby keeping the overhead down.

Open a new Flash document and save it in the same folder as all of the other files for this application. In the Document Class window of the Property inspector, type TestVid
and save the file.

To test actual the application, you will need an FLV file. You can convert an existing video file (AVI or MOV format) or use any FLV file on hand. Place the file in the same folder as the application. Figure 5 shows what your initial state machine looks like when completed.

Initial state of the video player
Figure 5. Initial state of the video player

The UI is simple and relates to the transitions: Stop and Start (playing video). Furthermore, you can see the relationship between the video playing and the trace() statement showing what happens when you click the button. For example, if you click Start and the video is already playing, nothing new occurs because the startPlay() function in the Play state does nothing other than offering a trace() statement to the effect that you're already playing. As an added bonus, you get to see the length of your FLV file from the metadata function.

Adding states and hierarchical state machines

A fundamental feature of virtually all design patterns is their ability to expand and accept change. The kind of change you're expecting in an application to some extent determines the type of design pattern you select. In this particular application, you are adding states.

The first state to add to the state machine is a Pause state. This state exists only in the Play state; you cannot get there directly from the Stop state. Once in the Play state, the user can turn the Pause state on and off. A hierarchical state diagram depicts this new state accurately (see Figure 6).

Hierarchical statechart including the Pause state
Figure 6. Hierarchical statechart including the Pause state

The hierarchy is a simple one. The first level is the Play and Stop states. Within the Play state is the Pause and No Pause states.

Because the Pause function is a toggle between the Play and Pause states, the No Pause state is exactly the same as the Play state. So rather than creating "pause start" and "pause stop" functions, you can establish a Do Pause behavior that acts differently in either state. In the Pause state, the Do Pause behavior returns to the default Play state; in the Play state, it goes to the Pause state.

ActionScript 3.0 has two different options for creating a Pause state. First, you can create controls around NetStream.pause() and NetStream.resume() that stand as two different NetStream methods. In previous versions of ActionScript, only the pause() method was available and it worked as a toggle. Second, you can use the new method NetStream.togglePause() . This new method is actually just the old method with a new name. Sticking with the statechart depicted in Figure 6, this application uses the togglePause() method.

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.


More Like This