586 lines
42 KiB
JavaScript
586 lines
42 KiB
JavaScript
|
|
import * as plugins from './plugins.js';
|
||
|
|
import { FfmpegCommand } from './classes.ffmpegcommand.js';
|
||
|
|
/**
|
||
|
|
* SmartFfmpeg - A fast Node.js module for media file conversion using ffmpeg
|
||
|
|
*
|
||
|
|
* @example Modern Builder API
|
||
|
|
* ```typescript
|
||
|
|
* const ffmpeg = new SmartFfmpeg();
|
||
|
|
*
|
||
|
|
* // File to file with fluent API
|
||
|
|
* await ffmpeg.create()
|
||
|
|
* .input('/path/to/input.mp4')
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .audioBitrate('128k')
|
||
|
|
* .size(1920, 1080)
|
||
|
|
* .output('/path/to/output.mp4')
|
||
|
|
* .run();
|
||
|
|
*
|
||
|
|
* // Buffer to buffer
|
||
|
|
* const outputBuffer = await ffmpeg.create()
|
||
|
|
* .input(inputBuffer, { format: 'mp4' })
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .toBuffer('webm');
|
||
|
|
*
|
||
|
|
* // With progress tracking
|
||
|
|
* await ffmpeg.create()
|
||
|
|
* .input('/path/to/input.mp4')
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .onProgress(p => console.log(`${p.percent?.toFixed(1)}%`))
|
||
|
|
* .output('/path/to/output.mp4')
|
||
|
|
* .run();
|
||
|
|
*
|
||
|
|
* // Stream output
|
||
|
|
* const stream = ffmpeg.create()
|
||
|
|
* .input('/path/to/input.mp4')
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .toStream('mp4');
|
||
|
|
* ```
|
||
|
|
*
|
||
|
|
* @example Legacy API (still supported)
|
||
|
|
* ```typescript
|
||
|
|
* await ffmpeg.convert('input.mp4', 'output.webm', {
|
||
|
|
* videoCodec: 'libvpx-vp9',
|
||
|
|
* audioBitrate: '128k'
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export class SmartFfmpeg {
|
||
|
|
ffmpegPath;
|
||
|
|
ffprobePath;
|
||
|
|
constructor() {
|
||
|
|
this.ffmpegPath = plugins.ffmpegBinaryPath;
|
||
|
|
this.ffprobePath = plugins.ffprobeBinaryPath;
|
||
|
|
}
|
||
|
|
// ==================== BUILDER API ====================
|
||
|
|
/**
|
||
|
|
* Create a new FfmpegCommand builder for fluent API usage
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* await ffmpeg.create()
|
||
|
|
* .input('/path/to/input.mp4')
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .crf(23)
|
||
|
|
* .output('/path/to/output.mp4')
|
||
|
|
* .run();
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
create() {
|
||
|
|
return new FfmpegCommand(this.ffmpegPath, this.ffprobePath);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Shorthand to create a command with input already set
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```typescript
|
||
|
|
* // File input
|
||
|
|
* await ffmpeg.input('/path/to/input.mp4')
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .output('/path/to/output.mp4')
|
||
|
|
* .run();
|
||
|
|
*
|
||
|
|
* // Buffer input
|
||
|
|
* const output = await ffmpeg.input(buffer, { format: 'mp4' })
|
||
|
|
* .videoCodec('libx264')
|
||
|
|
* .toBuffer('webm');
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
input(source, options) {
|
||
|
|
return this.create().input(source, options);
|
||
|
|
}
|
||
|
|
// ==================== LEGACY API ====================
|
||
|
|
/**
|
||
|
|
* Get media file information using ffprobe
|
||
|
|
*/
|
||
|
|
async getMediaInfo(inputPath) {
|
||
|
|
const args = [
|
||
|
|
'-v', 'quiet',
|
||
|
|
'-print_format', 'json',
|
||
|
|
'-show_format',
|
||
|
|
'-show_streams',
|
||
|
|
inputPath
|
||
|
|
];
|
||
|
|
const result = await this.runProcess(this.ffprobePath, args);
|
||
|
|
const data = JSON.parse(result.stdout);
|
||
|
|
return {
|
||
|
|
format: {
|
||
|
|
filename: data.format.filename,
|
||
|
|
formatName: data.format.format_name,
|
||
|
|
formatLongName: data.format.format_long_name,
|
||
|
|
duration: parseFloat(data.format.duration) || 0,
|
||
|
|
size: parseInt(data.format.size, 10) || 0,
|
||
|
|
bitrate: parseInt(data.format.bit_rate, 10) || 0,
|
||
|
|
},
|
||
|
|
streams: (data.streams || []).map((stream) => this.parseStreamInfo(stream)),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Convert media file with specified options
|
||
|
|
*/
|
||
|
|
async convert(inputPath, outputPath, options = {}) {
|
||
|
|
const args = this.buildConversionArgs(inputPath, outputPath, options);
|
||
|
|
await this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Convert media file with progress reporting
|
||
|
|
*/
|
||
|
|
async convertWithProgress(inputPath, outputPath, options = {}, onProgress) {
|
||
|
|
// Get duration for progress percentage calculation
|
||
|
|
let totalDuration;
|
||
|
|
try {
|
||
|
|
const info = await this.getMediaInfo(inputPath);
|
||
|
|
totalDuration = info.format.duration;
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
// Continue without duration info
|
||
|
|
}
|
||
|
|
const args = ['-progress', 'pipe:1', ...this.buildConversionArgs(inputPath, outputPath, options)];
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const process = plugins.child_process.spawn(this.ffmpegPath, args);
|
||
|
|
let progressData = {};
|
||
|
|
process.stdout?.on('data', (data) => {
|
||
|
|
const lines = data.toString().split('\n');
|
||
|
|
for (const line of lines) {
|
||
|
|
const [key, value] = line.split('=');
|
||
|
|
if (!key || !value)
|
||
|
|
continue;
|
||
|
|
switch (key.trim()) {
|
||
|
|
case 'frame':
|
||
|
|
progressData.frame = parseInt(value, 10);
|
||
|
|
break;
|
||
|
|
case 'fps':
|
||
|
|
progressData.fps = parseFloat(value);
|
||
|
|
break;
|
||
|
|
case 'total_size':
|
||
|
|
progressData.size = parseInt(value, 10);
|
||
|
|
break;
|
||
|
|
case 'out_time_ms':
|
||
|
|
progressData.time = parseInt(value, 10) / 1000000;
|
||
|
|
if (totalDuration && progressData.time) {
|
||
|
|
progressData.percent = Math.min(100, (progressData.time / totalDuration) * 100);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case 'bitrate':
|
||
|
|
progressData.bitrate = value.trim();
|
||
|
|
break;
|
||
|
|
case 'speed':
|
||
|
|
progressData.speed = value.trim();
|
||
|
|
break;
|
||
|
|
case 'progress':
|
||
|
|
if (value.trim() === 'continue' || value.trim() === 'end') {
|
||
|
|
if (onProgress && progressData.frame !== undefined) {
|
||
|
|
onProgress(progressData);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
let stderr = '';
|
||
|
|
process.stderr?.on('data', (data) => {
|
||
|
|
stderr += data.toString();
|
||
|
|
});
|
||
|
|
process.on('close', (code) => {
|
||
|
|
if (code === 0) {
|
||
|
|
resolve();
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
reject(new Error(`FFmpeg exited with code ${code}: ${stderr}`));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
process.on('error', (err) => {
|
||
|
|
reject(err);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Extract audio from video file
|
||
|
|
*/
|
||
|
|
async extractAudio(inputPath, outputPath, options = {}) {
|
||
|
|
const conversionOptions = {
|
||
|
|
...options,
|
||
|
|
noVideo: true,
|
||
|
|
};
|
||
|
|
await this.convert(inputPath, outputPath, conversionOptions);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Remove audio from video file
|
||
|
|
*/
|
||
|
|
async removeAudio(inputPath, outputPath, options = {}) {
|
||
|
|
const conversionOptions = {
|
||
|
|
...options,
|
||
|
|
noAudio: true,
|
||
|
|
};
|
||
|
|
await this.convert(inputPath, outputPath, conversionOptions);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Take a screenshot at a specific time
|
||
|
|
*/
|
||
|
|
async screenshot(inputPath, outputPath, options) {
|
||
|
|
const args = [
|
||
|
|
'-y',
|
||
|
|
'-ss', this.formatTime(options.time),
|
||
|
|
'-i', inputPath,
|
||
|
|
'-vframes', '1',
|
||
|
|
];
|
||
|
|
if (options.width || options.height) {
|
||
|
|
const scale = this.buildScaleFilter(options.width, options.height);
|
||
|
|
args.push('-vf', scale);
|
||
|
|
}
|
||
|
|
if (options.format === 'jpg' && options.quality) {
|
||
|
|
args.push('-q:v', String(Math.round((100 - options.quality) / 100 * 31)));
|
||
|
|
}
|
||
|
|
else if (options.format === 'webp' && options.quality) {
|
||
|
|
args.push('-quality', String(options.quality));
|
||
|
|
}
|
||
|
|
args.push(outputPath);
|
||
|
|
await this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate multiple thumbnails from video
|
||
|
|
*/
|
||
|
|
async generateThumbnails(inputPath, outputDir, options) {
|
||
|
|
const info = await this.getMediaInfo(inputPath);
|
||
|
|
const duration = info.format.duration;
|
||
|
|
const interval = duration / (options.count + 1);
|
||
|
|
const pattern = options.filenamePattern || 'thumb_%d';
|
||
|
|
const ext = options.format || 'png';
|
||
|
|
const outputPaths = [];
|
||
|
|
for (let i = 1; i <= options.count; i++) {
|
||
|
|
const time = interval * i;
|
||
|
|
const filename = pattern.replace('%d', String(i).padStart(3, '0')) + '.' + ext;
|
||
|
|
const outputPath = plugins.path.join(outputDir, filename);
|
||
|
|
await this.screenshot(inputPath, outputPath, {
|
||
|
|
time,
|
||
|
|
width: options.width,
|
||
|
|
height: options.height,
|
||
|
|
format: options.format,
|
||
|
|
});
|
||
|
|
outputPaths.push(outputPath);
|
||
|
|
}
|
||
|
|
return outputPaths;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resize video
|
||
|
|
*/
|
||
|
|
async resize(inputPath, outputPath, width, height, options = {}) {
|
||
|
|
await this.convert(inputPath, outputPath, { ...options, width, height });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Change video frame rate
|
||
|
|
*/
|
||
|
|
async changeFrameRate(inputPath, outputPath, fps, options = {}) {
|
||
|
|
await this.convert(inputPath, outputPath, { ...options, fps });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Trim media file
|
||
|
|
*/
|
||
|
|
async trim(inputPath, outputPath, startTime, duration, options = {}) {
|
||
|
|
await this.convert(inputPath, outputPath, { ...options, startTime, duration });
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Convert to GIF
|
||
|
|
*/
|
||
|
|
async toGif(inputPath, outputPath, options = {}) {
|
||
|
|
const args = ['-y'];
|
||
|
|
if (options.startTime !== undefined) {
|
||
|
|
args.push('-ss', this.formatTime(options.startTime));
|
||
|
|
}
|
||
|
|
args.push('-i', inputPath);
|
||
|
|
if (options.duration !== undefined) {
|
||
|
|
args.push('-t', this.formatTime(options.duration));
|
||
|
|
}
|
||
|
|
// Build filter for GIF optimization
|
||
|
|
const filters = [];
|
||
|
|
if (options.fps) {
|
||
|
|
filters.push(`fps=${options.fps}`);
|
||
|
|
}
|
||
|
|
if (options.width || options.height) {
|
||
|
|
filters.push(this.buildScaleFilter(options.width, options.height));
|
||
|
|
}
|
||
|
|
// Split filter for palette generation (better GIF quality)
|
||
|
|
filters.push('split[s0][s1]');
|
||
|
|
filters.push('[s0]palettegen[p]');
|
||
|
|
filters.push('[s1][p]paletteuse');
|
||
|
|
args.push('-filter_complex', filters.join(','));
|
||
|
|
args.push(outputPath);
|
||
|
|
await this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Concatenate multiple media files
|
||
|
|
*/
|
||
|
|
async concat(inputPaths, outputPath, options = {}) {
|
||
|
|
// Create concat file content
|
||
|
|
const concatContent = inputPaths
|
||
|
|
.map(p => `file '${p.replace(/'/g, "'\\''")}'`)
|
||
|
|
.join('\n');
|
||
|
|
// Write to temp file
|
||
|
|
const tempFile = plugins.path.join(plugins.path.dirname(outputPath), `.concat_${Date.now()}.txt`);
|
||
|
|
// Write concat file using SmartFs
|
||
|
|
const fs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||
|
|
await fs.file(tempFile).write(concatContent);
|
||
|
|
try {
|
||
|
|
const args = [
|
||
|
|
'-y',
|
||
|
|
'-f', 'concat',
|
||
|
|
'-safe', '0',
|
||
|
|
'-i', tempFile,
|
||
|
|
];
|
||
|
|
if (options.videoCodec) {
|
||
|
|
args.push('-c:v', options.videoCodec);
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
args.push('-c', 'copy');
|
||
|
|
}
|
||
|
|
if (options.audioCodec) {
|
||
|
|
args.push('-c:a', options.audioCodec);
|
||
|
|
}
|
||
|
|
args.push(outputPath);
|
||
|
|
await this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
finally {
|
||
|
|
// Clean up temp file
|
||
|
|
await fs.file(tempFile).delete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add audio to video
|
||
|
|
*/
|
||
|
|
async addAudio(videoPath, audioPath, outputPath, options = {}) {
|
||
|
|
const args = [
|
||
|
|
'-y',
|
||
|
|
'-i', videoPath,
|
||
|
|
'-i', audioPath,
|
||
|
|
'-c:v', options.videoCodec || 'copy',
|
||
|
|
'-c:a', options.audioCodec || 'aac',
|
||
|
|
];
|
||
|
|
if (options.audioBitrate) {
|
||
|
|
args.push('-b:a', options.audioBitrate);
|
||
|
|
}
|
||
|
|
if (options.shortest) {
|
||
|
|
args.push('-shortest');
|
||
|
|
}
|
||
|
|
args.push('-map', '0:v:0', '-map', '1:a:0');
|
||
|
|
args.push(outputPath);
|
||
|
|
await this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get available encoders
|
||
|
|
*/
|
||
|
|
async getEncoders() {
|
||
|
|
const result = await this.runProcess(this.ffmpegPath, ['-encoders', '-hide_banner']);
|
||
|
|
const lines = result.stdout.split('\n');
|
||
|
|
const encoders = [];
|
||
|
|
let started = false;
|
||
|
|
for (const line of lines) {
|
||
|
|
if (line.includes('------')) {
|
||
|
|
started = true;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (started && line.trim()) {
|
||
|
|
const match = line.match(/^\s*[A-Z.]+\s+(\S+)/);
|
||
|
|
if (match) {
|
||
|
|
encoders.push(match[1]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return encoders;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get available decoders
|
||
|
|
*/
|
||
|
|
async getDecoders() {
|
||
|
|
const result = await this.runProcess(this.ffmpegPath, ['-decoders', '-hide_banner']);
|
||
|
|
const lines = result.stdout.split('\n');
|
||
|
|
const decoders = [];
|
||
|
|
let started = false;
|
||
|
|
for (const line of lines) {
|
||
|
|
if (line.includes('------')) {
|
||
|
|
started = true;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (started && line.trim()) {
|
||
|
|
const match = line.match(/^\s*[A-Z.]+\s+(\S+)/);
|
||
|
|
if (match) {
|
||
|
|
decoders.push(match[1]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return decoders;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get available formats
|
||
|
|
*/
|
||
|
|
async getFormats() {
|
||
|
|
const result = await this.runProcess(this.ffmpegPath, ['-formats', '-hide_banner']);
|
||
|
|
const lines = result.stdout.split('\n');
|
||
|
|
const formats = [];
|
||
|
|
let started = false;
|
||
|
|
for (const line of lines) {
|
||
|
|
if (line.includes('--')) {
|
||
|
|
started = true;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (started && line.trim()) {
|
||
|
|
const match = line.match(/^\s*[DE ]+\s+(\S+)/);
|
||
|
|
if (match) {
|
||
|
|
formats.push(match[1]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return formats;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Run ffmpeg with raw arguments
|
||
|
|
*/
|
||
|
|
async runRaw(args) {
|
||
|
|
return this.runProcess(this.ffmpegPath, args);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Run ffprobe with raw arguments
|
||
|
|
*/
|
||
|
|
async runProbeRaw(args) {
|
||
|
|
return this.runProcess(this.ffprobePath, args);
|
||
|
|
}
|
||
|
|
// ============ Private Methods ============
|
||
|
|
buildConversionArgs(inputPath, outputPath, options) {
|
||
|
|
const args = [];
|
||
|
|
// Overwrite flag
|
||
|
|
if (options.overwrite !== false) {
|
||
|
|
args.push('-y');
|
||
|
|
}
|
||
|
|
// Input seeking (before -i for fast seeking)
|
||
|
|
if (options.startTime !== undefined) {
|
||
|
|
args.push('-ss', this.formatTime(options.startTime));
|
||
|
|
}
|
||
|
|
args.push('-i', inputPath);
|
||
|
|
// Duration (after -i)
|
||
|
|
if (options.duration !== undefined) {
|
||
|
|
args.push('-t', this.formatTime(options.duration));
|
||
|
|
}
|
||
|
|
// Video options
|
||
|
|
if (options.noVideo) {
|
||
|
|
args.push('-vn');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
if (options.videoCodec) {
|
||
|
|
args.push('-c:v', options.videoCodec);
|
||
|
|
}
|
||
|
|
if (options.videoBitrate) {
|
||
|
|
args.push('-b:v', options.videoBitrate);
|
||
|
|
}
|
||
|
|
if (options.crf !== undefined) {
|
||
|
|
args.push('-crf', String(options.crf));
|
||
|
|
}
|
||
|
|
if (options.preset) {
|
||
|
|
args.push('-preset', options.preset);
|
||
|
|
}
|
||
|
|
if (options.fps) {
|
||
|
|
args.push('-r', String(options.fps));
|
||
|
|
}
|
||
|
|
if (options.width || options.height) {
|
||
|
|
args.push('-vf', this.buildScaleFilter(options.width, options.height));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Audio options
|
||
|
|
if (options.noAudio) {
|
||
|
|
args.push('-an');
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
if (options.audioCodec) {
|
||
|
|
args.push('-c:a', options.audioCodec);
|
||
|
|
}
|
||
|
|
if (options.audioBitrate) {
|
||
|
|
args.push('-b:a', options.audioBitrate);
|
||
|
|
}
|
||
|
|
if (options.sampleRate) {
|
||
|
|
args.push('-ar', String(options.sampleRate));
|
||
|
|
}
|
||
|
|
if (options.audioChannels) {
|
||
|
|
args.push('-ac', String(options.audioChannels));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Output format
|
||
|
|
if (options.format) {
|
||
|
|
args.push('-f', options.format);
|
||
|
|
}
|
||
|
|
// Extra arguments
|
||
|
|
if (options.extraArgs) {
|
||
|
|
args.push(...options.extraArgs);
|
||
|
|
}
|
||
|
|
args.push(outputPath);
|
||
|
|
return args;
|
||
|
|
}
|
||
|
|
buildScaleFilter(width, height) {
|
||
|
|
const w = width ? String(width) : '-1';
|
||
|
|
const h = height ? String(height) : '-1';
|
||
|
|
// Use -2 instead of -1 to ensure even dimensions (required for some codecs)
|
||
|
|
return `scale=${w === '-1' ? '-2' : w}:${h === '-1' ? '-2' : h}`;
|
||
|
|
}
|
||
|
|
formatTime(time) {
|
||
|
|
if (typeof time === 'string') {
|
||
|
|
return time;
|
||
|
|
}
|
||
|
|
// Convert seconds to HH:MM:SS.mmm format
|
||
|
|
const hours = Math.floor(time / 3600);
|
||
|
|
const minutes = Math.floor((time % 3600) / 60);
|
||
|
|
const seconds = time % 60;
|
||
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toFixed(3).padStart(6, '0')}`;
|
||
|
|
}
|
||
|
|
parseStreamInfo(stream) {
|
||
|
|
const info = {
|
||
|
|
index: stream.index,
|
||
|
|
codecName: stream.codec_name,
|
||
|
|
codecLongName: stream.codec_long_name,
|
||
|
|
codecType: stream.codec_type,
|
||
|
|
};
|
||
|
|
if (stream.codec_type === 'video') {
|
||
|
|
info.width = stream.width;
|
||
|
|
info.height = stream.height;
|
||
|
|
if (stream.r_frame_rate) {
|
||
|
|
const [num, den] = stream.r_frame_rate.split('/').map(Number);
|
||
|
|
info.frameRate = den ? num / den : num;
|
||
|
|
}
|
||
|
|
if (stream.bit_rate) {
|
||
|
|
info.bitrate = parseInt(stream.bit_rate, 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (stream.codec_type === 'audio') {
|
||
|
|
info.sampleRate = parseInt(stream.sample_rate, 10);
|
||
|
|
info.channels = stream.channels;
|
||
|
|
if (stream.bit_rate) {
|
||
|
|
info.bitrate = parseInt(stream.bit_rate, 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (stream.duration) {
|
||
|
|
info.duration = parseFloat(stream.duration);
|
||
|
|
}
|
||
|
|
return info;
|
||
|
|
}
|
||
|
|
runProcess(command, args) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const process = plugins.child_process.spawn(command, args);
|
||
|
|
let stdout = '';
|
||
|
|
let stderr = '';
|
||
|
|
process.stdout?.on('data', (data) => {
|
||
|
|
stdout += data.toString();
|
||
|
|
});
|
||
|
|
process.stderr?.on('data', (data) => {
|
||
|
|
stderr += data.toString();
|
||
|
|
});
|
||
|
|
process.on('close', (code) => {
|
||
|
|
if (code === 0) {
|
||
|
|
resolve({ stdout, stderr });
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
reject(new Error(`Process exited with code ${code}: ${stderr}`));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
process.on('error', (err) => {
|
||
|
|
reject(err);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zbWFydGZmbXBlZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2NsYXNzZXMuc21hcnRmZm1wZWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSxjQUFjLENBQUM7QUFFeEMsT0FBTyxFQUFFLGFBQWEsRUFBeUMsTUFBTSw0QkFBNEIsQ0FBQztBQVNsRzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7R0E0Q0c7QUFDSCxNQUFNLE9BQU8sV0FBVztJQUNkLFVBQVUsQ0FBUztJQUNuQixXQUFXLENBQVM7SUFFNUI7UUFDRSxJQUFJLENBQUMsVUFBVSxHQUFHLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQztRQUMzQyxJQUFJLENBQUMsV0FBVyxHQUFHLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQztJQUMvQyxDQUFDO0lBRUQsd0RBQXdEO0lBRXhEOzs7Ozs7Ozs7Ozs7T0FZRztJQUNILE1BQU07UUFDSixPQUFPLElBQUksYUFBYSxDQUFDLElBQUksQ0FBQyxVQUFVLEVBQUUsSUFBSSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBQzlELENBQUM7SUFFRDs7Ozs7Ozs7Ozs7Ozs7OztPQWdCRztJQUNILEtBQUssQ0FBQyxNQUFvQixFQUFFLE9BQXVCO1FBQ2pELE9BQU8sSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDLEtBQUssQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDOUMsQ0FBQztJQUVELHVEQUF1RDtJQUV2RDs7T0FFRztJQUNJLEtBQUssQ0FBQyxZQUFZLENBQUMsU0FBaUI7UUFDekMsTUFBTSxJQUFJLEdBQUc7WUFDWCxJQUFJLEVBQUUsT0FBTztZQUNiLGVBQWUsRUFBRSxNQUFNO1lBQ3ZCLGNBQWM7WUFDZCxlQUFlO1lBQ2YsU0FBUztTQUNWLENBQUM7UUFFRixNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFD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
|