1- import { spawn } from "node:child_process"
21import debug from "debug"
2+ import assert from "node:assert"
3+ import { spawn } from "node:child_process"
34import { createReadStream , createWriteStream } from "node:fs"
45import { unlink } from "node:fs/promises"
56import { tmpdir } from "node:os"
@@ -9,93 +10,6 @@ import { PassThrough } from "node:stream"
910const dbg = debug ( "ffmpeg-stream" )
1011const EXIT_CODES = [ 0 , 255 ]
1112
12- /**
13- * @param {import("node:stream").Readable | import("node:stream").Writable } stream
14- * @param {string } name
15- */
16- function debugStream ( stream , name ) {
17- stream . on ( "error" , err => {
18- dbg ( `${ name } error: ${ err . message } ` )
19- } )
20- stream . on (
21- "data" ,
22- /** @type {(data: Buffer | string) => void } */ data => {
23- dbg ( `${ name } data: ${ data . length } bytes` )
24- } ,
25- )
26- stream . on ( "finish" , ( ) => {
27- dbg ( `${ name } finish` )
28- } )
29- }
30-
31- /**
32- * @param {string } [prefix]
33- * @param {string } [suffix]
34- */
35- function getTmpPath ( prefix = "" , suffix = "" ) {
36- const dir = tmpdir ( )
37- const id = Math . random ( ) . toString ( 32 ) . substr ( 2 , 10 )
38- return join ( dir , `${ prefix } ${ id } ${ suffix } ` )
39- }
40-
41- /**
42- * FFmpeg options object.
43- *
44- * These are the same options that you normally pass to the ffmpeg command in the terminal.
45- * Documentation for individual options can be found in the [ffmpeg docs](https://ffmpeg.org/ffmpeg.html#Main-options).
46- *
47- * To specify a boolean option, set it to `true`.
48- * To specify an option multiple times, use an array.
49- * Options with nullish or `false` values are ignored.
50- *
51- * @example
52- *
53- * ```js
54- * const options = { f: "image2", vcodec: "png" }
55- * ```
56- *
57- * @typedef {Record<string, string | number | boolean | Array<string | null | undefined> | null | undefined> } ConverterOptions
58- */
59-
60- /**
61- * @ignore
62- * @internal
63- * @typedef {Object } ConverterPipe
64- * @property {"input" | "output" } type
65- * @property {ConverterOptions } options
66- * @property {string } file
67- * @property {() => Promise<void> } [onBegin]
68- * @property {(process: import("node:child_process").ChildProcess) => void } [onSpawn]
69- * @property {() => Promise<void> } [onFinish]
70- */
71-
72- /**
73- * @param {ConverterOptions } options
74- * @returns {string[] }
75- */
76- function getArgs ( options ) {
77- /** @type {string[] } */
78- const args = [ ]
79-
80- for ( const [ option , value ] of Object . entries ( options ) ) {
81- if ( Array . isArray ( value ) ) {
82- for ( const element of value ) {
83- if ( element != null ) {
84- args . push ( `-${ option } ` )
85- args . push ( String ( element ) )
86- }
87- }
88- } else if ( value != null && value !== false ) {
89- args . push ( `-${ option } ` )
90- if ( typeof value != "boolean" ) {
91- args . push ( String ( value ) )
92- }
93- }
94- }
95-
96- return args
97- }
98-
9913/**
10014 * A class which wraps a FFmpeg process.
10115 *
@@ -154,7 +68,7 @@ export class Converter {
15468 * This builds a command like the one you would normally use in the terminal.
15569 *
15670 * @param {string } file Path to the input file.
157- * @param {ConverterOptions } [options] FFmpeg options for this input.
71+ * @param {ConverterPipeOptions } [options] FFmpeg options for this input.
15872 *
15973 * @example
16074 *
@@ -183,7 +97,7 @@ export class Converter {
18397 * This builds a command like the one you would normally use in the terminal.
18498 *
18599 * @param {string } file Path to the output file.
186- * @param {ConverterOptions } [options] FFmpeg options for this output.
100+ * @param {ConverterPipeOptions } [options] FFmpeg options for this output.
187101 *
188102 * @example
189103 *
@@ -211,9 +125,10 @@ export class Converter {
211125 *
212126 * Internally, it adds a special `pipe:<number>` input argument to the FFmpeg command.
213127 *
214- * Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options), which specifies the format of the input data.
128+ * Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options),
129+ * which specifies the format of the input data.
215130 *
216- * @param {ConverterOptions } options FFmpeg options for this input.
131+ * @param {ConverterPipeOptions } options FFmpeg options for this input.
217132 * @returns {import("node:stream").Writable } A stream which will be written to the FFmpeg process' stdio.
218133 *
219134 * @example
@@ -255,9 +170,10 @@ export class Converter {
255170 *
256171 * Internally, it adds a special `pipe:<number>` output argument to the FFmpeg command.
257172 *
258- * Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options), which specifies the format of the output data.
173+ * Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options),
174+ * which specifies the format of the output data.
259175 *
260- * @param {ConverterOptions } options FFmpeg options for this output.
176+ * @param {ConverterPipeOptions } options FFmpeg options for this output.
261177 * @returns {import("node:stream").Readable } A stream which will be read from the FFmpeg process' stdio.
262178 *
263179 * @example
@@ -299,7 +215,7 @@ export class Converter {
299215 *
300216 * Use this method if the format you want to read doesn't support non-seekable input.
301217 *
302- * @param {ConverterOptions } options FFmpeg options for this input.
218+ * @param {ConverterPipeOptions } options FFmpeg options for this input.
303219 * @returns {import("node:stream").Writable } A stream which will be written to the temporary file.
304220 */
305221 createBufferedInputStream ( options ) {
@@ -338,7 +254,7 @@ export class Converter {
338254 *
339255 * Use this method if the format you want to write doesn't support non-seekable output.
340256 *
341- * @param {ConverterOptions } options FFmpeg options for this output.
257+ * @param {ConverterPipeOptions } options FFmpeg options for this output.
342258 * @returns {import("node:stream").Readable } A stream which will be read from the temporary file.
343259 */
344260 createBufferedOutputStream ( options ) {
@@ -384,11 +300,11 @@ export class Converter {
384300 pipes . push ( pipe )
385301 }
386302
387- const command = this . getSpawnArgs ( )
303+ const args = this . getSpawnArgs ( )
388304 const stdio = this . getStdioArg ( )
389- dbg ( `spawn: ${ this . ffmpegPath } ${ command . join ( " " ) } ` )
305+ dbg ( `spawn: ${ this . ffmpegPath } ${ args . join ( " " ) } ` )
390306 dbg ( `spawn stdio: ${ stdio . join ( " " ) } ` )
391- this . process = spawn ( this . ffmpegPath , command , { stdio } )
307+ this . process = spawn ( this . ffmpegPath , args , { stdio } )
392308 const finished = this . handleProcess ( )
393309
394310 for ( const pipe of this . pipes ) {
@@ -427,6 +343,7 @@ export class Converter {
427343 }
428344
429345 /**
346+ * Returns stdio pipes which can be passed to {@link spawn}.
430347 * @private
431348 * @returns {Array<"ignore" | "pipe"> }
432349 */
@@ -440,25 +357,26 @@ export class Converter {
440357 }
441358
442359 /**
360+ * Returns arguments which can be passed to {@link spawn}.
443361 * @private
444362 * @returns {string[] }
445363 */
446364 getSpawnArgs ( ) {
447365 /** @type {string[] } */
448- const command = [ ]
366+ const args = [ ]
449367
450368 for ( const pipe of this . pipes ) {
451369 if ( pipe . type !== "input" ) continue
452- command . push ( ...getArgs ( pipe . options ) )
453- command . push ( "-i" , pipe . file )
370+ args . push ( ...stringifyArgs ( pipe . options ) )
371+ args . push ( "-i" , pipe . file )
454372 }
455373 for ( const pipe of this . pipes ) {
456374 if ( pipe . type !== "output" ) continue
457- command . push ( ...getArgs ( pipe . options ) )
458- command . push ( pipe . file )
375+ args . push ( ...stringifyArgs ( pipe . options ) )
376+ args . push ( pipe . file )
459377 }
460378
461- return command
379+ return args
462380 }
463381
464382 /**
@@ -470,7 +388,7 @@ export class Converter {
470388 /** @type {string[] } */
471389 const logLines = [ ]
472390
473- if ( this . process == null ) return reject ( Error ( `Converter not started` ) )
391+ assert ( this . process != null , "process should be initialized" )
474392
475393 if ( this . process . stderr != null ) {
476394 this . process . stderr . setEncoding ( "utf8" )
@@ -509,3 +427,96 @@ export class Converter {
509427 } )
510428 }
511429}
430+
431+ /**
432+ * Stringifies FFmpeg options object into command line arguments array.
433+ *
434+ * @param {ConverterPipeOptions } options
435+ * @returns {string[] }
436+ */
437+ function stringifyArgs ( options ) {
438+ /** @type {string[] } */
439+ const args = [ ]
440+
441+ for ( const [ option , value ] of Object . entries ( options ) ) {
442+ if ( Array . isArray ( value ) ) {
443+ for ( const element of value ) {
444+ if ( element != null ) {
445+ args . push ( `-${ option } ` )
446+ args . push ( String ( element ) )
447+ }
448+ }
449+ } else if ( value != null && value !== false ) {
450+ args . push ( `-${ option } ` )
451+ if ( typeof value != "boolean" ) {
452+ args . push ( String ( value ) )
453+ }
454+ }
455+ }
456+
457+ return args
458+ }
459+
460+ /**
461+ * Returns a random file path in the system's temporary directory.
462+ *
463+ * @param {string } [prefix]
464+ * @param {string } [suffix]
465+ */
466+ function getTmpPath ( prefix = "" , suffix = "" ) {
467+ const dir = tmpdir ( )
468+ const id = Math . random ( ) . toString ( 32 ) . substr ( 2 , 10 )
469+ return join ( dir , `${ prefix } ${ id } ${ suffix } ` )
470+ }
471+
472+ /**
473+ * @param {import("node:stream").Readable | import("node:stream").Writable } stream
474+ * @param {string } name
475+ */
476+ function debugStream ( stream , name ) {
477+ stream . on ( "error" , err => {
478+ dbg ( `${ name } error: ${ err . message } ` )
479+ } )
480+ stream . on (
481+ "data" ,
482+ /** @type {(data: Buffer | string) => void } */ data => {
483+ dbg ( `${ name } data: ${ data . length } bytes` )
484+ } ,
485+ )
486+ stream . on ( "finish" , ( ) => {
487+ dbg ( `${ name } finish` )
488+ } )
489+ }
490+
491+ /**
492+ * Options object for a single input or output of a {@link Converter}.
493+ *
494+ * These are the same options that you normally pass to the ffmpeg command in the terminal.
495+ * Documentation for individual options can be found in the [ffmpeg docs](https://ffmpeg.org/ffmpeg.html#Main-options).
496+ *
497+ * To specify a boolean option, set it to `true`.
498+ * To specify an option multiple times, use an array.
499+ * Options with nullish or `false` values are ignored.
500+ *
501+ * @example
502+ *
503+ * ```js
504+ * const options = { f: "image2", vcodec: "png" }
505+ * ```
506+ *
507+ * @typedef {Record<string, string | number | boolean | Array<string | null | undefined> | null | undefined> } ConverterPipeOptions
508+ */
509+
510+ /**
511+ * Data about a single input or output of a {@link Converter}.
512+ *
513+ * @ignore
514+ * @internal
515+ * @typedef {Object } ConverterPipe
516+ * @property {"input" | "output" } type
517+ * @property {ConverterPipeOptions } options
518+ * @property {string } file
519+ * @property {() => Promise<void> } [onBegin]
520+ * @property {(process: import("node:child_process").ChildProcess) => void } [onSpawn]
521+ * @property {() => Promise<void> } [onFinish]
522+ */
0 commit comments