UNPKG

homebridge-plugin-utils

Version:

Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.

645 lines 32.9 kB
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved. * * ffmpeg/record.ts: Provide FFmpeg process control to support livestreaming and HomeKit Secure Video. */ import { HKSV_IDR_INTERVAL, HKSV_TIMEOUT } from "./settings.js"; import { runWithTimeout } from "../util.js"; import { BOX_HEADER_SIZE } from "./fmp4.js"; import { FfmpegProcess } from "./process.js"; import { once } from "node:events"; // Utility to map HKSV audio recording codec types to their AAC Object Type identifiers. We also use satisfies here to ensure we account for any future changes that // would require updating this mapping. const translateAudioRecordingCodecType = { [1 /* AudioRecordingCodecType.AAC_ELD */]: "38", [0 /* AudioRecordingCodecType.AAC_LC */]: "1" }; // Utility to map audio sample rates to strings. We also use satisfies here to ensure we account for any future changes that would require updating this mapping. const translateAudioSampleRate = { [0 /* AudioRecordingSamplerate.KHZ_8 */]: "8", [1 /* AudioRecordingSamplerate.KHZ_16 */]: "16", [2 /* AudioRecordingSamplerate.KHZ_24 */]: "24", [3 /* AudioRecordingSamplerate.KHZ_32 */]: "32", [4 /* AudioRecordingSamplerate.KHZ_44_1 */]: "44.1", [5 /* AudioRecordingSamplerate.KHZ_48 */]: "48" }; // ISO BMFF box type constants encoded as 32-bit integers for comparison without string allocation in the box-parsing hot path. const BOX_TYPE_MDAT = 0x6D646174; const BOX_TYPE_MOOF = 0x6D6F6F66; const BOX_TYPE_MOOV = 0x6D6F6F76; // Reusable empty buffer sentinel for the box-parsing loop. Avoids repeated zero-byte allocations on every box reset. const EMPTY_BUFFER = Buffer.alloc(0); // Known HKSV-related errors due to occasional inconsistencies produced by the input stream and FFmpeg's own occasional quirkiness. Compiled once at module scope // rather than on every error event. const FFMPEG_KNOWN_HKSV_ERROR = new RegExp([ "(Cannot determine format of input stream 0:0 after EOF)", "(Could not write header \\(incorrect codec parameters \\?\\): Broken pipe)", "(Could not write header for output file #0)", "(Error closing file: Broken pipe)", "(Error splitting the input into NAL units\\.)", "(Invalid data found when processing input)", "(moov atom not found)" ].join("|")); /** * Abstract base class for fMP4 FFmpeg processes. Owns the shared command line skeleton (preamble, video mapping, movflags, audio encoding, output format) and the fMP4 * box-parsing loop. Subclasses provide mode-specific pieces (input args, encoder selection, box handling) via protected hook methods, following the template method * pattern. * * @see FfmpegRecordingProcess * @see FfmpegLivestreamProcess * @see FfmpegProcess * @see {@link https://ffmpeg.org/ffmpeg.html | FFmpeg Documentation} */ class FfmpegFMp4Process extends FfmpegProcess { isLoggingErrors; // The HomeKit recording configuration and resolved base options are stored as protected fields so subclass hook methods can reference them without needing their own // copies of the shared state. fMp4Options; recordingConfig; /** * Constructs a new fMP4 FFmpeg process. Stores shared state and applies defaults to the base options. The command line is not assembled here...subclasses call * `buildCommandLine()` after their own initialization to trigger the template method assembly. * * @param ffmpegOptions - FFmpeg configuration options. * @param recordingConfig - HomeKit recording configuration for the session. * @param fMp4Options - Partial base options with defaults applied for any unset fields. * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`. */ constructor(ffmpegOptions, recordingConfig, fMp4Options = {}, isVerbose = false) { // Initialize our parent. super(ffmpegOptions); // We want to log errors when they occur. this.isLoggingErrors = true; // Store the recording configuration for use by subclass hook methods. this.recordingConfig = recordingConfig; // Apply defaults to the base options and store them. Subclasses store their own mode-specific options separately. this.fMp4Options = { audioFilters: fMp4Options.audioFilters ?? [], audioStream: fMp4Options.audioStream ?? 0, codec: fMp4Options.codec ?? "h264", enableAudio: fMp4Options.enableAudio ?? true, hardwareDecoding: fMp4Options.hardwareDecoding ?? (this.options.codecSupport.ffmpegVersion.startsWith("8.") ? this.options.config.hardwareDecoding : false), hardwareTranscoding: fMp4Options.hardwareTranscoding ?? this.options.config.hardwareTranscoding, transcodeAudio: fMp4Options.transcodeAudio ?? true, videoFilters: fMp4Options.videoFilters ?? [], videoStream: fMp4Options.videoStream ?? 0 }; // Store the verbose flag for use during command line assembly. We don't build the command line here...subclasses call buildCommandLine() after initializing their // own state, which avoids the virtual-call-from-constructor problem. this._isVerbose = isVerbose; } // Per-instance verbose flag, distinct from the inherited isVerbose which reflects the global codecSupport.verbose setting. We keep both so either a global debug // setting or a per-session opt-in can enable verbose FFmpeg logging...the check in buildCommandLine() ORs them together. _isVerbose; // Assembles the FFmpeg command line by calling hook methods in the standard order. The shared skeleton lives here; mode-specific pieces come from subclass overrides. // Subclasses call this as the last step of their constructor, after their own state is fully initialized. buildCommandLine() { // Configure our video parameters for our input: // // -hide_banner Suppress printing the startup banner in FFmpeg. // -nostats Suppress printing progress reports while encoding in FFmpeg. // -fflags flags Set the format flags to discard any corrupt packets rather than exit. // -err_detect ignore_err Ignore decoding errors and continue rather than exit. // -max_delay 500000 Set an upper limit on how much time FFmpeg can take in demuxing packets, in microseconds. this.commandLineArgs = [ "-hide_banner", "-nostats", "-fflags", "+discardcorrupt", "-err_detect", "ignore_err", ...this.options.videoDecoder(this.fMp4Options.codec), "-max_delay", "500000", // Mode-specific input arguments (RTSP input for livestream, stdin pipe for recording). ...this.inputArgs(), // Mode-specific separate audio input arguments (livestream with a separate audio endpoint, empty for recording). ...this.separateAudioInputArgs() ]; // Configure our recording options for the video stream: // // -map 0:v:X Selects the video track from the input. this.commandLineArgs.push("-map", "0:v:" + this.fMp4Options.videoStream.toString(), // Mode-specific video encoder arguments (copy for livestream, recordEncoder for recording). ...this.videoEncoderArgs()); // Configure our video filters, if we have them. if (this.fMp4Options.videoFilters.length) { this.commandLineArgs.push("-filter:v", this.fMp4Options.videoFilters.join(", ")); } // Mode-specific post-filter arguments (frag_duration for livestream, empty for recording). this.commandLineArgs.push(...this.postFilterArgs()); // -movflags flags In the generated fMP4 stream: set the default-base-is-moof flag in the header, write an initial empty MOOV box, start a new fragment // at each keyframe, skip creating a segment index (SIDX) box in fragments, and skip writing the final MOOV trailer since it's unneeded. // -flush_packets 1 Ensure we flush our write buffer after each muxed packet. // -reset_timestamps Reset timestamps at the beginning of each segment. // -metadata Set the metadata to the name of the camera to distinguish between FFmpeg sessions. this.commandLineArgs.push("-movflags", "default_base_moof+empty_moov+frag_keyframe+skip_sidx+skip_trailer", "-flush_packets", "1", "-reset_timestamps", "1", "-metadata", "comment=" + this.options.name() + " " + this.metadataLabel()); // Assemble the audio encoding block. This is shared between both modes...the only mode-specific piece is which FFmpeg input index carries the audio stream. let transcodeAudio = this.fMp4Options.transcodeAudio; if (this.fMp4Options.enableAudio) { // Configure the audio portion of the command line. Options we use are: // // -map N:a:X? Selects the audio stream from input N, if it exists. The input index is 0 when audio and video share the same input, or 1 when a // separate audio input has been configured. this.commandLineArgs.push("-map", this.audioInputIndex().toString() + ":a:" + this.fMp4Options.audioStream.toString() + "?"); // Configure our audio filters, if we have them. if (this.fMp4Options.audioFilters.length) { this.commandLineArgs.push("-filter:a", this.fMp4Options.audioFilters.join(", ")); // Audio filters require transcoding. If the user has decided to filter, we enforce this requirement even if they wanted to copy the audio stream. transcodeAudio = true; } if (transcodeAudio) { // Configure the audio portion of the command line. Options we use are: // // -codec:a Encode using the codecs available to us on given platforms. // -profile:a Specify either low-complexity AAC or enhanced low-delay AAC for HKSV events. // -ar samplerate Sample rate to use for this audio. This is specified by HKSV. // -ac number Set the number of audio channels. this.commandLineArgs.push(...this.options.audioEncoder({ codec: this.recordingConfig.audioCodec.type }), "-profile:a", translateAudioRecordingCodecType[this.recordingConfig.audioCodec.type], "-ar", translateAudioSampleRate[this.recordingConfig.audioCodec.samplerate] + "k", "-ac", (this.recordingConfig.audioCodec.audioChannels ?? 1).toString()); } else { // Configure the audio portion of the command line. Options we use are: // // -codec:a copy Copy the audio stream, since it's already in AAC. this.commandLineArgs.push("-codec:a", "copy"); } } // Configure our video parameters for outputting our final stream: // // -f mp4 Tell ffmpeg that it should create an MP4-encoded output stream. // pipe:1 Output the stream to standard output. this.commandLineArgs.push("-f", "mp4", "pipe:1"); // Additional logging, but only if we're debugging. if (this._isVerbose || this.isVerbose) { this.commandLineArgs.unshift("-loglevel", "level+verbose"); } } /** * Prepares and configures the FFmpeg process for reading and parsing output fMP4 data. The box parsing loop is shared...each complete box is dispatched to the * subclass via handleParsedBox(). */ configureProcess() { let dataListener; // Call our parent to get started. super.configureProcess(); // Initialize our variables that we need to process incoming FFmpeg packets. let header = EMPTY_BUFFER; let bufferRemaining = EMPTY_BUFFER; let dataLength = 0; let type = 0; // Process FFmpeg output and parse out the fMP4 stream it's generating. Here, we take on the task of parsing the fMP4 stream that's being generated and split it up // into the MP4 boxes that HAP-NodeJS is ultimately expecting. this.process?.stdout.on("data", dataListener = (buffer) => { // If we have anything left from the last buffer we processed, prepend it to this buffer. if (bufferRemaining.length > 0) { buffer = Buffer.concat([bufferRemaining, buffer]); bufferRemaining = EMPTY_BUFFER; } let offset = 0; // The MP4 container format is well-documented and designed around the concept of boxes. A box (or atom as they used to be called) is at the center of an MP4 // container. It's composed of an 8-byte header, followed by the data payload it carries. for (;;) { let data; // No existing header, let's start a new box. if (!header.length) { // If there aren't enough bytes for a complete box header, save them for the next chunk. if (buffer.length < BOX_HEADER_SIZE) { bufferRemaining = buffer; break; } // Grab the header. The first four bytes represents the length of the entire box. Second four bytes represent the box type. header = buffer.subarray(0, BOX_HEADER_SIZE); // Now we retrieve the length of the box. dataLength = header.readUInt32BE(0); // Read the box type as a 32-bit integer to avoid per-box string allocation. Box types are 4-byte ASCII codes ("moof", "mdat", etc.) - a legacy of Apple's // original QuickTime "atoms" from 1991, carried forward when MPEG-4 Part 12 standardized the container as ISO BMFF and renamed atoms to "boxes." type = header.readUInt32BE(4); // Finally, we get the data portion of the box. data = buffer.subarray(BOX_HEADER_SIZE, dataLength); // Mark our data offset so we account for the length of the data header and subtract it from the overall length to capture just the data portion. dataLength -= offset = BOX_HEADER_SIZE; } else { // Grab the data from our buffer. data = buffer.subarray(0, dataLength); offset = 0; } // If we don't have enough data in this buffer, save what we have for the next buffer we see and append it there. if (data.length < dataLength) { bufferRemaining = data; break; } // Dispatch the complete box to the subclass for mode-specific handling. this.handleParsedBox(header, data, dataLength, type); // Prepare to start a new box for the next buffer that we will be processing. header = EMPTY_BUFFER; type = 0; // We've parsed an entire box, and there's no more data in this buffer to parse. if (buffer.length === (offset + dataLength)) { dataLength = 0; break; } // If there's anything left in the buffer, move us to the new box and let's keep iterating. buffer = buffer.subarray(offset + dataLength); dataLength = 0; } }); // Make sure we cleanup our listeners when we're done. this.process?.once("exit", () => { this.process?.stdout.off("data", dataListener); }); } /** * Stops the FFmpeg process and performs cleanup. Subclasses override this to emit mode-specific events before calling super, which handles the shared teardown and * emits the "close" event. */ stopProcess() { // Call our parent to get started. super.stopProcess(); // Signal that the process has ended. this._isEnded = true; this.emit("close"); } /** * Stops the FFmpeg process and logs errors if specified. * * @param logErrors - If `true`, logs FFmpeg errors. Defaults to the internal process logging state. * * @example * * ```ts * process.stop(); * ``` */ stop(logErrors = this.isLoggingErrors) { const savedLogErrors = this.isLoggingErrors; // Flag whether we should log abnormal exits (e.g. being killed) or not. this.isLoggingErrors = logErrors; // Call our parent to finish the job. super.stop(); // Restore our previous logging state. this.isLoggingErrors = savedLogErrors; } /** * Logs errors from FFmpeg process execution, handling known benign HKSV stream errors gracefully. * * @param exitCode - The exit code from the FFmpeg process. * @param signal - The signal (if any) used to terminate the process. */ logFfmpegError(exitCode, signal) { // If we're ignoring errors, we're done. if (!this.isLoggingErrors) { return; } // See if we know about this error. if (this.stderrLog.some(x => FFMPEG_KNOWN_HKSV_ERROR.test(x))) { this.log.error("FFmpeg ended unexpectedly due to issues processing the media stream. This error can be safely ignored - it will occur occasionally."); return; } // Otherwise, revert to our default logging in our parent. super.logFfmpegError(exitCode, signal); } } /** * Manages a HomeKit Secure Video recording FFmpeg process. * * @example * * ```ts * const process = new FfmpegRecordingProcess(ffmpegOptions, recordingConfig, 30, true, 5000000, 0); * process.start(); * ``` * * @see FfmpegFMp4Process * * @category FFmpeg */ export class FfmpegRecordingProcess extends FfmpegFMp4Process { /** * Indicates whether the recording has timed out waiting for FFmpeg output. */ isTimedOut; fps; probesize; recordingBuffer; timeshift; /** * Constructs a new FFmpeg recording process for HKSV events. * * @param options - FFmpeg configuration options. * @param recordingConfig - HomeKit recording configuration for the session. * @param fMp4Options - fMP4 recording options. * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`. */ constructor(options, recordingConfig, fMp4Options = {}, isVerbose = false) { super(options, recordingConfig, fMp4Options, isVerbose); // Store recording-specific options. this.fps = fMp4Options.fps ?? 30; this.isTimedOut = false; this.probesize = fMp4Options.probesize ?? 5000000; this.recordingBuffer = []; this.timeshift = fMp4Options.timeshift ?? 0; // Assemble the FFmpeg command line now that all state is initialized. this.buildCommandLine(); } // Recording input: read fMP4 data from standard input with low-delay optimizations and an optional timeshift for HKSV event alignment. // // -flags low_delay Tell FFmpeg to optimize for low delay / realtime decoding. // -probesize number How many bytes should be analyzed for stream information. // -f mp4 Tell FFmpeg that it should expect an MP4-encoded input stream. // -i pipe:0 Use standard input to get video data. // -ss Fast forward to where HKSV is expecting us to be for a recording event. inputArgs() { return [ "-flags", "low_delay", "-probesize", this.probesize.toString(), "-f", "mp4", "-i", "pipe:0", "-ss", this.timeshift.toString() + "ms" ]; } // Recordings always read audio from the primary input...no separate audio source. separateAudioInputArgs() { return []; } // Audio is always on the primary input (index 0) for recordings. audioInputIndex() { return 0; } // Recordings transcode video using the platform-appropriate encoder for HKSV. videoEncoderArgs() { return this.options.recordEncoder({ bitrate: this.recordingConfig.videoCodec.parameters.bitRate, fps: this.recordingConfig.videoCodec.resolution[2], hardwareDecoding: this.fMp4Options.hardwareDecoding, hardwareTranscoding: this.fMp4Options.hardwareTranscoding, height: this.recordingConfig.videoCodec.resolution[1], idrInterval: HKSV_IDR_INTERVAL, inputFps: this.fps, level: this.recordingConfig.videoCodec.parameters.level, profile: this.recordingConfig.videoCodec.parameters.profile, width: this.recordingConfig.videoCodec.resolution[0] }); } // Recordings have no post-filter arguments. postFilterArgs() { return []; } // Metadata label identifying this as an HKSV event recording. metadataLabel() { return "HKSV Event"; } // Each parsed box is queued in the recording buffer for consumption by segmentGenerator(). handleParsedBox(header, data, dataLength, type) { this.recordingBuffer.push({ data, header, length: dataLength, type }); this.emit("mp4box"); } /** * Stops the FFmpeg process and performs cleanup, ensuring the segment generator can exit. */ stopProcess() { // Emit mp4box to unblock segmentGenerator() if it's waiting, then let the base class handle the rest. this._isEnded = true; this.emit("mp4box"); super.stopProcess(); } /** * Asynchronously generates complete segments from FFmpeg output, formatted for HomeKit Secure Video. * * This async generator yields fMP4 segments as Buffers, or ends on process termination or timeout. * * @yields A Buffer containing a complete MP4 segment suitable for HomeKit. * * @example * * ```ts * for await(const segment of process.segmentGenerator()) { * * // Process each segment for HomeKit. * } * ``` */ async *segmentGenerator() { let segment = []; // Loop forever, generating either FTYP/MOOV box pairs or MOOF/MDAT box pairs for HomeKit Secure Video. for (;;) { // FFmpeg has finished its output - we're done. if (this._isEnded) { return; } // If the buffer is empty, wait for our FFmpeg process to produce more boxes. if (!this.recordingBuffer.length) { // Segments are output by FFmpeg according to our specified IDR interval. If we don't see a segment within the timeframe we need for HKSV's timing requirements, // we flag it accordingly and return null back to the generator that's calling us. // eslint-disable-next-line no-await-in-loop await runWithTimeout(once(this, "mp4box"), HKSV_TIMEOUT); } // Grab the next fMP4 box from our buffer. const box = this.recordingBuffer.shift(); // FFmpeg hasn't produced any output. Given the time-sensitive nature of HKSV that constrains us to no more than 5 seconds to provide the next segment, we're done. if (!box) { this.isTimedOut = true; return; } // Queue up this fMP4 box to send back to HomeKit. segment.push(box.header, box.data); // What we want to send are two types of complete segments, made up of multiple MP4 boxes: // // - a complete MOOV box, usually with an accompanying FTYP box, that's sent at the very beginning of any valid fMP4 stream. HomeKit Secure Video looks for this // before anything else. // // - a complete MOOF/MDAT pair. MOOF describes the sample locations and their sizes and MDAT contains the actual audio and video data related to that segment. Think // of MOOF as the audio/video data "header", and MDAT as the "payload". // // Once we see these, we combine all the segments in our queue to send back to HomeKit. if ((box.type === BOX_TYPE_MOOV) || (box.type === BOX_TYPE_MDAT)) { yield Buffer.concat(segment); segment = []; } } } } /** * Manages a HomeKit livestream FFmpeg process for generating fMP4 segments. * * @example * * ```ts * const process = new FfmpegLivestreamProcess(ffmpegOptions, recordingConfig, url, 30, true); * process.start(); * * const initSegment = await process.getInitSegment(); * ``` * * @see FfmpegFMp4Process * * @category FFmpeg */ export class FfmpegLivestreamProcess extends FfmpegFMp4Process { /** * Optional override for the fMP4 fragment duration, in milliseconds. When set, the `-frag_duration` argument is updated before starting the FFmpeg process. */ segmentLength; // Set to true during separateAudioInputArgs() when a separate audio input is configured, so that audioInputIndex() returns the correct FFmpeg input index. _hasAudioInput; _initSegment; _initSegmentParts; hasInitSegment; livestreamOptions; /** * Constructs a new FFmpeg livestream process. * * @param options - FFmpeg configuration options. * @param recordingConfig - HomeKit recording configuration for the session. * @param livestreamOptions - livestream segmenting options. * @param isVerbose - If `true`, enables more verbose logging for debugging purposes. Defaults to `false`. */ constructor(options, recordingConfig, livestreamOptions, isVerbose = false) { super(options, recordingConfig, livestreamOptions, isVerbose); // Store livestream-specific options. this._hasAudioInput = false; this._initSegment = Buffer.alloc(0); this._initSegmentParts = []; this.hasInitSegment = false; this.livestreamOptions = livestreamOptions; // Assemble the FFmpeg command line now that all state is initialized. this.buildCommandLine(); } // Livestream input: connect to an RTSP source with direct I/O and TCP transport. // // -avioflags direct Tell FFmpeg to minimize buffering to reduce latency for more realtime processing. // -rtsp_transport tcp Tell the RTSP stream handler that we're looking for a TCP connection. // -i url RTSPS URL to get our input stream from. inputArgs() { return [ "-avioflags", "direct", "-rtsp_transport", "tcp", "-i", this.livestreamOptions.url ]; } // If a separate audio input has been configured, build the FFmpeg input arguments for it. This enables support for devices like DoorBird where video and audio are // served from different endpoints. separateAudioInputArgs() { if (!this.fMp4Options.enableAudio || !this.livestreamOptions.audioInput) { return []; } const args = []; // Normalize the audio input configuration. A plain string is treated as a URL shorthand. const audioInput = (typeof this.livestreamOptions.audioInput === "string") ? { url: this.livestreamOptions.audioInput } : this.livestreamOptions.audioInput; // When a raw audio format is specified, we need to explicitly tell FFmpeg how to interpret the incoming stream since it cannot probe raw audio sources. // // -f format Specify the raw audio format (e.g., mulaw, alaw, s16le). // -ar sampleRate Specify the audio sample rate in Hz. // -ac channels Specify the number of audio channels. if (audioInput.format) { args.push("-f", audioInput.format, "-ar", (audioInput.sampleRate ?? 8000).toString(), "-ac", (audioInput.channels ?? 1).toString()); } // For RTSP and RTSPS audio sources, we explicitly request TCP transport to match the behavior we use for the primary video input. if (["rtsp://", "rtsps://"].some((protocol) => audioInput.url.toLowerCase().startsWith(protocol))) { args.push("-rtsp_transport", "tcp"); } // -i url Audio input URL. args.push("-i", audioInput.url); // Track that we have a separate audio input so audioInputIndex() returns the correct value. this._hasAudioInput = true; return args; } // When a separate audio input is configured, audio is on the second FFmpeg input (index 1). Otherwise it shares the primary input (index 0). audioInputIndex() { return this._hasAudioInput ? 1 : 0; } // Livestreams remux the video stream directly without transcoding. videoEncoderArgs() { return ["-codec:v", "copy"]; } // Livestreams emit fMP4 fragments at one-second intervals by default. // // -frag_duration number Length of each fMP4 fragment, in microseconds. postFilterArgs() { return ["-frag_duration", "1000000"]; } // Metadata label identifying this as a livestream buffer. metadataLabel() { return "Livestream Buffer"; } // Livestream box handling: accumulate the initialization segment (everything before the first moof box), then emit each subsequent box as a segment event. handleParsedBox(header, data, _dataLength, type) { // If this is part of the initialization segment, store it for future use. if (!this.hasInitSegment) { // The initialization segment is everything before the first moof box. Once we've seen a moof box, we know we've captured it in full. We collect the parts into an // array and concatenate once at the end to avoid creating intermediate buffers on every pre-moof box. if (type === BOX_TYPE_MOOF) { this._initSegment = Buffer.concat(this._initSegmentParts); this._initSegmentParts = []; this.hasInitSegment = true; this.emit("initsegment"); } else { this._initSegmentParts.push(header, data); } } if (this.hasInitSegment) { // We only emit segments once we have the initialization segment. this.emit("segment", Buffer.concat([header, data])); } } /** * Starts the FFmpeg process, adjusting the fragment duration if segmentLength has been set. * * @example * * ```ts * process.start(); * ``` */ start() { if (this.segmentLength !== undefined) { const fragIndex = this.commandLineArgs.indexOf("-frag_duration"); if (fragIndex !== -1) { this.commandLineArgs[fragIndex + 1] = (this.segmentLength * 1000).toString(); } } // Start the FFmpeg session. super.start(); } /** * Gets the fMP4 initialization segment generated by FFmpeg for the livestream. * * @returns A promise resolving to the initialization segment as a Buffer. * * @example * * ```ts * const initSegment = await process.getInitSegment(); * ``` */ async getInitSegment() { // If we have the initialization segment, return it. if (this.hasInitSegment) { return this._initSegment; } // Wait until the initialization segment is available. await once(this, "initsegment"); return this._initSegment; } /** * Returns the initialization segment as a Buffer, or null if not yet available. * * @returns The initialization segment Buffer, or `null` if not yet generated. * * @example * * ```ts * const init = process.initSegment; * if(init) { * * // Use the initialization segment. * } * ``` */ get initSegment() { if (!this.hasInitSegment) { return null; } return this._initSegment; } } //# sourceMappingURL=record.js.map