mirror of https://github.com/calzoneman/sync.git
Merge pull request #479 from calzoneman/ffmpeg-fixes
Resolve #476 - ffmpeg improvements
This commit is contained in:
commit
0a0b125c8b
|
@ -202,6 +202,9 @@ channel-blacklist: []
|
||||||
# * ffmpeg must be installed on the server
|
# * ffmpeg must be installed on the server
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
# Executable name for ffprobe if it is not "ffprobe". On Debian and Ubuntu (on which
|
||||||
|
# libav is used rather than ffmpeg proper), this is "avprobe"
|
||||||
|
ffprobe-exec: 'ffprobe'
|
||||||
|
|
||||||
link-domain-blacklist: []
|
link-domain-blacklist: []
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,8 @@ var defaults = {
|
||||||
},
|
},
|
||||||
"channel-blacklist": [],
|
"channel-blacklist": [],
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
enabled: false
|
enabled: false,
|
||||||
|
"ffprobe-exec": "ffprobe"
|
||||||
},
|
},
|
||||||
"link-domain-blacklist": [],
|
"link-domain-blacklist": [],
|
||||||
setuid: {
|
setuid: {
|
||||||
|
|
260
lib/ffmpeg.js
260
lib/ffmpeg.js
|
@ -2,6 +2,8 @@ var Logger = require("./logger");
|
||||||
var Config = require("./config");
|
var Config = require("./config");
|
||||||
var spawn = require("child_process").spawn;
|
var spawn = require("child_process").spawn;
|
||||||
|
|
||||||
|
var USE_JSON = true;
|
||||||
|
|
||||||
var acceptedCodecs = {
|
var acceptedCodecs = {
|
||||||
"mov/h264": true,
|
"mov/h264": true,
|
||||||
"flv/h264": true,
|
"flv/h264": true,
|
||||||
|
@ -19,6 +21,124 @@ var audioOnlyContainers = {
|
||||||
"mp3": true
|
"mp3": true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function readOldFormat(buf) {
|
||||||
|
var lines = buf.split("\n");
|
||||||
|
var tmp = { tags: {} };
|
||||||
|
var data = {
|
||||||
|
streams: []
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.forEach(function (line) {
|
||||||
|
if (line.match(/\[stream\]|\[format\]/i)) {
|
||||||
|
return;
|
||||||
|
} else if (line.match(/\[\/stream\]/i)) {
|
||||||
|
data.streams.push(tmp);
|
||||||
|
tmp = { tags: {} };
|
||||||
|
} else if (line.match(/\[\/format\]/i)) {
|
||||||
|
data.format = tmp;
|
||||||
|
tmp = { tags: {} };
|
||||||
|
} else {
|
||||||
|
var kv = line.split("=");
|
||||||
|
var key = kv[0].toLowerCase();
|
||||||
|
if (key.indexOf("tag:") === 0) {
|
||||||
|
tmp.tags[key.split(":")[1]] = kv[1];
|
||||||
|
} else {
|
||||||
|
tmp[key] = kv[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reformatData(data) {
|
||||||
|
var reformatted = {};
|
||||||
|
|
||||||
|
var duration = parseInt(data.format.duration, 10);
|
||||||
|
if (isNaN(duration)) duration = "--:--";
|
||||||
|
reformatted.duration = Math.ceil(duration);
|
||||||
|
|
||||||
|
var bitrate = parseInt(data.format.bit_rate, 10) / 1000;
|
||||||
|
if (isNaN(bitrate)) bitrate = 0;
|
||||||
|
reformatted.bitrate = bitrate;
|
||||||
|
|
||||||
|
reformatted.title = data.format.tags ? data.format.tags.title : null;
|
||||||
|
var container = data.format.format_name.split(",")[0];
|
||||||
|
|
||||||
|
data.streams.forEach(function (stream) {
|
||||||
|
if (stream.codec_type === "video") {
|
||||||
|
reformatted.vcodec = stream.codec_name;
|
||||||
|
if (!reformatted.title && stream.tags) {
|
||||||
|
reformatted.title = stream.tags.title;
|
||||||
|
}
|
||||||
|
} else if (stream.codec_type === "audio") {
|
||||||
|
reformatted.acodec = stream.codec_name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reformatted.vcodec && !(audioOnlyContainers.hasOwnProperty(container))) {
|
||||||
|
reformatted.type = [container, reformatted.vcodec].join("/");
|
||||||
|
reformatted.medium = "video";
|
||||||
|
} else if (reformatted.acodec) {
|
||||||
|
reformatted.type = [container, reformatted.acodec].join("/");
|
||||||
|
reformatted.medium = "audio";
|
||||||
|
}
|
||||||
|
|
||||||
|
return reformatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.ffprobe = function ffprobe(filename, cb) {
|
||||||
|
var childErr;
|
||||||
|
var args = ["-show_streams", "-show_format", filename];
|
||||||
|
if (USE_JSON) args = ["-of", "json"].concat(args);
|
||||||
|
var child = spawn(Config.get("ffmpeg.ffprobe-exec"), args);
|
||||||
|
var stdout = "";
|
||||||
|
var stderr = "";
|
||||||
|
|
||||||
|
child.on("error", function (err) {
|
||||||
|
childErr = err;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", function (data) {
|
||||||
|
stdout += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on("data", function (data) {
|
||||||
|
stderr += data;
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", function (code) {
|
||||||
|
if (code !== 0) {
|
||||||
|
if (stderr.match(/unrecognized option|json/i) && USE_JSON) {
|
||||||
|
Logger.errlog.log("Warning: ffprobe does not support -of json. " +
|
||||||
|
"Assuming it will have old output format.");
|
||||||
|
USE_JSON = false;
|
||||||
|
return ffprobe(filename, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!childErr) childErr = new Error(stderr);
|
||||||
|
return cb(childErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result;
|
||||||
|
if (USE_JSON) {
|
||||||
|
try {
|
||||||
|
result = JSON.parse(stdout);
|
||||||
|
} catch (e) {
|
||||||
|
return cb(new Error("Unable to parse ffprobe output: " + e.message));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
result = readOldFormat(stdout);
|
||||||
|
} catch (e) {
|
||||||
|
return cb(new Error("Unable to parse ffprobe output: " + e.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
exports.query = function (filename, cb) {
|
exports.query = function (filename, cb) {
|
||||||
if (!Config.get("ffmpeg.enabled")) {
|
if (!Config.get("ffmpeg.enabled")) {
|
||||||
return cb("Raw file playback is not enabled on this server");
|
return cb("Raw file playback is not enabled on this server");
|
||||||
|
@ -29,56 +149,53 @@ exports.query = function (filename, cb) {
|
||||||
"or HTTPS");
|
"or HTTPS");
|
||||||
}
|
}
|
||||||
|
|
||||||
ffprobe(filename, function (err, meta) {
|
exports.ffprobe(filename, function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (meta && meta.stderr && meta.stderr.match(/Protocol not found/)) {
|
if (err.message && err.message.match(/protocol not found/i)) {
|
||||||
return cb("Link uses a protocol unsupported by this server's ffmpeg");
|
return cb("Link uses a protocol unsupported by this server's ffmpeg");
|
||||||
} else if (err.code && err.code === "ENOENT") {
|
} else if (err.code && err.code === "ENOENT") {
|
||||||
return cb("Server is missing ffprobe");
|
return cb("Failed to execute `ffprobe`. Set ffmpeg.ffprobe-exec to " +
|
||||||
|
"the correct name of the executable in config.yaml. If " +
|
||||||
|
"you are using Debian or Ubuntu, it is probably avprobe.");
|
||||||
} else {
|
} else {
|
||||||
Logger.errlog.log(err.stack || err);
|
Logger.errlog.log(err.stack || err);
|
||||||
return cb("Unable to query file data with ffmpeg");
|
return cb("Unable to query file data with ffmpeg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
meta = parse(meta);
|
try {
|
||||||
if (meta == null) {
|
data = reformatData(data);
|
||||||
return cb("Unknown error");
|
} catch (e) {
|
||||||
|
Logger.errlog.log(e.stack || e);
|
||||||
|
return cb("Unable to query file data with ffmpeg");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVideo(meta)) {
|
if (data.medium === "video") {
|
||||||
var codec = meta.container + "/" + meta.vcodec;
|
if (!acceptedCodecs.hasOwnProperty(data.type)) {
|
||||||
|
return cb("Unsupported video codec " + data.type);
|
||||||
if (!(codec in acceptedCodecs)) {
|
|
||||||
return cb("Unsupported video codec " + codec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = {
|
data = {
|
||||||
title: meta.title || "Raw Video",
|
title: data.title || "Raw Video",
|
||||||
duration: Math.ceil(meta.seconds) || "--:--",
|
duration: data.duration,
|
||||||
bitrate: meta.bitrate,
|
bitrate: data.bitrate,
|
||||||
codec: codec
|
codec: data.type
|
||||||
};
|
};
|
||||||
|
|
||||||
cb(null, data);
|
cb(null, data);
|
||||||
} else if (isAudio(meta)) {
|
} else if (data.medium === "audio") {
|
||||||
var codec = meta.acodec;
|
if (!acceptedAudioCodecs.hasOwnProperty(data.acodec)) {
|
||||||
|
return cb("Unsupported audio codec " + data.acodec);
|
||||||
if (!(codec in acceptedAudioCodecs)) {
|
|
||||||
return cb("Unsupported audio codec " + codec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = {
|
data = {
|
||||||
title: meta.title || "Raw Audio",
|
title: data.title || "Raw Audio",
|
||||||
duration: Math.ceil(meta.seconds) || "--:--",
|
duration: data.duration,
|
||||||
bitrate: meta.bitrate,
|
bitrate: data.bitrate,
|
||||||
codec: codec
|
codec: data.acodec
|
||||||
};
|
};
|
||||||
|
|
||||||
cb(null, data);
|
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 {
|
} else {
|
||||||
return cb("Parsed metadata did not contain a valid video or audio stream. " +
|
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 " +
|
"Either the file is invalid or it has a format unsupported by " +
|
||||||
|
@ -86,88 +203,3 @@ exports.query = function (filename, cb) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function isVideo(meta) {
|
|
||||||
return meta.vcodec && !(meta.container in audioOnlyContainers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAudio(meta) {
|
|
||||||
return meta.acodec;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parse(meta) {
|
|
||||||
if (meta == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!meta.format) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = {};
|
|
||||||
meta.streams.forEach(function (s) {
|
|
||||||
if (s.codec_type === "video") {
|
|
||||||
data.vcodec = s.codec_name;
|
|
||||||
} else if (s.codec_type === "audio") {
|
|
||||||
data.acodec = s.codec_name;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
data.container = meta.format.format_name.split(",")[0];
|
|
||||||
data.bitrate = parseInt(meta.format.bit_rate) / 1000;
|
|
||||||
if (meta.format["tag:title"]) {
|
|
||||||
data.title = meta.format["tag:title"];
|
|
||||||
}
|
|
||||||
data.seconds = Math.ceil(parseFloat(meta.format.duration));
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ffprobe(filename, cb) {
|
|
||||||
var err;
|
|
||||||
var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]);
|
|
||||||
ff.on("error", function (err_) {
|
|
||||||
err = err_;
|
|
||||||
});
|
|
||||||
|
|
||||||
var outbuf = "";
|
|
||||||
var errbuf = "";
|
|
||||||
ff.stdout.on("data", function (data) {
|
|
||||||
outbuf += data;
|
|
||||||
});
|
|
||||||
ff.stderr.on("data", function (data) {
|
|
||||||
errbuf += data;
|
|
||||||
});
|
|
||||||
|
|
||||||
ff.on("close", function (code) {
|
|
||||||
if (code !== 0) {
|
|
||||||
if (!err) {
|
|
||||||
err = "ffprobe exited with nonzero exit code";
|
|
||||||
}
|
|
||||||
return cb(err, { stderr: errbuf });
|
|
||||||
}
|
|
||||||
|
|
||||||
var lines = outbuf.split("\n");
|
|
||||||
var streams = [];
|
|
||||||
var format = {};
|
|
||||||
var data = {};
|
|
||||||
lines.forEach(function (line) {
|
|
||||||
if (line.match(/\[stream\]|\[format\]/i)) {
|
|
||||||
return;
|
|
||||||
} else if (line.match(/\[\/stream\]/i)) {
|
|
||||||
streams.push(data);
|
|
||||||
data = {};
|
|
||||||
} else if (line.match(/\[\/format\]/i)) {
|
|
||||||
format = data;
|
|
||||||
data = {};
|
|
||||||
} else {
|
|
||||||
var kv = line.split("=");
|
|
||||||
data[kv[0].toLowerCase()] = kv[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cb(null, {
|
|
||||||
streams: streams,
|
|
||||||
format: format
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue