diff --git a/player/base.coffee b/player/base.coffee index 6f282275..f7c47e03 100644 --- a/player/base.coffee +++ b/player/base.coffee @@ -1,5 +1,8 @@ -class Player +window.Player = class Player constructor: (data) -> + if not (this instanceof Player) + return new Player(data) + @setMediaProperties(data) @paused = false @@ -29,91 +32,3 @@ class Player getVolume: (cb) -> cb(VOLUME) - -window.Player = Player - -window.removeOld = (replace) -> - $('#sc_volume').remove() - replace ?= $('
').addClass('embed-responsive-item') - old = $('#ytapiplayer') - replace.insertBefore(old) - old.remove() - replace.attr('id', 'ytapiplayer') - return replace - -TYPE_MAP = - yt: 'YouTubePlayer' - -window.loadMediaPlayer = (data) -> - if data.type of TYPE_MAP - ctor = window[TYPE_MAP[data.type]] - window.PLAYER = new ctor(data) - -window.handleMediaUpdate = (data) -> - PLAYER = window.PLAYER - - # Do not update if the current time is past the end of the video, unless - # the video has length 0 (which is a special case for livestreams) - if typeof PLAYER.mediaLength is 'number' and - PLAYER.mediaLength > 0 and - data.currentTime > PLAYER.mediaLength - return - - # Negative currentTime indicates a lead-in for clients to load the video, - # but not play it yet (helps with initial buffering) - waiting = data.currentTime < 0 - - # Load a new video in the same player if the ID changed - if data.id and data.id != PLAYER.mediaId - if data.currentTime < 0 - data.currentTime = 0 - PLAYER.load(data) - PLAYER.play() - - if waiting - console.log('waiting') - # YouTube player has a race condition that crashes the player if - # play(), seek(0), and pause() are called quickly without waiting - # for events to fire. Setting a flag variable that is checked in the - # event handler mitigates this. - if PLAYER.type is 'yt' - PLAYER.pauseSeekRaceCondition = true - else - PLAYER.seekTo(0) - PLAYER.pause() - else if PLAYER.type is 'yt' - PLAYER.pauseSeekRaceCondition = false - - if CLIENT.leader or not USEROPTS.synch - return - - if data.paused and not PLAYER.paused - PLAYER.seekTo(data.currentTime) - PLAYER.pause() - else if PLAYER.paused - PLAYER.play() - - PLAYER.getTime((seconds) -> - time = data.currentTime - diff = (time - seconds) or time - accuracy = USEROPTS.sync_accuracy - - # Dailymotion can't seek very accurately in Flash due to keyframe - # placement. Accuracy should not be set lower than 5 or the video - # may be very choppy. - if PLAYER.type is 'dm' - accuracy = Math.max(accuracy, 5) - - - if diff > accuracy - # The player is behind the correct time - PLAYER.seekTo(time) - else if diff < -accuracy - # The player is ahead of the correct time - # Don't seek all the way back, to account for possible buffering. - # However, do seek all the way back for Dailymotion due to the - # keyframe issue mentioned above. - if PLAYER.type isnt 'dm' - time += 1 - PLAYER.seekTo(time) - ) diff --git a/player/update.coffee b/player/update.coffee new file mode 100644 index 00000000..5982988a --- /dev/null +++ b/player/update.coffee @@ -0,0 +1,89 @@ +TYPE_MAP = + yt: YouTubePlayer + vi: VimeoPlayer + +window.loadMediaPlayer = (data) -> + if data.type of TYPE_MAP + console.log data + try + window.PLAYER = TYPE_MAP[data.type](data) + catch e + console.error e + console.log(window.PLAYER) + +window.handleMediaUpdate = (data) -> + PLAYER = window.PLAYER + + # Do not update if the current time is past the end of the video, unless + # the video has length 0 (which is a special case for livestreams) + if typeof PLAYER.mediaLength is 'number' and + PLAYER.mediaLength > 0 and + data.currentTime > PLAYER.mediaLength + return + + # Negative currentTime indicates a lead-in for clients to load the video, + # but not play it yet (helps with initial buffering) + waiting = data.currentTime < 0 + + # Load a new video in the same player if the ID changed + if data.id and data.id != PLAYER.mediaId + if data.currentTime < 0 + data.currentTime = 0 + PLAYER.load(data) + PLAYER.play() + + if waiting + # YouTube player has a race condition that crashes the player if + # play(), seek(0), and pause() are called quickly without waiting + # for events to fire. Setting a flag variable that is checked in the + # event handler mitigates this. + if PLAYER instanceof YouTubePlayer + PLAYER.pauseSeekRaceCondition = true + else + PLAYER.seekTo(0) + PLAYER.pause() + else if PLAYER instanceof YouTubePlayer + PLAYER.pauseSeekRaceCondition = false + + if CLIENT.leader or not USEROPTS.synch + return + + if data.paused and not PLAYER.paused + PLAYER.seekTo(data.currentTime) + PLAYER.pause() + else if PLAYER.paused + PLAYER.play() + + PLAYER.getTime((seconds) -> + time = data.currentTime + diff = (time - seconds) or time + accuracy = USEROPTS.sync_accuracy + + # Dailymotion can't seek very accurately in Flash due to keyframe + # placement. Accuracy should not be set lower than 5 or the video + # may be very choppy. + if PLAYER.mediaType is 'dm' # TODO: instanceof DailymotionPlayer + accuracy = Math.max(accuracy, 5) + + + if diff > accuracy + # The player is behind the correct time + PLAYER.seekTo(time) + else if diff < -accuracy + # The player is ahead of the correct time + # Don't seek all the way back, to account for possible buffering. + # However, do seek all the way back for Dailymotion due to the + # keyframe issue mentioned above. + if PLAYER.mediaType isnt 'dm' # TODO: instanceof DailymotionPlayer + time += 1 + PLAYER.seekTo(time) + ) + +window.removeOld = (replace) -> + $('#sc_volume').remove() + replace ?= $('').addClass('embed-responsive-item') + old = $('#ytapiplayer') + replace.insertBefore(old) + old.remove() + replace.attr('id', 'ytapiplayer') + return replace diff --git a/player/vimeo.coffee b/player/vimeo.coffee new file mode 100644 index 00000000..8803194d --- /dev/null +++ b/player/vimeo.coffee @@ -0,0 +1,80 @@ +window.VimeoPlayer = class VimeoPlayer extends Player + constructor: (data) -> + if not (this instanceof VimeoPlayer) + return new VimeoPlayer(data) + + @load(data) + + load: (data) -> + @setMediaProperties(data) + + waitUntilDefined(window, '$f', => + video = $('') + removeOld(video) + video.attr( + src: "https://player.vimeo.com/video/#{data.id}?api=1&player_id=ytapiplayer" + webkitallowfullscreen: true + mozallowfullscreen: true + allowfullscreen: true + ) + + if USEROPTS.wmode_transparent + video.attr('wmode', 'transparent') + + $f(video[0]).addEvent('ready', => + @vimeo = $f(video[0]) + @play() + + @vimeo.addEvent('finish', => + if CLIENT.leader + socket.emit('playNext') + ) + + @vimeo.addEvent('pause', => + @paused = true + if CLIENT.leader + sendVideoUpdate() + ) + + @vimeo.addEvent('play', => + @paused = false + if CLIENT.leader + sendVideoUpdate() + ) + + @setVolume(VOLUME) + ) + ) + + play: -> + @paused = false + if @vimeo + @vimeo.api('play') + + pause: -> + @paused = true + if @vimeo + @vimeo.api('pause') + + seekTo: (time) -> + if @vimeo + @vimeo.api('seekTo', time) + + setVolume: (volume) -> + if @vimeo + @vimeo.api('setVolume', volume) + + getTime: (cb) -> + if @vimeo + @vimeo.api('getCurrentTime', (time) -> + # I will never understand why Vimeo returns current time as a string + cb(parseFloat(time)) + ) + else + cb(0) + + getVolume: (cb) -> + if @vimeo + @vimeo.api('getVolume', cb) + else + cb(VOLUME) diff --git a/player/youtube.coffee b/player/youtube.coffee index fc2d20da..ef3af95e 100644 --- a/player/youtube.coffee +++ b/player/youtube.coffee @@ -1,5 +1,8 @@ -class YouTubePlayer extends Player +window.YouTubePlayer = class YouTubePlayer extends Player constructor: (data) -> + if not (this instanceof YouTubePlayer) + return new YouTubePlayer(data) + @setMediaProperties(data) @qualityRaceCondition = true @pauseSeekRaceCondition = false @@ -24,7 +27,7 @@ class YouTubePlayer extends Player ) load: (data) -> - super(data) + @setMediaProperties(data) if @yt @yt.loadVideoById(data.id, data.currentTime) @qualityRaceCondition = true @@ -32,7 +35,7 @@ class YouTubePlayer extends Player @yt.setPlaybackQuality(USEROPTS.default_quality) onReady: -> - @yt.setVolume(VOLUME) + @setVolume(VOLUME) onStateChange: (ev) -> # For some reason setting the quality doesn't work @@ -93,5 +96,3 @@ class YouTubePlayer extends Player cb(@yt.getVolume() / 100) else cb(VOLUME) - -window.YouTubePlayer = YouTubePlayer diff --git a/www/js/callbacks.js b/www/js/callbacks.js index c222ace4..9fe978f2 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -824,20 +824,27 @@ Callbacks = { return; } - /* Failsafe */ + // Failsafe if (isNaN(VOLUME) || VOLUME > 1 || VOLUME < 0) { VOLUME = 1; } - var shouldResize = $("#ytapiplayer").html() === ""; - + // Persist the user's volume preference from the the player, if possible if (PLAYER && typeof PLAYER.getVolume === "function") { + var name = PLAYER.__proto__.constructor.name; PLAYER.getVolume(function (v) { + console.log(name, v) if (typeof v === "number") { if (v < 0 || v > 1) { + // Dailymotion's API was wrong once and caused a huge + // headache. This alert is here to make debugging easier. alert("Something went wrong with retrieving the volume. " + "Please tell calzoneman the following: " + - JSON.stringify({ v: v, t: PLAYER.type, i: PLAYER.videoId })); + JSON.stringify({ + v: v, + t: PLAYER.mediaType, + i: PLAYER.mediaId + })); } else { VOLUME = v; setOpt("volume", VOLUME); @@ -846,42 +853,21 @@ Callbacks = { }); } - if (CHANNEL.opts.allow_voteskip) + // Reset voteskip since the video changed + if (CHANNEL.opts.allow_voteskip) { $("#voteskip").attr("disabled", false); + } $("#currenttitle").text("Currently Playing: " + data.title); - if (data.type != "sc" && PLAYER.type == "sc") - // [](/goddamnitmango) - fixSoundcloudShit(); + // TODO: fix this + setTimeout(function () { + if (data.type !== PLAYER.mediaType) { + loadMediaPlayer(data); + } - if (data.type != "jw" && PLAYER.type == "jw") { - // Is it so hard to not mess up my DOM? - $("").attr("id", "ytapiplayer") - .insertBefore($("#ytapiplayer_wrapper")); - $("#ytapiplayer_wrapper").remove(); - } - - if (data.type === "fi") { - data.url = data.id; - } - - if (NO_VIMEO && data.type === "vi" && data.meta.direct) { - data = vimeoSimulator2014(data); - } - - /* - * Google Docs now uses the same simulator as Google+ - */ - if (data.type === "gp" || data.type === "gd") { - data = googlePlusSimulator2014(data); - } - - if (data.type != PLAYER.type) { - loadMediaPlayer(data); - } - - handleMediaUpdate(data); + handleMediaUpdate(data); + }, 100); }, mediaUpdate: function(data) { diff --git a/www/js/player-new.js b/www/js/player-new.js index bc98109a..10c8376f 100644 --- a/www/js/player-new.js +++ b/www/js/player-new.js @@ -1,10 +1,13 @@ (function() { - var Player, TYPE_MAP, VideoJSPlayer, YouTubePlayer, + var Player, TYPE_MAP, VimeoPlayer, YouTubePlayer, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; - Player = (function() { + window.Player = Player = (function() { function Player(data) { + if (!(this instanceof Player)) { + return new Player(data); + } this.setMediaProperties(data); this.paused = false; } @@ -47,104 +50,113 @@ })(); - window.Player = Player; + window.VimeoPlayer = VimeoPlayer = (function(superClass) { + extend(VimeoPlayer, superClass); - window.removeOld = function(replace) { - var old; - $('#sc_volume').remove(); - if (replace == null) { - replace = $('').addClass('embed-responsive-item'); - } - old = $('#ytapiplayer'); - replace.insertBefore(old); - old.remove(); - replace.attr('id', 'ytapiplayer'); - return replace; - }; - - TYPE_MAP = { - yt: 'YouTubePlayer' - }; - - window.loadMediaPlayer = function(data) { - var ctor; - if (data.type in TYPE_MAP) { - ctor = window[TYPE_MAP[data.type]]; - return window.PLAYER = new ctor(data); - } - }; - - window.handleMediaUpdate = function(data) { - var PLAYER, waiting; - PLAYER = window.PLAYER; - if (typeof PLAYER.mediaLength === 'number' && PLAYER.mediaLength > 0 && data.currentTime > PLAYER.mediaLength) { - return; - } - waiting = data.currentTime < 0; - if (data.id && data.id !== PLAYER.mediaId) { - if (data.currentTime < 0) { - data.currentTime = 0; + function VimeoPlayer(data) { + if (!(this instanceof VimeoPlayer)) { + return new VimeoPlayer(data); } - PLAYER.load(data); - PLAYER.play(); + this.load(data); } - if (waiting) { - console.log('waiting'); - if (PLAYER.type === 'yt') { - PLAYER.pauseSeekRaceCondition = true; - } else { - PLAYER.seekTo(0); - PLAYER.pause(); - } - } else if (PLAYER.type === 'yt') { - PLAYER.pauseSeekRaceCondition = false; - } - if (CLIENT.leader || !USEROPTS.synch) { - return; - } - if (data.paused && !PLAYER.paused) { - PLAYER.seekTo(data.currentTime); - PLAYER.pause(); - } else if (PLAYER.paused) { - PLAYER.play(); - } - return PLAYER.getTime(function(seconds) { - var accuracy, diff, time; - time = data.currentTime; - diff = (time - seconds) || time; - accuracy = USEROPTS.sync_accuracy; - if (PLAYER.type === 'dm') { - accuracy = Math.max(accuracy, 5); - } - if (diff > accuracy) { - return PLAYER.seekTo(time); - } else if (diff < -accuracy) { - if (PLAYER.type !== 'dm') { - time += 1; - } - return PLAYER.seekTo(time); - } - }); - }; - VideoJSPlayer = (function(superClass) { - extend(VideoJSPlayer, superClass); - - function VideoJSPlayer(data) {} - - VideoJSPlayer.prototype.load = function(data) { - var video; - return video = $('').addClass('video-js vjs-default-skin embed-responsive-item'); + VimeoPlayer.prototype.load = function(data) { + this.setMediaProperties(data); + return waitUntilDefined(window, '$f', (function(_this) { + return function() { + var video; + video = $(''); + removeOld(video); + video.attr({ + src: "https://player.vimeo.com/video/" + data.id + "?api=1&player_id=ytapiplayer", + webkitallowfullscreen: true, + mozallowfullscreen: true, + allowfullscreen: true + }); + if (USEROPTS.wmode_transparent) { + video.attr('wmode', 'transparent'); + } + return $f(video[0]).addEvent('ready', function() { + _this.vimeo = $f(video[0]); + _this.play(); + _this.vimeo.addEvent('finish', function() { + if (CLIENT.leader) { + return socket.emit('playNext'); + } + }); + _this.vimeo.addEvent('pause', function() { + _this.paused = true; + if (CLIENT.leader) { + return sendVideoUpdate(); + } + }); + _this.vimeo.addEvent('play', function() { + _this.paused = false; + if (CLIENT.leader) { + return sendVideoUpdate(); + } + }); + return _this.setVolume(VOLUME); + }); + }; + })(this)); }; - return VideoJSPlayer; + VimeoPlayer.prototype.play = function() { + this.paused = false; + if (this.vimeo) { + return this.vimeo.api('play'); + } + }; + + VimeoPlayer.prototype.pause = function() { + this.paused = true; + if (this.vimeo) { + return this.vimeo.api('pause'); + } + }; + + VimeoPlayer.prototype.seekTo = function(time) { + if (this.vimeo) { + return this.vimeo.api('seekTo', time); + } + }; + + VimeoPlayer.prototype.setVolume = function(volume) { + if (this.vimeo) { + return this.vimeo.api('setVolume', volume); + } + }; + + VimeoPlayer.prototype.getTime = function(cb) { + if (this.vimeo) { + return this.vimeo.api('getCurrentTime', function(time) { + return cb(parseFloat(time)); + }); + } else { + return cb(0); + } + }; + + VimeoPlayer.prototype.getVolume = function(cb) { + if (this.vimeo) { + return this.vimeo.api('getVolume', cb); + } else { + return cb(VOLUME); + } + }; + + return VimeoPlayer; })(Player); - YouTubePlayer = (function(superClass) { + window.YouTubePlayer = YouTubePlayer = (function(superClass) { extend(YouTubePlayer, superClass); function YouTubePlayer(data) { + if (!(this instanceof YouTubePlayer)) { + return new YouTubePlayer(data); + } this.setMediaProperties(data); this.qualityRaceCondition = true; this.pauseSeekRaceCondition = false; @@ -155,7 +167,6 @@ wmode = USEROPTS.wmode_transparent ? 'transparent' : 'opaque'; return _this.yt = new YT.Player('ytapiplayer', { videoId: data.id, - startSeconds: data.currentTime, playerVars: { autohide: 1, autoplay: 1, @@ -174,7 +185,7 @@ } YouTubePlayer.prototype.load = function(data) { - YouTubePlayer.__super__.load.call(this, data); + this.setMediaProperties(data); if (this.yt) { this.yt.loadVideoById(data.id, data.currentTime); this.qualityRaceCondition = true; @@ -185,7 +196,7 @@ }; YouTubePlayer.prototype.onReady = function() { - return this.yt.setVolume(VOLUME); + return this.setVolume(VOLUME); }; YouTubePlayer.prototype.onStateChange = function(ev) { @@ -263,6 +274,88 @@ })(Player); - window.YouTubePlayer = YouTubePlayer; + TYPE_MAP = { + yt: YouTubePlayer, + vi: VimeoPlayer + }; + + window.loadMediaPlayer = function(data) { + var e; + if (data.type in TYPE_MAP) { + console.log(data); + try { + window.PLAYER = TYPE_MAP[data.type](data); + } catch (_error) { + e = _error; + console.error(e); + } + return console.log(window.PLAYER); + } + }; + + window.handleMediaUpdate = function(data) { + var PLAYER, waiting; + PLAYER = window.PLAYER; + if (typeof PLAYER.mediaLength === 'number' && PLAYER.mediaLength > 0 && data.currentTime > PLAYER.mediaLength) { + return; + } + waiting = data.currentTime < 0; + if (data.id && data.id !== PLAYER.mediaId) { + if (data.currentTime < 0) { + data.currentTime = 0; + } + PLAYER.load(data); + PLAYER.play(); + } + if (waiting) { + if (PLAYER instanceof YouTubePlayer) { + PLAYER.pauseSeekRaceCondition = true; + } else { + PLAYER.seekTo(0); + PLAYER.pause(); + } + } else if (PLAYER instanceof YouTubePlayer) { + PLAYER.pauseSeekRaceCondition = false; + } + if (CLIENT.leader || !USEROPTS.synch) { + return; + } + if (data.paused && !PLAYER.paused) { + PLAYER.seekTo(data.currentTime); + PLAYER.pause(); + } else if (PLAYER.paused) { + PLAYER.play(); + } + return PLAYER.getTime(function(seconds) { + var accuracy, diff, time; + time = data.currentTime; + diff = (time - seconds) || time; + accuracy = USEROPTS.sync_accuracy; + if (PLAYER.mediaType === 'dm') { + accuracy = Math.max(accuracy, 5); + } + if (diff > accuracy) { + return PLAYER.seekTo(time); + } else if (diff < -accuracy) { + if (PLAYER.mediaType !== 'dm') { + time += 1; + } + return PLAYER.seekTo(time); + } + }); + }; + + window.removeOld = function(replace) { + var old; + $('#sc_volume').remove(); + if (replace == null) { + replace = $('').addClass('embed-responsive-item'); + } + old = $('#ytapiplayer'); + replace.insertBefore(old); + old.remove(); + replace.attr('id', 'ytapiplayer'); + return replace; + }; }).call(this);