ffmpeg-progress 1.5.0 → 1.7.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/README.md +58 -0
- package/bin.js +2 -0
- package/cli.js +112 -0
- package/core.d.ts +16 -2
- package/core.js +84 -36
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -42,6 +42,54 @@ await convertFile({
|
|
|
42
42
|
timer.end()
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
**Convert video with custom FFmpeg arguments**:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { convertFile } from 'ffmpeg-progress'
|
|
49
|
+
|
|
50
|
+
// H.264 codec with YUV420p pixel format and 2000k bitrate
|
|
51
|
+
await convertFile({
|
|
52
|
+
inFile: 'input.mp4',
|
|
53
|
+
outFile: 'output.mp4',
|
|
54
|
+
ffmpegArgs: ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-b:v', '2000k'],
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Resize video - Scale to 720p
|
|
58
|
+
await convertFile({
|
|
59
|
+
inFile: 'input.mp4',
|
|
60
|
+
outFile: 'output.mp4',
|
|
61
|
+
ffmpegArgs: ['-vf', 'scale=1280:720'],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Extract audio - Convert video to MP3
|
|
65
|
+
await convertFile({
|
|
66
|
+
inFile: 'input.mp4',
|
|
67
|
+
outFile: 'output.mp3',
|
|
68
|
+
ffmpegArgs: ['-vn', '-acodec', 'libmp3lame', '-ab', '192k'],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Quality preset - Use H.264 with slow preset and CRF quality
|
|
72
|
+
await convertFile({
|
|
73
|
+
inFile: 'input.mp4',
|
|
74
|
+
outFile: 'output.mp4',
|
|
75
|
+
ffmpegArgs: ['-c:v', 'libx264', '-preset', 'slow', '-crf', '23'],
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Rotate video**:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { rotateVideo } from 'ffmpeg-progress'
|
|
83
|
+
|
|
84
|
+
await rotateVideo({
|
|
85
|
+
inFile: 'test/in.mp4',
|
|
86
|
+
outFile: 'test/rotated.mp4',
|
|
87
|
+
angle: 90, // Rotate 90° clockwise (or 180, 270)
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Progress monitoring is also available similar to `convertFile` using `onDuration`, `onProgress`, etc.
|
|
92
|
+
|
|
45
93
|
**Get video resolution**:
|
|
46
94
|
|
|
47
95
|
```typescript
|
|
@@ -98,6 +146,16 @@ export function convertFile(
|
|
|
98
146
|
args: {
|
|
99
147
|
inFile: string
|
|
100
148
|
outFile: string
|
|
149
|
+
ffmpegArgs?: string[]
|
|
150
|
+
} & ProgressArgs,
|
|
151
|
+
): Promise<void>
|
|
152
|
+
|
|
153
|
+
export function rotateVideo(
|
|
154
|
+
args: {
|
|
155
|
+
inFile: string
|
|
156
|
+
outFile: string
|
|
157
|
+
/** degrees to rotate in clockwise direction */
|
|
158
|
+
angle: 90 | 180 | 270
|
|
101
159
|
} & ProgressArgs,
|
|
102
160
|
): Promise<void>
|
|
103
161
|
|
package/bin.js
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const child_process_1 = require("child_process");
|
|
4
|
+
const core_1 = require("./core");
|
|
5
|
+
let args = process.argv.slice(2);
|
|
6
|
+
for (let arg of args) {
|
|
7
|
+
if (arg == '-h' || arg == '--help') {
|
|
8
|
+
console.log(`
|
|
9
|
+
ffmpeg-progress - A progress monitor for FFmpeg operations
|
|
10
|
+
|
|
11
|
+
USAGE:
|
|
12
|
+
ffmpeg-progress <option>
|
|
13
|
+
ffmpeg-progress <ffmpeg-args...>
|
|
14
|
+
ffmpeg <ffmpeg-args...> 2>&1 | ffmpeg-progress
|
|
15
|
+
|
|
16
|
+
EXAMPLES:
|
|
17
|
+
# as drop-in replacement for ffmpeg:
|
|
18
|
+
ffmpeg-progress -i input.mp4 -c:v libx264 output.mp4
|
|
19
|
+
|
|
20
|
+
# pipe from ffmpeg output (need to redirect stderr):
|
|
21
|
+
ffmpeg -i input.mp4 -c:v libx264 output.mp4 2>&1 | ffmpeg-progress
|
|
22
|
+
|
|
23
|
+
OPTIONS:
|
|
24
|
+
-h, --help show this help message and exit
|
|
25
|
+
-v, --version show version and exit
|
|
26
|
+
|
|
27
|
+
NOTES:
|
|
28
|
+
- Pipe mode requires redirecting ffmpeg stderr to stdout (2>&1)
|
|
29
|
+
- Wrapped mode automatically captures ffmpeg progress from stderr
|
|
30
|
+
- ffmpeg outputs progress information to stderr, not stdout
|
|
31
|
+
`.trim());
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (arg == '-version' || arg == '--version') {
|
|
35
|
+
let pkg = require('./package.json');
|
|
36
|
+
console.log(`ffmpeg-progress ${pkg.version}`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let errorLines = [];
|
|
41
|
+
function checkOverwrite(chunk) {
|
|
42
|
+
let str = chunk.toString();
|
|
43
|
+
if (str.includes('Overwrite?')) {
|
|
44
|
+
console.error(str);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
errorLines.push(str);
|
|
48
|
+
}
|
|
49
|
+
function f(time) {
|
|
50
|
+
return (0, core_1.secondsToString)(Math.round(time));
|
|
51
|
+
}
|
|
52
|
+
let lastMessageLength = 0;
|
|
53
|
+
function writeProgress(message) {
|
|
54
|
+
let output = '\r' + message.padEnd(lastMessageLength, ' ');
|
|
55
|
+
lastMessageLength = message.length;
|
|
56
|
+
process.stdout.write(output);
|
|
57
|
+
}
|
|
58
|
+
let startTime = 0;
|
|
59
|
+
function onProgress(args) {
|
|
60
|
+
startTime ||= Date.now();
|
|
61
|
+
let passedTime = Date.now() - startTime;
|
|
62
|
+
let progress = `${f(args.currentSeconds)}/${f(args.totalSeconds)}`;
|
|
63
|
+
let speed = (args.currentSeconds / (passedTime / 1000)).toFixed(1);
|
|
64
|
+
let elapsed = f(passedTime / 1000);
|
|
65
|
+
let eta = f((args.totalSeconds - args.currentSeconds) /
|
|
66
|
+
(args.currentSeconds / (passedTime / 1000)));
|
|
67
|
+
writeProgress(`progress=${progress} speed=${speed}x elapsed=${elapsed} eta=${eta}`);
|
|
68
|
+
}
|
|
69
|
+
if (args.length == 0) {
|
|
70
|
+
console.log('reading ffmpeg output from pipe...');
|
|
71
|
+
(0, core_1.attachStream)({
|
|
72
|
+
stream: process.stdin,
|
|
73
|
+
onData: checkOverwrite,
|
|
74
|
+
onProgress,
|
|
75
|
+
}).on('end', () => {
|
|
76
|
+
process.stdout.write('\n');
|
|
77
|
+
console.log('end of ffmpeg output.');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
let cmd = 'ffmpeg';
|
|
82
|
+
for (let arg of args) {
|
|
83
|
+
let str = JSON.stringify(arg);
|
|
84
|
+
if (str == `"${arg}"`) {
|
|
85
|
+
cmd += ' ' + arg;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
cmd += ' ' + str;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log('> ' + cmd);
|
|
92
|
+
let childProcess = (0, child_process_1.spawn)('ffmpeg', args, {
|
|
93
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
(0, core_1.attachChildProcess)({
|
|
96
|
+
childProcess,
|
|
97
|
+
onStderr: checkOverwrite,
|
|
98
|
+
onProgress,
|
|
99
|
+
})
|
|
100
|
+
.then(() => {
|
|
101
|
+
process.stdout.write('\n');
|
|
102
|
+
console.log('ffmpeg process finished.');
|
|
103
|
+
})
|
|
104
|
+
.catch(error => {
|
|
105
|
+
process.stderr.write('\n');
|
|
106
|
+
for (let line of errorLines) {
|
|
107
|
+
console.error(line);
|
|
108
|
+
}
|
|
109
|
+
console.error('ffmpeg process error:', error);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
}
|
package/core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ChildProcessWithoutNullStreams } from 'child_process';
|
|
1
|
+
import { ChildProcessByStdio, ChildProcessWithoutNullStreams } from 'child_process';
|
|
2
|
+
import Stream from 'stream';
|
|
2
3
|
/** @description
|
|
3
4
|
* from "00:01:00.03" to 60.03;
|
|
4
5
|
* from "N/A" to NaN;
|
|
@@ -27,6 +28,7 @@ export type ScanVideoResult = {
|
|
|
27
28
|
export declare function parseVideoMetadata(stdout: string): ScanVideoResult;
|
|
28
29
|
export declare function scanVideo(file: string): Promise<ScanVideoResult>;
|
|
29
30
|
export type ProgressArgs = {
|
|
31
|
+
onStderr?: (chunk: Buffer) => void;
|
|
30
32
|
onData?: (chunk: Buffer) => void;
|
|
31
33
|
onDuration?: (duration: string) => void;
|
|
32
34
|
onTime?: (time: string) => void;
|
|
@@ -43,10 +45,22 @@ export type OnProgressArgs = {
|
|
|
43
45
|
export declare function convertFile(args: {
|
|
44
46
|
inFile: string;
|
|
45
47
|
outFile: string;
|
|
48
|
+
/** e.g. ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-b:v', '2000k'] */
|
|
49
|
+
ffmpegArgs?: string[];
|
|
50
|
+
} & ProgressArgs): Promise<void>;
|
|
51
|
+
export declare function rotateVideo(args: {
|
|
52
|
+
inFile: string;
|
|
53
|
+
outFile: string;
|
|
54
|
+
/** degrees to rotate in clockwise direction */
|
|
55
|
+
angle: 90 | 180 | 270;
|
|
46
56
|
} & ProgressArgs): Promise<void>;
|
|
47
57
|
export declare function attachChildProcess(args: {
|
|
48
|
-
childProcess: ChildProcessWithoutNullStreams
|
|
58
|
+
childProcess: ChildProcessWithoutNullStreams | ChildProcessByStdio<null, Stream.Readable, Stream.Readable>;
|
|
49
59
|
} & ProgressArgs): Promise<void>;
|
|
60
|
+
export declare function attachStream(args: {
|
|
61
|
+
stream: Stream.Readable;
|
|
62
|
+
abort?: () => void;
|
|
63
|
+
} & ProgressArgs): Stream.Readable;
|
|
50
64
|
export declare function estimateOutSize(args: {
|
|
51
65
|
inSize: number;
|
|
52
66
|
currentOutSize: number;
|
package/core.js
CHANGED
|
@@ -5,7 +5,9 @@ exports.secondsToString = secondsToString;
|
|
|
5
5
|
exports.parseVideoMetadata = parseVideoMetadata;
|
|
6
6
|
exports.scanVideo = scanVideo;
|
|
7
7
|
exports.convertFile = convertFile;
|
|
8
|
+
exports.rotateVideo = rotateVideo;
|
|
8
9
|
exports.attachChildProcess = attachChildProcess;
|
|
10
|
+
exports.attachStream = attachStream;
|
|
9
11
|
exports.estimateOutSize = estimateOutSize;
|
|
10
12
|
exports.getVideoResolution = getVideoResolution;
|
|
11
13
|
exports.getVideoDuration = getVideoDuration;
|
|
@@ -136,53 +138,54 @@ function scanVideo(file) {
|
|
|
136
138
|
});
|
|
137
139
|
}
|
|
138
140
|
async function convertFile(args) {
|
|
139
|
-
|
|
141
|
+
if (args.inFile === args.outFile) {
|
|
142
|
+
throw new Error('ffmpeg cannot edit files in-place, input and output file cannot be the same.');
|
|
143
|
+
}
|
|
144
|
+
let ffmpegCommand = ['-y', '-i', args.inFile];
|
|
145
|
+
if (args.ffmpegArgs) {
|
|
146
|
+
ffmpegCommand.push(...args.ffmpegArgs);
|
|
147
|
+
}
|
|
148
|
+
ffmpegCommand.push(args.outFile);
|
|
149
|
+
let childProcess = (0, child_process_1.spawn)('ffmpeg', ffmpegCommand);
|
|
140
150
|
return attachChildProcess({ childProcess, ...args });
|
|
141
151
|
}
|
|
152
|
+
async function rotateVideo(args) {
|
|
153
|
+
let filter;
|
|
154
|
+
switch (args.angle) {
|
|
155
|
+
case 90:
|
|
156
|
+
// 90° clockwise
|
|
157
|
+
filter = 'transpose=1';
|
|
158
|
+
break;
|
|
159
|
+
case 180:
|
|
160
|
+
// 180° rotation
|
|
161
|
+
filter = 'transpose=1,transpose=1';
|
|
162
|
+
break;
|
|
163
|
+
case 270:
|
|
164
|
+
// 270° clockwise (or 90° counterclockwise)
|
|
165
|
+
filter = 'transpose=2';
|
|
166
|
+
break;
|
|
167
|
+
default:
|
|
168
|
+
throw new Error(`Unsupported rotation angle: ${args.angle}. Supported angles are 90, 180, or 270 degrees.`);
|
|
169
|
+
}
|
|
170
|
+
return convertFile({
|
|
171
|
+
...args,
|
|
172
|
+
ffmpegArgs: ['-vf', filter, '-c:a', 'copy'],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
142
175
|
async function attachChildProcess(args) {
|
|
143
176
|
let { code, signal } = await new Promise((resolve, reject) => {
|
|
144
177
|
let { childProcess } = args;
|
|
145
|
-
let duration = '';
|
|
146
|
-
let time = '';
|
|
147
|
-
let totalSeconds = 0;
|
|
148
|
-
let lastSeconds = 0;
|
|
149
178
|
function abort() {
|
|
150
179
|
childProcess.kill('SIGKILL');
|
|
151
180
|
}
|
|
152
181
|
if (args.onData) {
|
|
153
182
|
childProcess.stdout.on('data', args.onData);
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
totalSeconds = parseToSeconds(duration);
|
|
161
|
-
if (args.onDuration) {
|
|
162
|
-
args.onDuration(duration);
|
|
163
|
-
}
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
match = str.match(/frame=\s*\d+\s+fps=.*time=([0-9:.]+)\s/);
|
|
167
|
-
if (match) {
|
|
168
|
-
time = match[1];
|
|
169
|
-
if (args.onTime) {
|
|
170
|
-
args.onTime(time);
|
|
171
|
-
}
|
|
172
|
-
if (args.onProgress) {
|
|
173
|
-
let currentSeconds = parseToSeconds(time);
|
|
174
|
-
let deltaSeconds = currentSeconds - lastSeconds;
|
|
175
|
-
lastSeconds = currentSeconds;
|
|
176
|
-
args.onProgress({
|
|
177
|
-
deltaSeconds,
|
|
178
|
-
currentSeconds,
|
|
179
|
-
totalSeconds,
|
|
180
|
-
time,
|
|
181
|
-
duration,
|
|
182
|
-
abort,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
184
|
+
attachStream({
|
|
185
|
+
stream: childProcess.stderr,
|
|
186
|
+
onData: undefined,
|
|
187
|
+
abort,
|
|
188
|
+
...args,
|
|
186
189
|
});
|
|
187
190
|
childProcess.on('exit', (code, signal) => {
|
|
188
191
|
resolve({ code, signal });
|
|
@@ -192,6 +195,51 @@ async function attachChildProcess(args) {
|
|
|
192
195
|
return;
|
|
193
196
|
throw new Error(`ffmpeg exit abnormally, exit code: ${code}, signal: ${signal}`);
|
|
194
197
|
}
|
|
198
|
+
function attachStream(args) {
|
|
199
|
+
let abort = args.abort || (() => { });
|
|
200
|
+
let duration = '';
|
|
201
|
+
let time = '';
|
|
202
|
+
let totalSeconds = 0;
|
|
203
|
+
let lastSeconds = 0;
|
|
204
|
+
return args.stream.on('data', (data) => {
|
|
205
|
+
if (args.onData) {
|
|
206
|
+
args.onData(data);
|
|
207
|
+
}
|
|
208
|
+
if (args.onStderr) {
|
|
209
|
+
args.onStderr(data);
|
|
210
|
+
}
|
|
211
|
+
let str = data.toString();
|
|
212
|
+
let match = str.match(/Duration: ([0-9:.]+),/);
|
|
213
|
+
if (match) {
|
|
214
|
+
duration = match[1];
|
|
215
|
+
totalSeconds = parseToSeconds(duration);
|
|
216
|
+
if (args.onDuration) {
|
|
217
|
+
args.onDuration(duration);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
match = str.match(/frame=\s*\d+\s+fps=.*time=([0-9:.]+)\s/);
|
|
222
|
+
if (match) {
|
|
223
|
+
time = match[1];
|
|
224
|
+
if (args.onTime) {
|
|
225
|
+
args.onTime(time);
|
|
226
|
+
}
|
|
227
|
+
if (args.onProgress) {
|
|
228
|
+
let currentSeconds = parseToSeconds(time);
|
|
229
|
+
let deltaSeconds = currentSeconds - lastSeconds;
|
|
230
|
+
lastSeconds = currentSeconds;
|
|
231
|
+
args.onProgress({
|
|
232
|
+
deltaSeconds,
|
|
233
|
+
currentSeconds,
|
|
234
|
+
totalSeconds,
|
|
235
|
+
time,
|
|
236
|
+
duration,
|
|
237
|
+
abort,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
195
243
|
function estimateOutSize(args) {
|
|
196
244
|
let estimatedRate = args.currentOutSize / args.currentSeconds;
|
|
197
245
|
let remindSeconds = args.totalSeconds - args.currentSeconds;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ffmpeg-progress",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Extract progress from ffmpeg child_process",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ffmpeg",
|
|
@@ -29,15 +29,21 @@
|
|
|
29
29
|
},
|
|
30
30
|
"main": "core.js",
|
|
31
31
|
"types": "./core.d.ts",
|
|
32
|
+
"bin": {
|
|
33
|
+
"ffmpeg-progress": "./bin.js"
|
|
34
|
+
},
|
|
32
35
|
"directories": {
|
|
33
36
|
"test": "test"
|
|
34
37
|
},
|
|
35
38
|
"files": [
|
|
39
|
+
"bin.js",
|
|
40
|
+
"cli.js",
|
|
36
41
|
"core.d.ts",
|
|
37
42
|
"core.js"
|
|
38
43
|
],
|
|
39
44
|
"scripts": {
|
|
40
|
-
"
|
|
45
|
+
"clean": "rm -f *.js && git checkout bin.js",
|
|
46
|
+
"mocha": "npm run clean && ts-mocha \"*.{test,spec}.ts\"",
|
|
41
47
|
"test": "npm run mocha && tsc --noEmit",
|
|
42
48
|
"build": "tsc -p ."
|
|
43
49
|
},
|