diff --git a/lib/channel/channel.js b/lib/channel/channel.js index 0f573185..2ea5f70f 100644 --- a/lib/channel/channel.js +++ b/lib/channel/channel.js @@ -114,6 +114,7 @@ Channel.prototype.initModules = function () { "./opts" : "options", "./library" : "library", "./playlist" : "playlist", + "./mediarefresher": "mediarefresher", "./voteskip" : "voteskip", "./poll" : "poll", "./kickban" : "kickban", @@ -281,9 +282,11 @@ Channel.prototype.notifyModules = function (fn, args) { Channel.prototype.joinUser = function (user, data) { var self = this; + self.activeLock.lock(); self.waitFlag(Flags.C_READY, function () { /* User closed the connection before the channel finished loading */ if (user.socket.disconnected) { + self.activeLock.release(); return; } @@ -292,6 +295,7 @@ Channel.prototype.joinUser = function (user, data) { if (err) { Logger.errlog.log("user.refreshAccount failed at Channel.joinUser"); Logger.errlog.log(err.stack); + self.activeLock.release(); return; } @@ -303,6 +307,7 @@ Channel.prototype.joinUser = function (user, data) { function afterAccount() { if (self.dead || user.socket.disconnected) { + if (self.activeLock) self.activeLock.release(); return; } @@ -311,11 +316,11 @@ Channel.prototype.joinUser = function (user, data) { if (user.account.channelRank !== user.account.globalRank) { user.socket.emit("rank", user.account.effectiveRank); } - self.activeLock.lock(); self.acceptUser(user); } else { user.account.channelRank = 0; user.account.effectiveRank = user.account.globalRank; + self.activeLock.release(); } }); } diff --git a/lib/channel/chat.js b/lib/channel/chat.js index 1775012b..fc67e9e7 100644 --- a/lib/channel/chat.js +++ b/lib/channel/chat.js @@ -131,7 +131,7 @@ ChatModule.prototype.handleChatMsg = function (user, data) { } data.meta = meta; - this.channel.checkModules("onUserChat", [user, data], function (err, result) { + this.channel.checkModules("onUserPreChat", [user, data], function (err, result) { if (result === ChannelModule.PASSTHROUGH) { self.processChatMsg(user, data); } diff --git a/lib/channel/drink.js b/lib/channel/drink.js index bb702f43..91959725 100644 --- a/lib/channel/drink.js +++ b/lib/channel/drink.js @@ -11,7 +11,7 @@ DrinkModule.prototype.onUserPostJoin = function (user) { user.socket.emit("drinkCount", this.drinks); }; -DrinkModule.prototype.onUserChat = function (user, data, cb) { +DrinkModule.prototype.onUserPreChat = function (user, data, cb) { var msg = data.msg; var perms = this.channel.modules.permissions; if (msg.match(/^\/d-?[0-9]*/) && perms.canCallDrink(user)) { diff --git a/lib/channel/mediarefresher.js b/lib/channel/mediarefresher.js new file mode 100644 index 00000000..b5759a61 --- /dev/null +++ b/lib/channel/mediarefresher.js @@ -0,0 +1,106 @@ +var ChannelModule = require("./module"); +var Config = require("../config"); +var InfoGetter = require("../get-info"); +var Logger = require("../logger"); + +function MediaRefresherModule(channel) { + ChannelModule.apply(this, arguments); + this._interval = false; + this._media = null; +} + +MediaRefresherModule.prototype = Object.create(ChannelModule.prototype); + +MediaRefresherModule.prototype.onPreMediaChange = function (data, cb) { + if (this._interval) clearInterval(this._interval); + + this._media = data; + + switch (data.type) { + case "gd": + return this.initGoogleDocs(data, function () { + cb(null, ChannelModule.PASSTHROUGH); + }); + case "vi": + return this.initVimeo(data, function () { + cb(null, ChannelModule.PASSTHROUGH); + }); + default: + return cb(null, ChannelModule.PASSTHROUGH); + } +}; + +MediaRefresherModule.prototype.initGoogleDocs = function (data, cb) { + var self = this; + self.refreshGoogleDocs(data, cb); + + /* + * Refresh every 55 minutes. + * The expiration is 1 hour, but refresh 5 minutes early to be safe + */ + self._interval = setInterval(function () { + self.refreshGoogleDocs(data); + }, 55 * 60 * 1000); +}; + +MediaRefresherModule.prototype.initVimeo = function (data, cb) { + if (!Config.get("vimeo-workaround")) { + if (cb) cb(); + return; + } + + var self = this; + self.channel.activeLock.lock(); + InfoGetter.vimeoWorkaround(data.id, function (hack) { + if (self._media === data) { + self.channel.logger.log("[mediarefresher] Refreshed vimeo video with ID " + + data.id); + data.meta.direct = hack; + } + self.channel.activeLock.release(); + + if (cb) cb(); + }); +}; + +MediaRefresherModule.prototype.refreshGoogleDocs = function (media, cb) { + var self = this; + + if (self.dead || self.channel.dead) { + return; + } + + self.channel.activeLock.lock(); + InfoGetter.getMedia(media.id, "gd", function (err, data) { + switch (err) { + case "HTTP 302": + case "Video not found": + case "Private video": + self.channel.activeLock.release(); + if (cb) cb(); + return; + default: + if (err) { + Logger.errlog.log("Google Docs refresh failed for ID " + media.id + + ": " + err); + self.channel.activeLock.release(); + if (cb) cb(); + return; + } + } + + if (media !== self._media) { + self.channel.activeLock.release(); + if (cb) cb(); + return; + } + + self.channel.logger.log("[mediarefresher] Refreshed Google Docs video with ID " + + media.id); + media.meta = data.meta; + self.channel.activeLock.release(); + if (cb) cb(); + }); +}; + +module.exports = MediaRefresherModule; diff --git a/lib/channel/module.js b/lib/channel/module.js index 430efab3..c8c19ec0 100644 --- a/lib/channel/module.js +++ b/lib/channel/module.js @@ -54,7 +54,14 @@ ChannelModule.prototype = { /** * Called when a chatMsg event is received */ - onUserChat: function (user, data, cb) { + onUserPreChat: function (user, data, cb) { + cb(null, ChannelModule.PASSTHROUGH); + }, + + /** + * Called before a new video begins playing + */ + onPreMediaChange: function (data, cb) { cb(null, ChannelModule.PASSTHROUGH); }, diff --git a/lib/channel/playlist.js b/lib/channel/playlist.js index bf4a17d7..4dcd9197 100644 --- a/lib/channel/playlist.js +++ b/lib/channel/playlist.js @@ -977,73 +977,51 @@ PlaylistModule.prototype.startPlayback = function (time) { var media = self.current.media; media.reset(); - var continuePlayback = function () { - if (self.leader != null) { - media.paused = false; - media.currentTime = time || 0; - self.sendChangeMedia(self.channel.users); - self.channel.notifyModules("onMediaChange", self.current.media); - return; - } - - /* Lead-in time of 3 seconds to allow clients to buffer */ - time = time || -3; - media.paused = time < 0; - media.currentTime = time; - - /* Module was already leading, stop the previous timer */ - if (self._leadInterval) { - clearInterval(self._leadInterval); - self._leadInterval = false; - } - - self.sendChangeMedia(self.channel.users); - self.channel.notifyModules("onMediaChange", self.current.media); - - /* Only start the timer if the media item is not live, i.e. has a duration */ - if (media.seconds > 0) { - self._lastUpdate = Date.now(); - self._leadInterval = setInterval(function() { - self._leadLoop(); - }, 1000); - } - - /* Google Docs autorefresh */ - if (self._gdRefreshTimer) { - clearInterval(self._gdRefreshTimer); - self._gdRefreshTimer = false; - } - - if (media.type === "gd") { - self._gdRefreshTimer = setInterval(self.refreshGoogleDocs.bind(self), 3600000); - if (media.meta.expiration && media.meta.expiration < Date.now() + 3600000) { - setTimeout(self.refreshGoogleDocs.bind(self), media.meta.expiration - Date.now()); + if (self.leader != null) { + media.paused = false; + media.currentTime = time || 0; + self.channel.checkModules("onPreMediaChange", [self.current.media], + function () { + /* + * onPreMediaChange doesn't care about the callback result. + * Its purpose is to allow modification of playback data before + * users are sent a changeMedia + */ + self.sendChangeMedia(self.channel.users); } - } - }; - - if (media.type === "vi" && !media.meta.direct && Config.get("vimeo-workaround")) { - self.channel.activeLock.lock(); - vimeoWorkaround(media.id, function (direct) { - self.channel.activeLock.release(); - if (self.current && self.current.media === media) { - self.current.media.meta.direct = direct; - continuePlayback(); - } - }); + ); return; - } else if (media.type === "gd" && isExpired(media) && !media.meta.failed) { - self.channel.activeLock.lock(); - self.refreshGoogleDocs(function () { - self.channel.activeLock.release(); - if (self.current && self.current.media === media) { - continuePlayback(); - } - }); - return; - } else { - continuePlayback(); } + + /* Lead-in time of 3 seconds to allow clients to buffer */ + time = time || -3; + media.paused = time < 0; + media.currentTime = time; + + /* Module was already leading, stop the previous timer */ + if (self._leadInterval) { + clearInterval(self._leadInterval); + self._leadInterval = false; + } + + self.channel.checkModules("onPreMediaChange", [self.current.media], + function () { + /* + * onPreMediaChange currently doesn't care about the callback result. + * Its purpose is to allow modification of playback data before + * users are sent a changeMedia + */ + self.sendChangeMedia(self.channel.users); + + /* Only start the timer if the media item is not live, i.e. has a duration */ + if (media.seconds > 0) { + self._lastUpdate = Date.now(); + self._leadInterval = setInterval(function() { + self._leadLoop(); + }, 1000); + } + } + ); } const UPDATE_INTERVAL = Config.get("playlist.update-interval"); diff --git a/lib/server.js b/lib/server.js index 56890a98..754843c7 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,7 +9,7 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const VERSION = "3.3.0"; +const VERSION = "3.3.1"; var singleton = null; var Config = require("./config"); diff --git a/package.json b/package.json index 05785221..56efe4cb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Calvin Montgomery", "name": "CyTube", "description": "Online media synchronizer and chat", - "version": "3.3.0", + "version": "3.3.1", "repository": { "url": "http://github.com/calzoneman/sync" }, diff --git a/www/js/callbacks.js b/www/js/callbacks.js index d18af9f6..f495528a 100644 --- a/www/js/callbacks.js +++ b/www/js/callbacks.js @@ -850,16 +850,16 @@ Callbacks = { }); } - if(CHANNEL.opts.allow_voteskip) + if (CHANNEL.opts.allow_voteskip) $("#voteskip").attr("disabled", false); $("#currenttitle").text("Currently Playing: " + data.title); - if(data.type != "sc" && PLAYER.type == "sc") + if (data.type != "sc" && PLAYER.type == "sc") // [](/goddamnitmango) fixSoundcloudShit(); - if(data.type != "jw" && PLAYER.type == "jw") { + if (data.type != "jw" && PLAYER.type == "jw") { // Is it so hard to not mess up my DOM? $("
").attr("id", "ytapiplayer") .insertBefore($("#ytapiplayer_wrapper")); @@ -874,60 +874,18 @@ Callbacks = { data.url = data.id; } - /* - VIMEO SIMULATOR 2014 - - Vimeo decided to block my domain. After repeated emails, they refused to - unblock it. Rather than give in to their demands, there is a serverside - option which extracts direct links to the h264 encoded MP4 video files. - These files can be loaded in a custom player to allow Vimeo playback without - triggering their dumb API domain block. - - It's a little bit hacky, but my only other option is to keep buying new - domains every time one gets blocked. No thanks to Vimeo, who were of no help - and unwilling to compromise on the issue. - */ if (NO_VIMEO && data.type === "vi" && data.meta.direct) { - data.type = "fi"; - // For browsers that don't support native h264 playback - if (USEROPTS.no_h264) { - data.forceFlash = true; - } - - /* Convert youtube-style quality key to vimeo workaround quality */ - var q = { - small: "mobile", - medium: "sd", - large: "sd", - hd720: "hd", - hd1080:"hd", - highres: "hd" - }[USEROPTS.default_quality] || "sd"; - - var fallback = { - hd: "sd", - sd: "mobile", - mobile: false - }; - - while (!(q in data.meta.direct) && q != false) { - q = fallback[q]; - } - - if (!q) { - q = "sd"; - } - - data.url = data.meta.direct[q].url; + data = vimeoSimulator2014(data); } + /* RTMP player has been replaced with the general flash player */ if (data.type === "rt") { data.url = data.id; data.type = "fi"; data.forceFlash = true; } - if(data.type != PLAYER.type) { + if (data.type != PLAYER.type) { loadMediaPlayer(data); } diff --git a/www/js/player.js b/www/js/player.js index 58ee605c..b40d28dc 100644 --- a/www/js/player.js +++ b/www/js/player.js @@ -1002,7 +1002,15 @@ var GoogleDocsPlayer = function (data) { self.videoLength = data.seconds; self.paused = false; var wmode = USEROPTS.wmode_transparent ? "transparent" : "opaque"; + var meta = data.meta; + if (!meta || !meta.object || !meta.params) { + // Reset videoId so that a changeMedia with the appropriate data + // will properly reset the player + self.videoId = ""; + return; + } + self.player = $("", meta.object)[0]; $(self.player).attr("data", meta.object.data); $(self.player).attr("width", VWIDTH) diff --git a/www/js/util.js b/www/js/util.js index b1e42eaf..31dc2c67 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -2674,3 +2674,53 @@ function formatScriptAccessPrefs() { }); }); } + +/* + VIMEO SIMULATOR 2014 + + Vimeo decided to block my domain. After repeated emails, they refused to + unblock it. Rather than give in to their demands, there is a serverside + option which extracts direct links to the h264 encoded MP4 video files. + These files can be loaded in a custom player to allow Vimeo playback without + triggering their dumb API domain block. + + It's a little bit hacky, but my only other option is to keep buying new + domains every time one gets blocked. No thanks to Vimeo, who were of no help + and unwilling to compromise on the issue. +*/ +function vimeoSimulator2014(data) { + /* Vimeo Simulator uses the raw file player */ + data.type = "fi"; + + /* For browsers that don't support native h264 playback */ + if (USEROPTS.no_h264) { + data.forceFlash = true; + } + + /* Convert youtube-style quality key to vimeo workaround quality */ + var q = { + small: "mobile", + medium: "sd", + large: "sd", + hd720: "hd", + hd1080:"hd", + highres: "hd" + }[USEROPTS.default_quality] || "sd"; + + var fallback = { + hd: "sd", + sd: "mobile", + mobile: false + }; + + /* Pick highest quality less than or equal to user's preference from the options */ + while (!(q in data.meta.direct) && q != false) { + q = fallback[q]; + } + if (!q) { + q = "sd"; + } + + data.url = data.meta.direct[q].url; + return data; +}