Add raw video/audio playback with ffmpeg
This commit is contained in:
commit
02771e6623
17 changed files with 428 additions and 141 deletions
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,6 +133,7 @@ PlaylistModule.prototype.load = function (data) {
|
|||
|
||||
PlaylistModule.prototype.save = function (data) {
|
||||
var arr = this.items.toArray().map(function (item) {
|
||||
/* Clear Google Docs and Vimeo meta */
|
||||
if (item.media && item.media.meta) {
|
||||
delete item.media.meta.object;
|
||||
delete item.media.meta.params;
|
||||
|
|
@ -313,8 +313,11 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
/* Specifying a custom title is currently only allowed for custom media */
|
||||
if (typeof data.title !== "string" || data.type !== "cu") {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
|
@ -348,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);
|
||||
|
|
@ -397,6 +406,7 @@ PlaylistModule.prototype.handleQueue = function (user, data) {
|
|||
title: data.title,
|
||||
link: link,
|
||||
temp: temp,
|
||||
shouldAddToLibrary: !temp,
|
||||
queueby: queueby,
|
||||
duration: duration,
|
||||
maxlength: maxlength
|
||||
|
|
@ -430,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();
|
||||
|
|
@ -864,14 +876,33 @@ PlaylistModule.prototype._addItem = function (media, data, user, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
/* Warn about high bitrate for raw files */
|
||||
if (media.type === "fi" && media.meta.bitrate > 1000) {
|
||||
user.socket.emit("queueWarn", {
|
||||
msg: "This video has a bitrate over 1000kbps. Clients with slow " +
|
||||
"connections may experience lots of buffering."
|
||||
});
|
||||
}
|
||||
|
||||
/* Warn about possibly unsupported formats */
|
||||
if (media.type === "fi" && media.meta.codec.indexOf("/") !== -1 &&
|
||||
media.meta.codec !== "mov/h264" &&
|
||||
media.meta.codec !== "flv/h264") {
|
||||
user.socket.emit("queueWarn", {
|
||||
msg: "The codec <code>" + media.meta.codec + "</code> 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 () {
|
||||
|
|
@ -893,7 +924,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@ var defaults = {
|
|||
"max-items": 4000,
|
||||
"update-interval": 5
|
||||
},
|
||||
"channel-blacklist": []
|
||||
"channel-blacklist": [],
|
||||
ffmpeg: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
var db = require("../database");
|
||||
var Logger = require("../logger");
|
||||
var Q = require("q");
|
||||
|
||||
const DB_VERSION = 1;
|
||||
const DB_VERSION = 2;
|
||||
|
||||
module.exports.checkVersion = function () {
|
||||
db.query("SELECT `key`,`value` FROM `meta` WHERE `key`=?", ["db_version"], function (err, rows) {
|
||||
|
|
@ -18,6 +19,9 @@ module.exports.checkVersion = function () {
|
|||
});
|
||||
} else {
|
||||
var v = parseInt(rows[0].value);
|
||||
if (v >= DB_VERSION) {
|
||||
return;
|
||||
}
|
||||
var next = function () {
|
||||
if (v < DB_VERSION) {
|
||||
update(v++, next);
|
||||
|
|
@ -32,5 +36,33 @@ module.exports.checkVersion = function () {
|
|||
};
|
||||
|
||||
function update(version, cb) {
|
||||
setImmediate(cb);
|
||||
if (version === 1) {
|
||||
addMetaColumnToLibraries(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) {
|
||||
return r[Object.keys(r)[0]];
|
||||
}).filter(function (r) {
|
||||
return r.match(/_library$/);
|
||||
});
|
||||
|
||||
var queue = [];
|
||||
rows.forEach(function (table) {
|
||||
queue.push(Q.nfcall(db.query, "ALTER TABLE `" + table + "` ADD meta TEXT")
|
||||
.then(function () {
|
||||
Logger.syslog.log("Added meta column to " + table);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return Q.all(queue);
|
||||
}).catch(function (err) {
|
||||
Logger.errlog.log("Adding meta column to library tables failed: " + err);
|
||||
}).done(cb);
|
||||
}
|
||||
|
|
|
|||
108
lib/ffmpeg.js
Normal file
108
lib/ffmpeg.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
var Logger = require("./logger");
|
||||
var Config = require("./config");
|
||||
var Metadata;
|
||||
var enabled = false;
|
||||
|
||||
function init() {
|
||||
if (Config.get("ffmpeg.enabled")) {
|
||||
try {
|
||||
Metadata = require("fluent-ffmpeg").Metadata;
|
||||
Logger.syslog.log("Enabling raw file support with fluent-ffmpeg");
|
||||
enabled = true;
|
||||
} catch (e) {
|
||||
Logger.errlog.log("Failed to load fluent-ffmpeg. Did you remember to " +
|
||||
"execute `npm install fluent-ffmpeg` ?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var acceptedCodecs = {
|
||||
"mov/h264": true,
|
||||
"flv/h264": true,
|
||||
"matroska/vp8": true,
|
||||
"matroska/vp9": true,
|
||||
"ogg/theora": true
|
||||
};
|
||||
|
||||
var acceptedAudioCodecs = {
|
||||
"mp3": true,
|
||||
"vorbis": true
|
||||
};
|
||||
|
||||
var audioOnlyContainers = {
|
||||
"mp3": true
|
||||
};
|
||||
|
||||
exports.query = function (filename, cb) {
|
||||
if (!Metadata) {
|
||||
init();
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return cb("Raw file playback is not enabled on this server");
|
||||
}
|
||||
|
||||
if (!filename.match(/^https?:\/\//)) {
|
||||
return cb("Raw file playback is only supported for links accessible via HTTP " +
|
||||
"or HTTPS");
|
||||
}
|
||||
|
||||
new Metadata(filename, function (meta, err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if (isVideo(meta)) {
|
||||
var video = meta.video;
|
||||
var codec = video.container + "/" + video.codec;
|
||||
|
||||
if (!(codec in acceptedCodecs)) {
|
||||
return cb("Unsupported video codec " + codec);
|
||||
}
|
||||
|
||||
var data = {
|
||||
title: meta.title || "Raw Video",
|
||||
duration: Math.ceil(meta.durationsec),
|
||||
bitrate: video.bitrate,
|
||||
codec: codec
|
||||
};
|
||||
|
||||
cb(null, data);
|
||||
} else if (isAudio(meta)) {
|
||||
var audio = meta.audio;
|
||||
var codec = audio.codec;
|
||||
|
||||
if (!(codec in acceptedAudioCodecs)) {
|
||||
return cb("Unsupported audio codec " + codec);
|
||||
}
|
||||
|
||||
var data = {
|
||||
title: meta.title || "Raw Audio",
|
||||
duration: Math.ceil(meta.durationsec),
|
||||
bitrate: audio.bitrate,
|
||||
codec: codec
|
||||
};
|
||||
|
||||
cb(null, data);
|
||||
} else if (data.ffmpegErr.match(/Protocol not found/)) {
|
||||
return cb("This server is unable to load videos over the " +
|
||||
filename.split(":")[0] + " protocol.");
|
||||
} else {
|
||||
return cb("Parsed metadata did not contain a valid video or audio stream. " +
|
||||
"Either the file is invalid or it has a format unsupported by " +
|
||||
"this server's version of ffmpeg.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function isVideo(meta) {
|
||||
return meta.video &&
|
||||
meta.video.bitrate > 0 &&
|
||||
meta.video.container &&
|
||||
meta.video.codec &&
|
||||
!(meta.video.container in audioOnlyContainers);
|
||||
}
|
||||
|
||||
function isAudio(meta) {
|
||||
return meta.audio && meta.audio.bitrate > 0 && meta.audio.codec;
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ 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.
|
||||
*/
|
||||
|
||||
var http = require("http");
|
||||
var https = require("https");
|
||||
var domain = require("domain");
|
||||
|
|
@ -17,6 +16,7 @@ var Media = require("./media");
|
|||
var CustomEmbedFilter = require("./customembed").filter;
|
||||
var Server = require("./server");
|
||||
var Config = require("./config");
|
||||
var ffmpeg = require("./ffmpeg");
|
||||
|
||||
var urlRetrieve = function (transport, options, callback) {
|
||||
// Catch any errors that crop up along the way of the request
|
||||
|
|
@ -766,6 +766,21 @@ var Getters = {
|
|||
callback(res, null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* ffmpeg for raw files */
|
||||
fi: function (id, cb) {
|
||||
ffmpeg.query(id, function (err, data) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var m = new Media(id, data.title, data.duration, "fi", {
|
||||
bitrate: data.bitrate,
|
||||
codec: data.codec
|
||||
});
|
||||
cb(null, m);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
16
lib/media.js
16
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,
|
||||
|
|
@ -31,7 +35,9 @@ Media.prototype = {
|
|||
object: this.meta.object,
|
||||
params: this.meta.params,
|
||||
direct: this.meta.direct,
|
||||
restricted: this.meta.restricted
|
||||
restricted: this.meta.restricted,
|
||||
codec: this.meta.codec,
|
||||
bitrate: this.meta.bitrate
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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.1.0";
|
||||
const VERSION = "3.2.0";
|
||||
var singleton = null;
|
||||
var Config = require("./config");
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue