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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('./cli')
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
- let childProcess = (0, child_process_1.spawn)('ffmpeg', ['-y', '-i', args.inFile, args.outFile]);
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
- childProcess.stderr.on('data', (data) => {
156
- let str = data.toString();
157
- let match = str.match(/Duration: ([0-9:.]+),/);
158
- if (match) {
159
- duration = match[1];
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.5.0",
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
- "mocha": "rm -f *.js && ts-mocha \"*.{test,spec}.ts\"",
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
  },