(function( Popcorn, window, document ) { var CURRENT_TIME_MONITOR_MS = 16, EMPTY_STRING = "", VIMEO_HOST = "https://player.vimeo.com"; // Utility wrapper around postMessage interface function VimeoPlayer( vimeoIFrame ) { var self = this, url = vimeoIFrame.src.split('?')[0], muted = 0; if( url.substr(0, 2) === '//' ) { url = window.location.protocol + url; } function sendMessage( method, params ) { var data = JSON.stringify({ method: method, value: params }); // The iframe has been destroyed, it just doesn't know it if ( !vimeoIFrame.contentWindow ) { return; } vimeoIFrame.contentWindow.postMessage( data, url ); } var methods = ( "play pause paused seekTo unload getCurrentTime getDuration " + "getVideoEmbedCode getVideoHeight getVideoWidth getVideoUrl " + "getColor setColor setLoop getVolume setVolume addEventListener" ).split(" "); methods.forEach( function( method ) { // All current methods take 0 or 1 args, always send arg0 self[ method ] = function( arg0 ) { sendMessage( method, arg0 ); }; }); } function HTMLVimeoVideoElement( id ) { // Vimeo iframe API requires postMessage if( !window.postMessage ) { throw "ERROR: HTMLVimeoVideoElement requires window.postMessage"; } var self = new Popcorn._MediaElementProto(), parent = typeof id === "string" ? Popcorn.dom.find( id ) : id, elem = document.createElement( "iframe" ), 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, // Vimeo seems to use .77 as default volume: 1, // Vimeo has no concept of muted, store volume values // such that muted===0 is unmuted, and muted>0 is muted. muted: 0, currentTime: 0, duration: NaN, ended: false, paused: true, error: null }, playerReady = false, playerUID = Popcorn.guid(), player, playerPaused = true, playerReadyCallbacks = [], timeUpdateInterval, currentTimeInterval, lastCurrentTime = 0; // Namespace all events we'll produce self._eventNamespace = Popcorn.guid( "HTMLVimeoVideoElement::" ); self.parentNode = parent; // Mark type as Vimeo self._util.type = "Vimeo"; function addPlayerReadyCallback( callback ) { playerReadyCallbacks.unshift( callback ); } function onPlayerReady( event ) { player.addEventListener( 'loadProgress' ); player.addEventListener( 'playProgress' ); player.addEventListener( 'play' ); player.addEventListener( 'pause' ); player.addEventListener( 'finish' ); player.addEventListener( 'seek' ); player.getDuration(); impl.networkState = self.NETWORK_LOADING; self.dispatchEvent( "loadstart" ); self.dispatchEvent( "progress" ); } function updateDuration( newDuration ) { var oldDuration = impl.duration; if( oldDuration !== newDuration ) { impl.duration = newDuration; self.dispatchEvent( "durationchange" ); // Deal with first update of duration if( isNaN( oldDuration ) ) { impl.networkState = self.NETWORK_IDLE; impl.readyState = self.HAVE_METADATA; self.dispatchEvent( "loadedmetadata" ); self.dispatchEvent( "loadeddata" ); impl.readyState = self.HAVE_FUTURE_DATA; self.dispatchEvent( "canplay" ); impl.readyState = self.HAVE_ENOUGH_DATA; self.dispatchEvent( "canplaythrough" ); // Auto-start if necessary if( impl.autoplay ) { self.play(); } var i = playerReadyCallbacks.length; while( i-- ) { playerReadyCallbacks[ i ](); delete playerReadyCallbacks[ i ]; } } } } function getDuration() { if( !playerReady ) { // Queue a getDuration() call so we have correct duration info for loadedmetadata addPlayerReadyCallback( function() { getDuration(); } ); } player.getDuration(); } function destroyPlayer() { if( !( playerReady && player ) ) { return; } clearInterval( currentTimeInterval ); player.pause(); window.removeEventListener( 'message', onStateChange, false ); parent.removeChild( elem ); elem = document.createElement( "iframe" ); } self.play = function() { impl.paused = false; if( !playerReady ) { addPlayerReadyCallback( function() { self.play(); } ); return; } player.play(); }; function changeCurrentTime( aTime ) { if( !playerReady ) { addPlayerReadyCallback( function() { changeCurrentTime( aTime ); } ); return; } onSeeking(); player.seekTo( aTime ); } function onSeeking() { impl.seeking = true; self.dispatchEvent( "seeking" ); } function onSeeked() { impl.seeking = false; self.dispatchEvent( "timeupdate" ); self.dispatchEvent( "seeked" ); self.dispatchEvent( "canplay" ); self.dispatchEvent( "canplaythrough" ); } self.pause = function() { impl.paused = true; if( !playerReady ) { addPlayerReadyCallback( function() { self.pause(); } ); return; } player.pause(); }; function onPause() { impl.paused = true; if ( !playerPaused ) { playerPaused = true; clearInterval( timeUpdateInterval ); self.dispatchEvent( "pause" ); } } function onTimeUpdate() { self.dispatchEvent( "timeupdate" ); } function onPlay() { if( impl.ended ) { changeCurrentTime( 0 ); } if ( !currentTimeInterval ) { currentTimeInterval = setInterval( monitorCurrentTime, CURRENT_TIME_MONITOR_MS ) ; // Only 1 play when video.loop=true if ( impl.loop ) { self.dispatchEvent( "play" ); } } timeUpdateInterval = setInterval( onTimeUpdate, self._util.TIMEUPDATE_MS ); impl.paused = false; if( playerPaused ) { playerPaused = false; // Only 1 play when video.loop=true if ( !impl.loop ) { self.dispatchEvent( "play" ); } self.dispatchEvent( "playing" ); } } function onEnded() { if( impl.loop ) { changeCurrentTime( 0 ); self.play(); } else { impl.ended = true; self.dispatchEvent( "ended" ); } } function onCurrentTime( aTime ) { var currentTime = impl.currentTime = aTime; if( currentTime !== lastCurrentTime ) { self.dispatchEvent( "timeupdate" ); } lastCurrentTime = impl.currentTime; } // We deal with the startup load messages differently than // we will once the player is fully ready and loaded. // When the player is "ready" it is playable, but not // yet seekable. We need to force a play() to get data // to download (mimic preload=auto), or seeks will fail. function startupMessage( event ) { if( event.origin !== VIMEO_HOST ) { return; } var data; try { data = JSON.parse( event.data ); } catch ( ex ) { console.warn( ex ); } if ( data.player_id != playerUID ) { return; } switch ( data.event ) { case "ready": player = new VimeoPlayer( elem ); player.addEventListener( "loadProgress" ); player.addEventListener( "pause" ); player.setVolume( 0 ); player.play(); break; case "loadProgress": var duration = parseFloat( data.data.duration ); if( duration > 0 && !playerReady ) { playerReady = true; player.pause(); } break; case "pause": player.setVolume( 1 ); // Switch message pump to use run-time message callback vs. startup window.removeEventListener( "message", startupMessage, false ); window.addEventListener( "message", onStateChange, false ); onPlayerReady(); break; } } function onStateChange( event ) { if( event.origin !== VIMEO_HOST ) { return; } var data; try { data = JSON.parse( event.data ); } catch ( ex ) { console.warn( ex ); } if ( data.player_id != playerUID ) { return; } // Methods switch ( data.method ) { case "getCurrentTime": onCurrentTime( parseFloat( data.value ) ); break; case "getDuration": updateDuration( parseFloat( data.value ) ); break; case "getVolume": onVolume( parseFloat( data.value ) ); break; } // Events switch ( data.event ) { case "loadProgress": self.dispatchEvent( "progress" ); updateDuration( parseFloat( data.data.duration ) ); break; case "playProgress": onCurrentTime( parseFloat( data.data.seconds ) ); break; case "play": onPlay(); break; case "pause": onPause(); break; case "finish": onEnded(); break; case "seek": onCurrentTime( parseFloat( data.data.seconds ) ); onSeeked(); break; } } function monitorCurrentTime() { player.getCurrentTime(); } 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; if( playerReady ) { destroyPlayer(); } playerReady = false; var src = self._util.parseUri( aSrc ), queryKey = src.queryKey, key, optionsArray = [ // Vimeo API options first "api=1", "player_id=" + playerUID, // Turn off as much of the metadata/branding as possible "title=0", "byline=0", "portrait=0" ]; // Sync loop and autoplay based on URL params, and delete. // We'll manage both internally. impl.loop = queryKey.loop === "1" || impl.loop; delete queryKey.loop; impl.autoplay = queryKey.autoplay === "1" || impl.autoplay; delete queryKey.autoplay; // Create the base vimeo player string. It will always have query string options src = VIMEO_HOST + '/video/' + ( /\d+$/ ).exec( src.path ) + "?"; for( key in queryKey ) { if ( queryKey.hasOwnProperty( key ) ) { optionsArray.push( encodeURIComponent( key ) + "=" + encodeURIComponent( queryKey[ key ] ) ); } } src += optionsArray.join( "&" ); elem.id = playerUID; elem.style.width = "100%"; elem.style.height = "100%"; elem.frameBorder = 0; elem.webkitAllowFullScreen = true; elem.mozAllowFullScreen = true; elem.allowFullScreen = true; parent.appendChild( elem ); elem.src = src; window.addEventListener( "message", startupMessage, false ); } function onVolume( aValue ) { if( impl.volume !== aValue ) { impl.volume = aValue; self.dispatchEvent( "volumechange" ); } } function setVolume( aValue ) { impl.volume = aValue; if( !playerReady ) { addPlayerReadyCallback( function() { setVolume( aValue ); }); return; } player.setVolume( aValue ); self.dispatchEvent( "volumechange" ); } function getVolume() { // If we're muted, the volume is cached on impl.muted. return impl.muted > 0 ? impl.muted : impl.volume; } function setMuted( aMute ) { if( !playerReady ) { impl.muted = aMute ? 1 : 0; addPlayerReadyCallback( function() { setMuted( aMute ); }); return; } // Move the existing volume onto muted to cache // until we unmute, and set the volume to 0. if( aMute ) { impl.muted = impl.volume; setVolume( 0 ); } else { impl.muted = 0; setVolume( impl.muted ); } } function getMuted() { return impl.muted > 0; } 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 getVolume(); }, set: function( aValue ) { if( aValue < 0 || aValue > 1 ) { throw "Volume value must be between 0.0 and 1.0"; } setVolume( aValue ); } }, muted: { get: function() { return getMuted(); }, set: function( aValue ) { setMuted( self._util.isAttributeSet( aValue ) ); } }, error: { get: function() { return impl.error; } } }); self._canPlaySrc = Popcorn.HTMLVimeoVideoElement._canPlaySrc; self.canPlayType = Popcorn.HTMLVimeoVideoElement.canPlayType; return self; } Popcorn.HTMLVimeoVideoElement = function( id ) { return new HTMLVimeoVideoElement( id ); }; // Helper for identifying URLs we know how to play. Popcorn.HTMLVimeoVideoElement._canPlaySrc = function( url ) { return ( (/player.vimeo.com\/video\/\d+/).test( url ) || (/vimeo.com\/\d+/).test( url ) ) ? "probably" : EMPTY_STRING; }; // We'll attempt to support a mime type of video/x-vimeo Popcorn.HTMLVimeoVideoElement.canPlayType = function( type ) { return type === "video/x-vimeo" ? "probably" : EMPTY_STRING; }; }( Popcorn, window, document ));