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 . createHTMLSkeleton = function ( videoId , markers ) {
} ;
// timestamp can be either an int (number of seconds from beginning of the video) or a string ([hh:][mm:]ss)
Player . createHTMLMarker = function ( timestamp , text , ref ) {
if ( typeof ( timestamp ) == "string" ) {
var timeParts = timestamp . split ( ":" ) ;
var hours = ( timeParts . length == 3 ? parseInt ( timeParts [ 0 ] , 10 ) : 0 ) ;
var minutes = ( timeParts . length > 1 ? parseInt ( timeParts [ timeParts . length - 2 ] , 10 ) : 0 ) ;
var seconds = parseInt ( timeParts [ timeParts . length - 1 ] , 10 ) ;
timestamp = hours * 60 * 60 + minutes * 60 + seconds ;
}
var marker = document . createElement ( "DIV" ) ;
marker . classList . add ( "marker" ) ;
marker . setAttribute ( "data-timestamp" , timestamp ) ;
if ( ref !== undefined && ref !== null ) {
marker . setAttribute ( "data-ref" , ref ) ;
}
var content = document . createElement ( "DIV" ) ;
content . classList . add ( "content" ) ;
var markerTime = "(" ;
var hours = Math . floor ( timestamp / 60 / 60 ) ;
var minutes = Math . floor ( timestamp / 60 ) % 60 ;
var seconds = timestamp % 60 ;
if ( hours > 0 ) {
markerTime += ( hours < 10 ? "0" + hours : hours ) + ":" ;
}
markerTime += ( minutes < 10 ? "0" + minutes : minutes ) + ":" + ( seconds < 10 ? "0" + seconds : seconds ) + ")" ;
content . textContent = markerTime + " " + text ;
marker . appendChild ( content ) ;
var fadedProgress = document . createElement ( "DIV" ) ;
fadedProgress . classList . add ( "progress" ) ;
fadedProgress . classList . add ( "faded" ) ;
fadedProgress . appendChild ( content . cloneNode ( true ) ) ;
marker . appendChild ( fadedProgress ) ;
var progress = document . createElement ( "DIV" ) ;
progress . classList . add ( "progress" ) ;
progress . classList . add ( "main" ) ;
progress . appendChild ( content . cloneNode ( true ) ) ;
marker . appendChild ( progress ) ;
return marker ;
} ;
Player . parseHMHAnnotation = function ( text ) {
var annotation = {
title : null ,
videoId : null ,
author : null ,
markers : [ ]
} ;
var lines = text . split ( "\n" ) ;
var mode = "none" ;
for ( var i = 0 ; i < lines . length ; ++ i ) {
var line = lines [ i ] ;
if ( line == "---" ) {
mode = "none" ;
} else if ( line . startsWith ( "title:" ) ) {
annotation . title = line . slice ( 7 ) . replace ( /"/g , "" ) ;
} else if ( line . startsWith ( "videoId:" ) ) {
annotation . videoId = line . slice ( 9 ) . replace ( /"/g , "" ) ;
} else if ( line . startsWith ( "author:" ) ) {
annotation . author = line . slice ( 8 ) . replace ( /"/g , "" ) ; // Miblo, where's the author field?
} else if ( line . startsWith ( "markers" ) ) {
mode = "markers" ;
} else if ( mode == "markers" ) {
var match = line . match ( /"((\d+):)?(\d+):(\d+)": "(.+)"/ ) ;
var marker = {
timestamp : ( match [ 2 ] ? parseInt ( match [ 2 ] , 10 ) : 0 ) * 60 * 60 + parseInt ( match [ 3 ] , 10 ) * 60 + parseInt ( match [ 4 ] , 10 ) ,
text : match [ 5 ] . replace ( /\\"/g , "\"" )
}
annotation . markers . push ( marker ) ;
}
}
return annotation ;
} ;
Player . parseAnnotation = function ( text ) {
} ;
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 ;