From 1135dd8ec0f0b4316f22bc9d8ff751a6360e1cfe Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 29 Aug 2026 14:08:48 -0700 Subject: [PATCH] fix: go2rtc live provider should work with non-proxied absolute URLs (#2724) - Closes: #2719 --- src/components/live/providers/go2rtc/index.ts | 61 +++++++++++-------- .../live/providers/go2rtc.browser.test.ts | 32 ++++++++++ 2 files changed, 67 insertions(+), 26 deletions(-) create mode 100644 tests/components/live/providers/go2rtc.browser.test.ts diff --git a/src/components/live/providers/go2rtc/index.ts b/src/components/live/providers/go2rtc/index.ts index e84b4f3b..49195db6 100644 --- a/src/components/live/providers/go2rtc/index.ts +++ b/src/components/live/providers/go2rtc/index.ts @@ -43,7 +43,10 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer @property({ attribute: true, type: Boolean }) public controls = false; + // The player and the URL it is built for. private _player?: VideoRTC; + private _playerSource: string | null = null; + private _hasLiveError = false; private _mediaPlayerController = new VideoMediaPlayerController( @@ -52,22 +55,18 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer () => this.controls, ); - private _signedURLController = new SignedURLController( - this, - () => { - const endpoint = this.camera?.getEndpoints()?.go2rtc; - if (!this.hass || !endpoint) { - return {}; - } - return { - hass: this.hass, - endpoint, - proxyConfig: this.camera?.getLiveProxyConfig(), - proxyEndpointOptions: { websocket: true }, - }; - }, - () => this._createPlayer(), - ); + private _signedURLController = new SignedURLController(this, () => { + const endpoint = this.camera?.getEndpoints()?.go2rtc; + if (!this.hass || !endpoint) { + return {}; + } + return { + hass: this.hass, + endpoint, + proxyConfig: this.camera?.getLiveProxyConfig(), + proxyEndpointOptions: { websocket: true }, + }; + }); public async getMediaPlayerController(): Promise { return this._mediaPlayerController; @@ -92,15 +91,12 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer // DISCONNECT_TIMEOUT. this._player?.reset(); this._player = undefined; + this._playerSource = null; } - private _createPlayer(): void { - const src = this._signedURLController.getValue(); - if (!src) { - return; - } - + private _createPlayer(src: string): void { this._player = new VideoRTC(); + this._playerSource = src; this._player.targetID = this.targetID ?? null; this._player.mediaPlayerController = this._mediaPlayerController; this._player.src = src; @@ -111,14 +107,12 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer if (cameraConfig?.go2rtc?.modes && cameraConfig.go2rtc.modes.length) { this._player.mode = cameraConfig.go2rtc.modes.join(','); } - - this.requestUpdate(); } protected willUpdate(changedProps: PropertyValues): void { if (changedProps.has('camera')) { - // Clear old player; the new one is created by the - // SignedURLController's valueChangeCallback once the URL resolves. + // Stop streaming the old camera immediately; `update` builds the new + // player as soon as that camera's URL is known. this._destroyPlayer(); } @@ -137,6 +131,21 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer } } + // The signed URL controller announces only the URLs it resolves + // asynchronously, so the player is built from whatever URL it currently + // holds. Lit runs that controller's own update between `willUpdate` and + // `update`, so this overrides the latter. + protected update(changedProps: PropertyValues): void { + const src = this._signedURLController.getValue(); + if (src !== this._playerSource) { + this._destroyPlayer(); + if (src) { + this._createPlayer(src); + } + } + super.update(changedProps); + } + protected render(): TemplateResult | void { const error = this._signedURLController.getError(); if (error) { diff --git a/tests/components/live/providers/go2rtc.browser.test.ts b/tests/components/live/providers/go2rtc.browser.test.ts new file mode 100644 index 00000000..fb8c0d1a --- /dev/null +++ b/tests/components/live/providers/go2rtc.browser.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import type { VideoRTC } from '../../../../src/components/live/providers/go2rtc/video-rtc'; +import type { RawAdvancedCameraCardConfig } from '../../../../src/config/types'; +import { MountedCardFactory, type MountedCard } from '../../../browser/mounted-card'; +import { CAMERA_ENTITY, createGenericCameraHASS } from '../../../browser/test-utils'; + +const PLAYER_SELECTOR = 'advanced-camera-card-live-go2rtc-player'; + +const mount = async (camera: RawAdvancedCameraCardConfig): Promise => + await MountedCardFactory.createFromSource( + { + type: 'custom:advanced-camera-card', + cameras: [{ camera_entity: CAMERA_ENTITY, live_provider: 'go2rtc', ...camera }], + performance: { features: { card_loading_indicator: false } }, + }, + createGenericCameraHASS(), + ); + +describe('AdvancedCameraCardGo2RTC', () => { + it('should play a go2rtc server that is reached without signing or proxying', async () => { + // An absolute URL is not Home Assistant's to sign, and proxying is off, so + // the stream URL needs no resolving at all. + const card = await mount({ + go2rtc: { url: 'http://localhost:1984', stream: 'office' }, + proxy: { live: false }, + }); + + const player = await card.waitForSelector(PLAYER_SELECTOR); + expect(player.wsURL).toBe('ws://localhost:1984/api/ws?src=office'); + }); +});