/** * FFmpeg process management with AbortSignal-based lifecycle. * * This module defines the `FfmpegProcess` base class, the foundation every other FFmpeg class in `homebridge-plugin-utils` builds on. Construction spawns the child; the * composed {@link AbortSignal} exposed as `this.signal` is the single source of truth for the process's lifetime. Every teardown path - external `abort()`, parent * signal aborting, natural exit, spawn failure, optional startup timeout - converges on the same signal. * * The class is an {@link AsyncDisposable} so callers can manage the process with `await using` for scope-bound lifetimes. It is intentionally not an `EventEmitter`: * ready/exit are {@link Promise}s, stderr accumulates to {@link FfmpegProcess.stderrLog}, and fine-grained termination hooks are registered against `this.signal`. * * Key features: * * - Spawn-on-construction. No `start()` step, no configure-then-run. * - {@link AbortSignal}-driven teardown. Node's native `spawn({ signal, killSignal })` owns the kill path; no manual SIGKILL fallback timer. * - `ready` promise resolves when FFmpeg produces its first stderr byte (the earliest reliable "we are actually running" signal). * - `exited` promise resolves with the child's exit code and signal once it terminates. Rejects with `signal.reason` only when the child never started (e.g., `ENOENT`). * - Reason-based teardown logging: `"failed"` dumps stderr at ERROR, `"timeout"` logs at WARN, other reasons log at DEBUG. * * @module */ import { HbpuAbortError } from "../util.ts"; import type { HomebridgePluginLogging, Nullable } from "../util.ts"; import type { Readable, Writable } from "node:stream"; import type { FfmpegOptions } from "./options.ts"; /** * Structured exit information surfaced through {@link FfmpegProcess.exited}. * * @property exitCode - The process exit code, or `null` when the process was terminated by a signal. * @property exitSignal - The signal name (e.g., `"SIGKILL"`) that terminated the process, or `null` when the process exited normally. * * @category FFmpeg */ export interface FfmpegProcessExitInfo { exitCode: Nullable; exitSignal: Nullable; } /** * Construction-time options for {@link FfmpegProcess}. * * @property args - Optional. FFmpeg command-line arguments. Defaults to an empty array. * @property signal - Optional. Parent {@link AbortSignal} to compose with the process's internal controller. When the parent aborts, the process tears down. * @property startupTimeout - Optional. If FFmpeg does not produce stderr output within this many milliseconds, the process is aborted with * `HbpuAbortError("timeout")`. * * @category FFmpeg */ export interface FfmpegProcessInit { args?: string[]; signal?: AbortSignal; startupTimeout?: number; } /** * Base class providing FFmpeg process management with signal-driven lifecycle. * * Construction spawns the child immediately. The composed `this.signal` is the single source of truth for the process's lifetime: external `abort()`, a parent signal * firing, a non-zero exit, a spawn failure, and (optionally) a startup-timeout all converge on the same signal. Subclasses register additional `"abort"` listeners on * `this.signal` rather than overriding `abort()` or `[Symbol.asyncDispose]`. * * @example * * ```ts * // Scope-bound: the child is guaranteed to be torn down when the block exits, regardless of success or exception. * await using proc = new FfmpegProcess(options, { args: [ "-i", "input.mp4", "-f", "null", "-" ] }); * * await proc.ready; * const { exitCode } = await proc.exited; * ``` * * @example * * ```ts * // Signal-driven: parent controls teardown through its own AbortController. * const controller = new AbortController(); * const proc = new FfmpegProcess(options, { args, signal: controller.signal }); * * // Later, from anywhere with the controller: * controller.abort(); * ``` * * @see {@link https://ffmpeg.org/documentation.html | FFmpeg Documentation} * * @see {@link https://nodejs.org/api/child_process.html#child_processspawncommand-args-options | Node.js child_process.spawn} * * @see FfmpegOptions * * @category FFmpeg */ export declare class FfmpegProcess implements AsyncDisposable { #private; /** * The composed abort signal representing this process's lifetime. Aborts exactly once when the child exits, the parent signal fires, or `abort()` is called; the * reason encoded on `signal.reason` names the cause (see {@link HbpuAbortReason}). Subclasses and external callers attach `"abort"` listeners to this signal when they * need scope-bound teardown hooks of their own. */ readonly signal: AbortSignal; /** * Resolves when FFmpeg has produced its first stderr byte - the earliest point at which we can reliably say the child is running. Rejects with `this.signal.reason` * when the process aborts before becoming ready (external abort, spawn failure, startup timeout, early natural exit). */ readonly ready: Promise; /** * Resolves with the child's exit code and signal once the process terminates. Rejects with `this.signal.reason` only when the child never started (e.g., the FFmpeg * binary could not be located); in every other case it resolves with the actual exit information, even when the abort reason is `"failed"`. */ readonly exited: Promise; /** * Writable standard input stream for the FFmpeg process. */ readonly stdin: Writable; /** * Readable standard output stream. Subclasses that consume this stream internally narrow the public type to `never` via `declare`. */ readonly stdout: Readable; /** * Readable standard error stream. Primarily useful to callers who want to observe stderr in addition to the accumulated {@link FfmpegProcess.stderrLog}; most callers * should prefer `stderrLog` since the class already buffers lines for them. */ readonly stderr: Readable; /** * The FFmpeg options instance this process was constructed with. Exposed for subclasses that need access to the shared configuration (codec support, logger, etc.). */ protected readonly options: FfmpegOptions; /** * Logger instance resolved from `options.log`. Shared by all log output in this class and subclasses. */ protected readonly log: HomebridgePluginLogging; /** * The resolved FFmpeg command-line arguments this process was spawned with. Retained so teardown logging can include the original command in error reports. */ protected readonly args: readonly string[]; /** * Protected alias for {@link FfmpegProcess.stdin}. Subclasses that publicly narrow `stdin` to `never` still consume the underlying stream through this typed path. */ protected readonly _stdin: Writable; /** * Protected alias for {@link FfmpegProcess.stdout}. Subclasses that publicly narrow `stdout` to `never` (because an internal consumer such as `Mp4SegmentAssembler` * owns the stream) still consume the underlying stream through this typed path. */ protected readonly _stdout: Readable; /** * Protected alias for {@link FfmpegProcess.stderr}. Provided for symmetry with `_stdin` / `_stdout`. */ protected readonly _stderr: Readable; /** * Construct and spawn a new FFmpeg process. * * Spawning happens synchronously as part of construction: by the time the constructor returns, the child is already running (or has already scheduled its `"error"` * event for a spawn failure). There is no separate `start()` step. * * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name). * @param init - Optional init options. See {@link FfmpegProcessInit}. */ constructor(options: FfmpegOptions, init?: FfmpegProcessInit); /** * Abort the process and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged. * * Safe to call more than once: subsequent calls are no-ops because the underlying signal only aborts once. Calling `abort()` after natural exit is also safe for the * same reason. * * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors (`TimeoutError`, `AbortError`) also interoperate by convention. */ abort(reason?: unknown): void; /** * `AsyncDisposable` implementation. Aborts the process (defaulting to `"shutdown"`) and awaits actual exit before returning, so callers using `await using` are * guaranteed the child has terminated by the time the block exits. * * @returns A promise that resolves once the child has fully exited. */ [Symbol.asyncDispose](): Promise; /** * `true` once `this.signal` has aborted. Derived from the signal; no independent state. */ get aborted(): boolean; /** * `true` when the abort reason was `HbpuAbortError("failed")`. Covers spawn failures and non-zero natural exits. Derived from `this.signal.reason`; no stored flag. */ get hasError(): boolean; /** * `true` when the abort reason indicates a timeout. Matches both the canonical `HbpuAbortError("timeout")` and the platform `TimeoutError` emitted by * `AbortSignal.timeout()`. The branching lives in {@link isTimeoutReason} so this getter stays a one-line delegation and every resource class in the library * shares one definition of "timeout." */ get isTimedOut(): boolean; /** * The accumulated stderr lines this process has produced, preserved across teardown for post-mortem inspection. The array is returned as a readonly view to make the * intent explicit: callers read from it, they do not mutate it. */ get stderrLog(): readonly string[]; /** * Log the "failed" teardown branch. Overridable by subclasses that want to substitute a bespoke message for known benign error shapes before falling through to the * canonical ERROR dump. * * The default implementation emits two ERROR lines (an "ended unexpectedly" summary and the command that produced it), then dumps every accumulated stderr line at * ERROR level. Subclasses may inspect `this.stderrLog` and `reason.cause`, log a friendly message, and return early to suppress the dump; or call * `super.logFailedTeardown(reason)` to emit the canonical dump after prepending their own context. * * @param reason - The `"failed"` reason that drove this teardown. Its `cause` field carries structured exit info (`{ exitCode, exitSignal }`) for natural non-zero * exits, or the underlying `Error` for spawn failures. */ protected logFailedTeardown(reason: HbpuAbortError): void; /** * Log the "timeout" teardown branch. Overridable by subclasses for which an inter-segment / watchdog timeout is benign by default - e.g. an HKSV recording, which * ends exactly this way whenever its segment source quiets. * * The default implementation emits a single WARN: a stall that trips the watchdog on a general FFmpeg process (a live stream) genuinely is a problem. A subclass may * override to demote the reap to debug and leave the severity verdict to the consumer that holds the input-feed and reachability context. * * @param _reason - The `"timeout"` abort reason that drove this teardown. Part of the seam contract so an override can inspect it (parallel to `logFailedTeardown`), * but neither the default body nor the recording override reads it - a watchdog timeout carries no actionable cause - so it is `_`-prefixed. */ protected logTimeoutTeardown(_reason: HbpuAbortError): void; } //# sourceMappingURL=process.d.ts.map