Skip to content

Commit

Permalink
chore: organize code
Browse files Browse the repository at this point in the history
  • Loading branch information
phaux committed Aug 3, 2024
1 parent 4bb8e3e commit 741ae2b
Showing 1 changed file with 117 additions and 106 deletions.
223 changes: 117 additions & 106 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from "node:child_process"
import debug from "debug"
import assert from "node:assert"
import { spawn } from "node:child_process"
import { createReadStream, createWriteStream } from "node:fs"
import { unlink } from "node:fs/promises"
import { tmpdir } from "node:os"
Expand All @@ -9,93 +10,6 @@ import { PassThrough } from "node:stream"
const dbg = debug("ffmpeg-stream")
const EXIT_CODES = [0, 255]

/**
* @param {import("node:stream").Readable | import("node:stream").Writable} stream
* @param {string} name
*/
function debugStream(stream, name) {
stream.on("error", err => {
dbg(`${name} error: ${err.message}`)
})
stream.on(
"data",
/** @type {(data: Buffer | string) => void} */ data => {
dbg(`${name} data: ${data.length} bytes`)
},
)
stream.on("finish", () => {
dbg(`${name} finish`)
})
}

/**
* @param {string} [prefix]
* @param {string} [suffix]
*/
function getTmpPath(prefix = "", suffix = "") {
const dir = tmpdir()
const id = Math.random().toString(32).substr(2, 10)
return join(dir, `${prefix}${id}${suffix}`)
}

/**
* FFmpeg options object.
*
* These are the same options that you normally pass to the ffmpeg command in the terminal.
* Documentation for individual options can be found in the [ffmpeg docs](https://ffmpeg.org/ffmpeg.html#Main-options).
*
* To specify a boolean option, set it to `true`.
* To specify an option multiple times, use an array.
* Options with nullish or `false` values are ignored.
*
* @example
*
* ```js
* const options = { f: "image2", vcodec: "png" }
* ```
*
* @typedef {Record<string, string | number | boolean | Array<string | null | undefined> | null | undefined>} ConverterOptions
*/

/**
* @ignore
* @internal
* @typedef {Object} ConverterPipe
* @property {"input" | "output"} type
* @property {ConverterOptions} options
* @property {string} file
* @property {() => Promise<void>} [onBegin]
* @property {(process: import("node:child_process").ChildProcess) => void} [onSpawn]
* @property {() => Promise<void>} [onFinish]
*/

/**
* @param {ConverterOptions} options
* @returns {string[]}
*/
function getArgs(options) {
/** @type {string[]} */
const args = []

for (const [option, value] of Object.entries(options)) {
if (Array.isArray(value)) {
for (const element of value) {
if (element != null) {
args.push(`-${option}`)
args.push(String(element))
}
}
} else if (value != null && value !== false) {
args.push(`-${option}`)
if (typeof value != "boolean") {
args.push(String(value))
}
}
}

return args
}

/**
* A class which wraps a FFmpeg process.
*
Expand Down Expand Up @@ -154,7 +68,7 @@ export class Converter {
* This builds a command like the one you would normally use in the terminal.
*
* @param {string} file Path to the input file.
* @param {ConverterOptions} [options] FFmpeg options for this input.
* @param {ConverterPipeOptions} [options] FFmpeg options for this input.
*
* @example
*
Expand Down Expand Up @@ -183,7 +97,7 @@ export class Converter {
* This builds a command like the one you would normally use in the terminal.
*
* @param {string} file Path to the output file.
* @param {ConverterOptions} [options] FFmpeg options for this output.
* @param {ConverterPipeOptions} [options] FFmpeg options for this output.
*
* @example
*
Expand Down Expand Up @@ -211,9 +125,10 @@ export class Converter {
*
* Internally, it adds a special `pipe:<number>` input argument to the FFmpeg command.
*
* Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options), which specifies the format of the input data.
* Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options),
* which specifies the format of the input data.
*
* @param {ConverterOptions} options FFmpeg options for this input.
* @param {ConverterPipeOptions} options FFmpeg options for this input.
* @returns {import("node:stream").Writable} A stream which will be written to the FFmpeg process' stdio.
*
* @example
Expand Down Expand Up @@ -255,9 +170,10 @@ export class Converter {
*
* Internally, it adds a special `pipe:<number>` output argument to the FFmpeg command.
*
* Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options), which specifies the format of the output data.
* Remember to specify the [`f` option](https://ffmpeg.org/ffmpeg.html#Main-options),
* which specifies the format of the output data.
*
* @param {ConverterOptions} options FFmpeg options for this output.
* @param {ConverterPipeOptions} options FFmpeg options for this output.
* @returns {import("node:stream").Readable} A stream which will be read from the FFmpeg process' stdio.
*
* @example
Expand Down Expand Up @@ -299,7 +215,7 @@ export class Converter {
*
* Use this method if the format you want to read doesn't support non-seekable input.
*
* @param {ConverterOptions} options FFmpeg options for this input.
* @param {ConverterPipeOptions} options FFmpeg options for this input.
* @returns {import("node:stream").Writable} A stream which will be written to the temporary file.
*/
createBufferedInputStream(options) {
Expand Down Expand Up @@ -338,7 +254,7 @@ export class Converter {
*
* Use this method if the format you want to write doesn't support non-seekable output.
*
* @param {ConverterOptions} options FFmpeg options for this output.
* @param {ConverterPipeOptions} options FFmpeg options for this output.
* @returns {import("node:stream").Readable} A stream which will be read from the temporary file.
*/
createBufferedOutputStream(options) {
Expand Down Expand Up @@ -384,11 +300,11 @@ export class Converter {
pipes.push(pipe)
}

const command = this.getSpawnArgs()
const args = this.getSpawnArgs()
const stdio = this.getStdioArg()
dbg(`spawn: ${this.ffmpegPath} ${command.join(" ")}`)
dbg(`spawn: ${this.ffmpegPath} ${args.join(" ")}`)
dbg(`spawn stdio: ${stdio.join(" ")}`)
this.process = spawn(this.ffmpegPath, command, { stdio })
this.process = spawn(this.ffmpegPath, args, { stdio })
const finished = this.handleProcess()

for (const pipe of this.pipes) {
Expand Down Expand Up @@ -427,6 +343,7 @@ export class Converter {
}

/**
* Returns stdio pipes which can be passed to {@link spawn}.
* @private
* @returns {Array<"ignore" | "pipe">}
*/
Expand All @@ -440,25 +357,26 @@ export class Converter {
}

/**
* Returns arguments which can be passed to {@link spawn}.
* @private
* @returns {string[]}
*/
getSpawnArgs() {
/** @type {string[]} */
const command = []
const args = []

for (const pipe of this.pipes) {
if (pipe.type !== "input") continue
command.push(...getArgs(pipe.options))
command.push("-i", pipe.file)
args.push(...stringifyArgs(pipe.options))
args.push("-i", pipe.file)
}
for (const pipe of this.pipes) {
if (pipe.type !== "output") continue
command.push(...getArgs(pipe.options))
command.push(pipe.file)
args.push(...stringifyArgs(pipe.options))
args.push(pipe.file)
}

return command
return args
}

/**
Expand All @@ -470,7 +388,7 @@ export class Converter {
/** @type {string[]} */
const logLines = []

if (this.process == null) return reject(Error(`Converter not started`))
assert(this.process != null, "process should be initialized")

if (this.process.stderr != null) {
this.process.stderr.setEncoding("utf8")
Expand Down Expand Up @@ -509,3 +427,96 @@ export class Converter {
})
}
}

/**
* Stringifies FFmpeg options object into command line arguments array.
*
* @param {ConverterPipeOptions} options
* @returns {string[]}
*/
function stringifyArgs(options) {
/** @type {string[]} */
const args = []

for (const [option, value] of Object.entries(options)) {
if (Array.isArray(value)) {
for (const element of value) {
if (element != null) {
args.push(`-${option}`)
args.push(String(element))
}
}
} else if (value != null && value !== false) {
args.push(`-${option}`)
if (typeof value != "boolean") {
args.push(String(value))
}
}
}

return args
}

/**
* Returns a random file path in the system's temporary directory.
*
* @param {string} [prefix]
* @param {string} [suffix]
*/
function getTmpPath(prefix = "", suffix = "") {
const dir = tmpdir()
const id = Math.random().toString(32).substr(2, 10)
return join(dir, `${prefix}${id}${suffix}`)
}

/**
* @param {import("node:stream").Readable | import("node:stream").Writable} stream
* @param {string} name
*/
function debugStream(stream, name) {
stream.on("error", err => {
dbg(`${name} error: ${err.message}`)
})
stream.on(
"data",
/** @type {(data: Buffer | string) => void} */ data => {
dbg(`${name} data: ${data.length} bytes`)
},
)
stream.on("finish", () => {
dbg(`${name} finish`)
})
}

/**
* Options object for a single input or output of a {@link Converter}.
*
* These are the same options that you normally pass to the ffmpeg command in the terminal.
* Documentation for individual options can be found in the [ffmpeg docs](https://ffmpeg.org/ffmpeg.html#Main-options).
*
* To specify a boolean option, set it to `true`.
* To specify an option multiple times, use an array.
* Options with nullish or `false` values are ignored.
*
* @example
*
* ```js
* const options = { f: "image2", vcodec: "png" }
* ```
*
* @typedef {Record<string, string | number | boolean | Array<string | null | undefined> | null | undefined>} ConverterPipeOptions
*/

/**
* Data about a single input or output of a {@link Converter}.
*
* @ignore
* @internal
* @typedef {Object} ConverterPipe
* @property {"input" | "output"} type
* @property {ConverterPipeOptions} options
* @property {string} file
* @property {() => Promise<void>} [onBegin]
* @property {(process: import("node:child_process").ChildProcess) => void} [onSpawn]
* @property {() => Promise<void>} [onFinish]
*/

0 comments on commit 741ae2b

Please sign in to comment.