2014-06-01 18:43:18 +00:00
|
|
|
var Logger = require("./logger");
|
|
|
|
var Config = require("./config");
|
2014-06-09 04:03:29 +00:00
|
|
|
var spawn = require("child_process").spawn;
|
2014-06-01 18:43:18 +00:00
|
|
|
|
|
|
|
var acceptedCodecs = {
|
|
|
|
"mov/h264": true,
|
2014-06-04 04:21:00 +00:00
|
|
|
"flv/h264": true,
|
|
|
|
"matroska/vp8": true,
|
|
|
|
"matroska/vp9": true,
|
2014-06-07 17:45:52 +00:00
|
|
|
"ogg/theora": true
|
2014-06-01 18:43:18 +00:00
|
|
|
};
|
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
var acceptedAudioCodecs = {
|
|
|
|
"mp3": true,
|
|
|
|
"vorbis": true
|
|
|
|
};
|
|
|
|
|
2014-06-07 17:45:52 +00:00
|
|
|
var audioOnlyContainers = {
|
|
|
|
"mp3": true
|
|
|
|
};
|
|
|
|
|
2014-06-01 18:43:18 +00:00
|
|
|
exports.query = function (filename, cb) {
|
2014-06-09 04:03:29 +00:00
|
|
|
if (!Config.get("ffmpeg.enabled")) {
|
2014-06-01 18:43:18 +00:00
|
|
|
return cb("Raw file playback is not enabled on this server");
|
|
|
|
}
|
|
|
|
|
2014-06-07 17:45:52 +00:00
|
|
|
if (!filename.match(/^https?:\/\//)) {
|
|
|
|
return cb("Raw file playback is only supported for links accessible via HTTP " +
|
|
|
|
"or HTTPS");
|
|
|
|
}
|
|
|
|
|
2014-06-08 04:25:48 +00:00
|
|
|
ffprobe(filename, function (err, meta) {
|
2014-06-01 18:43:18 +00:00
|
|
|
if (err) {
|
2014-06-09 04:03:29 +00:00
|
|
|
if (meta.stderr && meta.stderr.match(/Protocol not found/)) {
|
|
|
|
return cb("Link uses a protocol unsupported by this server's ffmpeg");
|
|
|
|
} else {
|
|
|
|
return cb("Unable to query file data with ffmpeg");
|
|
|
|
}
|
2014-06-08 04:25:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
meta = parse(meta);
|
|
|
|
if (meta == null) {
|
|
|
|
return cb("Unknown error");
|
2014-06-01 18:43:18 +00:00
|
|
|
}
|
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
if (isVideo(meta)) {
|
2014-06-08 04:25:48 +00:00
|
|
|
var codec = meta.container + "/" + meta.vcodec;
|
2014-06-01 18:43:18 +00:00
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
if (!(codec in acceptedCodecs)) {
|
|
|
|
return cb("Unsupported video codec " + codec);
|
|
|
|
}
|
2014-06-01 18:43:18 +00:00
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
var data = {
|
|
|
|
title: meta.title || "Raw Video",
|
2014-12-03 04:21:52 +00:00
|
|
|
duration: Math.ceil(meta.seconds) || "--:--",
|
2014-06-08 04:25:48 +00:00
|
|
|
bitrate: meta.bitrate,
|
2014-06-06 04:58:26 +00:00
|
|
|
codec: codec
|
|
|
|
};
|
|
|
|
|
|
|
|
cb(null, data);
|
|
|
|
} else if (isAudio(meta)) {
|
2014-06-08 04:25:48 +00:00
|
|
|
var codec = meta.acodec;
|
2014-06-06 04:58:26 +00:00
|
|
|
|
|
|
|
if (!(codec in acceptedAudioCodecs)) {
|
|
|
|
return cb("Unsupported audio codec " + codec);
|
|
|
|
}
|
2014-06-01 18:43:18 +00:00
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
var data = {
|
|
|
|
title: meta.title || "Raw Audio",
|
2014-12-03 04:21:52 +00:00
|
|
|
duration: Math.ceil(meta.seconds) || "--:--",
|
2014-06-08 04:25:48 +00:00
|
|
|
bitrate: meta.bitrate,
|
2014-06-06 04:58:26 +00:00
|
|
|
codec: codec
|
|
|
|
};
|
2014-06-01 18:43:18 +00:00
|
|
|
|
2014-06-06 04:58:26 +00:00
|
|
|
cb(null, data);
|
2014-06-07 17:45:52 +00:00
|
|
|
} else if (data.ffmpegErr.match(/Protocol not found/)) {
|
|
|
|
return cb("This server is unable to load videos over the " +
|
|
|
|
filename.split(":")[0] + " protocol.");
|
2014-06-06 04:58:26 +00:00
|
|
|
} else {
|
|
|
|
return cb("Parsed metadata did not contain a valid video or audio stream. " +
|
2014-06-09 04:03:29 +00:00
|
|
|
"Either the file is invalid or it has a format unsupported by " +
|
2014-06-06 04:58:26 +00:00
|
|
|
"this server's version of ffmpeg.");
|
|
|
|
}
|
2014-06-01 18:43:18 +00:00
|
|
|
});
|
|
|
|
};
|
2014-06-06 04:58:26 +00:00
|
|
|
|
|
|
|
function isVideo(meta) {
|
2014-06-08 04:25:48 +00:00
|
|
|
return meta.vcodec && !(meta.container in audioOnlyContainers);
|
2014-06-06 04:58:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function isAudio(meta) {
|
2014-06-08 04:25:48 +00:00
|
|
|
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.tags) {
|
|
|
|
data.title = meta.format.tags.title;
|
|
|
|
}
|
|
|
|
data.seconds = Math.ceil(parseFloat(meta.format.duration));
|
|
|
|
return data;
|
2014-06-06 04:58:26 +00:00
|
|
|
}
|
2014-06-09 04:03:29 +00:00
|
|
|
|
|
|
|
function ffprobe(filename, cb) {
|
|
|
|
var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]);
|
|
|
|
|
|
|
|
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) {
|
|
|
|
return cb("ffprobe exited with nonzero exit code", { 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]] = kv[1];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
cb(null, {
|
|
|
|
streams: streams,
|
|
|
|
format: format
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|