@mediaproc/video 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +25 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/compress.d.ts.map +1 -1
- package/dist/commands/compress.js +18 -14
- package/dist/commands/compress.js.map +1 -1
- package/dist/commands/convert.d.ts.map +1 -1
- package/dist/commands/convert.js +40 -29
- package/dist/commands/convert.js.map +1 -1
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +98 -31
- package/dist/commands/extract.js.map +1 -1
- package/dist/commands/merge.d.ts.map +1 -1
- package/dist/commands/merge.js +161 -47
- package/dist/commands/merge.js.map +1 -1
- package/dist/commands/metadata.d.ts +3 -0
- package/dist/commands/metadata.d.ts.map +1 -0
- package/dist/commands/metadata.js +310 -0
- package/dist/commands/metadata.js.map +1 -0
- package/dist/commands/resize.d.ts.map +1 -1
- package/dist/commands/resize.js +50 -16
- package/dist/commands/resize.js.map +1 -1
- package/dist/commands/transcode.d.ts.map +1 -1
- package/dist/commands/transcode.js +39 -23
- package/dist/commands/transcode.js.map +1 -1
- package/dist/commands/trim.d.ts.map +1 -1
- package/dist/commands/trim.js +55 -20
- package/dist/commands/trim.js.map +1 -1
- package/dist/register.d.ts +2 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +28 -8
- package/dist/register.js.map +1 -1
- package/dist/types.d.ts +32 -35
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ffmpeg-output.d.ts +7 -3
- package/dist/utils/ffmpeg-output.d.ts.map +1 -1
- package/dist/utils/ffmpeg-output.js +56 -65
- package/dist/utils/ffmpeg-output.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +26 -22
- package/dist/utils/ffmpeg.d.ts.map +1 -1
- package/dist/utils/ffmpeg.js +211 -93
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/ffmpegLogger.d.ts +3 -10
- package/dist/utils/ffmpegLogger.d.ts.map +1 -1
- package/dist/utils/ffmpegLogger.js +15 -51
- package/dist/utils/ffmpegLogger.js.map +1 -1
- package/package.json +3 -7
- package/bin/cli.js +0 -5
|
@@ -1,94 +1,85 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Style FFmpeg output line with appropriate colors and formatting (video only)
|
|
4
4
|
*/
|
|
5
5
|
export function styleFFmpegOutput(line) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (!line)
|
|
6
|
+
const trimmed = line.trim();
|
|
7
|
+
if (!trimmed)
|
|
9
8
|
return '';
|
|
10
|
-
//
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Parse progress line
|
|
21
|
-
const frameMatch = line.match(/frame=\s*(\d+)/);
|
|
22
|
-
const fpsMatch = line.match(/fps=\s*([\d.]+)/);
|
|
23
|
-
const timeMatch = line.match(/time=\s*([\d:\.]+)/);
|
|
24
|
-
const speedMatch = line.match(/speed=\s*([\d.]+x)/);
|
|
25
|
-
const sizeMatch = line.match(/size=\s*(\d+\w+)/);
|
|
26
|
-
const bitrateMatch = line.match(/bitrate=\s*([\d.]+\w+)/);
|
|
27
|
-
let output = chalk.cyan('⚡ Progress: ');
|
|
9
|
+
// Progress updates (frame=, fps=, time=, speed=)
|
|
10
|
+
if (trimmed.match(/frame=|fps=|time=|speed=/)) {
|
|
11
|
+
// Parse progress line for richer output
|
|
12
|
+
const frameMatch = trimmed.match(/frame=\s*(\d+)/);
|
|
13
|
+
const fpsMatch = trimmed.match(/fps=\s*([\d.]+)/);
|
|
14
|
+
const timeMatch = trimmed.match(/time=\s*([\d:\.]+)/);
|
|
15
|
+
const speedMatch = trimmed.match(/speed=\s*([\d.]+x)/);
|
|
16
|
+
const sizeMatch = trimmed.match(/size=\s*(\d+\w+)/);
|
|
17
|
+
const bitrateMatch = trimmed.match(/bitrate=\s*([\d.]+\w+)/);
|
|
18
|
+
let output = chalk.cyan.bold('\u23f3 Progress: ');
|
|
28
19
|
if (frameMatch)
|
|
29
20
|
output += chalk.white(`Frame ${frameMatch[1]} `);
|
|
30
21
|
if (fpsMatch)
|
|
31
|
-
output += chalk.gray(
|
|
22
|
+
output += chalk.gray(`\u2022 ${fpsMatch[1]} fps `);
|
|
32
23
|
if (timeMatch)
|
|
33
|
-
output += chalk.white(
|
|
24
|
+
output += chalk.white(`\u2022 ${timeMatch[1]} `);
|
|
34
25
|
if (sizeMatch)
|
|
35
|
-
output += chalk.gray(
|
|
26
|
+
output += chalk.gray(`\u2022 ${sizeMatch[1]} `);
|
|
36
27
|
if (bitrateMatch)
|
|
37
|
-
output += chalk.gray(
|
|
28
|
+
output += chalk.gray(`\u2022 ${bitrateMatch[1]} `);
|
|
38
29
|
if (speedMatch)
|
|
39
|
-
output += chalk.green(
|
|
30
|
+
output += chalk.green(`\u2022 ${speedMatch[1]}`);
|
|
40
31
|
return output;
|
|
41
32
|
}
|
|
42
|
-
// Input
|
|
43
|
-
if (
|
|
44
|
-
return chalk.blue
|
|
33
|
+
// Input file info
|
|
34
|
+
if (trimmed.startsWith('Input #') || trimmed.includes('Duration:') || trimmed.includes('Stream #')) {
|
|
35
|
+
return chalk.blue(trimmed);
|
|
45
36
|
}
|
|
46
|
-
//
|
|
47
|
-
if (
|
|
48
|
-
return chalk.
|
|
37
|
+
// Output file info
|
|
38
|
+
if (trimmed.startsWith('Output #')) {
|
|
39
|
+
return chalk.green(trimmed);
|
|
49
40
|
}
|
|
50
|
-
//
|
|
51
|
-
if (
|
|
52
|
-
return chalk.
|
|
41
|
+
// Warnings
|
|
42
|
+
if (trimmed.toLowerCase().includes('warning')) {
|
|
43
|
+
return chalk.yellow(trimmed);
|
|
53
44
|
}
|
|
54
|
-
//
|
|
55
|
-
if (
|
|
56
|
-
return chalk.
|
|
45
|
+
// Errors
|
|
46
|
+
if (trimmed.toLowerCase().includes('error') || trimmed.toLowerCase().includes('failed')) {
|
|
47
|
+
return chalk.red(trimmed);
|
|
57
48
|
}
|
|
58
|
-
// Success messages
|
|
59
|
-
if (
|
|
60
|
-
return chalk.green(
|
|
49
|
+
// Success messages
|
|
50
|
+
if (trimmed.includes('successfully') || trimmed.includes('complete')) {
|
|
51
|
+
return chalk.green(trimmed);
|
|
61
52
|
}
|
|
62
|
-
// Default
|
|
63
|
-
return chalk.
|
|
53
|
+
// Default gray for other lines
|
|
54
|
+
return chalk.gray(trimmed);
|
|
64
55
|
}
|
|
65
56
|
/**
|
|
66
|
-
* Check if line should be displayed based on
|
|
57
|
+
* Check if a line should be displayed based on content (video only)
|
|
67
58
|
*/
|
|
68
|
-
export function shouldDisplayLine(line
|
|
69
|
-
|
|
70
|
-
if (!
|
|
59
|
+
export function shouldDisplayLine(line) {
|
|
60
|
+
const trimmed = line.trim();
|
|
61
|
+
if (!trimmed)
|
|
71
62
|
return false;
|
|
72
63
|
// Always show errors and warnings
|
|
73
|
-
if (
|
|
64
|
+
if (trimmed.toLowerCase().includes('error') || trimmed.toLowerCase().includes('failed') || trimmed.toLowerCase().includes('warning'))
|
|
74
65
|
return true;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (line.includes('frame=') && line.includes('time=')) {
|
|
66
|
+
// Show progress and status lines always
|
|
67
|
+
if (trimmed.match(/frame=|fps=|time=|speed=/))
|
|
78
68
|
return true;
|
|
69
|
+
// Show input/output info
|
|
70
|
+
if (trimmed.startsWith('Input #') || trimmed.startsWith('Output #'))
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Log FFmpeg output (video only, plain, no styling)
|
|
76
|
+
*/
|
|
77
|
+
export function logFFmpegOutput(output, verbose = false) {
|
|
78
|
+
const lines = output.split('\n');
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (shouldDisplayLine(line) || verbose) {
|
|
81
|
+
console.log(line);
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
|
-
// Show important info
|
|
81
|
-
if (line.includes('Input #') || line.includes('Output #') || line.includes('Stream #')) {
|
|
82
|
-
return verbose;
|
|
83
|
-
}
|
|
84
|
-
// Show duration and metadata if verbose
|
|
85
|
-
if (line.includes('Duration:') || line.includes('Metadata:')) {
|
|
86
|
-
return verbose;
|
|
87
|
-
}
|
|
88
|
-
// Filter out configuration and build info unless very verbose
|
|
89
|
-
if (line.includes('configuration:') || line.includes('built with')) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
return verbose;
|
|
93
84
|
}
|
|
94
85
|
//# sourceMappingURL=ffmpeg-output.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg-output.js","sourceRoot":"","sources":["../../src/utils/ffmpeg-output.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ffmpeg-output.js","sourceRoot":"","sources":["../../src/utils/ffmpeg-output.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/ffmpeg.d.ts
CHANGED
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
export interface VideoMetadata {
|
|
2
2
|
duration: number;
|
|
3
|
+
codec: string;
|
|
3
4
|
width: number;
|
|
4
5
|
height: number;
|
|
5
|
-
codec: string;
|
|
6
|
-
fps: number;
|
|
7
6
|
bitrate: number;
|
|
8
7
|
format: string;
|
|
9
8
|
}
|
|
9
|
+
export interface StreamInfo {
|
|
10
|
+
hasVideo: boolean;
|
|
11
|
+
hasAudio: boolean;
|
|
12
|
+
videoStreamIndex: number;
|
|
13
|
+
audioStreamIndex: number;
|
|
14
|
+
audioStreams: number[];
|
|
15
|
+
videoStreams: number[];
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
export declare function runFFmpeg(args: string[], verbose?: boolean, onOutput?: (line: string) => void): Promise<void>;
|
|
14
|
-
/**
|
|
15
|
-
* Get video metadata using ffprobe
|
|
18
|
+
* Check if ffmpeg and ffprobe are available, with styled output
|
|
19
|
+
* If strict=true, both must be present. If strict=false, only ffmpeg is checked.
|
|
16
20
|
*/
|
|
17
|
-
export declare function
|
|
21
|
+
export declare function checkFFmpeg(strict?: boolean): Promise<boolean>;
|
|
18
22
|
/**
|
|
19
|
-
*
|
|
23
|
+
* Format file size to human-readable string (styled)
|
|
20
24
|
*/
|
|
21
|
-
export declare function
|
|
25
|
+
export declare function formatFileSize(bytes: number): string;
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
27
|
+
* Format duration to HH:MM:SS (styled)
|
|
24
28
|
*/
|
|
25
|
-
export declare function
|
|
29
|
+
export declare function formatDuration(seconds: number): string;
|
|
26
30
|
/**
|
|
27
|
-
*
|
|
31
|
+
* Parse time string (HH:MM:SS or seconds) to seconds
|
|
28
32
|
*/
|
|
29
|
-
export declare function
|
|
33
|
+
export declare function parseTime(time: string): number;
|
|
30
34
|
/**
|
|
31
|
-
*
|
|
35
|
+
* Format bitrate to human-readable string (styled)
|
|
32
36
|
*/
|
|
33
|
-
export declare function
|
|
37
|
+
export declare function formatBitrate(bitrate: number): string;
|
|
34
38
|
/**
|
|
35
|
-
*
|
|
39
|
+
* Execute ffmpeg command with detailed output and robust error handling
|
|
36
40
|
*/
|
|
37
|
-
export declare function
|
|
41
|
+
export declare function runFFmpeg(args: string[], verbose?: boolean, onOutput?: (line: string, styledLine?: string) => void): Promise<void>;
|
|
38
42
|
/**
|
|
39
|
-
*
|
|
43
|
+
* Get video metadata using ffprobe with detailed output and error reporting
|
|
40
44
|
*/
|
|
41
|
-
export declare function
|
|
45
|
+
export declare function getVideoMetadata(input: string): Promise<VideoMetadata>;
|
|
42
46
|
/**
|
|
43
|
-
*
|
|
47
|
+
* Get stream information (audio/video streams) from a media file
|
|
44
48
|
*/
|
|
45
|
-
export declare function
|
|
49
|
+
export declare function getStreamInfo(input: string): Promise<StreamInfo>;
|
|
46
50
|
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/utils/ffmpeg.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/utils/ffmpeg.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,UAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAuBlE;AAgBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMpD;AAGD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKtD;AAGD;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW9C;AAGD;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIrD;AAED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,UAAQ,EACf,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI,GACrD,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAkE5E;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmDtE"}
|
package/dist/utils/ffmpeg.js
CHANGED
|
@@ -1,51 +1,169 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { shouldDisplayLine, logFFmpegOutput } from './ffmpegLogger.js';
|
|
3
|
+
import { styleFFmpegOutput } from './ffmpeg-output.js';
|
|
4
|
+
// FFmpeg install guidance links (all platforms)
|
|
5
|
+
const FFMPEG_GUIDES = [
|
|
6
|
+
{ label: 'Official guide', url: 'https://ffmpeg.org/download.html' },
|
|
7
|
+
{ label: 'Community guide', url: 'https://github.com/adaptlearning/adapt_authoring/wiki/Installing-FFmpeg' },
|
|
8
|
+
{ label: 'Multi-platform guide', url: 'https://github.com/ffmpegwasm/ffmpeg.wasm/blob/main/docs/install.md' },
|
|
9
|
+
];
|
|
10
|
+
function printFFmpegInstallGuidance() {
|
|
11
|
+
const lines = [
|
|
12
|
+
'✗ FFmpeg or ffprobe not found on your system.',
|
|
13
|
+
'Please install FFmpeg and ffprobe to use video features.',
|
|
14
|
+
...FFMPEG_GUIDES.map(g => `${g.label}: ${g.url}`),
|
|
15
|
+
'After installation, ensure ffmpeg and ffprobe are available in your PATH.'
|
|
16
|
+
];
|
|
17
|
+
lines.forEach(line => console.error(styleFFmpegOutput(line)));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if ffmpeg and ffprobe are available, with styled output
|
|
21
|
+
* If strict=true, both must be present. If strict=false, only ffmpeg is checked.
|
|
22
|
+
*/
|
|
23
|
+
export async function checkFFmpeg(strict = false) {
|
|
24
|
+
// Check ffmpeg
|
|
25
|
+
const ffmpegAvailable = await new Promise((resolve) => {
|
|
26
|
+
try {
|
|
27
|
+
const ffmpeg = spawn('ffmpeg', ['-version']);
|
|
28
|
+
ffmpeg.on('close', (code) => resolve(code === 0));
|
|
29
|
+
ffmpeg.on('error', () => resolve(false));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
resolve(false);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (!strict)
|
|
36
|
+
return ffmpegAvailable;
|
|
37
|
+
// Check ffprobe
|
|
38
|
+
const ffprobeAvailable = await new Promise((resolve) => {
|
|
39
|
+
try {
|
|
40
|
+
const ffprobe = spawn('ffprobe', ['-version']);
|
|
41
|
+
ffprobe.on('close', (code) => resolve(code === 0));
|
|
42
|
+
ffprobe.on('error', () => resolve(false));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
resolve(false);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return ffmpegAvailable && ffprobeAvailable;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Centralized check and guidance for ffmpeg/ffprobe availability
|
|
52
|
+
*/
|
|
53
|
+
async function ensureFFmpegAvailable() {
|
|
54
|
+
const available = await checkFFmpeg(true);
|
|
55
|
+
if (!available) {
|
|
56
|
+
printFFmpegInstallGuidance();
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Format file size to human-readable string (styled)
|
|
63
|
+
*/
|
|
64
|
+
export function formatFileSize(bytes) {
|
|
65
|
+
if (bytes === 0)
|
|
66
|
+
return '0 B';
|
|
67
|
+
const k = 1024;
|
|
68
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
69
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
70
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
71
|
+
}
|
|
4
72
|
/**
|
|
5
|
-
*
|
|
73
|
+
* Format duration to HH:MM:SS (styled)
|
|
74
|
+
*/
|
|
75
|
+
export function formatDuration(seconds) {
|
|
76
|
+
const h = Math.floor(seconds / 3600);
|
|
77
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
78
|
+
const s = Math.floor(seconds % 60);
|
|
79
|
+
return [h, m, s].map(v => v.toString().padStart(2, '0')).join(':');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse time string (HH:MM:SS or seconds) to seconds
|
|
83
|
+
*/
|
|
84
|
+
export function parseTime(time) {
|
|
85
|
+
if (/^\d+(\.\d+)?$/.test(time)) {
|
|
86
|
+
return parseFloat(time);
|
|
87
|
+
}
|
|
88
|
+
const parts = time.split(':').map(p => parseInt(p));
|
|
89
|
+
if (parts.length === 3) {
|
|
90
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
91
|
+
}
|
|
92
|
+
else if (parts.length === 2) {
|
|
93
|
+
return parts[0] * 60 + parts[1];
|
|
94
|
+
}
|
|
95
|
+
return parseFloat(time);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Format bitrate to human-readable string (styled)
|
|
99
|
+
*/
|
|
100
|
+
export function formatBitrate(bitrate) {
|
|
101
|
+
if (bitrate === 0)
|
|
102
|
+
return 'unknown';
|
|
103
|
+
const kbps = bitrate / 1000;
|
|
104
|
+
return `${Math.round(kbps)} kbps`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Execute ffmpeg command with detailed output and robust error handling
|
|
6
108
|
*/
|
|
7
109
|
export async function runFFmpeg(args, verbose = false, onOutput) {
|
|
110
|
+
if (!(await ensureFFmpegAvailable()))
|
|
111
|
+
return;
|
|
8
112
|
return new Promise((resolve, reject) => {
|
|
9
113
|
const ffmpeg = spawn('ffmpeg', args);
|
|
10
114
|
let stderr = '';
|
|
115
|
+
let allOutput = '';
|
|
11
116
|
ffmpeg.stderr.on('data', (data) => {
|
|
12
117
|
const output = data.toString();
|
|
13
118
|
stderr += output;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
119
|
+
allOutput += output;
|
|
120
|
+
output.split('\n').forEach((line) => {
|
|
121
|
+
if (shouldDisplayLine(line) || verbose) {
|
|
122
|
+
const styled = styleFFmpegOutput(line);
|
|
123
|
+
logFFmpegOutput(line);
|
|
124
|
+
if (onOutput) {
|
|
125
|
+
onOutput(line, styled);
|
|
19
126
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
127
|
+
else {
|
|
128
|
+
if (styled)
|
|
129
|
+
console.log(styled);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
25
133
|
});
|
|
26
134
|
ffmpeg.on('close', (code) => {
|
|
27
135
|
if (code === 0) {
|
|
136
|
+
console.log(styleFFmpegOutput('FFmpeg finished successfully.'));
|
|
28
137
|
resolve();
|
|
29
138
|
}
|
|
30
139
|
else {
|
|
140
|
+
stderr.split('\n').forEach((line) => {
|
|
141
|
+
if (shouldDisplayLine(line)) {
|
|
142
|
+
logFFmpegOutput(line);
|
|
143
|
+
const styled = styleFFmpegOutput(line);
|
|
144
|
+
if (styled)
|
|
145
|
+
console.error(styled);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
31
148
|
reject(new Error(`FFmpeg failed with code ${code}\n${stderr}`));
|
|
32
149
|
}
|
|
33
150
|
});
|
|
34
151
|
ffmpeg.on('error', (error) => {
|
|
152
|
+
console.error(styleFFmpegOutput(`Failed to start ffmpeg: ${error.message}`));
|
|
35
153
|
reject(new Error(`Failed to start ffmpeg: ${error.message}`));
|
|
36
154
|
});
|
|
37
155
|
});
|
|
38
156
|
}
|
|
39
157
|
/**
|
|
40
|
-
* Get video metadata using ffprobe
|
|
158
|
+
* Get video metadata using ffprobe with detailed output and error reporting
|
|
41
159
|
*/
|
|
42
160
|
export async function getVideoMetadata(input) {
|
|
161
|
+
if (!(await ensureFFmpegAvailable()))
|
|
162
|
+
return Promise.reject(new Error('FFmpeg/ffprobe not installed'));
|
|
43
163
|
return new Promise((resolve, reject) => {
|
|
44
164
|
const ffprobe = spawn('ffprobe', [
|
|
45
|
-
'-v',
|
|
46
|
-
'
|
|
47
|
-
'-print_format',
|
|
48
|
-
'json',
|
|
165
|
+
'-v', 'quiet',
|
|
166
|
+
'-print_format', 'json',
|
|
49
167
|
'-show_format',
|
|
50
168
|
'-show_streams',
|
|
51
169
|
input,
|
|
@@ -56,10 +174,20 @@ export async function getVideoMetadata(input) {
|
|
|
56
174
|
stdout += data.toString();
|
|
57
175
|
});
|
|
58
176
|
ffprobe.stderr.on('data', (data) => {
|
|
59
|
-
|
|
177
|
+
const output = data.toString();
|
|
178
|
+
stderr += output;
|
|
179
|
+
output.split('\n').forEach((line) => {
|
|
180
|
+
if (shouldDisplayLine(line)) {
|
|
181
|
+
logFFmpegOutput(line);
|
|
182
|
+
const styled = styleFFmpegOutput(line);
|
|
183
|
+
if (styled)
|
|
184
|
+
console.error(styled);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
60
187
|
});
|
|
61
188
|
ffprobe.on('close', (code) => {
|
|
62
189
|
if (code !== 0) {
|
|
190
|
+
console.error(styleFFmpegOutput(`ffprobe failed with code ${code}`));
|
|
63
191
|
reject(new Error(`ffprobe failed: ${stderr}`));
|
|
64
192
|
return;
|
|
65
193
|
}
|
|
@@ -67,101 +195,91 @@ export async function getVideoMetadata(input) {
|
|
|
67
195
|
const data = JSON.parse(stdout);
|
|
68
196
|
const videoStream = data.streams.find((s) => s.codec_type === 'video');
|
|
69
197
|
if (!videoStream) {
|
|
198
|
+
console.error(styleFFmpegOutput('No video stream found in file.'));
|
|
70
199
|
reject(new Error('No video stream found'));
|
|
71
200
|
return;
|
|
72
201
|
}
|
|
73
202
|
const metadata = {
|
|
74
203
|
duration: parseFloat(data.format.duration) || 0,
|
|
204
|
+
codec: videoStream.codec_name || 'unknown',
|
|
75
205
|
width: videoStream.width || 0,
|
|
76
206
|
height: videoStream.height || 0,
|
|
77
|
-
|
|
78
|
-
fps: eval(videoStream.r_frame_rate) || 0,
|
|
79
|
-
bitrate: parseInt(data.format.bit_rate) || 0,
|
|
207
|
+
bitrate: parseInt(data.format.bit_rate) || parseInt(videoStream.bit_rate) || 0,
|
|
80
208
|
format: data.format.format_name || 'unknown',
|
|
81
209
|
};
|
|
210
|
+
// Detailed output of metadata
|
|
211
|
+
console.log(styleFFmpegOutput('Video Metadata:'));
|
|
212
|
+
Object.entries(metadata).forEach(([key, value]) => {
|
|
213
|
+
console.log(styleFFmpegOutput(` ${key}: ${value}`));
|
|
214
|
+
});
|
|
82
215
|
resolve(metadata);
|
|
83
216
|
}
|
|
84
217
|
catch (error) {
|
|
85
|
-
|
|
218
|
+
console.error(styleFFmpegOutput(`Failed to parse ffprobe output: ${error.message}`));
|
|
219
|
+
reject(new Error(`Failed to parse ffprobe output: ${error.message}`));
|
|
86
220
|
}
|
|
87
221
|
});
|
|
222
|
+
// Handle spawn error to prevent memory leaks
|
|
88
223
|
ffprobe.on('error', (error) => {
|
|
224
|
+
console.error(styleFFmpegOutput(`Failed to start ffprobe: ${error.message}`));
|
|
89
225
|
reject(new Error(`Failed to start ffprobe: ${error.message}`));
|
|
90
226
|
});
|
|
91
227
|
});
|
|
92
228
|
}
|
|
93
229
|
/**
|
|
94
|
-
*
|
|
230
|
+
* Get stream information (audio/video streams) from a media file
|
|
95
231
|
*/
|
|
96
|
-
export async function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
232
|
+
export async function getStreamInfo(input) {
|
|
233
|
+
if (!(await ensureFFmpegAvailable()))
|
|
234
|
+
return Promise.reject(new Error('FFmpeg/ffprobe not installed'));
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const ffprobe = spawn('ffprobe', [
|
|
237
|
+
'-v', 'quiet',
|
|
238
|
+
'-print_format', 'json',
|
|
239
|
+
'-show_streams',
|
|
240
|
+
input,
|
|
241
|
+
]);
|
|
242
|
+
let stdout = '';
|
|
243
|
+
let stderr = '';
|
|
244
|
+
ffprobe.stdout.on('data', (data) => {
|
|
245
|
+
stdout += data.toString();
|
|
246
|
+
});
|
|
247
|
+
ffprobe.stderr.on('data', (data) => {
|
|
248
|
+
stderr += data.toString();
|
|
249
|
+
});
|
|
250
|
+
ffprobe.on('close', (code) => {
|
|
251
|
+
if (code !== 0) {
|
|
252
|
+
reject(new Error(`ffprobe failed: ${stderr}`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const data = JSON.parse(stdout);
|
|
257
|
+
const videoStreams = data.streams
|
|
258
|
+
.map((s, idx) => ({ ...s, index: idx }))
|
|
259
|
+
.filter((s) => s.codec_type === 'video')
|
|
260
|
+
.map((s) => s.index);
|
|
261
|
+
const audioStreams = data.streams
|
|
262
|
+
.map((s, idx) => ({ ...s, index: idx }))
|
|
263
|
+
.filter((s) => s.codec_type === 'audio')
|
|
264
|
+
.map((s) => s.index);
|
|
265
|
+
const streamInfo = {
|
|
266
|
+
hasVideo: videoStreams.length > 0,
|
|
267
|
+
hasAudio: audioStreams.length > 0,
|
|
268
|
+
videoStreamIndex: videoStreams[0] ?? -1,
|
|
269
|
+
audioStreamIndex: audioStreams[0] ?? -1,
|
|
270
|
+
videoStreams,
|
|
271
|
+
audioStreams,
|
|
272
|
+
};
|
|
273
|
+
resolve(streamInfo);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
reject(new Error(`Failed to parse ffprobe output: ${error.message}`));
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
// Handle spawn error to prevent memory leaks
|
|
280
|
+
ffprobe.on('error', (error) => {
|
|
281
|
+
reject(new Error(`Failed to start ffprobe: ${error.message}`));
|
|
282
|
+
});
|
|
111
283
|
});
|
|
112
284
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Validate input file exists
|
|
115
|
-
*/
|
|
116
|
-
export function validateInputFile(input) {
|
|
117
|
-
const inputPath = resolve(input);
|
|
118
|
-
if (!fileExists(inputPath)) {
|
|
119
|
-
throw new Error(`Input file not found: ${input}`);
|
|
120
|
-
}
|
|
121
|
-
return inputPath;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Generate output filename if not provided
|
|
125
|
-
*/
|
|
126
|
-
export function generateOutputPath(input, suffix, extension) {
|
|
127
|
-
const inputPath = resolve(input);
|
|
128
|
-
const ext = extension || inputPath.split('.').pop();
|
|
129
|
-
const base = inputPath.substring(0, inputPath.lastIndexOf('.'));
|
|
130
|
-
return `${base}_${suffix}.${ext}`;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Format time for display (seconds to HH:MM:SS)
|
|
134
|
-
*/
|
|
135
|
-
export function formatDuration(seconds) {
|
|
136
|
-
const hrs = Math.floor(seconds / 3600);
|
|
137
|
-
const mins = Math.floor((seconds % 3600) / 60);
|
|
138
|
-
const secs = Math.floor(seconds % 60);
|
|
139
|
-
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Parse time string (HH:MM:SS) to seconds
|
|
143
|
-
*/
|
|
144
|
-
export function parseTimeToSeconds(time) {
|
|
145
|
-
const parts = time.split(':').map(Number);
|
|
146
|
-
if (parts.length === 3) {
|
|
147
|
-
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
148
|
-
}
|
|
149
|
-
else if (parts.length === 2) {
|
|
150
|
-
return parts[0] * 60 + parts[1];
|
|
151
|
-
}
|
|
152
|
-
return parts[0];
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Format file size
|
|
156
|
-
*/
|
|
157
|
-
export function formatFileSize(bytes) {
|
|
158
|
-
const units = ['B', 'KB', 'MB', 'GB'];
|
|
159
|
-
let size = bytes;
|
|
160
|
-
let unitIndex = 0;
|
|
161
|
-
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
162
|
-
size /= 1024;
|
|
163
|
-
unitIndex++;
|
|
164
|
-
}
|
|
165
|
-
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
166
|
-
}
|
|
167
285
|
//# sourceMappingURL=ffmpeg.js.map
|