1/* 2 * jPlayer Plugin for jQuery JavaScript Library 3 * http://www.jplayer.org 4 * 5 * Copyright (c) 2009 - 2014 Happyworm Ltd 6 * Licensed under the MIT license. 7 * http://opensource.org/licenses/MIT 8 * 9 * Author: Mark J Panaghiston 10 * Date: 29th January 2013 11 */ 12 13package happyworm.jPlayer { 14 import flash.display.Sprite; 15 16 import flash.media.Video; 17 import flash.media.SoundTransform; 18 19 import flash.net.NetConnection; 20 import flash.net.NetStream; 21 22 import flash.utils.Timer; 23 24 import flash.events.NetStatusEvent; 25 import flash.events.SecurityErrorEvent; 26 import flash.events.TimerEvent; 27 28 public class JplayerMp4 extends Sprite { 29 30 public var myVideo:Video = new Video(); 31 private var myConnection:NetConnection; 32 private var myStream:NetStream; 33 34 private var myTransform:SoundTransform = new SoundTransform(); 35 36 public var myStatus:JplayerStatus = new JplayerStatus(); 37 38 private var timeUpdateTimer:Timer = new Timer(250, 0); // Matched to HTML event freq 39 private var progressTimer:Timer = new Timer(250, 0); // Matched to HTML event freq 40 private var seekingTimer:Timer = new Timer(100, 0); // Internal: How often seeking is checked to see if it is over. 41 42 public function JplayerMp4(volume:Number) { 43 myConnection = new NetConnection(); 44 myConnection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); 45 myConnection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); 46 myVideo.smoothing = true; 47 this.addChild(myVideo); 48 49 timeUpdateTimer.addEventListener(TimerEvent.TIMER, timeUpdateHandler); 50 progressTimer.addEventListener(TimerEvent.TIMER, progressHandler); 51 seekingTimer.addEventListener(TimerEvent.TIMER, seekingHandler); 52 53 myStatus.volume = volume; 54 } 55 private function progressUpdates(active:Boolean):void { 56 if(active) { 57 progressTimer.start(); 58 } else { 59 progressTimer.stop(); 60 } 61 } 62 private function progressHandler(e:TimerEvent):void { 63 if(myStatus.isLoading) { 64 if(getLoadRatio() == 1) { // Close as can get to a loadComplete event since client.onPlayStatus only works with FMS 65 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "progressHandler: loadComplete")); 66 myStatus.loaded(); 67 progressUpdates(false); 68 } 69 } 70 progressEvent(); 71 } 72 private function progressEvent():void { 73 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "progressEvent:")); 74 updateStatusValues(); 75 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PROGRESS, myStatus)); 76 } 77 private function timeUpdates(active:Boolean):void { 78 if(active) { 79 timeUpdateTimer.start(); 80 } else { 81 timeUpdateTimer.stop(); 82 } 83 } 84 private function timeUpdateHandler(e:TimerEvent):void { 85 timeUpdateEvent(); 86 } 87 private function timeUpdateEvent():void { 88 updateStatusValues(); 89 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_TIMEUPDATE, myStatus)); 90 } 91 private function seeking(active:Boolean):void { 92 if(active) { 93 if(!myStatus.isSeeking) { 94 seekingEvent(); 95 } 96 seekingTimer.start(); 97 } else { 98 if(myStatus.isSeeking) { 99 seekedEvent(); 100 } 101 seekingTimer.stop(); 102 } 103 } 104 private function seekingHandler(e:TimerEvent):void { 105 if(getSeekTimeRatio() <= getLoadRatio()) { 106 seeking(false); 107 if(myStatus.playOnSeek) { 108 myStatus.playOnSeek = false; // Capture the flag. 109 play(myStatus.pausePosition); // Must pass time or the seek time is never set. 110 } else { 111 pause(myStatus.pausePosition); // Must pass time or the stream.time is read. 112 } 113 } else if(myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration) { 114 // Illegal seek time 115 seeking(false); 116 pause(0); 117 } 118 } 119 private function seekingEvent():void { 120 myStatus.isSeeking = true; 121 updateStatusValues(); 122 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKING, myStatus)); 123 } 124 private function seekedEvent():void { 125 myStatus.isSeeking = false; 126 updateStatusValues(); 127 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKED, myStatus)); 128 } 129 private function netStatusHandler(e:NetStatusEvent):void { 130 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "netStatusHandler: '" + e.info.code + "'")); 131 switch(e.info.code) { 132 case "NetConnection.Connect.Success": 133 connectStream(); 134 break; 135 case "NetStream.Play.Start": 136 // This event code occurs once, when the media is opened. Equiv to loadOpen() in mp3 player. 137 myStatus.loading(); 138 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus)); 139 progressUpdates(true); 140 // See onMetaDataHandler() for other condition, since duration is vital. 141 break; 142 case "NetStream.Play.Stop": 143 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "NetStream.Play.Stop: getDuration() - getCurrentTime() = " + (getDuration() - getCurrentTime()))); 144 145 // Check if media is at the end (or close) otherwise this was due to download bandwidth stopping playback. ie., Download is not fast enough. 146 if(Math.abs(getDuration() - getCurrentTime()) < 150) { // Testing found 150ms worked best for M4A files, where playHead(99.9) caused a stuck state due to firing with ~116ms left to play. 147 endedEvent(); 148 } 149 break; 150 case "NetStream.Seek.InvalidTime": 151 // Used for capturing invalid set times and clicks on the end of the progress bar. 152 endedEvent(); 153 break; 154 case "NetStream.Play.StreamNotFound": 155 myStatus.error(); // Resets status except the src, and it sets srcError property. 156 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ERROR, myStatus)); 157 break; 158 } 159 // "NetStream.Seek.Notify" event code is not very useful. It occurs after every seek(t) command issued and does not appear to wait for the media to be ready. 160 } 161 private function endedEvent():void { 162 var wasPlaying:Boolean = myStatus.isPlaying; 163 pause(0); 164 timeUpdates(false); 165 timeUpdateEvent(); 166 if(wasPlaying) { 167 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ENDED, myStatus)); 168 } 169 } 170 private function securityErrorHandler(event:SecurityErrorEvent):void { 171 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "securityErrorHandler.")); 172 } 173 private function connectStream():void { 174 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "connectStream.")); 175 var customClient:Object = new Object(); 176 customClient.onMetaData = onMetaDataHandler; 177 // customClient.onPlayStatus = onPlayStatusHandler; // According to the forums and my tests, onPlayStatus only works with FMS (Flash Media Server). 178 myStream = null; 179 myStream = new NetStream(myConnection); 180 myStream.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); 181 myStream.client = customClient; 182 myVideo.attachNetStream(myStream); 183 setVolume(myStatus.volume); 184 myStream.play(myStatus.src); 185 } 186 public function setFile(src:String):void { 187 if(myStream != null) { 188 myStream.close(); 189 } 190 myVideo.clear(); 191 progressUpdates(false); 192 timeUpdates(false); 193 194 myStatus.reset(); 195 myStatus.src = src; 196 myStatus.srcSet = true; 197 timeUpdateEvent(); 198 } 199 public function clearFile():void { 200 setFile(""); 201 myStatus.srcSet = false; 202 } 203 public function load():Boolean { 204 if(myStatus.loadRequired()) { 205 myStatus.startingDownload(); 206 myConnection.connect(null); 207 return true; 208 } else { 209 return false; 210 } 211 } 212 public function play(time:Number = NaN):Boolean { 213 var wasPlaying:Boolean = myStatus.isPlaying; 214 215 if(!isNaN(time) && myStatus.srcSet) { 216 if(myStatus.isPlaying) { 217 myStream.pause(); 218 myStatus.isPlaying = false; 219 } 220 myStatus.pausePosition = time; 221 } 222 223 if(myStatus.isStartingDownload) { 224 myStatus.playOnLoad = true; // Raise flag, captured in onMetaDataHandler() 225 return true; 226 } else if(myStatus.loadRequired()) { 227 myStatus.playOnLoad = true; // Raise flag, captured in onMetaDataHandler() 228 return load(); 229 } else if((myStatus.isLoading || myStatus.isLoaded) && !myStatus.isPlaying) { 230 if(myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration) { // The time is invalid, ie., past the end. 231 myStream.pause(); // Since it is playing by default at this point. 232 myStatus.pausePosition = 0; 233 myStream.seek(0); 234 timeUpdates(false); 235 timeUpdateEvent(); 236 if(wasPlaying) { // For when playing and then get a play(huge) 237 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus)); 238 } 239 } else if(getSeekTimeRatio() > getLoadRatio()) { // Use an estimate based on the downloaded amount 240 myStatus.playOnSeek = true; 241 seeking(true); 242 myStream.pause(); // Since it is playing by default at this point. 243 } else { 244 if(!isNaN(time)) { // Avoid using seek() when it is already correct. 245 myStream.seek(myStatus.pausePosition/1000); // Since time is in ms and seek() takes seconds 246 } 247 myStatus.isPlaying = true; // Set immediately before playing. Could affects events. 248 myStream.resume(); 249 timeUpdates(true); 250 if(!wasPlaying) { 251 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAY, myStatus)); 252 } 253 } 254 return true; 255 } else { 256 return false; 257 } 258 } 259 public function pause(time:Number = NaN):Boolean { 260 myStatus.playOnLoad = false; // Reset flag in case load/play issued immediately before this command, ie., before onMetadata() event. 261 myStatus.playOnSeek = false; // Reset flag in case play(time) issued before the command and is still seeking to time set. 262 263 var wasPlaying:Boolean = myStatus.isPlaying; 264 265 // To avoid possible loops with timeupdate and pause(time). A pause() does not have the problem. 266 var alreadyPausedAtTime:Boolean = false; 267 if(!isNaN(time) && myStatus.pausePosition == time) { 268 alreadyPausedAtTime = true; 269 } 270 271 // Need to wait for metadata to load before ever issuing a pause. The metadata handler will call this function if needed, when ready. 272 if(myStream != null && myStatus.metaDataReady) { // myStream is a null until the 1st media is loaded. ie., The 1st ever setMedia being followed by a pause() or pause(t). 273 myStream.pause(); 274 } 275 if(myStatus.isPlaying) { 276 myStatus.isPlaying = false; 277 myStatus.pausePosition = myStream.time * 1000; 278 } 279 280 if(!isNaN(time) && myStatus.srcSet) { 281 myStatus.pausePosition = time; 282 } 283 284 if(wasPlaying) { 285 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus)); 286 } 287 288 if(myStatus.isStartingDownload) { 289 return true; 290 } else if(myStatus.loadRequired()) { 291 if(time > 0) { // We do not want the stop() command, which does pause(0), causing a load operation. 292 return load(); 293 } else { 294 return true; // Technically the pause(0) succeeded. ie., It did nothing, since nothing was required. 295 } 296 } else if(myStatus.isLoading || myStatus.isLoaded) { 297 if(myStatus.metaDataReady && myStatus.pausePosition > myStatus.duration) { // The time is invalid, ie., past the end. 298 myStatus.pausePosition = 0; 299 myStream.seek(0); 300 seekedEvent(); // Deals with seeking effect when using setMedia() then pause(huge). NB: There is no preceeding seeking event. 301 } else if(!isNaN(time)) { 302 if(getSeekTimeRatio() > getLoadRatio()) { // Use an estimate based on the downloaded amount 303 seeking(true); 304 } else { 305 if(myStatus.metaDataReady) { // Otherwise seek(0) will stop the metadata loading. 306 myStream.seek(myStatus.pausePosition/1000); 307 } 308 } 309 } 310 timeUpdates(false); 311 // Need to be careful with timeupdate event, otherwise a pause in a timeupdate event can cause a loop. 312 // Neither pause() nor pause(time) will cause a timeupdate loop. 313 if(wasPlaying || !isNaN(time) && !alreadyPausedAtTime) { 314 timeUpdateEvent(); 315 } 316 return true; 317 } else { 318 return false; 319 } 320 } 321 public function playHead(percent:Number):Boolean { 322 var time:Number = percent * getDuration() * getLoadRatio() / 100; 323 if(myStatus.isPlaying || myStatus.playOnLoad || myStatus.playOnSeek) { 324 return play(time); 325 } else { 326 return pause(time); 327 } 328 } 329 public function setVolume(v:Number):void { 330 myStatus.volume = v; 331 myTransform.volume = v; 332 if(myStream != null) { 333 myStream.soundTransform = myTransform; 334 } 335 } 336 private function updateStatusValues():void { 337 myStatus.seekPercent = 100 * getLoadRatio(); 338 myStatus.currentTime = getCurrentTime(); 339 myStatus.currentPercentRelative = 100 * getCurrentRatioRel(); 340 myStatus.currentPercentAbsolute = 100 * getCurrentRatioAbs(); 341 myStatus.duration = getDuration(); 342 } 343 public function getLoadRatio():Number { 344 if((myStatus.isLoading || myStatus.isLoaded) && myStream.bytesTotal > 0) { 345 return myStream.bytesLoaded / myStream.bytesTotal; 346 } else if (myStatus.isLoaded && myStream.bytesLoaded > 0) { 347 return 1; 348 } else { 349 return 0; 350 } 351 } 352 public function getDuration():Number { 353 return myStatus.duration; // Set from meta data. 354 } 355 public function getCurrentTime():Number { 356 if(myStatus.isPlaying) { 357 return myStream.time * 1000; 358 } else { 359 return myStatus.pausePosition; 360 } 361 } 362 public function getCurrentRatioRel():Number { 363 if((getLoadRatio() > 0) && (getCurrentRatioAbs() <= getLoadRatio())) { 364 return getCurrentRatioAbs() / getLoadRatio(); 365 } else { 366 return 0; 367 } 368 } 369 public function getCurrentRatioAbs():Number { 370 if(getDuration() > 0) { 371 return getCurrentTime() / getDuration(); 372 } else { 373 return 0; 374 } 375 } 376 public function getSeekTimeRatio():Number { 377 if(getDuration() > 0) { 378 return myStatus.pausePosition / getDuration(); 379 } else { 380 return 1; 381 } 382 } 383 public function onMetaDataHandler(info:Object):void { // Used in connectStream() in myStream.client object. 384 // This event occurs when jumping to the start of static files! ie., seek(0) will cause this event to occur. 385 if(!myStatus.metaDataReady) { 386 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "onMetaDataHandler: " + info.duration + " | " + info.width + "x" + info.height)); 387 388 myStatus.metaDataReady = true; // Set flag so that this event only effects jPlayer the 1st time. 389 myStatus.metaData = info; 390 myStatus.duration = info.duration * 1000; // Only available via Meta Data. 391 if(info.width != undefined) { 392 myVideo.width = myStatus.videoWidth = info.width; 393 } 394 if(info.height != undefined) { 395 myVideo.height = myStatus.videoHeight = info.height; 396 } 397 398 if(myStatus.playOnLoad) { 399 myStatus.playOnLoad = false; // Capture the flag 400 if(myStatus.pausePosition > 0 ) { // Important for setMedia followed by play(time). 401 play(myStatus.pausePosition); 402 } else { 403 play(); // Not always sending pausePosition avoids the extra seek(0) for a normal play() command. 404 } 405 } else { 406 pause(myStatus.pausePosition); // Always send the pausePosition. Important for setMedia() followed by pause(time). Deals with not reading stream.time with setMedia() and play() immediately followed by stop() or pause(0) 407 } 408 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADEDMETADATA, myStatus)); 409 } else { 410 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "onMetaDataHandler: Already read (NO EFFECT)")); 411 } 412 } 413 } 414} 415