sortSources = (sources) -> if not sources console.error('sortSources() called with null source list') return [] qualities = ['2160', '1440', '1080', '720', '540', '480', '360', '240'] pref = String(USEROPTS.default_quality) if USEROPTS.default_quality == 'best' pref = '2160' idx = qualities.indexOf(pref) if idx < 0 idx = 5 # 480p qualityOrder = qualities.slice(idx).concat(qualities.slice(0, idx).reverse()) qualityOrder.unshift('auto') sourceOrder = [] flvOrder = [] for quality in qualityOrder if quality of sources flv = [] nonflv = [] sources[quality].forEach((source) -> source.quality = quality if source.contentType == 'video/flv' flv.push(source) else nonflv.push(source) ) sourceOrder = sourceOrder.concat(nonflv) flvOrder = flvOrder.concat(flv) return sourceOrder.concat(flvOrder).map((source) -> type: source.contentType src: source.link quality: source.quality ) getSourceLabel = (source) -> if source.quality is 'auto' return 'auto' else return "#{source.quality}p #{source.type.split('/')[1]}" waitUntilDefined(window, 'videojs', => videojs.options.flash.swf = '/video-js.swf' ) window.VideoJSPlayer = class VideoJSPlayer extends Player constructor: (data) -> if not (this instanceof VideoJSPlayer) return new VideoJSPlayer(data) @load(data) loadPlayer: (data) -> waitUntilDefined(window, 'videojs', => attrs = width: '100%' height: '100%' if @mediaType == 'cm' and data.meta.textTracks attrs.crossorigin = 'anonymous' video = $('<video/>') .addClass('video-js vjs-default-skin embed-responsive-item') .attr(attrs) removeOld(video) @sources = sortSources(data.meta.direct) if @sources.length == 0 console.error('VideoJSPlayer::constructor(): data.meta.direct has no sources!') @mediaType = null return @sourceIdx = 0 @sources.forEach((source) -> $('<source/>').attr( src: source.src type: source.type res: source.quality label: getSourceLabel(source) ).appendTo(video) ) # TODO: Refactor VideoJSPlayer to use a preLoad()/load()/postLoad() pattern # VideoJSPlayer should provide the core functionality and logic for specific # dependent player types (gdrive) should be an extension if data.meta.gdrive_subtitles data.meta.gdrive_subtitles.available.forEach((subt) -> label = subt.lang_original if subt.name label += " (#{subt.name})" $('<track/>').attr( src: "/gdvtt/#{data.id}/#{subt.lang}/#{subt.name}.vtt?\ vid=#{data.meta.gdrive_subtitles.vid}" kind: 'subtitles' srclang: subt.lang label: label ).appendTo(video) ) if data.meta.textTracks data.meta.textTracks.forEach((track) -> label = track.name $('<track/>').attr( src: track.url kind: 'subtitles' type: track.type label: label ).appendTo(video) ) @player = videojs(video[0], autoplay: true, controls: true, plugins: videoJsResolutionSwitcher: default: @sources[0].quality ) @player.ready(=> @player.on('error', => err = @player.error() if err and err.code == 4 console.error('Caught error, trying next source') @sourceIdx++ if @sourceIdx < @sources.length @player.src(@sources[@sourceIdx]) else console.error('Out of sources, video will not play') if @mediaType is 'gd' and not window.hasDriveUserscript window.promptToInstallDriveUserscript() ) @setVolume(VOLUME) @player.on('ended', -> if CLIENT.leader socket.emit('playNext') ) @player.on('pause', => @paused = true if CLIENT.leader sendVideoUpdate() ) @player.on('play', => @paused = false if CLIENT.leader sendVideoUpdate() ) # Workaround for IE-- even after seeking completes, the loading # spinner remains. @player.on('seeked', => $('.vjs-waiting').removeClass('vjs-waiting') ) # Workaround for Chrome-- it seems that the click bindings for # the subtitle menu aren't quite set up until after the ready # event finishes, so set a timeout for 1ms to force this code # not to run until the ready() function returns. setTimeout(-> $('#ytapiplayer .vjs-subtitles-button .vjs-menu-item').each((i, elem) -> textNode = elem.childNodes[0] if textNode.textContent == localStorage.lastSubtitle elem.click() elem.onclick = -> if elem.attributes['aria-checked'].value == 'true' localStorage.lastSubtitle = textNode.textContent ) , 1) ) ) load: (data) -> @setMediaProperties(data) # Note: VideoJS does have facilities for loading new videos into the # existing player object, however it appears to be pretty glitchy when # a video can't be played (either previous or next video). It's safer # to just reset the entire thing. @destroy() @loadPlayer(data) play: -> @paused = false if @player and @player.readyState() > 0 @player.play() pause: -> @paused = true if @player and @player.readyState() > 0 @player.pause() seekTo: (time) -> if @player and @player.readyState() > 0 @player.currentTime(time) setVolume: (volume) -> if @player @player.volume(volume) getTime: (cb) -> if @player and @player.readyState() > 0 cb(@player.currentTime()) else cb(0) getVolume: (cb) -> if @player and @player.readyState() > 0 if @player.muted() cb(0) else cb(@player.volume()) else cb(VOLUME) destroy: -> removeOld() if @player @player.dispose()