homebridge-plugin-utils
Version:
Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.
206 lines (205 loc) • 8.1 kB
TypeScript
/**
* RTP and RTCP packet demultiplexer and UDP port management for FFmpeg-based HomeKit livestreaming.
*
* This module supplies classes and helpers to support realtime streaming via FFmpeg in Homebridge and similar HomeKit environments. It enables the demultiplexing of RTP
* and RTCP packets on a single UDP port, as required by HomeKit and RFC 5761, working around FFmpeg’s lack of native support for RTP/RTCP multiplexing. It also manages
* the allocation and tracking of UDP ports for RTP and RTCP, helping prevent conflicts in dynamic, multi-session streaming scenarios.
*
* Key features:
*
* - Demultiplexes RTP and RTCP packets received on a single UDP port, forwarding them to the correct FFmpeg destinations for HomeKit livestream compatibility.
* - Injects periodic heartbeat messages to keep two-way audio streams alive with FFmpeg’s strict timeout requirements.
* - Dynamically allocates and reserves UDP ports for RTP/RTCP, supporting consecutive port pairing for correct FFmpeg operation.
* - Event-driven architecture for integration with plugin or automation logic.
*
* Designed for plugin developers and advanced users implementing HomeKit livestreaming, audio/video bridging, or similar applications requiring precise RTP/RTCP handling
* with FFmpeg.
*
* @module
*/
import { EventEmitter } from "node:events";
import type { HomebridgePluginLogging } from "../util.js";
/**
* Utility for demultiplexing RTP and RTCP packets on a single UDP port for HomeKit compatibility.
*
* FFmpeg does not support multiplexing RTP and RTCP data on a single UDP port (RFC 5761) and HomeKit requires this for livestreaming. This class listens on a UDP port
* and demultiplexes RTP and RTCP traffic, forwarding them to separate RTP and RTCP ports as required by FFmpeg.
*
* Credit to [dgreif](https://github.com/dgreif), [brandawg93](https://github.com/brandawg93), and [Sunoo](https://github.com/Sunoo) for foundational ideas and
* collaboration.
*
* @example
*
* ```ts
* // Create an RtpDemuxer to split packets for FFmpeg compatibility.
* const demuxer = new RtpDemuxer("ipv4", 50000, 50002, 50004, log);
*
* // Close the demuxer when finished.
* demuxer.close();
* ```
*
* @see {@link https://tools.ietf.org/html/rfc5761 | RFC 5761}
* @see {@link https://github.com/homebridge/homebridge-camera-ffmpeg | homebridge-camera-ffmpeg}
*
* @category FFmpeg
*/
export declare class RtpDemuxer extends EventEmitter {
private heartbeatTimer;
private heartbeatMsg;
private _isRunning;
private log?;
private inputPort;
readonly socket: import("node:dgram").Socket;
/**
* Constructs a new RtpDemuxer for a specified IP family and port set.
*
* @param ipFamily - The IP family: "ipv4" or "ipv6".
* @param inputPort - The UDP port to listen on for incoming packets.
* @param rtcpPort - The UDP port to forward RTCP packets to.
* @param rtpPort - The UDP port to forward RTP packets to.
* @param log - Logger instance for debug and error messages.
*
* @example
*
* ```ts
* const demuxer = new RtpDemuxer("ipv4", 50000, 50002, 50004, log);
* ```
*/
constructor(ipFamily: ("ipv4" | "ipv6"), inputPort: number, rtcpPort: number, rtpPort: number, log: HomebridgePluginLogging);
/**
* Sends periodic heartbeat messages to the RTP port to keep the FFmpeg process alive.
*
* This is necessary because FFmpeg times out input streams if it does not receive data for more than five seconds.
*
* @param port - The RTP port to send the heartbeat to.
*/
private heartbeat;
/**
* Closes the demuxer, its socket, and any heartbeat timers.
*
* @example
*
* ```ts
* demuxer.close();
* ```
*/
close(): void;
/**
* Extracts the RTP payload type from a UDP packet.
*
* Used internally to distinguish RTP from RTCP messages.
*
* @param message - The UDP packet buffer.
*
* @returns The RTP payload type as a number.
*/
private getPayloadType;
/**
* Determines if the provided UDP packet is an RTP message.
*
* @param message - The UDP packet buffer.
*
* @returns `true` if the packet is RTP, `false` if RTCP.
*/
private isRtpMessage;
/**
* Indicates if the demuxer is running and accepting packets.
*
* @returns `true` if running, otherwise `false`.
*
* @example
*
* ```ts
* if(demuxer.isRunning) {
* // Demuxer is active.
* }
* ```
*/
get isRunning(): boolean;
}
/**
* Allocates and tracks UDP ports for RTP and RTCP to avoid port conflicts in environments with high network activity.
*
* This utility class is used to find and reserve available UDP ports for demuxing FFmpeg streams or other network activities.
*
* @example
*
* ```ts
* const allocator = new RtpPortAllocator();
*
* // Reserve two consecutive ports for RTP and RTCP.
* const rtpPort = await allocator.reserve("ipv4", 2);
*
* // Cancel reservation if not needed.
* allocator.cancel(rtpPort);
* ```
*
* @category FFmpeg
*/
export declare class RtpPortAllocator {
private portsInUse;
/**
* Instantiates a new RTP port allocator and tracker.
*/
constructor();
/**
* Finds an available UDP port by attempting to bind a new socket.
*
* Loops until an available port not already marked as in use is found.
*
* @param ipFamily - "ipv4" or "ipv6".
* @param port - Optional. The port to try to bind to. If 0, selects a random port.
*
* @returns A promise resolving to the available port number, or `-1` on error.
*/
private getPort;
/**
* Internal method to reserve one or two consecutive UDP ports for FFmpeg or network use.
*
* If two ports are reserved, ensures they are consecutive for RTP and RTCP usage. Returns the first port in the sequence, or `-1` if we're unable to allocate.
*
* @param ipFamily - Optional. "ipv4" or "ipv6". Defaults to "ipv4".
* @param portCount - Optional. The number of consecutive ports to reserve (1 or 2). Defaults to 1.
* @param attempts - Internal. The number of allocation attempts. Used for recursion.
*
* @returns A promise resolving to the first reserved port, or `-1` if unavailable.
*/
private _reserve;
/**
* Reserves one or two consecutive UDP ports for FFmpeg or network use.
*
* If two ports are reserved, ensures they are consecutive for RTP and RTCP usage. Returns the first port in the sequence, or `-1` if we're unable to allocate.
*
* @param ipFamily - Optional. "ipv4" or "ipv6". Defaults to "ipv4".
* @param portCount - Optional. The number of consecutive ports to reserve (1 or 2). Defaults to 1.
*
* @returns A promise resolving to the first reserved port, or `-1` if unavailable.
*
* @remarks FFmpeg currently lacks the ability to specify both the RTP and RTCP ports. FFmpeg always assumes, by convention, that when you specify an RTP port, the RTCP
* port is the RTP port + 1. In order to work around that challenge, we need to always ensure that when we reserve multiple ports for RTP (primarily for two-way audio
* use cases) that we we are reserving consecutive ports only.
*
* @example
*
* ```ts
* // Reserve a single port.
* const port = await allocator.reserve();
*
* // Reserve two consecutive ports for RTP/RTCP.
* const rtpPort = await allocator.reserve("ipv4", 2);
* ```
*/
reserve(ipFamily?: ("ipv4" | "ipv6"), portCount?: (1 | 2)): Promise<number>;
/**
* Cancels and releases a previously reserved port, making it available for future use.
*
* @param port - The port number to release.
*
* @example
*
* ```ts
* allocator.cancel(50000);
* ```
*/
cancel(port: number): void;
}