From 1d1630fb50d359b3e130b239e231ad6f19213196 Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Tue, 3 Jun 2014 21:21:00 -0700 Subject: [PATCH] Implement raw file queues --- config.template.yaml | 7 +++++ lib/channel/library.js | 3 +- lib/channel/permissions.js | 5 ++++ lib/channel/playlist.js | 40 +++++++++++++++++++++----- lib/database/channels.js | 11 +++++-- lib/database/update.js | 2 ++ lib/ffmpeg.js | 7 +++-- lib/media.js | 12 +++++--- lib/utilities.js | 4 +++ www/js/ui.js | 59 +++++++++++++++++++++++++++++--------- www/js/util.js | 21 ++++++++++++++ 11 files changed, 140 insertions(+), 31 deletions(-) diff --git a/config.template.yaml b/config.template.yaml index 1073791b..049cbf8b 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -175,3 +175,10 @@ aggressive-gc: false # Allows you to blacklist certain channels. Users will be automatically kicked # upon trying to join one. channel-blacklist: [] + +# If you have ffmpeg installed, you can query metadata from raw files, allowing +# server-synched raw file playback. This requires the following: +# * ffmpeg must be installed on the server +# * you must install the fluent-ffmpeg module (npm install fluent-ffmpeg) +ffmpeg: + enabled: false diff --git a/lib/channel/library.js b/lib/channel/library.js index e286e608..ece7a825 100644 --- a/lib/channel/library.js +++ b/lib/channel/library.js @@ -36,7 +36,8 @@ LibraryModule.prototype.getItem = function (id, cb) { if (err) { cb(err, null); } else { - cb(null, new Media(row.id, row.title, row.seconds, row.type, {})); + var meta = JSON.parse(row.meta || "{}"); + cb(null, new Media(row.id, row.title, row.seconds, row.type, meta)); } }); }; diff --git a/lib/channel/permissions.js b/lib/channel/permissions.js index 3820725f..33966674 100644 --- a/lib/channel/permissions.js +++ b/lib/channel/permissions.js @@ -16,6 +16,7 @@ const DEFAULT_PERMISSIONS = { oplaylistjump: 1.5, oplaylistaddlist: 1.5, playlistaddcustom: 3, // Add custom embed to the playlist + playlistaddrawfile: 2, // Add raw file to the playlist playlistaddlive: 1.5, // Add a livestream to the playlist exceedmaxlength: 2, // Add a video longer than the maximum length set addnontemp: 2, // Add a permanent video to the playlist @@ -195,6 +196,10 @@ PermissionsModule.prototype.canAddCustom = function (account) { return this.hasPermission(account, "playlistaddcustom"); }; +PermissionsModule.prototype.canAddRawFile = function (account) { + return this.hasPermission(account, "playlistaddrawfile"); +}; + PermissionsModule.prototype.canMoveVideo = function (account) { return this.hasPermission(account, "playlistmove"); }; diff --git a/lib/channel/playlist.js b/lib/channel/playlist.js index f0a96a8e..71a72cdc 100644 --- a/lib/channel/playlist.js +++ b/lib/channel/playlist.js @@ -110,9 +110,8 @@ PlaylistModule.prototype.load = function (data) { var i = 0; playlist.pos = parseInt(playlist.pos); playlist.pl.forEach(function (item) { - /* Backwards compatibility */ var m = new Media(item.media.id, item.media.title, item.media.seconds, - item.media.type); + item.media.type, item.media.meta || {}); var newitem = new PlaylistItem(m, { uid: self._nextuid++, temp: item.temp, @@ -134,7 +133,12 @@ PlaylistModule.prototype.load = function (data) { PlaylistModule.prototype.save = function (data) { var arr = this.items.toArray().map(function (m) { - delete m.meta; + /* Clear Google Docs and Vimeo meta */ + if (m.meta) { + delete m.meta.object; + delete m.meta.params; + delete m.meta.direct; + } return m; }); var pos = 0; @@ -309,7 +313,10 @@ PlaylistModule.prototype.handleQueue = function (user, data) { return; } - /* Specifying a custom title is currently only allowed for custom media */ + /** + * Specifying a custom title is currently only allowed for custom media + * and raw files + */ if (typeof data.title !== "string" || (data.type !== "cu" && data.type !== "fi")) { data.title = false; } @@ -344,6 +351,12 @@ PlaylistModule.prototype.handleQueue = function (user, data) { link: link }); return; + } else if (type === "fi" && !perms.canAddRawFile(user)) { + user.socket.emit("queueFail", { + msg: "You don't have permission to add raw video files", + link: link + }); + return; } var temp = data.temp || !perms.canAddNonTemp(user); @@ -393,6 +406,7 @@ PlaylistModule.prototype.handleQueue = function (user, data) { title: data.title, link: link, temp: temp, + shouldAddToLibrary: temp, queueby: queueby, duration: duration, maxlength: maxlength @@ -426,6 +440,8 @@ PlaylistModule.prototype.queueStandard = function (user, data) { } if (item !== null) { + /* Don't re-cache data we got from the library */ + data.shouldAddToLibrary = false; self._addItem(item, data, user, function () { lock.release(); self.channel.activeLock.release(); @@ -868,14 +884,24 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) { }); } + /* Warn about possibly unsupported formats */ + if (media.type === "fi" && media.meta.codec !== "mov/h264" && + media.meta.codec !== "flv/h264") { + user.socket.emit("queueWarn", { + msg: "The codec " + media.meta.codec + " is not supported " + + "by all browsers, and is not supported by the flash fallback layer. " + + "This video may not play for some users." + }); + } + var item = new PlaylistItem(media, { uid: self._nextuid++, temp: data.temp, queueby: data.queueby }); - if (data.title && media.type === "cu") { - media.title = data.title; + if (data.title && (media.type === "cu" || media.type === "fi")) { + media.setTitle(data.title); } var success = function () { @@ -897,7 +923,7 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) { u.socket.emit("setPlaylistMeta", self.meta); }); - if (!data.temp && !util.isLive(media.type)) { + if (data.shouldAddToLibrary && !util.isLive(media.type)) { if (self.channel.modules.library) { self.channel.modules.library.cacheMedia(media); } diff --git a/lib/database/channels.js b/lib/database/channels.js index 1b54da6c..c8e2702b 100644 --- a/lib/database/channels.js +++ b/lib/database/channels.js @@ -440,9 +440,14 @@ module.exports = { return; } - db.query("INSERT INTO `chan_" + chan + "_library` (id, title, seconds, type) " + - "VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id", - [media.id, media.title, media.seconds, media.type], callback); + var meta = JSON.stringify({ + bitrate: media.meta.bitrate, + codec: media.meta.codec + }); + + db.query("INSERT INTO `chan_" + chan + "_library` (id, title, seconds, type, meta) " + + "VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE id=id", + [media.id, media.title, media.seconds, media.type, meta], callback); }, /** diff --git a/lib/database/update.js b/lib/database/update.js index badf0218..526e49e8 100644 --- a/lib/database/update.js +++ b/lib/database/update.js @@ -39,6 +39,8 @@ function update(version, cb) { } function addMetaColumnToLibraries(cb) { + Logger.syslog.log("[database] db version indicates channel libraries don't have " + + "meta column. Updating..."); Q.nfcall(db.query, "SHOW TABLES") .then(function (rows) { rows = rows.map(function (r) { diff --git a/lib/ffmpeg.js b/lib/ffmpeg.js index 305d9323..4ebb9adc 100644 --- a/lib/ffmpeg.js +++ b/lib/ffmpeg.js @@ -18,7 +18,10 @@ function init() { var acceptedCodecs = { "mov/h264": true, - "matroska/vp8": true + "flv/h264": true, + "matroska/vp8": true, + "matroska/vp9": true, + "ogg/theora": true, }; exports.query = function (filename, cb) { @@ -43,7 +46,7 @@ exports.query = function (filename, cb) { var codec = video.container + "/" + video.codec; if (!(codec in acceptedCodecs)) { - return cb("Unsupported codec " + codec); + return cb("Unsupported video codec " + codec); } var data = { diff --git a/lib/media.js b/lib/media.js index 1c0ee658..7cfcda81 100644 --- a/lib/media.js +++ b/lib/media.js @@ -6,10 +6,7 @@ function Media(id, title, seconds, type, meta) { } this.id = id; - this.title = title; - if (this.title.length > 100) { - this.title = this.title.substring(0, 97) + "..."; - } + this.setTitle(title); this.seconds = seconds === "--:--" ? 0 : parseInt(seconds); this.duration = util.formatTime(seconds); @@ -20,6 +17,13 @@ function Media(id, title, seconds, type, meta) { } Media.prototype = { + setTitle: function (title) { + this.title = title; + if (this.title.length > 100) { + this.title = this.title.substring(0, 97) + "..."; + } + }, + pack: function () { return { id: this.id, diff --git a/lib/utilities.js b/lib/utilities.js index 81848173..c62c2428 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -261,6 +261,10 @@ return "http://imgur.com/a/" + id; case "us": return "http://ustream.tv/" + id; + case "gd": + return "https://docs.google.com/file/d/" + id; + case "fi": + return id; default: return ""; } diff --git a/www/js/ui.js b/www/js/ui.js index 9f68ba9a..590035d9 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -340,12 +340,16 @@ function queue(pos, src) { var link = $("#mediaurl").val(); var data = parseMediaLink(link); var duration = undefined; + var title = undefined; if (link.indexOf("jw:") === 0) { duration = parseInt($("#addfromurl-duration-val").val()); if (duration <= 0 || isNaN(duration)) { duration = undefined; } } + if (data.type === "fi") { + title = $("#addfromurl-title-val").val(); + } if (data.id == null || data.type == null) { makeAlert("Error", "Failed to parse link. Please check that it is correct", @@ -354,11 +358,13 @@ function queue(pos, src) { } else { $("#mediaurl").val(""); $("#addfromurl-duration").remove(); + $("#addfromurl-title").remove(); socket.emit("queue", { id: data.id, type: data.type, pos: pos, duration: duration, + title: title, temp: $(".add-temp").prop("checked") }); } @@ -373,21 +379,46 @@ $("#ce_queue_end").click(queue.bind(this, "end", "customembed")); $("#mediaurl").keyup(function(ev) { if (ev.keyCode === 13) { queue("end", "url"); - } else if ($("#mediaurl").val().indexOf("jw:") === 0) { - var duration = $("#addfromurl-duration"); - if (duration.length === 0) { - duration = $("
") - .attr("id", "addfromurl-duration") - .appendTo($("#addfromurl")); - $("").text("JWPlayer Duration (seconds) (optional)") - .appendTo(duration); - $("").addClass("form-control") - .attr("type", "text") - .attr("id", "addfromurl-duration-val") - .appendTo($("#addfromurl-duration")); - } } else { - $("#addfromurl-duration").remove(); + if ($("#mediaurl").val().indexOf("jw:") === 0) { + var duration = $("#addfromurl-duration"); + if (duration.length === 0) { + duration = $("
") + .attr("id", "addfromurl-duration") + .appendTo($("#addfromurl")); + $("").text("JWPlayer Duration (seconds) (optional)") + .appendTo(duration); + $("").addClass("form-control") + .attr("type", "text") + .attr("id", "addfromurl-duration-val") + .appendTo($("#addfromurl-duration")); + } + } else { + $("#addfromurl-duration").remove(); + } + + var url = $("#mediaurl").val().split("?")[0]; + if (url.match(/^https?:\/\/(.*)?\.(flv|mp4|ogg|webm)$/)) { + var title = $("#addfromurl-title"); + if (title.length === 0) { + title = $("
") + .attr("id", "addfromurl-title") + .appendTo($("#addfromurl")); + $("").text("Title (optional)") + .appendTo(title); + $("").addClass("form-control") + .attr("type", "text") + .attr("id", "addfromurl-title-val") + .keyup(function (ev) { + if (ev.keyCode === 13) { + queue("end", "url"); + } + }) + .appendTo($("#addfromurl-title")); + } + } else { + $("#addfromurl-title").remove(); + } } }); diff --git a/www/js/util.js b/www/js/util.js index 81ea6573..6ea40f56 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -58,6 +58,8 @@ function formatURL(data) { return "http://ustream.tv/" + data.id; case "gd": return "https://docs.google.com/file/d/" + data.id; + case "fi": + return data.id; default: return "#"; } @@ -1284,6 +1286,24 @@ function parseMediaLink(url) { }; } + /* Raw file */ + var tmp = url.split("?")[0]; + if (tmp.match(/^https?:\/\//)) { + if (tmp.match(/\.(mp4|flv|webm|ogg)$/)) { + return { + id: url, + type: "fi" + }; + } else { + Callbacks.queueFail({ + link: url, + msg: "The file you are attempting to queue does not match the supported " + + "file extensions mp4, flv, webm, ogg." + }); + throw new Error("ERROR_QUEUE_UNSUPPORTED_EXTENSION"); + } + } + return { id: null, type: null @@ -1728,6 +1748,7 @@ function genPermissionsEditor() { makeOption("Queue playlist", "playlistaddlist", standard, CHANNEL.perms.playlistaddlist+""); makeOption("Queue livestream", "playlistaddlive", standard, CHANNEL.perms.playlistaddlive+""); makeOption("Embed custom media", "playlistaddcustom", standard, CHANNEL.perms.playlistaddcustom + ""); + makeOption("Add raw video file", "playlistaddrawfile", standard, CHANNEL.perms.playlistaddrawfile + ""); makeOption("Exceed maximum media length", "exceedmaxlength", standard, CHANNEL.perms.exceedmaxlength+""); makeOption("Add nontemporary media", "addnontemp", standard, CHANNEL.perms.addnontemp+""); makeOption("Temp/untemp playlist item", "settemp", standard, CHANNEL.perms.settemp+"");