(function( Popcorn, window, document ) { var CURRENT_TIME_MONITOR_MS = 10, EMPTY_STRING = "", // Example: http://www.youtube.com/watch?v=12345678901 regexYouTube = /^.*(?:\/|v=)(.{11})/, ABS = Math.abs, // Setup for YouTube API ytReady = false, ytLoading = false, ytCallbacks = []; function onYouTubeIframeAPIReady() { var callback; if ( YT.loaded ) { ytReady = true; while( ytCallbacks.length ) { callback = ytCallbacks.shift(); callback(); } } else { setTimeout( onYouTubeIframeAPIReady, 250 ); } } function isYouTubeReady() { var script; // If we area already waiting, do nothing. if( !ytLoading ) { // If script is already there, check if it is loaded. if ( window.YT ) { onYouTubeIframeAPIReady(); } else { script = document.createElement( "script" ); script.addEventListener( "load", onYouTubeIframeAPIReady, false); script.src = "https://www.youtube.com/iframe_api"; document.head.appendChild( script ); } ytLoading = true; } return ytReady; } function addYouTubeCallback( callback ) { ytCallbacks.push( callback ); } function HTMLYouTubeVideoElement( id ) { // YouTube iframe API requires postMessage if( !window.postMessage ) { throw "ERROR: HTMLYouTubeVideoElement requires window.postMessage"; } var self = new Popcorn._MediaElementProto(), parent = typeof id === "string" ? document.querySelector( id ) : id, elem = document.createElement( "div" ), impl = { src: EMPTY_STRING, networkState: self.NETWORK_EMPTY, readyState: self.HAVE_NOTHING, seeking: false, autoplay: EMPTY_STRING, preload: EMPTY_STRING, controls: false, loop: false, poster: EMPTY_STRING, volume: 1, muted: false, currentTime: 0, duration: NaN, ended: false, paused: true, error: null }, playerReady = false, mediaReady = false, loopedPlay = false, player, playerPaused = true, mediaReadyCallbacks = [], playerState = -1, bufferedInterval, lastLoadedFraction = 0, currentTimeInterval, timeUpdateInterval; // Namespace all events we'll produce self._eventNamespace = Popcorn.guid( "HTMLYouTubeVideoElement::" ); self.parentNode = parent; // Mark this as YouTube self._util.type = "YouTube"; function addMediaReadyCallback( callback ) { mediaReadyCallbacks.push( callback ); } function catchRoguePlayEvent() { player.pauseVideo(); removeYouTubeEvent( "play", catchRoguePlayEvent ); addYouTubeEvent( "play", onPlay ); } function catchRoguePauseEvent() { addYouTubeEvent( "pause", onPause ); removeYouTubeEvent( "pause", catchRoguePauseEvent ); } function onPlayerReady( event ) { var onMuted = function() { if ( player.isMuted() ) { // force an initial play on the video, to remove autostart on initial seekTo. addYouTubeEvent( "play", onFirstPlay ); player.playVideo(); } else { setTimeout( onMuted, 0 ); } }; playerReady = true; // XXX: this should really live in cued below, but doesn't work. // Browsers using flash will have the pause() call take too long and cause some // sound to leak out. Muting before to prevent this. player.mute(); // ensure we are muted. onMuted(); } function onPlayerError(event) { // There's no perfect mapping to HTML5 errors from YouTube errors. var err = { name: "MediaError" }; switch( event.data ) { // invalid parameter case 2: err.message = "Invalid video parameter."; err.code = MediaError.MEDIA_ERR_ABORTED; break; // HTML5 Error case 5: err.message = "The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred."; err.code = MediaError.MEDIA_ERR_DECODE; // requested video not found case 100: err.message = "Video not found."; err.code = MediaError.MEDIA_ERR_NETWORK; break; // video can't be embedded by request of owner case 101: case 150: err.message = "Video not usable."; err.code = MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED; break; default: err.message = "Unknown error."; err.code = 5; } impl.error = err; self.dispatchEvent( "error" ); } function onReady() { addYouTubeEvent( "play", onPlay ); addYouTubeEvent( "pause", onPause ); // Set initial paused state if( impl.autoplay || !impl.paused ) { removeYouTubeEvent( "play", onReady ); impl.paused = false; addMediaReadyCallback(function() { if ( !impl.paused ) { onPlay(); } }); } // Ensure video will now be unmuted when playing due to the mute on initial load. if( !impl.muted ) { player.unMute(); } impl.readyState = self.HAVE_METADATA; self.dispatchEvent( "loadedmetadata" ); currentTimeInterval = setInterval( monitorCurrentTime, CURRENT_TIME_MONITOR_MS ); self.dispatchEvent( "loadeddata" ); impl.readyState = self.HAVE_FUTURE_DATA; self.dispatchEvent( "canplay" ); mediaReady = true; bufferedInterval = setInterval( monitorBuffered, 50 ); while( mediaReadyCallbacks.length ) { mediaReadyCallbacks[ 0 ](); mediaReadyCallbacks.shift(); } // We can't easily determine canplaythrough, but will send anyway. impl.readyState = self.HAVE_ENOUGH_DATA; self.dispatchEvent( "canplaythrough" ); } function onFirstPause() { removeYouTubeEvent( "pause", onFirstPause ); if ( player.getCurrentTime() > 0 ) { setTimeout( onFirstPause, 0 ); return; } if( impl.autoplay || !impl.paused ) { addYouTubeEvent( "play", onReady ); player.playVideo(); } else { onReady(); } } // This function needs duration and first play to be ready. function onFirstPlay() { removeYouTubeEvent( "play", onFirstPlay ); if ( player.getCurrentTime() === 0 ) { setTimeout( onFirstPlay, 0 ); return; } addYouTubeEvent( "pause", onFirstPause ); player.seekTo( 0 ); player.pauseVideo(); } function addYouTubeEvent( event, listener ) { self.addEventListener( "youtube-" + event, listener, false ); } function removeYouTubeEvent( event, listener ) { self.removeEventListener( "youtube-" + event, listener, false ); } function dispatchYouTubeEvent( event ) { self.dispatchEvent( "youtube-" + event ); } function onBuffering() { impl.networkState = self.NETWORK_LOADING; var newDuration = player.getDuration(); if (impl.duration !== newDuration) { impl.duration = newDuration; self.dispatchEvent( "durationchange" ); } self.dispatchEvent( "waiting" ); } addYouTubeEvent( "buffering", onBuffering ); addYouTubeEvent( "ended", onEnded ); function onPlayerStateChange( event ) { switch( event.data ) { // ended case YT.PlayerState.ENDED: dispatchYouTubeEvent( "ended" ); break; // playing case YT.PlayerState.PLAYING: dispatchYouTubeEvent( "play" ); break; // paused case YT.PlayerState.PAUSED: // Youtube fires a paused event before an ended event. // We have no need for this. if ( player.getDuration() !== player.getCurrentTime() ) { dispatchYouTubeEvent( "pause" ); } break; // buffering case YT.PlayerState.BUFFERING: dispatchYouTubeEvent( "buffering" ); break; // video cued case YT.PlayerState.CUED: // XXX: cued doesn't seem to fire reliably, bug in youtube api? break; } if ( event.data !== YT.PlayerState.BUFFERING && playerState === YT.PlayerState.BUFFERING ) { onProgress(); } playerState = event.data; } function destroyPlayer() { if( !( playerReady && player ) ) { return; } removeYouTubeEvent( "buffering", onBuffering ); removeYouTubeEvent( "ended", onEnded ); removeYouTubeEvent( "play", onPlay ); removeYouTubeEvent( "pause", onPause ); onPause(); mediaReady = false; loopedPlay = false; impl.currentTime = 0; mediaReadyCallbacks = []; clearInterval( currentTimeInterval ); clearInterval( bufferedInterval ); player.stopVideo(); player.clearVideo(); player.destroy(); elem = document.createElement( "div" ); } function changeSrc( aSrc ) { if( !self._canPlaySrc( aSrc ) ) { impl.error = { name: "MediaError", message: "Media Source Not Supported", code: MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED }; self.dispatchEvent( "error" ); return; } impl.src = aSrc; // Make sure YouTube is ready, and if not, register a callback if( !isYouTubeReady() ) { addYouTubeCallback( function() { changeSrc( aSrc ); } ); return; } if( playerReady ) { if( mediaReady ) { destroyPlayer(); } else { addMediaReadyCallback( function() { changeSrc( aSrc ); }); return; } } parent.appendChild( elem ); // Use any player vars passed on the URL var playerVars = self._util.parseUri( aSrc ).queryKey; // Remove the video id, since we don't want to pass it delete playerVars.v; // Sync autoplay, but manage internally impl.autoplay = playerVars.autoplay === "1" || impl.autoplay; delete playerVars.autoplay; // Sync loop, but manage internally impl.loop = playerVars.loop === "1" || impl.loop; delete playerVars.loop; // Don't show related videos when ending playerVars.rel = playerVars.rel || 0; // Don't show YouTube's branding playerVars.modestbranding = playerVars.modestbranding || 1; // Don't show annotations by default playerVars.iv_load_policy = playerVars.iv_load_policy || 3; // Disable keyboard controls by default playerVars.disablekb = playerVars.disablekb || 1; // Don't show video info before playing playerVars.showinfo = playerVars.showinfo || 0; // Specify our domain as origin for iframe security var domain = window.location.protocol === "file:" ? "*" : window.location.protocol + "//" + window.location.host; playerVars.origin = playerVars.origin || domain; // Show/hide controls. Sync with impl.controls and prefer URL value. playerVars.controls = playerVars.controls || impl.controls ? 2 : 0; impl.controls = playerVars.controls; // Set wmode to transparent to show video overlays playerVars.wmode = playerVars.wmode || "opaque"; if ( playerVars.html5 !== 0 ) { playerVars.html5 = 1; } // Get video ID out of youtube url aSrc = regexYouTube.exec( aSrc )[ 1 ]; player = new YT.Player( elem, { width: "100%", height: "100%", wmode: playerVars.wmode, videoId: aSrc, playerVars: playerVars, events: { 'onReady': onPlayerReady, 'onError': onPlayerError, 'onStateChange': onPlayerStateChange } }); impl.networkState = self.NETWORK_LOADING; self.dispatchEvent( "loadstart" ); self.dispatchEvent( "progress" ); } function monitorCurrentTime() { var playerTime = player.getCurrentTime(); if ( !impl.seeking ) { if ( ABS( impl.currentTime - playerTime ) > CURRENT_TIME_MONITOR_MS ) { onSeeking(); onSeeked(); } impl.currentTime = playerTime; } else if ( ABS( playerTime - impl.currentTime ) < 1 ) { onSeeked(); } } function monitorBuffered() { var fraction = player.getVideoLoadedFraction(); if ( fraction && lastLoadedFraction !== fraction ) { lastLoadedFraction = fraction; onProgress(); } } function changeCurrentTime( aTime ) { if ( aTime === impl.currentTime ) { return; } impl.currentTime = aTime; if( !mediaReady ) { addMediaReadyCallback( function() { onSeeking(); player.seekTo( aTime ); }); return; } onSeeking(); player.seekTo( aTime ); } function onTimeUpdate() { self.dispatchEvent( "timeupdate" ); } function onSeeking() { // a seek in youtube fires a paused event. // we don't want to listen for this, so this state catches the event. addYouTubeEvent( "pause", catchRoguePauseEvent ); removeYouTubeEvent( "pause", onPause ); impl.seeking = true; self.dispatchEvent( "seeking" ); } function onSeeked() { impl.ended = false; impl.seeking = false; self.dispatchEvent( "timeupdate" ); self.dispatchEvent( "seeked" ); self.dispatchEvent( "canplay" ); self.dispatchEvent( "canplaythrough" ); } function onPlay() { if( impl.ended ) { changeCurrentTime( 0 ); impl.ended = false; } timeUpdateInterval = setInterval( onTimeUpdate, self._util.TIMEUPDATE_MS ); impl.paused = false; if( playerPaused ) { playerPaused = false; // Only 1 play when video.loop=true if ( ( impl.loop && !loopedPlay ) || !impl.loop ) { loopedPlay = true; self.dispatchEvent( "play" ); } self.dispatchEvent( "playing" ); } } function onProgress() { self.dispatchEvent( "progress" ); } self.play = function() { impl.paused = false; if( !mediaReady ) { addMediaReadyCallback( function() { self.play(); }); return; } player.playVideo(); }; function onPause() { impl.paused = true; if ( !playerPaused ) { playerPaused = true; clearInterval( timeUpdateInterval ); self.dispatchEvent( "pause" ); } } self.pause = function() { impl.paused = true; if( !mediaReady ) { addMediaReadyCallback( function() { self.pause(); }); return; } // if a pause happens while seeking, ensure we catch it. // in youtube seeks fire pause events, and we don't want to listen to that. // except for the case of an actual pause. catchRoguePauseEvent(); player.pauseVideo(); }; function onEnded() { if( impl.loop ) { changeCurrentTime( 0 ); self.play(); } else { impl.ended = true; onPause(); // YouTube will fire a Playing State change after the video has ended, causing it to loop. addYouTubeEvent( "play", catchRoguePlayEvent ); removeYouTubeEvent( "play", onPlay ); self.dispatchEvent( "timeupdate" ); self.dispatchEvent( "ended" ); } } function setMuted( aValue ) { impl.muted = aValue; if( !mediaReady ) { addMediaReadyCallback( function() { setMuted( impl.muted ); }); return; } player[ aValue ? "mute" : "unMute" ](); self.dispatchEvent( "volumechange" ); } function getMuted() { // YouTube has isMuted(), but for sync access we use impl.muted return impl.muted; } Object.defineProperties( self, { src: { get: function() { return impl.src; }, set: function( aSrc ) { if( aSrc && aSrc !== impl.src ) { changeSrc( aSrc ); } } }, autoplay: { get: function() { return impl.autoplay; }, set: function( aValue ) { impl.autoplay = self._util.isAttributeSet( aValue ); } }, loop: { get: function() { return impl.loop; }, set: function( aValue ) { impl.loop = self._util.isAttributeSet( aValue ); } }, width: { get: function() { return self.parentNode.offsetWidth; } }, height: { get: function() { return self.parentNode.offsetHeight; } }, currentTime: { get: function() { return impl.currentTime; }, set: function( aValue ) { changeCurrentTime( aValue ); } }, duration: { get: function() { return impl.duration; } }, ended: { get: function() { return impl.ended; } }, paused: { get: function() { return impl.paused; } }, seeking: { get: function() { return impl.seeking; } }, readyState: { get: function() { return impl.readyState; } }, networkState: { get: function() { return impl.networkState; } }, volume: { get: function() { return impl.volume; }, set: function( aValue ) { if( aValue < 0 || aValue > 1 ) { throw "Volume value must be between 0.0 and 1.0"; } impl.volume = aValue; if( !mediaReady ) { addMediaReadyCallback( function() { self.volume = aValue; }); return; } player.setVolume( impl.volume * 100 ); self.dispatchEvent( "volumechange" ); } }, muted: { get: function() { return getMuted(); }, set: function( aValue ) { setMuted( self._util.isAttributeSet( aValue ) ); } }, error: { get: function() { return impl.error; } }, buffered: { get: function () { var timeRanges = { start: function( index ) { if ( index === 0 ) { return 0; } //throw fake DOMException/INDEX_SIZE_ERR throw "INDEX_SIZE_ERR: DOM Exception 1"; }, end: function( index ) { if ( index === 0 ) { if ( !impl.duration ) { return 0; } return impl.duration * lastLoadedFraction; } //throw fake DOMException/INDEX_SIZE_ERR throw "INDEX_SIZE_ERR: DOM Exception 1"; }, length: 1 }; return timeRanges; }, configurable: true } }); self._canPlaySrc = Popcorn.HTMLYouTubeVideoElement._canPlaySrc; self.canPlayType = Popcorn.HTMLYouTubeVideoElement.canPlayType; return self; } Popcorn.HTMLYouTubeVideoElement = function( id ) { return new HTMLYouTubeVideoElement( id ); }; // Helper for identifying URLs we know how to play. Popcorn.HTMLYouTubeVideoElement._canPlaySrc = function( url ) { return (/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu).*(?:\/|v=)(.{11})/).test( url ) ? "probably" : EMPTY_STRING; }; // We'll attempt to support a mime type of video/x-youtube Popcorn.HTMLYouTubeVideoElement.canPlayType = function( type ) { return type === "video/x-youtube" ? "probably" : EMPTY_STRING; }; }( Popcorn, window, document ));