So I’ve been working on creating a fully fledged player for the web site 1Click2Fame.com. A lot of people who have been following me on twitter have been asking me to publish my code, so here’s what you’ve all been waiting for.
This isn’t just your average FLV video player. I’m not going to go into great depths on creating stuff like the stop button, the seek bar and what not, I believe in creating clean and reusable code, so this tutorial is more about creating a great chromeless player API and then allowing you to go crazy with the UI.
So let’s begin! Start by firing up your favourite Actionscript 3 IDE and create a new project. All my classes belong in the package com.firestartermedia.as3 so subsequently, my chromeless player is part of my com.firestartermedia.as3.display.component, a lot maybe, but I do a lot of Actionscript 3 coding.
Let’s start with what we want to achieve from our chromeless player:
- We want it to play FLVs, obviously
- We want to be able to start, pause and resume it
- We want it to inform us of cue points and give us the ability to add our own cue points
- We want to be able to resize it without affecting it’s aspect ratio
- We want to be able to get it’s current loading and playing states
A lot maybe, but it’s all very achievable. Ready for the code? Here it is:
VideoPlayerChromeless.as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | package com.firestartermedia.lib.as3.display.component { import com.firestartermedia.lib.as3.events.VideoPlayerEvent; import com.firestartermedia.lib.as3.utils.ArrayUtil; import com.firestartermedia.lib.as3.utils.NumberUtil; import flash.display.Sprite; import flash.events.Event; import flash.events.NetStatusEvent; import flash.media.Video; import flash.net.NetConnection; import flash.net.NetStream; import flash.utils.setTimeout; public class VideoPlayerChromless extends Sprite { public var bufferTime:Number = 2; private var cuePoints:Array = [ ]; private var lastFiredCuePoint:Number = 0; private var metaData:Object = { }; private var video:Video = new Video(); private var isLoading:Boolean; private var isOverHalfWay:Boolean; private var isPlaying:Boolean; private var rawHeight:Number; private var rawWidth:Number; private var stream:NetStream; private var videoHeight:Number; private var videoWidth:Number; public function VideoPlayerChromless() { var connection:NetConnection = new NetConnection(); connection.addEventListener( NetStatusEvent.NET_STATUS, handleNetStatus ); connection.connect( null ); stream = new NetStream( connection ); stream.bufferTime = 5; stream.client = { onMetaData: handleOnMetaData }; stream.addEventListener( NetStatusEvent.NET_STATUS, handleNetStatus ); video.smoothing = true; video.attachNetStream( stream ); addChild( video ); } public function play(url:String):void { stream.bufferTime = bufferTime; stream.close(); stream.play( url ); isLoading = true; isOverHalfWay = false; isPlaying = false; addEventListener( Event.ENTER_FRAME, handleEnterFrame ); } private function handleEnterFrame(e:Event):void { if ( isLoading ) { checkLoadingStatus(); } if ( cuePoints.length > 0 ) { checkForCuePoints(); } } private function checkLoadingStatus():void { var progress:Object = loadingProgress; if ( progress.total === 1 ) { isLoading = false; dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.LOADED ) ); } else { dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.LOADING, progress ) ); } } private function checkForCuePoints():void { var time:Number = playingTime.current; var checkTime:Number = Math.floor( time ); var test:Number = ArrayUtil.search( cuePoints, checkTime ); var cuePoint:Number; if ( test > -1 ) { cuePoint = cuePoints[ test ]; if ( cuePoint > lastFiredCuePoint ) { dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.CUE_POINT, { point: cuePoint, halfway: false, finished: false } ) ); lastFiredCuePoint = cuePoint; } } if ( !isOverHalfWay && ( time / 2 ) > playingTime.total ) { isOverHalfWay = true; dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.HALF_WAY, { point: checkTime, halfway: true, finished: false } ) ); } } public function addCuePoint(seconds:Number):void { cuePoints.push( seconds ); } public function pause():void { stream.pause(); isPlaying = false; dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.PAUSED ) ); } public function resume():void { stream.resume(); isPlaying = true; dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.PLAYING ) ); } private function handleNetStatus(e:NetStatusEvent):void { var code:String = e.info.code; trace(code); switch ( code ) { case 'NetStream.Play.Start': dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.BUFFERING ) ); break; case 'NetStream.Buffer.Full': dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.STARTED ) ); resume(); break; case 'NetStream.Play.Stop': dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.ENDED, { point: playingTime.total, halfway: false, finished: true } ) ); break; case 'NetStream.Seek.InvalidTime': //dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.BUFFERING ) ); break; case 'NetStream.Seek.Notify': dispatchEvent( new VideoPlayerEvent( VideoPlayerEvent.BUFFERING ) ); break; } } private function handleOnMetaData(info:Object):void { metaData = info; } public function resize(width:Number=0, height:Number=0):void { rawHeight = height; rawWidth = width; if ( !metaData.hasOwnProperty( 'height') && !metaData.hasOwnProperty( 'width' ) ) { setTimeout( doResize, 250, width, height ); } else { doResize( width, height ); } } private function doResize(width:Number, height:Number):void { var targetHeight:Number = ( height > 0 ? height : videoHeight ); var targetWidth:Number = targetHeight * ( metaData.width / metaData.height ); if ( targetWidth > width ) { targetWidth = ( width > 0 ? width : videoWidth ); targetHeight = targetWidth * ( metaData.height / metaData.width ); } videoHeight = targetHeight; videoWidth = targetWidth; video.height = targetHeight; video.width = targetWidth; video.x = ( width / 2 ) - ( targetWidth / 2 ); video.y = ( height / 2 ) - ( targetHeight / 2 ); } public function get loadingProgress():Object { var progress:Object = { }; progress.total = stream.bytesLoaded / stream.bytesTotal; progress.bytesLoaded = stream.bytesLoaded; progress.bytesTotal = stream.bytesTotal; return progress; } public function get playingTime():Object { var time:Object = { }; time.current = stream.time; time.total = metaData.duration; time.formatted = NumberUtil.toTimeString( Math.round( stream.time ) ) + ' / ' + NumberUtil.toTimeString( Math.round( metaData.duration ) ); return time; } } } |
VideoPlayerEvent.as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | package com.firestartermedia.lib.as3.events { import flash.events.Event; public class VideoPlayerEvent extends Event { public static const NAME:String = 'VideoPlayerEvent'; public static const BUFFERING:String = NAME + 'Buffering'; public static const LOADING:String = NAME + 'Loading'; public static const LOADED:String = NAME + 'Loaded'; public static const STARTED:String = NAME + 'Started'; public static const PLAYING:String = NAME + 'Playing'; public static const PAUSED:String = NAME + 'Paused'; public static const CUE_POINT:String = NAME + 'CuePoint'; public static const HALF_WAY:String = NAME + 'HalfWay'; public static const ENDED:String = NAME + 'Ended'; public static const ERROR:String = NAME + 'Error'; public var data:Object; public function VideoPlayerEvent(type:String, data:Object=null, bubbles:Boolean=true, cancelable:Boolean=false) { super( type, bubbles, cancelable ); this.data = data; } } } |
A lot huh? Well, no fear, I’m going to go through it with you. Let’s first start with the main “VideoPlayerChromeless()” class:
The first thing that we do is construct the class by creating a new “NetConnection()”, then passing that to our new “NetStream()” connection. We then set the buffer time of our stream and also set the client object to handle the “onMetaData” event (confused why it’s not a proper event? Read this). We the also add an event listener to the stream so we can listen in on such events such as when it’s playing, when buffering has ended and so on. Finally we attach the stream to our video component, add it to the stage and then wait for the “play()” function to be fired.
When the user wants to play a video, the call the public function “play()” passing in the URL as a string. The function clears anything that may be playing already (meaning that you can have just one instance of the player rather than destroying it and then reloading it), sets the buffer time (in case it’s been changed, it’s a public variable by the way), and then continues to play the new URL.
I’ve set up my cue point handler to fire an event once the movie has reached half way, therefore this is why I have the “isOverHalfWay” variable as once the event has been fired, I don’t want to fire it again. I’ve also got “isLoading” and “isPlaying”, their uses will be discussed in a sec.
Lastly, we create a new event listener that’s fired every frame, in this listener we then check to see if it’s still loading, so the validity of the “isLoading” variable, and run the “checkLoadingStatus()” function where we check the progress and fire events. We then also check to see if we’re defining any cue points in the code, and this brings me on to the “checkForCuePoints()” function. Now I’ve written an article about this already so I can skip this bit out.
We then have the plain but useful “pause()” and “resume()” functions, that are self explanatory I think. Which brings us nicely to “handleNetStatus()”. This allows us to check the status of our stream. I know what you’re thinking: “why haven’t you used constants?” Well if Adobe had bothered to create constants rather than strings, I would’ve used them. So we run a switch on the event’s code and fire events depending on what the code is.
We then come on to the trickiest function “resize()”. We have to do some clever maths here. The first thing we do is check if the meta data has been loaded, the function before this one, named “handleOnMetaData()” just stores the meta data into a private variable. Once then meta data has loaded, we then run “doResize()” that does the hard work. The trick here is to use the video size that the meta data returns and then calculate the new height and width but while maintaining the aspect ratio.
The first thing I do is calculate the new height, I just check that the height specified is greater than 0, otherwise it reverts back to the var “videoHeight”. The I set the width by getting the “targetHeight” and multiplying it by the ratio of the width to the height, so meta data width divided by meta data height. I then check to see if the “targetWidth” is greater than the current width, if it is I then do the opposite of what I’ve just done, so I said the width and then calculate the “targetHeight” by calculating the ratio. This means that the video will always resize but keep in the same aspect ratio.
And finally we have two get functions: “loadingProgress()” and “playingTime()” that just return simple objects. You can use these objects to create preloaders and seek bars.
So there we have it! A nice, simple and small chromeless FLV video player class!
Tell me what you think! Any issues, just comment!




I bookmarked this page. Thank you for given this useful post…
Posted: July 1, 2009 at 10:24 am;