mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-07-28 23:04:59 +02:00
Remove fluent-ffmpeg dependency
This commit is contained in:
parent
8562b8d1b3
commit
b61ecefce4
35 changed files with 4405 additions and 50 deletions
261
server/libs/fluentFfmpeg/ffprobe.js
Normal file
261
server/libs/fluentFfmpeg/ffprobe.js
Normal file
|
@ -0,0 +1,261 @@
|
|||
/*jshint node:true, laxcomma:true*/
|
||||
'use strict';
|
||||
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
|
||||
function legacyTag(key) { return key.match(/^TAG:/); }
|
||||
function legacyDisposition(key) { return key.match(/^DISPOSITION:/); }
|
||||
|
||||
function parseFfprobeOutput(out) {
|
||||
var lines = out.split(/\r\n|\r|\n/);
|
||||
|
||||
lines = lines.filter(function (line) {
|
||||
return line.length > 0;
|
||||
});
|
||||
|
||||
var data = {
|
||||
streams: [],
|
||||
format: {},
|
||||
chapters: []
|
||||
};
|
||||
|
||||
function parseBlock(name) {
|
||||
var data = {};
|
||||
|
||||
var line = lines.shift();
|
||||
while (typeof line !== 'undefined') {
|
||||
if (line.toLowerCase() == '[/'+name+']') {
|
||||
return data;
|
||||
} else if (line.match(/^\[/)) {
|
||||
line = lines.shift();
|
||||
continue;
|
||||
}
|
||||
|
||||
var kv = line.match(/^([^=]+)=(.*)$/);
|
||||
if (kv) {
|
||||
if (!(kv[1].match(/^TAG:/)) && kv[2].match(/^[0-9]+(\.[0-9]+)?$/)) {
|
||||
data[kv[1]] = Number(kv[2]);
|
||||
} else {
|
||||
data[kv[1]] = kv[2];
|
||||
}
|
||||
}
|
||||
|
||||
line = lines.shift();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
var line = lines.shift();
|
||||
while (typeof line !== 'undefined') {
|
||||
if (line.match(/^\[stream/i)) {
|
||||
var stream = parseBlock('stream');
|
||||
data.streams.push(stream);
|
||||
} else if (line.match(/^\[chapter/i)) {
|
||||
var chapter = parseBlock('chapter');
|
||||
data.chapters.push(chapter);
|
||||
} else if (line.toLowerCase() === '[format]') {
|
||||
data.format = parseBlock('format');
|
||||
}
|
||||
|
||||
line = lines.shift();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = function(proto) {
|
||||
/**
|
||||
* A callback passed to the {@link FfmpegCommand#ffprobe} method.
|
||||
*
|
||||
* @callback FfmpegCommand~ffprobeCallback
|
||||
*
|
||||
* @param {Error|null} err error object or null if no error happened
|
||||
* @param {Object} ffprobeData ffprobe output data; this object
|
||||
* has the same format as what the following command returns:
|
||||
*
|
||||
* `ffprobe -print_format json -show_streams -show_format INPUTFILE`
|
||||
* @param {Array} ffprobeData.streams stream information
|
||||
* @param {Object} ffprobeData.format format information
|
||||
*/
|
||||
|
||||
/**
|
||||
* Run ffprobe on last specified input
|
||||
*
|
||||
* @method FfmpegCommand#ffprobe
|
||||
* @category Metadata
|
||||
*
|
||||
* @param {?Number} [index] 0-based index of input to probe (defaults to last input)
|
||||
* @param {?String[]} [options] array of output options to return
|
||||
* @param {FfmpegCommand~ffprobeCallback} callback callback function
|
||||
*
|
||||
*/
|
||||
proto.ffprobe = function() {
|
||||
var input, index = null, options = [], callback;
|
||||
|
||||
// the last argument should be the callback
|
||||
var callback = arguments[arguments.length - 1];
|
||||
|
||||
var ended = false
|
||||
function handleCallback(err, data) {
|
||||
if (!ended) {
|
||||
ended = true;
|
||||
callback(err, data);
|
||||
}
|
||||
};
|
||||
|
||||
// map the arguments to the correct variable names
|
||||
switch (arguments.length) {
|
||||
case 3:
|
||||
index = arguments[0];
|
||||
options = arguments[1];
|
||||
break;
|
||||
case 2:
|
||||
if (typeof arguments[0] === 'number') {
|
||||
index = arguments[0];
|
||||
} else if (Array.isArray(arguments[0])) {
|
||||
options = arguments[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (index === null) {
|
||||
if (!this._currentInput) {
|
||||
return handleCallback(new Error('No input specified'));
|
||||
}
|
||||
|
||||
input = this._currentInput;
|
||||
} else {
|
||||
input = this._inputs[index];
|
||||
|
||||
if (!input) {
|
||||
return handleCallback(new Error('Invalid input index'));
|
||||
}
|
||||
}
|
||||
|
||||
// Find ffprobe
|
||||
this._getFfprobePath(function(err, path) {
|
||||
if (err) {
|
||||
return handleCallback(err);
|
||||
} else if (!path) {
|
||||
return handleCallback(new Error('Cannot find ffprobe'));
|
||||
}
|
||||
|
||||
var stdout = '';
|
||||
var stdoutClosed = false;
|
||||
var stderr = '';
|
||||
var stderrClosed = false;
|
||||
|
||||
// Spawn ffprobe
|
||||
var src = input.isStream ? 'pipe:0' : input.source;
|
||||
var ffprobe = spawn(path, ['-show_streams', '-show_format'].concat(options, src), {windowsHide: true});
|
||||
|
||||
if (input.isStream) {
|
||||
// Skip errors on stdin. These get thrown when ffprobe is complete and
|
||||
// there seems to be no way hook in and close stdin before it throws.
|
||||
ffprobe.stdin.on('error', function(err) {
|
||||
if (['ECONNRESET', 'EPIPE', 'EOF'].indexOf(err.code) >= 0) { return; }
|
||||
handleCallback(err);
|
||||
});
|
||||
|
||||
// Once ffprobe's input stream closes, we need no more data from the
|
||||
// input
|
||||
ffprobe.stdin.on('close', function() {
|
||||
input.source.pause();
|
||||
input.source.unpipe(ffprobe.stdin);
|
||||
});
|
||||
|
||||
input.source.pipe(ffprobe.stdin);
|
||||
}
|
||||
|
||||
ffprobe.on('error', callback);
|
||||
|
||||
// Ensure we wait for captured streams to end before calling callback
|
||||
var exitError = null;
|
||||
function handleExit(err) {
|
||||
if (err) {
|
||||
exitError = err;
|
||||
}
|
||||
|
||||
if (processExited && stdoutClosed && stderrClosed) {
|
||||
if (exitError) {
|
||||
if (stderr) {
|
||||
exitError.message += '\n' + stderr;
|
||||
}
|
||||
|
||||
return handleCallback(exitError);
|
||||
}
|
||||
|
||||
// Process output
|
||||
var data = parseFfprobeOutput(stdout);
|
||||
|
||||
// Handle legacy output with "TAG:x" and "DISPOSITION:x" keys
|
||||
[data.format].concat(data.streams).forEach(function(target) {
|
||||
if (target) {
|
||||
var legacyTagKeys = Object.keys(target).filter(legacyTag);
|
||||
|
||||
if (legacyTagKeys.length) {
|
||||
target.tags = target.tags || {};
|
||||
|
||||
legacyTagKeys.forEach(function(tagKey) {
|
||||
target.tags[tagKey.substr(4)] = target[tagKey];
|
||||
delete target[tagKey];
|
||||
});
|
||||
}
|
||||
|
||||
var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition);
|
||||
|
||||
if (legacyDispositionKeys.length) {
|
||||
target.disposition = target.disposition || {};
|
||||
|
||||
legacyDispositionKeys.forEach(function(dispositionKey) {
|
||||
target.disposition[dispositionKey.substr(12)] = target[dispositionKey];
|
||||
delete target[dispositionKey];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
handleCallback(null, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ffprobe exit
|
||||
var processExited = false;
|
||||
ffprobe.on('exit', function(code, signal) {
|
||||
processExited = true;
|
||||
|
||||
if (code) {
|
||||
handleExit(new Error('ffprobe exited with code ' + code));
|
||||
} else if (signal) {
|
||||
handleExit(new Error('ffprobe was killed with signal ' + signal));
|
||||
} else {
|
||||
handleExit();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle stdout/stderr streams
|
||||
ffprobe.stdout.on('data', function(data) {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
ffprobe.stdout.on('close', function() {
|
||||
stdoutClosed = true;
|
||||
handleExit();
|
||||
});
|
||||
|
||||
ffprobe.stderr.on('data', function(data) {
|
||||
stderr += data;
|
||||
});
|
||||
|
||||
ffprobe.stderr.on('close', function() {
|
||||
stderrClosed = true;
|
||||
handleExit();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue