UNPKG

homebridge-plugin-utils

Version:

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

186 lines 7.9 kB
/* Copyright(C) 2017-2025, HJD (https://github.com/hjdhjd). All rights reserved. * * ffmpeg/stream.ts: Provide FFmpeg process control to support HomeKit livestreaming. */ import { FfmpegProcess } from "./process.js"; import { createSocket } from "node:dgram"; /** * Provides FFmpeg process management and socket handling to support HomeKit livestreaming sessions. * * This class extends `FfmpegProcess` to create, monitor, and terminate HomeKit-compatible video streams. Additionally, it invokes delegate hooks for error processing and * stream lifecycle management. * * @example * * ```ts * const streamingDelegate: HomebridgeStreamingDelegate = { * * controller, * stopStream: (sessionId) => { ... } // End-of-session cleanup code. * }; * * const process = new FfmpegStreamingProcess( * * streamingDelegate, * sessionId, * ffmpegOptions, * commandLineArgs, * { addressVersion: "ipv4", port: 5000 } * ); * ``` * * @see HomebridgeStreamingDelegate * @see FfmpegProcess * * @category FFmpeg */ export class FfmpegStreamingProcess extends FfmpegProcess { /* * The streaming delegate instance responsible for handling stream events and errors. */ delegate; /** * The unique session identifier for this streaming process. */ sessionId; /** * The timeout reference used to monitor UDP stream health. */ streamTimeout; /** * Constructs a new FFmpeg streaming process for a HomeKit session. * * Sets up required delegate hooks, creates UDP return sockets if needed, and starts the FFmpeg process. Automatically handles FFmpeg process errors and cleans up on * failures. * * @param delegate - The Homebridge streaming delegate for this session. * @param sessionId - The HomeKit session identifier for this stream. * @param ffmpegOptions - The FFmpeg configuration options. * @param commandLineArgs - FFmpeg command-line arguments. * @param returnPort - Optional. UDP port info for talkback support (used for two-way audio in HomeKit for cameras that support it). * @param callback - Optional. Callback invoked when the stream is ready or errors occur. * * @example * * ```ts * const process = new FfmpegStreamingProcess(delegate, sessionId, ffmpegOptions, commandLineArgs, { addressVersion: "ipv6", port: 6000 }); * ``` */ constructor(delegate, sessionId, ffmpegOptions, commandLineArgs, returnPort, callback) { // Initialize our parent. super(ffmpegOptions); this.delegate = delegate; this.delegate.adjustProbeSize ??= () => { }; this.delegate.ffmpegErrorCheck ??= () => undefined; this.delegate.stopStream ??= () => { }; this.sessionId = sessionId; // Create the return port for FFmpeg, if requested to do so. The only time we don't do this is when we're standing up // a two-way audio stream - in that case, the audio work is done through RtpSplitter and not here. if (returnPort) { this.createSocket(returnPort); } // Start it up, with appropriate error handling. this.start(commandLineArgs, callback, (errorMessage) => { // Stop the stream. this.delegate.stopStream?.(this.sessionId); // Let homebridge know what happened and stop the stream if we've already started. if (!this.isStarted && this.callback) { this.callback(new Error(errorMessage)); this.callback = null; return; } // Tell Homebridge to forcibly stop the streaming session. this.delegate.controller.forceStopStreamingSession(this.sessionId); this.delegate.stopStream?.(this.sessionId); }); } /** * Creates and binds a UDP socket for monitoring the health of the outgoing video stream. * * Listens for UDP "message" events, sets and clears timeouts, and handles error/cleanup scenarios. If no messages are received within 5 seconds, forcibly stops the * stream and informs the delegate. * * @param portInfo - Object containing the address version ("ipv4" or "ipv6") and port number. */ createSocket(portInfo) { let errorListener; let messageListener; const socket = createSocket(portInfo.addressVersion === "ipv6" ? "udp6" : "udp4"); // Cleanup after ourselves when the socket closes. socket.once("close", () => { if (this.streamTimeout) { clearTimeout(this.streamTimeout); } socket.off("error", errorListener); socket.off("message", messageListener); }); // Handle potential network errors. socket.on("error", errorListener = (error) => { this.log.error("Socket error: %s.", error.name); void this.delegate.stopStream?.(this.sessionId); }); // Manage our video streams in case we haven't received a stop request, but we're in fact dead zombies. socket.on("message", messageListener = () => { // Clear our last canary. if (this.streamTimeout) { clearTimeout(this.streamTimeout); } // Set our new canary. this.streamTimeout = setTimeout(() => { this.log.debug("Video stream appears to be inactive for 5 seconds. Stopping stream."); this.delegate.controller.forceStopStreamingSession(this.sessionId); void this.delegate.stopStream?.(this.sessionId); }, 5000); }); // Bind to the port we're opening. socket.bind(portInfo.port, (portInfo.addressVersion === "ipv6") ? "::1" : "127.0.0.1"); } /** * Returns the underlying FFmpeg child process, or null if the process is not running. * * @returns The current FFmpeg process, or `null` if not running. * * @example * * ```ts * const ffmpeg = process.ffmpegProcess; * * if(ffmpeg) { * * // Interact directly with the child process if necessary. * } * ``` */ get ffmpegProcess() { return this.process; } /** * Handle and logs FFmpeg process errors. * * If a known error condition is detected by the delegate, logs the custom message and returns. For "not enough frames to estimate rate; consider increasing probesize" * errors, invokes the delegate's `adjustProbeSize` hook for automatic tuning. Otherwise, falls back to the parent class's logging. * * @param exitCode - The exit code from FFmpeg. * @param signal - The signal, if any, used to terminate the process. */ logFfmpegError(exitCode, signal) { // We want to process known streaming-related errors due to the performance and latency tweaks we've made to the FFmpeg command line. In some cases we may inform the // user and take no action, in others, we tune our own internal parameters. // Process any specific errors our caller is interested in. const errTest = this.delegate.ffmpegErrorCheck?.(this.stderrLog); if (errTest) { this.log.error(errTest); return; } // Test for probesize errors. const probesizeRegex = new RegExp("not enough frames to estimate rate; consider increasing probesize"); if (this.stderrLog.some(logEntry => probesizeRegex.test(logEntry))) { // Let the streaming delegate know to adjust it's parameters for the next run and inform the user. this.delegate.adjustProbeSize?.(); return; } // Otherwise, revert to our default logging in our parent. super.logFfmpegError(exitCode, signal); } } //# sourceMappingURL=stream.js.map