Files
frigate/web/src/utils/webrtcUtil.ts
T
Josh HawkinsandGitHub 64d6366ac4 Live streaming tech selection (#24374)
* allow users to select live streaming technology

* fix webrtc being downgraded to mse on load

`useUserPersistence` seeds state with the default and loads asynchronously, so the first render always used `mse` instead of the saved choice, and `useWebRTCGloballyAvailable` reports `checking` until the probe settles and re-enters that state on every consumer mount, so a saved `webrtc` was rewritten to `mse` even after the probe had already passed. On Safari the MSE player then timed out and latched the jsmpeg fallback. A pending probe now counts as available, the player waits on `autoLive` until the stored preferences load, and `handleError` gates on the mode in use since the fallback flag no longer implies webrtc is untried. A rejected IndexedDB read also resolves `loaded` now, so a blocked store can't leave the player waiting forever.

* add support for configurable ICE servers in WebRTC player

* add mic error state, fix dialog overwriting saved choice and dashboard ignoring stream

* tweaks
2026-09-16 09:12:06 -05:00

109 lines
3.2 KiB
TypeScript

import { FrigateConfig } from "@/types/frigateConfig";
import { WebRTCUnavailableReason } from "@/types/live";
/**
* Helpers for detecting what the current browser can do over WebRTC.
* Used to gate the WebRTC live streaming option (e.g. H.265 is only
* decodable over WebRTC on Chrome 136+ / Safari 18+ with HEVC hardware).
*/
const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
{ urls: "stun:stun.l.google.com:19302" },
];
type ConfiguredIceServers = FrigateConfig["go2rtc"]["webrtc"]["ice_servers"];
/** ICE servers for browser peer connections, falling back to public STUN. */
export function webRTCIceServers(
configured: ConfiguredIceServers,
): RTCIceServer[] {
if (!configured?.length) {
return DEFAULT_ICE_SERVERS;
}
return configured.map((server) => ({
urls: server.urls,
username: server.username,
credential: server.credential,
}));
}
export function browserSupportsWebRTC(): boolean {
return typeof window !== "undefined" && "RTCPeerConnection" in window;
}
/** Codec aliases that refer to the same underlying codec. */
const CODEC_ALIASES: Record<string, string> = {
HEVC: "H265",
AVC: "H264",
};
function normalizeCodec(codec: string): string {
const upper = codec.toUpperCase();
return CODEC_ALIASES[upper] ?? upper;
}
/** The set of video codec names the browser can receive over WebRTC. */
export function browserWebRTCVideoCodecs(): string[] {
if (
typeof RTCRtpReceiver === "undefined" ||
typeof RTCRtpReceiver.getCapabilities !== "function"
) {
return [];
}
const caps = RTCRtpReceiver.getCapabilities("video");
if (!caps) return [];
const codecs = new Set<string>();
for (const codec of caps.codecs) {
// mimeType is like "video/H264"; ignore infra codecs (rtx, red, ulpfec)
const name = codec.mimeType.split("/")[1]?.toUpperCase();
if (!name || ["RTX", "RED", "ULPFEC", "FLEXFEC-03"].includes(name)) {
continue;
}
codecs.add(normalizeCodec(name));
}
return Array.from(codecs);
}
export function browserSupportsWebRTCVideoCodec(codec: string): boolean {
return browserWebRTCVideoCodecs().includes(normalizeCodec(codec));
}
const DOCS_URL =
"https://docs.frigate.video/configuration/live/#selecting-a-streaming-technology";
// Availability is consumed by several components at once, so without this the
// same fact prints once per consumer.
const loggedMessages = new Set<string>();
/**
* Logs why WebRTC is unavailable, supplementing the short message shown inline
* in the technology selector with the underlying detail.
*
* @param scope - Stream name for per-stream reasons, omitted for global ones
* @param reason - The reason surfaced in the UI
* @param description - The detail the UI message leaves out
*/
export function logWebRTCUnavailable(
scope: string | undefined,
reason: WebRTCUnavailableReason,
description: string,
): void {
const message = `${scope ? `${scope} - ` : ""}WebRTC unavailable '${reason}': ${description} See the documentation: ${DOCS_URL}`;
if (loggedMessages.has(message)) {
return;
}
loggedMessages.add(message);
// eslint-disable-next-line no-console
console.warn(message);
}
/** Clears the emitted messages (e.g. when go2rtc config changes). */
export function resetWebRTCUnavailableLog(): void {
loggedMessages.clear();
}