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 = $("