From 937ba647a7c33bec234661fbc4364129af992eb2 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 30 Jun 2026 16:02:52 -0700 Subject: [PATCH] fix: Fix audio for cameras with mixed WebRTC/HLS capabilities (#2557) - Closes #2479 --- docs/configuration/cameras/live-provider.md | 32 +++++ src/components/live/providers/ha.ts | 1 - src/patches/ha-camera-stream.ts | 124 +++++++++++++++++--- 3 files changed, 140 insertions(+), 17 deletions(-) diff --git a/docs/configuration/cameras/live-provider.md b/docs/configuration/cameras/live-provider.md index 961c2c8c..bbb8291c 100644 --- a/docs/configuration/cameras/live-provider.md +++ b/docs/configuration/cameras/live-provider.md @@ -43,6 +43,38 @@ cameras: > [`capabilities.force`](./README.md?id=capabilities) to skip metadata > detection entirely. +## `ha` + +The `ha` block configures use of the default Home Assistant (`ha`) live provider. It has no configuration options. + +```yaml +cameras: + - camera_entity: camera.office + live_provider: ha +``` + +The native stream provider in Home Assistant dynamically chooses between HLS and +WebRTC streams. It prefers the lowest-latency stream (WebRTC) and only falls +back to HLS when that is the only way to get audio. + +| WebRTC stream | HLS Stream | Muted? | Resulting Stream Selection | +| ------------- | ------------ | -------- | -------------------------- | +| Has audio | _Either_ | _Either_ | WebRTC (lowest latency) | +| Has no audio | Has audio | Yes | WebRTC (lowest latency) | +| Has no audio | Has audio | No | HLS (for audio) | +| Has no audio | Has no audio | _Either_ | WebRTC (lowest latency) | + +> [!NOTE] +> When using the `ha` provider through Advanced Camera Card, streams are chosen +> by the same logic as the table above (the logic Home Assistant uses). The one +> difference is **when the choice is made**: the card re-runs the selection +> whenever you unmute (whether from the card's mute button or the video's own +> controls), whereas Home Assistant effectively fixes "muted" per dashboard card +> and never re-selects after that. So for a camera whose low-latency stream has +> **no audio**, unmuting in the card switches you to the audio-enabled stream, +> which may take a moment to load and run with a little more latency That +> audio-enabled stream will remain loaded thereafter. + ## `image` All configuration is under: diff --git a/src/components/live/providers/ha.ts b/src/components/live/providers/ha.ts index 018701ab..d58d45ab 100644 --- a/src/components/live/providers/ha.ts +++ b/src/components/live/providers/ha.ts @@ -55,7 +55,6 @@ export class AdvancedCameraCardLiveHA extends LitElement implements MediaPlayer .hass=${this.hass} .stateObj=${cameraEntity ? this.hass.states[cameraEntity] : undefined} .controls=${this.controls} - .muted=${true} .targetID=${this.targetID} > `; diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index b888a48c..75efc1ac 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -17,8 +17,7 @@ import { type CSSResultGroup, type PropertyValues, } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import { query } from 'lit/decorators/query.js'; +import { customElement, property, state } from 'lit/decorators.js'; import { MediaLoadedInfoSourceController } from '../components-lib/media-loaded-info-source-controller.js'; @@ -57,11 +56,6 @@ void customElements.whenDefined('ha-camera-stream').then(() => { extends customElements.get('ha-camera-stream') implements MediaPlayer { - // Due to an obscure behavior when this card is casted, this element needs - // to use query rather than the ref directive to find the player. - @query('.player:not(.hidden)') - protected _player: MediaPlayer; - @property({ attribute: false }) public targetID?: string; @@ -78,6 +72,80 @@ void customElements.whenDefined('ha-camera-stream').then(() => { }, ); + // The currently-visible stream type, refreshed in `updated()`. + private _visibleStreamType: StreamType | null = null; + + // -------- Audio / stream selection model (hacking around HA!) -------- + // + // The HA frontend chooses between a camera's streams (e.g. low-latency + // WebRTC vs higher-latency HLS) from `muted`: when unmuted it switches to a + // stream that carries audio if the chosen one has none. HA sets `muted` + // statically per context (i.e. a stock card sets it once); the native + //