diff --git a/player/base.coffee b/player/base.coffee index a973cbea..6f282275 100644 --- a/player/base.coffee +++ b/player/base.coffee @@ -39,3 +39,81 @@ window.removeOld = (replace) -> 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/youtube.coffee b/player/youtube.coffee index 867c6148..fc2d20da 100644 --- a/player/youtube.coffee +++ b/player/youtube.coffee @@ -2,7 +2,7 @@ class YouTubePlayer extends Player constructor: (data) -> @setMediaProperties(data) @qualityRaceCondition = true - @pauseSeekRaceCondition = true + @pauseSeekRaceCondition = false waitUntilDefined(window, 'YT', => removeOld() @@ -39,7 +39,8 @@ class YouTubePlayer extends Player # until the first event has fired. if @qualityRaceCondition @qualityRaceCondition = false - @yt.setPlaybackQuality(USEROPTS.default_quality) + if USEROPTS.default_quality + @yt.setPlaybackQuality(USEROPTS.default_quality) # Similar to above, if you pause the video before the first PLAYING # event is emitted, weird things happen. @@ -57,12 +58,12 @@ class YouTubePlayer extends Player socket.emit('playNext') play: -> - super() + @paused = false if @yt @yt.playVideo() pause: -> - super() + @paused = true if @yt @yt.pauseVideo() @@ -81,10 +82,16 @@ class YouTubePlayer extends Player getTime: (cb) -> if @yt cb(@yt.getCurrentTime()) + else + cb(0) getVolume: (cb) -> if @yt if @yt.isMuted() - return 0 + cb(0) else - return @yt.getVolume() / 100.0 + cb(@yt.getVolume() / 100) + else + cb(VOLUME) + +window.YouTubePlayer = YouTubePlayer diff --git a/www/js/callbacks.js b/www/js/callbacks.js index 25e0a804..c222ace4 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -1097,7 +1097,7 @@ setupCallbacks = function() { Callbacks[key](data); } catch (e) { if (SOCKET_DEBUG) { - console.log("EXCEPTION: " + e.stack); + console.log("EXCEPTION: " + e + "\n" + e.stack); } } }); diff --git a/www/js/player-new.js b/www/js/player-new.js index dbf9cd04..bc98109a 100644 --- a/www/js/player-new.js +++ b/www/js/player-new.js @@ -1,5 +1,5 @@ (function() { - var Player, VideoJSPlayer, YouTubePlayer, + var Player, TYPE_MAP, VideoJSPlayer, 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; @@ -58,7 +58,73 @@ old = $('#ytapiplayer'); replace.insertBefore(old); old.remove(); - return replace.attr('id', 'ytapiplayer'); + 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; + } + PLAYER.load(data); + PLAYER.play(); + } + 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) { @@ -81,7 +147,7 @@ function YouTubePlayer(data) { this.setMediaProperties(data); this.qualityRaceCondition = true; - this.pauseSeekRaceCondition = true; + this.pauseSeekRaceCondition = false; waitUntilDefined(window, 'YT', (function(_this) { return function() { var wmode; @@ -89,6 +155,7 @@ wmode = USEROPTS.wmode_transparent ? 'transparent' : 'opaque'; return _this.yt = new YT.Player('ytapiplayer', { videoId: data.id, + startSeconds: data.currentTime, playerVars: { autohide: 1, autoplay: 1, @@ -124,7 +191,9 @@ YouTubePlayer.prototype.onStateChange = function(ev) { if (this.qualityRaceCondition) { this.qualityRaceCondition = false; - this.yt.setPlaybackQuality(USEROPTS.default_quality); + if (USEROPTS.default_quality) { + this.yt.setPlaybackQuality(USEROPTS.default_quality); + } } if (ev.data === YT.PlayerState.PLAYING && this.pauseSeekRaceCondition) { this.pause(); @@ -142,14 +211,14 @@ }; YouTubePlayer.prototype.play = function() { - YouTubePlayer.__super__.play.call(this); + this.paused = false; if (this.yt) { return this.yt.playVideo(); } }; YouTubePlayer.prototype.pause = function() { - YouTubePlayer.__super__.pause.call(this); + this.paused = true; if (this.yt) { return this.yt.pauseVideo(); } @@ -173,16 +242,20 @@ YouTubePlayer.prototype.getTime = function(cb) { if (this.yt) { return cb(this.yt.getCurrentTime()); + } else { + return cb(0); } }; YouTubePlayer.prototype.getVolume = function(cb) { if (this.yt) { if (this.yt.isMuted()) { - return 0; + return cb(0); } else { - return this.yt.getVolume() / 100.0; + return cb(this.yt.getVolume() / 100); } + } else { + return cb(VOLUME); } }; @@ -190,4 +263,6 @@ })(Player); + window.YouTubePlayer = YouTubePlayer; + }).call(this);