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: 15th December 2013 11 */ 12 13package happyworm.jPlayer { 14 import flash.display.Sprite; 15 16 import flash.media.Sound; 17 import flash.media.SoundChannel; 18 import flash.media.SoundLoaderContext; 19 import flash.media.SoundTransform; 20 import flash.net.URLRequest; 21 import flash.utils.Timer; 22 import flash.errors.IOError; 23 import flash.events.*; 24 25 public class JplayerMp3 extends Sprite { 26 private var mySound:Sound = new Sound(); 27 private var myChannel:SoundChannel = new SoundChannel(); 28 private var myContext:SoundLoaderContext = new SoundLoaderContext(3000, false); 29 private var myTransform:SoundTransform = new SoundTransform(); 30 private var myRequest:URLRequest = new URLRequest(); 31 32 private var timeUpdateTimer:Timer = new Timer(250, 0); // Matched to HTML event freq 33 private var progressTimer:Timer = new Timer(250, 0); // Matched to HTML event freq 34 private var seekingTimer:Timer = new Timer(100, 0); // Internal: How often seeking is checked to see if it is over. 35 private var playingTimer:Timer = new Timer(100, 0); // Internal: How often waiting/playing is checked. 36 private var waitingTimer:Timer = new Timer(3000, 0); // Internal: Check from loadstart to loadOpen. Generates a waiting event. 37 38 public var myStatus:JplayerStatus = new JplayerStatus(); 39 40 public function JplayerMp3(volume:Number) { 41 timeUpdateTimer.addEventListener(TimerEvent.TIMER, timeUpdateHandler); 42 progressTimer.addEventListener(TimerEvent.TIMER, progressHandler); 43 seekingTimer.addEventListener(TimerEvent.TIMER, seekingHandler); 44 playingTimer.addEventListener(TimerEvent.TIMER, playingHandler); 45 waitingTimer.addEventListener(TimerEvent.TIMER, waitingHandler); 46 setVolume(volume); 47 } 48 public function setFile(src:String):void { 49 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "setFile: " + src)); 50 if(myStatus.isPlaying) { 51 myChannel.stop(); 52 } 53 progressUpdates(false); 54 timeUpdates(false); 55 waitingTimer.stop(); 56 try { 57 mySound.close(); 58 } catch (err:IOError) { 59 // Occurs if the file is either yet to be opened or has finished downloading. 60 } 61 mySound = null; 62 mySound = new Sound(); 63 mySound.addEventListener(IOErrorEvent.IO_ERROR, errorHandler); 64 mySound.addEventListener(Event.OPEN, loadOpen); 65 mySound.addEventListener(Event.COMPLETE, loadComplete); 66 myRequest = new URLRequest(src); 67 myStatus.reset(); 68 myStatus.src = src; 69 myStatus.srcSet = true; 70 timeUpdateEvent(); 71 } 72 public function clearFile():void { 73 setFile(""); 74 myStatus.srcSet = false; 75 } 76 private function errorHandler(err:IOErrorEvent):void { 77 // MP3 player needs to stop progress and timeupdate events as they are started before the error occurs. 78 // NB: The MP4 player works differently and the error occurs before they are started. 79 progressUpdates(false); 80 timeUpdates(false); 81 myStatus.error(); // Resets status except the src, and it sets srcError property. 82 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ERROR, myStatus)); 83 } 84 private function loadOpen(e:Event):void { 85 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "loadOpen:")); 86 waitingTimer.stop(); 87 myStatus.loading(); 88 if(myStatus.playOnLoad) { 89 myStatus.playOnLoad = false; // Capture the flag 90 // this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus)); // So loadstart event happens before play event occurs. 91 play(); 92 } else { 93 // this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus)); 94 pause(); 95 } 96 progressUpdates(true); 97 } 98 private function loadComplete(e:Event):void { 99 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "loadComplete:")); 100 myStatus.loaded(); 101 progressUpdates(false); 102 progressEvent(); 103 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADEDMETADATA, myStatus)); 104 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_DURATIONCHANGE, myStatus)); 105 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAYTHROUGH, myStatus)); 106 } 107 private function soundCompleteHandler(e:Event):void { 108 myStatus.pausePosition = 0; 109 myStatus.isPlaying = false; 110 timeUpdates(false); 111 timeUpdateEvent(); 112 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_ENDED, myStatus)); 113 } 114 private function progressUpdates(active:Boolean):void { 115 // Using a timer rather than Flash's load progress event, because that event gave data at about 200Hz. The 10Hz timer is closer to HTML5 norm. 116 if(active) { 117 progressTimer.start(); 118 } else { 119 progressTimer.stop(); 120 } 121 } 122 private function progressHandler(e:TimerEvent):void { 123 progressEvent(); 124 } 125 private function progressEvent():void { 126 this.dispatchEvent(new JplayerEvent(JplayerEvent.DEBUG_MSG, myStatus, "progressEvent:")); 127 updateStatusValues(); 128 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PROGRESS, myStatus)); 129 } 130 private function timeUpdates(active:Boolean):void { 131 if(active) { 132 timeUpdateTimer.start(); 133 playingTimer.start(); 134 } else { 135 timeUpdateTimer.stop(); 136 playingTimer.stop(); 137 } 138 } 139 private function timeUpdateHandler(e:TimerEvent):void { 140 timeUpdateEvent(); 141 } 142 private function timeUpdateEvent():void { 143 updateStatusValues(); 144 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_TIMEUPDATE, myStatus)); 145 } 146 private function seeking(active:Boolean):void { 147 if(active) { 148 seekingEvent(); 149 waitingEvent(); 150 seekingTimer.start(); 151 } else { 152 seekingTimer.stop(); 153 } 154 } 155 private function seekingHandler(e:TimerEvent):void { 156 if(myStatus.pausePosition <= getDuration()) { 157 seekedEvent(); 158 seeking(false); 159 if(myStatus.playOnSeek) { 160 myStatus.playOnSeek = false; // Capture the flag. 161 play(); 162 } 163 } else if(myStatus.isLoaded && (myStatus.pausePosition > getDuration())) { 164 // Illegal seek time 165 seeking(false); 166 seekedEvent(); 167 pause(0); 168 } 169 } 170 private function seekingEvent():void { 171 myStatus.isSeeking = true; 172 updateStatusValues(); 173 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKING, myStatus)); 174 } 175 private function seekedEvent():void { 176 myStatus.isSeeking = false; 177 updateStatusValues(); 178 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_SEEKED, myStatus)); 179 } 180 private function playingHandler(e:TimerEvent):void { 181 checkPlaying(false); // Without forcing playing event. 182 } 183 private function checkPlaying(force:Boolean):void { 184 if(mySound.isBuffering) { 185 if(!myStatus.isWaiting) { 186 waitingEvent(); 187 } 188 } else { 189 if(myStatus.isWaiting || force) { 190 playingEvent(); 191 } 192 } 193 } 194 private function waitingEvent():void { 195 myStatus.isWaiting = true; 196 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_WAITING, myStatus)); 197 } 198 private function playingEvent():void { 199 myStatus.isWaiting = false; 200 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_CANPLAY, myStatus)); 201 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAYING, myStatus)); 202 } 203 private function waitingHandler(e:TimerEvent):void { 204 waitingTimer.stop(); 205 if(myStatus.playOnLoad) { 206 waitingEvent(); 207 } 208 } 209 public function load():Boolean { 210 if(myStatus.loadRequired()) { 211 myStatus.startingDownload(); 212 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_LOADSTART, myStatus)); 213 waitingTimer.start(); 214 mySound.load(myRequest, myContext); 215 return true; 216 } else { 217 return false; 218 } 219 } 220 public function play(time:Number = NaN):Boolean { 221 var wasPlaying:Boolean = myStatus.isPlaying; 222 223 if(!isNaN(time) && myStatus.srcSet) { 224 if(myStatus.isPlaying) { 225 myChannel.stop(); 226 myStatus.isPlaying = false; 227 } 228 myStatus.pausePosition = time; 229 } 230 231 if(myStatus.isStartingDownload) { 232 myStatus.playOnLoad = true; // Raise flag, captured in loadOpen() 233 return true; 234 } else if(myStatus.loadRequired()) { 235 myStatus.playOnLoad = true; // Raise flag, captured in loadOpen() 236 return load(); 237 } else if((myStatus.isLoading || myStatus.isLoaded) && !myStatus.isPlaying) { 238 if(myStatus.isLoaded && myStatus.pausePosition > getDuration()) { // The time is invalid, ie., past the end. 239 myStatus.pausePosition = 0; 240 timeUpdates(false); 241 timeUpdateEvent(); 242 if(wasPlaying) { // For when playing and then get a play(huge) 243 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus)); 244 } 245 } else if(myStatus.pausePosition > getDuration()) { 246 myStatus.playOnSeek = true; 247 seeking(true); 248 } else { 249 myStatus.isPlaying = true; // Set immediately before playing. Could affects events. 250 myChannel = mySound.play(myStatus.pausePosition); 251 myChannel.soundTransform = myTransform; 252 myChannel.addEventListener(Event.SOUND_COMPLETE, soundCompleteHandler); 253 timeUpdates(true); 254 if(!wasPlaying) { 255 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PLAY, myStatus)); 256 } 257 checkPlaying(true); // Force the playing event unless waiting, which will be dealt with in the playingTimer. 258 } 259 return true; 260 } else { 261 return false; 262 } 263 } 264 public function pause(time:Number = NaN):Boolean { 265 myStatus.playOnLoad = false; // Reset flag in case load/play issued immediately before this command, ie., before loadOpen() event. 266 myStatus.playOnSeek = false; // Reset flag in case play(time) issued before the command and is still seeking to time set. 267 268 var wasPlaying:Boolean = myStatus.isPlaying; 269 270 // To avoid possible loops with timeupdate and pause(time). A pause() does not have the problem. 271 var alreadyPausedAtTime:Boolean = false; 272 if(!isNaN(time) && myStatus.pausePosition == time) { 273 alreadyPausedAtTime = true; 274 } 275 276 if(myStatus.isPlaying) { 277 myStatus.isPlaying = false; 278 myChannel.stop(); 279 if(myChannel.position > 0) { // Required otherwise a fast play then pause causes myChannel.position to equal zero and not the correct value. ie., When it happens leave pausePosition alone. 280 myStatus.pausePosition = myChannel.position; 281 } 282 } 283 284 if(!isNaN(time) && myStatus.srcSet) { 285 myStatus.pausePosition = time; 286 } 287 288 if(wasPlaying) { 289 this.dispatchEvent(new JplayerEvent(JplayerEvent.JPLAYER_PAUSE, myStatus)); 290 } 291 292 if(myStatus.isStartingDownload) { 293 return true; 294 } else if(myStatus.loadRequired()) { 295 if(time > 0) { // We do not want the stop() command, which does pause(0), causing a load operation. 296 return load(); 297 } else { 298 return true; // Technically the pause(0) succeeded. ie., It did nothing, since nothing was required. 299 } 300 } else if(myStatus.isLoading || myStatus.isLoaded) { 301 if(myStatus.isLoaded && myStatus.pausePosition > getDuration()) { // The time is invalid, ie., past the end. 302 myStatus.pausePosition = 0; 303 } else if(myStatus.pausePosition > getDuration()) { 304 seeking(true); 305 } 306 timeUpdates(false); 307 // Need to be careful with timeupdate event, otherwise a pause in a timeupdate event can cause a loop. 308 // Neither pause() nor pause(time) will cause a timeupdate loop. 309 if(wasPlaying || !isNaN(time) && !alreadyPausedAtTime) { 310 timeUpdateEvent(); 311 } 312 return true; 313 } else { 314 return false; 315 } 316 } 317 public function playHead(percent:Number):Boolean { 318 var time:Number = percent * getDuration() / 100; 319 if(myStatus.isPlaying || myStatus.playOnLoad || myStatus.playOnSeek) { 320 return play(time); 321 } else { 322 return pause(time); 323 } 324 } 325 public function setVolume(v:Number):void { 326 myStatus.volume = v; 327 myTransform.volume = v; 328 myChannel.soundTransform = myTransform; 329 } 330 private function updateStatusValues():void { 331 myStatus.seekPercent = 100 * getLoadRatio(); 332 myStatus.currentTime = getCurrentTime(); 333 myStatus.currentPercentRelative = 100 * getCurrentRatioRel(); 334 myStatus.currentPercentAbsolute = 100 * getCurrentRatioAbs(); 335 myStatus.duration = getDuration(); 336 } 337 public function getLoadRatio():Number { 338 if((myStatus.isLoading || myStatus.isLoaded) && mySound.bytesTotal > 0) { 339 return mySound.bytesLoaded / mySound.bytesTotal; 340 } else if (myStatus.isLoaded && mySound.bytesLoaded > 0) { 341 return 1; 342 } else { 343 return 0; 344 } 345 } 346 public function getDuration():Number { 347 if(mySound.length > 0) { 348 return mySound.length; 349 } else { 350 return 0; 351 } 352 } 353 public function getCurrentTime():Number { 354 if(myStatus.isPlaying) { 355 return myChannel.position; 356 } else { 357 return myStatus.pausePosition; 358 } 359 } 360 public function getCurrentRatioRel():Number { 361 if((getDuration() > 0) && (getCurrentTime() <= getDuration())) { 362 return getCurrentTime() / getDuration(); 363 } else { 364 return 0; 365 } 366 } 367 public function getCurrentRatioAbs():Number { 368 return getCurrentRatioRel() * getLoadRatio(); 369 } 370 } 371} 372