/** * fMP4 FFmpeg processes for HomeKit Secure Video (HKSV) events and livestreaming. * * This module defines an abstract base {@link FfmpegFMp4Process} and its concrete fMP4-mode specializations, * {@link FfmpegRecordingProcess} for HKSV event recording (stdin pipe input, transcoded output) and * {@link FfmpegLivestreamProcess} for fMP4 livestreaming (RTSP input, codec copy). The base exists **solely to centralize * composition wiring** - it owns the internal {@link Mp4SegmentAssembler}, delegates `getInitSegment` / `segments` to it, * and propagates assembler teardown reasons up to the process. It contains no pipeline logic and no command-line assembly; * the byte-to-segment pipeline lives in the assembler so this base stays composition-only. * * Both concrete subclasses: * * - Spawn FFmpeg on construction and expose the inherited `signal`, `ready`, `exited`, `stdin`, `stderr`, `stderrLog`, * `abort()`, and `[Symbol.asyncDispose]`. * - Narrow the inherited public `stdout` to `never`, because the assembler owns the stream and a concurrent external reader * would race. * - Build their FFmpeg arg vector via the pure helper `buildFMp4CommandLine`, which takes fully-resolved options plus * mode-specific hook values and returns the vector. Neither subclass calls `super()` until the arg vector is finalized, * so the constructor-before-super contract is respected. * * The command-line hook values that differ per mode: * * | Hook | Recording | Livestream | * |--------------------------|--------------------------------------------|-----------------------------------------| * | `inputArgs` | `-i pipe:0` + probesize + `-ss` | `-i ` + `-rtsp_transport tcp` | * | `separateAudioInputArgs` | `[]` | Separate audio URL when configured | * | `audioInputIndex` | `0` | `0` or `1` (if separate audio) | * | `audioTarget` | `recordingConfig.audioCodec` (transcoding) | `init.audio` when provided | * | `videoEncoderArgs` | `options.recordEncoder(...)` | `-codec:v copy` | * | `postFilterArgs` | `[]` | `-frag_duration ` | * | `metadataLabel` | `"HKSV Event"` | `"Livestream Buffer"` | * * The shared pipeline primitive ({@link Mp4SegmentAssembler}) also means this module avoids template-method coupling between * the base and the concrete subclasses: no abstract hook methods, no mode-specific state on the base. The base is pure lifecycle * composition; the subclasses are pure args builders plus (for recording) a known-error message substitution. * * @module */ import { AudioRecordingCodecType, AudioRecordingSamplerate } from "./hap-enums.ts"; import type { HbpuAbortError, PartialWithId } from "../util.ts"; import type { CameraRecordingConfiguration } from "homebridge"; import type { FfmpegOptions } from "./options.ts"; import { FfmpegProcess } from "./process.ts"; import type { FfmpegProcessInit } from "./process.ts"; import type { Mp4Segment } from "./mp4-assembler.ts"; import type { Writable } from "node:stream"; /** * Base options shared by both fMP4 recording and livestream sessions. * * @property audioFilters - Audio filters for FFmpeg to process. These are passed as an array of filters. Recording-only: the livestream builder ignores this * field, driving its audio-filter decision from the `audio` target instead. * @property audioStream - Audio stream input to use, if the input contains multiple audio streams. Defaults to `0` (the first audio stream). * @property codec - The codec for the input video stream. Valid values are `av1`, `h264`, and `hevc` (`h265` is accepted as an alias for `hevc`). * Defaults to `h264`. * @property enableAudio - Indicates whether to enable audio or not. * @property hardwareDecoding - Enable hardware-accelerated video decoding if available. Defaults to what was specified in `ffmpegOptions` when FFmpeg is at least * 8.x; on an older FFmpeg the default is always `false` regardless of what `ffmpegOptions` specifies. * @property hardwareTranscoding - Enable hardware-accelerated video transcoding if available. Defaults to what was specified in `ffmpegOptions`. * @property transcodeAudio - Transcode audio to AAC. This can be set to false if the audio stream is already in AAC. Defaults to `true`. Recording-only: the * livestream builder ignores this field, driving its transcode decision from the `audio` target instead. * @property videoFilters - Video filters for FFmpeg to process. These are passed as an array of filters. * @property videoStream - Video stream input to use, if the input contains multiple video streams. Defaults to `0` (the first video stream). * * @category FFmpeg */ export interface FMp4BaseOptions { audioFilters: string[]; audioStream: number; codec: string; enableAudio: boolean; hardwareDecoding: boolean; hardwareTranscoding: boolean; transcodeAudio: boolean; videoFilters: string[]; videoStream: number; } /** * Configuration for a separate audio input source in an fMP4 livestream session. This interface describes the audio source when video and audio come from different * endpoints, such as cameras like DoorBird that expose audio through a separate HTTP API. * * When the audio source is a raw stream (not a self-describing container), specify `format`, `sampleRate`, and optionally `channels` so FFmpeg knows how to interpret * the input. For self-describing sources like RTSP or container-based HTTP streams, only `url` is required. * * @property channels - Optional. Number of audio channels. Defaults to `1`. * @property format - Optional. Raw audio format for the input stream. When set, FFmpeg is told to expect this format rather than probing the stream. Valid values * are `alaw` (G.711 A-law), `mulaw` (G.711 mu-law), and `s16le` (16-bit signed little-endian PCM). Omit for self-describing sources. * @property sampleRate - Optional. Audio sample rate in Hz (e.g., `8000`). Used when `format` is set. Defaults to `8000`. * @property url - The URL of the audio input source. * * @see FMp4LivestreamOptions * * @category FFmpeg */ export interface FMp4AudioInputConfig { channels?: number; format?: "alaw" | "mulaw" | "s16le"; sampleRate?: number; url: string; } /** * The resolved audio-encode target for fMP4 production. Its presence on an fMP4 command line is the single signal to transcode the audio stream to this target; its * absence means the already-encoded audio is copied through untouched. Any audio filters are carried inside the target because filtering requires transcoding - a filter * without a transcode is unrepresentable by construction, so the filters-require-transcoding rule holds declaratively rather than through a runtime override. * * @property channels - Optional. Number of output audio channels. Defaults to `1` when omitted. * @property codec - The AAC codec variant to encode to (low-complexity or enhanced low-delay). * @property filters - Optional. Audio filters applied ahead of the encoder. Supplying filters is what makes the transcode carry them; an empty or omitted list * transcodes without filtering. * @property samplerate - The output audio sample rate. * * @category FFmpeg */ export interface FMp4AudioTarget { channels?: number; codec: AudioRecordingCodecType; filters?: string[]; samplerate: AudioRecordingSamplerate; } /** * Options for configuring an fMP4 HKSV recording session. * * @property fps - The video frames per second for the session. Defaults to 30. * @property probesize - Number of bytes to analyze for stream information. Defaults to 5,000,000 bytes (mirrors FFmpeg's own default probesize). * @property timeshift - Timeshift offset for event-based recording (in milliseconds). Defaults to 0. * * @category FFmpeg */ export interface FMp4RecordingOptions extends FMp4BaseOptions { fps: number; probesize: number; timeshift: number; } /** * Options for configuring an fMP4 livestream session. * * @property audioInput - Optional. A separate audio input source. When provided, audio is read from this source instead of the primary `url`. Can be a URL string * for self-describing sources (e.g., RTSP), or an `FMp4AudioInputConfig` object for raw audio streams that require format metadata. * @property url - Source URL for livestream (RTSP) remuxing to fMP4. * * @see FMp4AudioInputConfig * * @category FFmpeg */ export interface FMp4LivestreamOptions extends FMp4BaseOptions { audioInput?: FMp4AudioInputConfig | string; url: string; } /** * Construction-time options for {@link FfmpegRecordingProcess}. * * @property recording - Optional. fMP4 recording options. Every field defaults when omitted; the interface surface matches {@link FMp4RecordingOptions} but with * all fields optional. * @property recordingConfig - The HomeKit recording configuration (resolution, codec profile, audio codec, sample rate, channels) produced by the HKSV delegate. * @property verbose - Optional. When `true`, FFmpeg is invoked with verbose logging (`-loglevel level+verbose`) regardless of the global * `codecSupport.verbose` flag. Defaults to `false`. * * @remarks Supplying `args` (inherited from {@link FfmpegProcessInit}) is an advanced escape hatch that replaces the auto-built command line entirely. When `args` is * present, the mode-specific config fields (`recording`, `verbose`) do not participate in command-line assembly - they become no-ops. Typical callers omit `args` and * let the class build the command line from the recording configuration. * * @see FfmpegProcessInit * @see FMp4RecordingOptions * * @category FFmpeg */ export interface FfmpegRecordingInit extends FfmpegProcessInit { recording?: Partial; recordingConfig: CameraRecordingConfiguration; verbose?: boolean; } /** * Construction-time options for {@link FfmpegLivestreamProcess}. * * @property audio - Optional. The resolved audio-encode target. When provided, the audio stream is transcoded to it (with any filters it carries); when * omitted, the already-encoded audio is copied through untouched. This is the livestream path's sole audio-filter source. * @property livestream - Livestream source configuration. `url` is required; other {@link FMp4BaseOptions} fields are optional and default when omitted. * @property segmentLength - Optional. fMP4 fragment duration in milliseconds, applied to `-frag_duration` at construction time. Defaults to 1000 ms (1 second). * @property verbose - Optional. When `true`, FFmpeg is invoked with verbose logging (`-loglevel level+verbose`) regardless of the global * `codecSupport.verbose` flag. Defaults to `false`. * * @remarks Supplying `args` (inherited from {@link FfmpegProcessInit}) is an advanced escape hatch that replaces the auto-built command line entirely. When `args` is * present, the mode-specific config fields (`audio`, `livestream`, `segmentLength`, `verbose`) do not participate in command-line assembly - they become no-ops. Typical * callers omit `args` and let the class build the command line from `livestream` + `audio`. * * @see FfmpegProcessInit * @see FMp4AudioTarget * @see FMp4LivestreamOptions * * @category FFmpeg */ export interface FfmpegLivestreamInit extends FfmpegProcessInit { audio?: FMp4AudioTarget; livestream: PartialWithId; segmentLength?: number; verbose?: boolean; } /** * Abstract base for FFmpeg processes that produce fragmented MP4 segments on their stdout. Owns the composition wiring between the process's stdout and an internal * {@link Mp4SegmentAssembler}, plus the bridge that propagates assembler teardown to the process when the assembler aborts for reasons the process's own exit handler * cannot discover on its own (watchdog timeout, source stream error). * * This base deliberately contains **no pipeline logic** and **no command-line assembly**. The byte-to-segment pipeline lives in {@link Mp4SegmentAssembler}, and each * concrete subclass builds its own FFmpeg arg vector. The base exists solely to consolidate the composition shape - internal assembler field, the delegating public * methods, and the bridge registration - that would otherwise duplicate across every fMP4 subclass. The constructor takes only `segmentTimeout` as a mode-specific knob * and holds no mode-specific state, so the base avoids template-method coupling with its subclasses. * * Subclasses must call `super(options, init, segmentTimeout?)` from their constructor, having already folded their subclass-specific init into a base-compatible * {@link FfmpegProcessInit} (typically by spreading their own init and setting `args` to their built command line). * * @see Mp4SegmentAssembler * @see FfmpegProcess * * @category FFmpeg */ export declare abstract class FfmpegFMp4Process extends FfmpegProcess { #private; /** * stdout is consumed internally by the assembler. The public type is narrowed to `never` so TypeScript callers cannot accidentally attach a concurrent reader. */ readonly stdout: never; /** * Construct and spawn a new fMP4 segment-producing process. * * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name). * @param init - Base-class init (FfmpegProcessInit) plus the finalized `args` built by the subclass. * @param segmentTimeout - Optional inter-segment watchdog in milliseconds. When set, the assembler aborts with * `HbpuAbortError("timeout")` if no media segment arrives within the window, and the bridge * propagates the timeout up to the process. Omit for subclasses that tolerate quiet periods * (e.g., livestreams). */ protected constructor(options: FfmpegOptions, init: FfmpegProcessInit, segmentTimeout?: number); /** * Resolve with the fMP4 initialization segment (typically `ftyp` + `moov`) once it appears on stdout. Rejects with `this.signal.reason` if the process aborts before * the initialization segment completes. * * @returns A promise resolving to the initialization segment bytes. */ getInitSegment(): Promise; /** * Async generator yielding each completed media segment (concatenated `moof` + `mdat` pair) as a single Buffer. Terminates cleanly when the process or the caller's * signal aborts, or when the underlying stdout ends. * * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process. * * @returns An async generator yielding media segment buffers in stream order. */ segments(init?: { signal?: AbortSignal; }): AsyncGenerator; /** * The number of assembled media segments buffered in the internal assembler but not yet pulled through {@link FfmpegFMp4Process.segments} - the consumer's catch-up * reserve when the FFmpeg source stalls. Delegates to {@link Mp4SegmentAssembler.bufferedSegments}. * * @returns The buffered-segment depth. */ get bufferedSegments(): number; /** * Async generator yielding the whole segment stream as a kind-tagged sequence: one {@link Mp4Segment} of kind `"init"` carrying the initialization bytes, then one of * kind `"media"` per completed media fragment. Delegates to {@link Mp4SegmentAssembler.stream}; it is a third view over the same pipeline as * {@link FfmpegFMp4Process.getInitSegment} / {@link FfmpegFMp4Process.segments} and shares their single-consumer contract - use one view or the other, never both. * * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process. * * @returns An async generator yielding one `"init"` segment followed by `"media"` segments in stream order. */ stream(init?: { signal?: AbortSignal; }): AsyncGenerator; } /** * The minimal surface a recording consumer reads off a recording process. This is the product half of the recording dependency-inversion seam: an HKSV recording * delegate depends on this narrow interface rather than the concrete {@link FfmpegRecordingProcess}, so a test (or any alternative segment source) can substitute a * fake without dragging FFmpeg into the consumer's dependency graph. The interface is type-only, so importing it costs a consumer nothing at runtime. * * Every member here is defined on {@link FfmpegFMp4Process} (`getInitSegment`, `segments`, `stream`, `bufferedSegments`) or inherited from {@link FfmpegProcess} * (`abort`, `isTimedOut`, `signal`, `stderrLog`, `stdin`), so the real {@link FfmpegRecordingProcess} satisfies it by inheritance and carries only an `implements` * annotation - zero runtime behavior change. This is deliberately the consumer's minimal surface, not the class's full surface: `ready`, `exited`, `stdout`, * `aborted`, `hasError`, and `[Symbol.asyncDispose]` are NOT here because the recording consumer does not read them. * * @see FfmpegRecordingProcess * @see RecordingProcessFactory * * @category FFmpeg */ export interface RecordingProcess { /** * Abort the recording process and tear it down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged. * * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}. */ abort(reason?: unknown): void; /** * The number of assembled media segments buffered but not yet pulled through {@link RecordingProcess.segments} - the consumer's catch-up reserve when the source * stalls. */ readonly bufferedSegments: number; /** * Resolve with the fMP4 initialization segment (typically `ftyp` + `moov`). Rejects with `signal.reason` if the process aborts before the initialization segment * completes. * * @returns A promise resolving to the initialization segment bytes. */ getInitSegment(): Promise; /** * `true` when the abort reason indicates a timeout (the inter-segment watchdog fired or the platform `TimeoutError` was raised). */ readonly isTimedOut: boolean; /** * Async generator yielding each completed media segment (a concatenated `moof` + `mdat` pair) as a single Buffer, in stream order. Terminates cleanly when the process * or the caller's signal aborts, or when the underlying source ends. * * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process. * * @returns An async generator yielding media segment buffers in stream order. */ segments(init?: { signal?: AbortSignal; }): AsyncGenerator; /** * The composed abort signal representing the recording process's lifetime. Aborts exactly once; the reason on `signal.reason` names the cause. */ readonly signal: AbortSignal; /** * The accumulated stderr lines the process produced, preserved across teardown for post-mortem inspection. A readonly view: callers read, they do not mutate. */ readonly stderrLog: readonly string[]; /** * Writable standard input stream the recording bytes are fed into. */ readonly stdin: Writable; /** * Async generator yielding the whole segment stream as a kind-tagged sequence: one {@link Mp4Segment} of kind `"init"` carrying the initialization bytes, then one of * kind `"media"` per completed media fragment. A third view over the same pipeline as {@link RecordingProcess.getInitSegment} / {@link RecordingProcess.segments} that * shares their single-consumer contract - a consumer uses this view or that pair, never both. * * @param init - Optional init options. `signal` composes with the process's own signal; aborting it terminates only this generator call, not the process. * * @returns An async generator yielding one `"init"` segment followed by `"media"` segments in stream order. */ stream(init?: { signal?: AbortSignal; }): AsyncGenerator; } /** * The creational half of the recording dependency-inversion seam: build a {@link RecordingProcess} from the shared options and the recording init. A consumer holds * this factory typed as the abstraction and constructs through it, so a test can substitute a factory that returns a fake recording process. The production factory is * {@link recordingProcessFactory}, whose `create` is exactly the {@link FfmpegRecordingProcess} constructor call - so routing construction through this seam is * behavior-neutral, mirroring HBUP's `streamingDelegateFactory` precedent. * * @see recordingProcessFactory * @see RecordingProcess * * @category FFmpeg */ export interface RecordingProcessFactory { /** * Construct a recording process for the supplied options and recording init. * * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name). * @param init - Recording init options. See {@link FfmpegRecordingInit}. * * @returns A new {@link RecordingProcess}. */ create(options: FfmpegOptions, init: FfmpegRecordingInit): RecordingProcess; } /** * FFmpeg process specialization for HomeKit Secure Video (HKSV) event recording. Builds its command line from the provided HKSV recording configuration and delegates * segment production to {@link FfmpegFMp4Process}. Overrides `FfmpegProcess.logFailedTeardown` to substitute a friendly user-facing message when the stderr log * matches one of the tolerated HKSV error patterns, suppressing the canonical ERROR dump for those known benign cases. Also overrides * `FfmpegProcess.logTimeoutTeardown` to demote the benign inter-segment watchdog reap to debug - a recording ends exactly this way when its segment source quiets, * so the base's WARN would be alarming. * * @example * * ```ts * await using proc = new FfmpegRecordingProcess(ffmpegOptions, { * * recording: { fps: 30, probesize: 5_000_000, timeshift: 0 }, * recordingConfig, * signal: delegate.abortController.signal * }); * * const init = await proc.getInitSegment(); * * for await (const segment of proc.segments()) { * * // Forward each media segment to HomeKit. * } * ``` * * @see FfmpegFMp4Process * @see FfmpegProcess * * @category FFmpeg */ export declare class FfmpegRecordingProcess extends FfmpegFMp4Process implements RecordingProcess { /** * Construct and spawn a new HKSV recording process. * * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name). * @param init - Init options. See {@link FfmpegRecordingInit}. */ constructor(options: FfmpegOptions, init: FfmpegRecordingInit); protected logFailedTeardown(reason: HbpuAbortError): void; protected logTimeoutTeardown(_reason: HbpuAbortError): void; } /** * The production {@link RecordingProcessFactory}: builds the concrete FFmpeg-backed recording process. A consumer holds this typed as the abstraction; a test substitutes * a fake factory. `create` is exactly the {@link FfmpegRecordingProcess} constructor call, so wiring construction through this seam is behavior-neutral. * * @see RecordingProcessFactory * * @category FFmpeg */ export declare const recordingProcessFactory: RecordingProcessFactory; /** * FFmpeg process specialization for fMP4 livestreaming from an RTSP source. Builds its command line from the provided livestream source and delegates segment production * to {@link FfmpegFMp4Process}. * * Used by HBUP as an alternative HKSV segment source when pulling directly from an RTSP URL (bypasses the Protect livestream API for debug and diagnostic scenarios). * Matches the polymorphic `{ getInitSegment(): Promise; segments(): AsyncGenerator }` interface HBUP uses across the native Protect livestream and this * class. Unlike recording, livestream does not enforce an inter-segment watchdog timeout: a live camera feed legitimately quiets down during low-motion periods and does * not carry HKSV's 5-second hard timing contract. Callers that need a liveness cap can compose their own timeout via the process's `signal`. * * @example * * ```ts * await using proc = new FfmpegLivestreamProcess(ffmpegOptions, { * * audio: { codec: AudioRecordingCodecType.AAC_LC, samplerate: AudioRecordingSamplerate.KHZ_16 }, * livestream: { url: "rtsp://camera/stream" }, * segmentLength: 1000, * signal: session.controller.signal * }); * * const init = await proc.getInitSegment(); * * for await (const segment of proc.segments()) { * * // Forward each media segment to the downstream consumer. * } * ``` * * @see FfmpegFMp4Process * @see FfmpegProcess * * @category FFmpeg */ export declare class FfmpegLivestreamProcess extends FfmpegFMp4Process { /** * Construct and spawn a new fMP4 livestream process. * * @param options - Shared {@link FfmpegOptions} configuration (codec support, logger, debug flag, name). * @param init - Init options. See {@link FfmpegLivestreamInit}. */ constructor(options: FfmpegOptions, init: FfmpegLivestreamInit); } //# sourceMappingURL=record.d.ts.map