1(function( Popcorn ) { 2 3 // combines calls of two function calls into one 4 var combineFn = function( first, second ) { 5 6 first = first || Popcorn.nop; 7 second = second || Popcorn.nop; 8 9 return function() { 10 11 first.apply( this, arguments ); 12 second.apply( this, arguments ); 13 }; 14 }; 15 16 // ID string matching 17 var rIdExp = /^(#([\w\-\_\.]+))$/; 18 19 Popcorn.player = function( name, player ) { 20 21 // return early if a player already exists under this name 22 if ( Popcorn[ name ] ) { 23 24 return; 25 } 26 27 player = player || {}; 28 29 var playerFn = function( target, src, options ) { 30 31 options = options || {}; 32 33 // List of events 34 var date = new Date() / 1000, 35 baselineTime = date, 36 currentTime = 0, 37 readyState = 0, 38 volume = 1, 39 muted = false, 40 events = {}, 41 42 // The container div of the resource 43 container = typeof target === "string" ? Popcorn.dom.find( target ) : target, 44 basePlayer = {}, 45 timeout, 46 popcorn; 47 48 if ( !Object.prototype.__defineGetter__ ) { 49 50 basePlayer = container || document.createElement( "div" ); 51 } 52 53 // copies a div into the media object 54 for( var val in container ) { 55 56 // don't copy properties if using container as baseplayer 57 if ( val in basePlayer ) { 58 59 continue; 60 } 61 62 if ( typeof container[ val ] === "object" ) { 63 64 basePlayer[ val ] = container[ val ]; 65 } else if ( typeof container[ val ] === "function" ) { 66 67 basePlayer[ val ] = (function( value ) { 68 69 // this is a stupid ugly kludgy hack in honour of Safari 70 // in Safari a NodeList is a function, not an object 71 if ( "length" in container[ value ] && !container[ value ].call ) { 72 73 return container[ value ]; 74 } else { 75 76 return function() { 77 78 return container[ value ].apply( container, arguments ); 79 }; 80 } 81 }( val )); 82 } else { 83 84 Popcorn.player.defineProperty( basePlayer, val, { 85 get: (function( value ) { 86 87 return function() { 88 89 return container[ value ]; 90 }; 91 }( val )), 92 set: Popcorn.nop, 93 configurable: true 94 }); 95 } 96 } 97 98 var timeupdate = function() { 99 100 date = new Date() / 1000; 101 102 if ( !basePlayer.paused ) { 103 104 basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime ); 105 basePlayer.dispatchEvent( "timeupdate" ); 106 timeout = setTimeout( timeupdate, 10 ); 107 } 108 109 baselineTime = date; 110 }; 111 112 basePlayer.play = function() { 113 114 this.paused = false; 115 116 if ( basePlayer.readyState >= 4 ) { 117 118 baselineTime = new Date() / 1000; 119 basePlayer.dispatchEvent( "play" ); 120 timeupdate(); 121 } 122 }; 123 124 basePlayer.pause = function() { 125 126 this.paused = true; 127 basePlayer.dispatchEvent( "pause" ); 128 }; 129 130 Popcorn.player.defineProperty( basePlayer, "currentTime", { 131 get: function() { 132 133 return currentTime; 134 }, 135 set: function( val ) { 136 137 // make sure val is a number 138 currentTime = +val; 139 basePlayer.dispatchEvent( "timeupdate" ); 140 141 return currentTime; 142 }, 143 configurable: true 144 }); 145 146 Popcorn.player.defineProperty( basePlayer, "volume", { 147 get: function() { 148 149 return volume; 150 }, 151 set: function( val ) { 152 153 // make sure val is a number 154 volume = +val; 155 basePlayer.dispatchEvent( "volumechange" ); 156 return volume; 157 }, 158 configurable: true 159 }); 160 161 Popcorn.player.defineProperty( basePlayer, "muted", { 162 get: function() { 163 164 return muted; 165 }, 166 set: function( val ) { 167 168 // make sure val is a number 169 muted = +val; 170 basePlayer.dispatchEvent( "volumechange" ); 171 return muted; 172 }, 173 configurable: true 174 }); 175 176 Popcorn.player.defineProperty( basePlayer, "readyState", { 177 get: function() { 178 179 return readyState; 180 }, 181 set: function( val ) { 182 183 readyState = val; 184 return readyState; 185 }, 186 configurable: true 187 }); 188 189 // Adds an event listener to the object 190 basePlayer.addEventListener = function( evtName, fn ) { 191 192 if ( !events[ evtName ] ) { 193 194 events[ evtName ] = []; 195 } 196 197 events[ evtName ].push( fn ); 198 return fn; 199 }; 200 201 // Removes an event listener from the object 202 basePlayer.removeEventListener = function( evtName, fn ) { 203 204 var i, 205 listeners = events[ evtName ]; 206 207 if ( !listeners ){ 208 209 return; 210 } 211 212 // walk backwards so we can safely splice 213 for ( i = events[ evtName ].length - 1; i >= 0; i-- ) { 214 215 if( fn === listeners[ i ] ) { 216 217 listeners.splice(i, 1); 218 } 219 } 220 221 return fn; 222 }; 223 224 // Can take event object or simple string 225 basePlayer.dispatchEvent = function( oEvent ) { 226 227 var evt, 228 self = this, 229 eventInterface, 230 eventName = oEvent.type; 231 232 // A string was passed, create event object 233 if ( !eventName ) { 234 235 eventName = oEvent; 236 eventInterface = Popcorn.events.getInterface( eventName ); 237 238 if ( eventInterface ) { 239 240 evt = document.createEvent( eventInterface ); 241 evt.initEvent( eventName, true, true, window, 1 ); 242 } 243 } 244 245 if ( events[ eventName ] ) { 246 247 for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) { 248 249 events[ eventName ][ i ].call( self, evt, self ); 250 } 251 } 252 }; 253 254 // Attempt to get src from playerFn parameter 255 basePlayer.src = src || ""; 256 basePlayer.duration = 0; 257 basePlayer.paused = true; 258 basePlayer.ended = 0; 259 260 options && options.events && Popcorn.forEach( options.events, function( val, key ) { 261 262 basePlayer.addEventListener( key, val, false ); 263 }); 264 265 // true and undefined returns on canPlayType means we should attempt to use it, 266 // false means we cannot play this type 267 if ( player._canPlayType( container.nodeName, src ) !== false ) { 268 269 if ( player._setup ) { 270 271 player._setup.call( basePlayer, options ); 272 } else { 273 274 // there is no setup, which means there is nothing to load 275 basePlayer.readyState = 4; 276 basePlayer.dispatchEvent( "loadedmetadata" ); 277 basePlayer.dispatchEvent( "loadeddata" ); 278 basePlayer.dispatchEvent( "canplaythrough" ); 279 } 280 } else { 281 282 // Asynchronous so that users can catch this event 283 setTimeout( function() { 284 basePlayer.dispatchEvent( "error" ); 285 }, 0 ); 286 } 287 288 popcorn = new Popcorn.p.init( basePlayer, options ); 289 290 if ( player._teardown ) { 291 292 popcorn.destroy = combineFn( popcorn.destroy, function() { 293 294 player._teardown.call( basePlayer, options ); 295 }); 296 } 297 298 return popcorn; 299 }; 300 301 playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop; 302 303 Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn; 304 }; 305 306 Popcorn.player.registry = {}; 307 308 Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) { 309 310 object.__defineGetter__( description, options.get || Popcorn.nop ); 311 object.__defineSetter__( description, options.set || Popcorn.nop ); 312 }; 313 314 // player queue is to help players queue things like play and pause 315 // HTML5 video's play and pause are asynch, but do fire in sequence 316 // play() should really mean "requestPlay()" or "queuePlay()" and 317 // stash a callback that will play the media resource when it's ready to be played 318 Popcorn.player.playerQueue = function() { 319 320 var _queue = [], 321 _running = false; 322 323 return { 324 next: function() { 325 326 _running = false; 327 _queue.shift(); 328 _queue[ 0 ] && _queue[ 0 ](); 329 }, 330 add: function( callback ) { 331 332 _queue.push(function() { 333 334 _running = true; 335 callback && callback(); 336 }); 337 338 // if there is only one item on the queue, start it 339 !_running && _queue[ 0 ](); 340 } 341 }; 342 }; 343 344 // Popcorn.smart will attempt to find you a wrapper or player. If it can't do that, 345 // it will default to using an HTML5 video in the target. 346 Popcorn.smart = function( target, src, options ) { 347 var node = typeof target === "string" ? Popcorn.dom.find( target ) : target, 348 i, srci, j, media, mediaWrapper, popcorn, srcLength, 349 // We leave HTMLVideoElement and HTMLAudioElement wrappers out 350 // of the mix, since we'll default to HTML5 video if nothing 351 // else works. Waiting on #1254 before we add YouTube to this. 352 wrappers = "HTMLYouTubeVideoElement HTMLVimeoVideoElement HTMLSoundCloudAudioElement HTMLNullVideoElement".split(" "); 353 354 if ( !node ) { 355 Popcorn.error( "Specified target `" + target + "` was not found." ); 356 return; 357 } 358 359 // If our src is not an array, create an array of one. 360 src = typeof src === "string" ? [ src ] : src; 361 362 // Loop through each src, and find the first playable. 363 for ( i = 0, srcLength = src.length; i < srcLength; i++ ) { 364 srci = src[ i ]; 365 366 // See if we can use a wrapper directly, if not, try players. 367 for ( j = 0; j < wrappers.length; j++ ) { 368 mediaWrapper = Popcorn[ wrappers[ j ] ]; 369 if ( mediaWrapper && mediaWrapper._canPlaySrc( srci ) === "probably" ) { 370 media = mediaWrapper( node ); 371 popcorn = Popcorn( media, options ); 372 // Set src, but not until after we return the media so the caller 373 // can get error events, if any. 374 setTimeout( function() { 375 media.src = srci; 376 }, 0 ); 377 return popcorn; 378 } 379 } 380 381 // No wrapper can play this, check players. 382 for ( var key in Popcorn.player.registry ) { 383 if ( Popcorn.player.registry.hasOwnProperty( key ) ) { 384 if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, srci ) ) { 385 // Popcorn.smart( player, src, /* options */ ) 386 return Popcorn[ key ]( node, srci, options ); 387 } 388 } 389 } 390 } 391 392 // If we don't have any players or wrappers that can handle this, 393 // Default to using HTML5 video. Similar to the HTMLVideoElement 394 // wrapper, we put a video in the div passed to us via: 395 // Popcorn.smart( div, src, options ) 396 var videoHTML, 397 videoElement, 398 videoID = Popcorn.guid( "popcorn-video-" ), 399 videoHTMLContainer = document.createElement( "div" ); 400 401 videoHTMLContainer.style.width = "100%"; 402 videoHTMLContainer.style.height = "100%"; 403 404 // If we only have one source, do not bother with source elements. 405 // This means we don't have the IE9 hack, 406 // and we can properly listen to error events. 407 // That way an error event can be told to backup to Flash if it fails. 408 if ( src.length === 1 ) { 409 videoElement = document.createElement( "video" ); 410 videoElement.id = videoID; 411 node.appendChild( videoElement ); 412 setTimeout( function() { 413 // Hack to decode html characters like & to & 414 var decodeDiv = document.createElement( "div" ); 415 decodeDiv.innerHTML = src[ 0 ]; 416 417 videoElement.src = decodeDiv.firstChild.nodeValue; 418 }, 0 ); 419 return Popcorn( '#' + videoID, options ); 420 } 421 422 node.appendChild( videoHTMLContainer ); 423 // IE9 doesn't like dynamic creation of source elements on <video> 424 // so we do it in one shot via innerHTML. 425 videoHTML = '<video id="' + videoID + '" preload=auto autobuffer>'; 426 for ( i = 0, srcLength = src.length; i < srcLength; i++ ) { 427 videoHTML += '<source src="' + src[ i ] + '">'; 428 } 429 videoHTML += "</video>"; 430 videoHTMLContainer.innerHTML = videoHTML; 431 432 if ( options && options.events && options.events.error ) { 433 node.addEventListener( "error", options.events.error, false ); 434 } 435 return Popcorn( '#' + videoID, options ); 436 }; 437})( Popcorn ); 438