diff --git a/src/card.ts b/src/card.ts index 4f90fc23..95e3859e 100644 --- a/src/card.ts +++ b/src/card.ts @@ -152,6 +152,7 @@ enum InitializationAspect { SIDE_LOAD_ELEMENTS = 'side-load-elements', MEDIA_PLAYERS = 'media-players', CAMERAS = 'cameras', + MICROPHONE = 'microphone', } /** @@ -1009,8 +1010,6 @@ class FrigateCard extends LitElement { setPerformanceCSSStyles(this, this._cardWideConfig?.performance); } - this._initializeBackground(); - if (changedProps.has('_view')) { this._setPropertiesForExpandedMode(); } @@ -1029,6 +1028,9 @@ class FrigateCard extends LitElement { this._getConfig().live.microphone.disconnect_seconds, ); } + + // Must be called after the microphoneController is created. + this._initializeBackground(); } protected _setPropertiesForMinMaxHeight(): void { @@ -1204,6 +1206,10 @@ class FrigateCard extends LitElement { } } + protected async _initializeMicrophone(): Promise { + await this._microphoneController?.connect(); + } + protected async _initializeMediaPlayers(hass: HomeAssistant): Promise { const isValidMediaPlayer = (entityID: string): boolean => { if (entityID.startsWith('media_player.')) { @@ -1299,19 +1305,33 @@ class FrigateCard extends LitElement { protected _initializeBackground(): void { const hass = this._hass; const config = this._getConfig(); - const needMediaPlayers = config.menu.buttons.media_player.enabled; - if (!hass || !config || !needMediaPlayers) { + if (!hass || !config) { return; } - if (this._initializer.isInitialized(InitializationAspect.MEDIA_PLAYERS)) { + if ( + this._initializer.isInitializedMultiple([ + ...(config.menu.buttons.media_player.enabled + ? [InitializationAspect.MEDIA_PLAYERS] + : []), + ...(config.live.microphone.connect_on_load + ? [InitializationAspect.MICROPHONE] + : []), + ]) + ) { return; } this._initializer .initializeMultipleIfNecessary({ - [InitializationAspect.MEDIA_PLAYERS]: async () => - await this._initializeMediaPlayers(hass), + ...(config.menu.buttons.media_player.enabled && { + [InitializationAspect.MEDIA_PLAYERS]: async () => + await this._initializeMediaPlayers(hass), + }), + ...(config.live.microphone.connect_on_load && { + [InitializationAspect.MICROPHONE]: async () => + await this._initializeMicrophone(), + }), }) .then((initialized) => { if (initialized) { @@ -1601,7 +1621,7 @@ class FrigateCard extends LitElement { // Must requestUpdate to show the correct microphone state in the // menu. - this._microphoneController?.connect().then(() => this.requestUpdate()); + this._initializeMicrophone().then(() => this.requestUpdate()); } else if (this._microphoneController?.isConnected()) { this._microphoneController.unmute(); this.requestUpdate(); diff --git a/src/components/live/live-go2rtc.ts b/src/components/live/live-go2rtc.ts index 478f460e..e050824d 100644 --- a/src/components/live/live-go2rtc.ts +++ b/src/components/live/live-go2rtc.ts @@ -40,7 +40,7 @@ class FrigateCardGo2RTCPlayer extends VideoRTC { constructor(microphoneStream?: MediaStream) { super(); if (microphoneStream) { - this._microphoneStream = this._microphoneStream; + this._microphoneStream = microphoneStream; } } diff --git a/src/const.ts b/src/const.ts index d83bff3b..b3235804 100644 --- a/src/const.ts +++ b/src/const.ts @@ -194,6 +194,8 @@ export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD = `${CONF_LIVE}.show_image_during_load` as const; export const CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS = `${CONF_LIVE}.microphone.disconnect_seconds` as const; +export const CONF_LIVE_MICROPHONE_CONNECT_ON_LOAD = + `${CONF_LIVE}.microphone.connect_on_load` as const; export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const; const CONF_IMAGE = 'image' as const; diff --git a/src/editor.ts b/src/editor.ts index 21aa0b02..8d8626fe 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -156,6 +156,7 @@ import { CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, MEDIA_CHUNK_SIZE_MAX, + CONF_LIVE_MICROPHONE_CONNECT_ON_LOAD, } from './const.js'; import { localize } from './localize/localize.js'; import { setLowPerformanceProfile } from './performance.js'; @@ -1878,6 +1879,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor { name: 'mdi:microphone' }, html` ${this._renderNumberInput(CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS)} + ${this._renderSwitch( + CONF_LIVE_MICROPHONE_CONNECT_ON_LOAD, + this._defaults.live.microphone.connect_on_load, + )} `, )} diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index cfbaeb7e..85d436e5 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -220,6 +220,7 @@ "lazy_load": "Live cameras are lazily loaded", "lazy_unload": "Live cameras are lazily unloaded", "microphone": { + "connect_on_load": "Connect microphone on card load", "disconnect_seconds": "Seconds after which to disconnect microphone (0=never)", "editor_label": "Microphone", "enabled": "Microphone enabled" diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index cfa9df35..bfb2bf03 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -220,6 +220,7 @@ "lazy_load": "Le telecamere dal vivo sono pigramente cariche", "lazy_unload": "Le telecamere dal vivo sono pigramente non caricate", "microphone": { + "connect_on_load": "", "disconnect_seconds": "", "editor_label": "", "enabled": "" diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 55a31aa8..98444fb4 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -220,6 +220,7 @@ "lazy_load": "As câmeras ao vivo são carregadas lentamente", "lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente", "microphone": { + "connect_on_load": "", "disconnect_seconds": "", "editor_label": "", "enabled": "" diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index 3ea3ad98..ae609ae2 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -212,6 +212,12 @@ "layout": "layout", "lazy_load": "As câmeras ao vivo são carregadas lentamente", "lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente", + "microphone": { + "connect_on_load": "", + "disconnect_seconds": "", + "editor_label": "", + "enabled": "" + }, "preload": "Pré-carregar a visualização ao vivo em segundo plano", "show_image_during_load": "Mostar imagem durante o carregamento", "transition_effect": "Efeito de transição de câmera ao vivo", diff --git a/src/types.ts b/src/types.ts index 41f15280..90af81ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -415,6 +415,7 @@ const imageBaseConfigSchema = z.object({ const microphoneConfigDefault = { disconnect_seconds: 60, + connect_on_load: false, }; const microphoneConfigSchema = z @@ -423,6 +424,7 @@ const microphoneConfigSchema = z .number() .min(0) .default(microphoneConfigDefault.disconnect_seconds), + connect_on_load: z.boolean().default(microphoneConfigDefault.connect_on_load), }) .default(microphoneConfigDefault); export type MicrophoneConfig = z.infer; diff --git a/tests/utils/debug.test.ts b/tests/utils/debug.test.ts index 7a3b8f2e..52f678a3 100644 --- a/tests/utils/debug.test.ts +++ b/tests/utils/debug.test.ts @@ -1,14 +1,16 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterAll, describe, expect, it, vi } from 'vitest'; import { log } from '../../src/utils/debug.js'; describe('log', () => { + const spy = vi.spyOn(global.console, 'debug').mockReturnValue(undefined); + afterAll(() => { + vi.resetAllMocks(); + }); it('should do nothing without debug logging set', () => { - const spy = vi.spyOn(global.console, 'debug'); log({}, 'foo'); expect(spy).not.toBeCalled(); }); it('should log debug when appropriately configured', () => { - const spy = vi.spyOn(global.console, 'debug'); log({ debug: { logging: true } }, 'foo'); expect(spy).toBeCalledWith('foo'); }); diff --git a/tests/utils/microphone.test.ts b/tests/utils/microphone.test.ts index 0a3c0aca..36b5197e 100644 --- a/tests/utils/microphone.test.ts +++ b/tests/utils/microphone.test.ts @@ -15,6 +15,7 @@ describe('MicrophoneController', () => { }); afterEach(() => { + vi.resetAllMocks(); vi.unstubAllGlobals; }); @@ -48,6 +49,8 @@ describe('MicrophoneController', () => { }); it('should be forbidden when permission denied', async () => { + // Don't actually log messages to the console during the test. + vi.spyOn(global.console, 'warn').mockReturnValue(undefined); const controller = new MicrophoneController(); navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error()); await controller.connect();