2017-03-10 14:19:25 +00:00
// refsCallback: (optional)
// Will be called when the player enters a marker that has a `data-ref` attribute. The value of `data-ref` will be passed to the function.
// When leaving a marker that a `data-ref` attribute, and entering a marker without one (or not entering a new marker at all), the function will be called with `null`.
function Player ( htmlContainer , refsCallback ) {
this . container = htmlContainer ;
this . markersContainer = this . container . querySelector ( ".markers_container" ) ;
this . videoContainer = this . container . querySelector ( ".video_container" ) ;
this . refsCallback = refsCallback || function ( ) { } ;
if ( ! this . videoContainer . getAttribute ( "data-videoId" ) ) {
console . error ( "Expected to find data-videoId attribute on" , this . videoContainer , "for player initialized on" , this . container ) ;
throw new Error ( "Missing data-videoId attribute." ) ;
}
this . markers = [ ] ;
var markerEls = this . markersContainer . querySelectorAll ( ".marker" ) ;
if ( markerEls . length == 0 ) {
console . error ( "No markers found in" , this . markersContainer , "for player initialized on" , this . container ) ;
throw new Error ( "Missing markers." ) ;
}
for ( var i = 0 ; i < markerEls . length ; ++ i ) {
var marker = {
timestamp : parseInt ( markerEls [ i ] . getAttribute ( "data-timestamp" ) , 10 ) ,
ref : markerEls [ i ] . getAttribute ( "data-ref" ) ,
endTime : ( i < markerEls . length - 1 ? parseInt ( markerEls [ i + 1 ] . getAttribute ( "data-timestamp" ) , 10 ) : null ) ,
el : markerEls [ i ] ,
fadedProgress : markerEls [ i ] . querySelector ( ".progress.faded" ) ,
progress : markerEls [ i ] . querySelector ( ".progress.main" ) ,
hoverx : null
} ;
marker . el . addEventListener ( "click" , this . onMarkerClick . bind ( this , marker ) ) ;
marker . el . addEventListener ( "mousemove" , this . onMarkerMouseMove . bind ( this , marker ) ) ;
marker . el . addEventListener ( "mouseleave" , this . onMarkerMouseLeave . bind ( this , marker ) ) ;
this . markers . push ( marker ) ;
}
this . currentMarker = null ;
this . currentMarkerIdx = null ;
this . youtubePlayer = null ;
this . youtubePlayerReady = false ;
this . playing = false ;
this . shouldPlay = false ;
2017-03-11 02:48:11 +00:00
this . buffering = false ;
this . pauseAfterBuffer = false ;
2017-03-10 14:19:25 +00:00
this . speed = 1 ;
this . currentTime = 0 ;
this . lastFrameTime = 0 ;
this . scrollTo = - 1 ;
this . scrollPosition = 0 ;
this . nextFrame = null ;
this . looping = false ;
this . markersContainer . addEventListener ( "wheel" , function ( ev ) {
this . scrollTo = - 1 ;
} . bind ( this ) ) ;
Player . initializeYoutube ( this . onYoutubeReady . bind ( this ) ) ;
this . updateSize ( ) ;
this . resume ( ) ;
}
// Start playing the video from the current position.
// If the player hasn't loaded yet, it will autoplay when ready.
Player . prototype . play = function ( ) {
if ( this . youtubePlayerReady ) {
if ( ! this . playing ) {
this . youtubePlayer . playVideo ( ) ;
}
2017-03-15 02:10:11 +00:00
this . pauseAfterBuffer = false ;
2017-03-10 14:19:25 +00:00
} else {
this . shouldPlay = true ;
}
} ;
// Pause the video at the current position.
// If the player hasn't loaded yet, it will not autoplay when ready. (This is the default)
Player . prototype . pause = function ( ) {
if ( this . youtubePlayerReady ) {
if ( this . playing ) {
this . youtubePlayer . pauseVideo ( ) ;
2017-03-11 02:48:11 +00:00
} else if ( this . buffering ) {
this . pauseAfterBuffer = true ;
2017-03-10 14:19:25 +00:00
}
} else {
this . shouldPlay = false ;
}
} ;
// Sets the current time. Does not affect play status.
// If the player hasn't loaded yet, it will seek to this time when ready.
Player . prototype . setTime = function ( time ) {
this . currentTime = time ;
if ( this . youtubePlayerReady ) {
this . currentTime = Math . max ( 0 , Math . min ( this . currentTime , this . youtubePlayer . getDuration ( ) ) ) ;
this . youtubePlayer . seekTo ( this . currentTime ) ;
}
this . updateProgress ( ) ;
} ;
Player . prototype . jumpToNextMarker = function ( ) {
var targetMarkerIdx = Math . min ( ( this . currentMarkerIdx === null ? 0 : this . currentMarkerIdx + 1 ) , this . markers . length - 1 ) ;
var targetTime = this . markers [ targetMarkerIdx ] . timestamp ;
this . setTime ( targetTime ) ;
this . play ( ) ;
} ;
Player . prototype . jumpToPrevMarker = function ( ) {
var targetMarkerIdx = Math . max ( 0 , ( this . currentMarkerIdx === null ? 0 : this . currentMarkerIdx - 1 ) ) ;
var targetTime = this . markers [ targetMarkerIdx ] . timestamp ;
this . setTime ( targetTime ) ;
this . play ( ) ;
} ;
// Call this after changing the size of the video container in order to update the youtube player.
Player . prototype . updateSize = function ( ) {
var width = this . videoContainer . offsetWidth ;
var height = width / 16 * 9 ;
this . markersContainer . style . height = height ;
if ( this . youtubePlayerReady ) {
this . youtubePlayer . setSize ( Math . floor ( width ) , Math . floor ( height ) ) ;
}
}
// Stops the per-frame work that the player does. Call when you want to hide or get rid of the player.
Player . prototype . halt = function ( ) {
this . pause ( ) ;
this . looping = false ;
if ( this . nextFrame ) {
cancelAnimationFrame ( this . nextFrame ) ;
this . nextFrame = null ;
}
}
// Resumes the per-frame work that the player does. Call when you want to show the player again after hiding.
Player . prototype . resume = function ( ) {
this . looping = true ;
if ( ! this . nextFrame ) {
this . doFrame ( ) ;
}
}
Player . initializeYoutube = function ( callback ) {
if ( window . APYoutubeAPIReady === undefined ) {
window . APYoutubeAPIReady = false ;
window . APCallbacks = ( callback ? [ callback ] : [ ] ) ;
window . onYouTubeIframeAPIReady = function ( ) {
window . APYoutubeAPIReady = true ;
for ( var i = 0 ; i < APCallbacks . length ; ++ i ) {
APCallbacks [ i ] ( ) ;
}
} ;
var scriptTag = document . createElement ( "SCRIPT" ) ;
scriptTag . setAttribute ( "type" , "text/javascript" ) ;
scriptTag . setAttribute ( "src" , "https://www.youtube.com/iframe_api" ) ;
document . body . appendChild ( scriptTag ) ;
} else if ( window . APYoutubeAPIReady === false ) {
window . APCallbacks . push ( callback ) ;
} else if ( window . APYoutubeAPIReady === true ) {
callback ( ) ;
}
}
// END PUBLIC INTERFACE
Player . prototype . onMarkerClick = function ( marker , ev ) {
var time = marker . timestamp ;
if ( this . currentMarker == marker && marker . hoverx !== null ) {
time += ( marker . endTime - marker . timestamp ) * marker . hoverx ;
}
this . setTime ( time ) ;
this . play ( ) ;
} ;
Player . prototype . onMarkerMouseMove = function ( marker , ev ) {
if ( this . currentMarker == marker ) {
marker . hoverx = ( ev . offsetX - marker . el . offsetLeft ) / marker . el . offsetWidth ;
}
} ;
Player . prototype . onMarkerMouseLeave = function ( marker , ev ) {
marker . hoverx = null ;
} ;
Player . prototype . updateProgress = function ( ) {
var prevMarker = this . currentMarker ;
this . currentMarker = null ;
this . currentMarkerIdx = null ;
for ( var i = 0 ; i < this . markers . length ; ++ i ) {
var marker = this . markers [ i ] ;
if ( marker . timestamp <= this . currentTime && this . currentTime < marker . endTime ) {
this . currentMarker = marker ;
this . currentMarkerIdx = i ;
break ;
}
}
if ( this . currentMarker ) {
var totalWidth = this . currentMarker . el . offsetWidth ;
var progress = ( this . currentTime - this . currentMarker . timestamp ) / ( this . currentMarker . endTime - this . currentMarker . timestamp ) ;
if ( this . currentMarker . hoverx === null ) {
var pixelWidth = progress * totalWidth ;
this . currentMarker . fadedProgress . style . width = Math . ceil ( pixelWidth ) + "px" ;
this . currentMarker . fadedProgress . style . opacity = pixelWidth - Math . floor ( pixelWidth ) ;
this . currentMarker . progress . style . width = Math . floor ( pixelWidth ) + "px" ;
} else {
this . currentMarker . fadedProgress . style . opacity = 1 ;
this . currentMarker . progress . style . width = Math . floor ( Math . min ( this . currentMarker . hoverx , progress ) * totalWidth ) + "px" ;
this . currentMarker . fadedProgress . style . width = Math . floor ( Math . max ( this . currentMarker . hoverx , progress ) * totalWidth ) + "px" ;
}
}
if ( this . currentMarker != prevMarker ) {
if ( prevMarker ) {
prevMarker . el . classList . remove ( "current" ) ;
prevMarker . fadedProgress . style . width = "0px" ;
prevMarker . progress . style . width = "0px" ;
prevMarker . hoverx = null ;
}
if ( this . currentMarker ) {
this . currentMarker . el . classList . add ( "current" ) ;
this . scrollTo = this . currentMarker . el . offsetTop + this . currentMarker . el . offsetHeight / 2.0 ;
this . scrollPosition = this . markersContainer . scrollTop ;
}
if ( this . currentMarker && this . currentMarker . ref ) {
this . refsCallback ( this . currentMarker . ref ) ;
} else if ( prevMarker && prevMarker . ref ) {
this . refsCallback ( null ) ;
}
}
} ;
Player . prototype . doFrame = function ( ) {
var now = performance . now ( ) ;
var delta = ( now - this . lastFrameTime ) / 1000.0 ;
this . lastFrameTime = now ;
if ( this . playing ) {
this . currentTime += delta * this . speed ;
}
this . updateProgress ( ) ;
if ( this . scrollTo >= 0 ) {
var targetPosition = this . scrollTo - this . markersContainer . offsetHeight / 2.0 ;
targetPosition = Math . max ( 0 , Math . min ( targetPosition , this . markersContainer . scrollHeight - this . markersContainer . offsetHeight ) ) ;
this . scrollPosition += ( targetPosition - this . scrollPosition ) * 0.1 ;
if ( Math . abs ( this . scrollPosition - targetPosition ) < 1.0 ) {
this . markersContainer . scrollTop = targetPosition ;
this . scrollTo = - 1 ;
} else {
this . markersContainer . scrollTop = this . scrollPosition ;
}
}
this . nextFrame = requestAnimationFrame ( this . doFrame . bind ( this ) ) ;
} ;
Player . prototype . onYoutubePlayerReady = function ( ) {
this . youtubePlayerReady = true ;
this . markers [ this . markers . length - 1 ] . endTime = this . youtubePlayer . getDuration ( ) ;
this . updateSize ( ) ;
this . youtubePlayer . setPlaybackQuality ( "hd1080" ) ;
if ( this . currentTime > 0 ) {
this . currentTime = Math . max ( 0 , Math . min ( this . currentTime , this . youtubePlayer . getDuration ( ) ) ) ;
this . youtubePlayer . seekTo ( this . currentTime , true ) ;
}
if ( this . shouldPlay ) {
this . youtubePlayer . playVideo ( ) ;
}
} ;
Player . prototype . onYoutubePlayerStateChange = function ( ev ) {
if ( ev . data == YT . PlayerState . PLAYING ) {
this . playing = true ;
this . currentTime = this . youtubePlayer . getCurrentTime ( ) ;
} else if ( ev . data == YT . PlayerState . PAUSED || ev . data == YT . PlayerState . BUFFERING ) {
this . playing = false ;
this . currentTime = this . youtubePlayer . getCurrentTime ( ) ;
this . updateProgress ( ) ;
} else {
this . playing = false ;
}
2017-03-11 02:48:11 +00:00
this . buffering = ev . data == YT . PlayerState . BUFFERING ;
if ( this . playing && this . pauseAfterBuffer ) {
this . pauseAfterBuffering = false ;
this . pause ( ) ;
}
2017-03-10 14:19:25 +00:00
} ;
Player . prototype . onYoutubePlayerPlaybackRateChange = function ( ev ) {
this . speed = ev . data ;
} ;
Player . prototype . onYoutubeReady = function ( ) {
var youtubePlayerDiv = document . createElement ( "DIV" ) ;
youtubePlayerDiv . id = "youtube_player_" + Player . youtubePlayerCount ++ ;
this . videoContainer . appendChild ( youtubePlayerDiv ) ;
this . youtubePlayer = new YT . Player ( youtubePlayerDiv . id , {
videoId : this . videoContainer . getAttribute ( "data-videoId" ) ,
width : this . videoContainer . offsetWidth ,
height : this . videoContainer . offsetWidth / 16 * 9 ,
events : {
"onReady" : this . onYoutubePlayerReady . bind ( this ) ,
"onStateChange" : this . onYoutubePlayerStateChange . bind ( this ) ,
"onPlaybackRateChange" : this . onYoutubePlayerPlaybackRateChange . bind ( this )
}
} ) ;
} ;
Player . youtubePlayerCount = 0 ;