Add support for connecting microphone on card load.

This commit is contained in:
Dermot Duffy
2023-05-07 15:17:39 -07:00
parent c78d4be0c3
commit ee7d02fc58
11 changed files with 55 additions and 12 deletions
+28 -8
View File
@@ -152,6 +152,7 @@ enum InitializationAspect {
SIDE_LOAD_ELEMENTS = 'side-load-elements', SIDE_LOAD_ELEMENTS = 'side-load-elements',
MEDIA_PLAYERS = 'media-players', MEDIA_PLAYERS = 'media-players',
CAMERAS = 'cameras', CAMERAS = 'cameras',
MICROPHONE = 'microphone',
} }
/** /**
@@ -1009,8 +1010,6 @@ class FrigateCard extends LitElement {
setPerformanceCSSStyles(this, this._cardWideConfig?.performance); setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
} }
this._initializeBackground();
if (changedProps.has('_view')) { if (changedProps.has('_view')) {
this._setPropertiesForExpandedMode(); this._setPropertiesForExpandedMode();
} }
@@ -1029,6 +1028,9 @@ class FrigateCard extends LitElement {
this._getConfig().live.microphone.disconnect_seconds, this._getConfig().live.microphone.disconnect_seconds,
); );
} }
// Must be called after the microphoneController is created.
this._initializeBackground();
} }
protected _setPropertiesForMinMaxHeight(): void { protected _setPropertiesForMinMaxHeight(): void {
@@ -1204,6 +1206,10 @@ class FrigateCard extends LitElement {
} }
} }
protected async _initializeMicrophone(): Promise<void> {
await this._microphoneController?.connect();
}
protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> { protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
const isValidMediaPlayer = (entityID: string): boolean => { const isValidMediaPlayer = (entityID: string): boolean => {
if (entityID.startsWith('media_player.')) { if (entityID.startsWith('media_player.')) {
@@ -1299,19 +1305,33 @@ class FrigateCard extends LitElement {
protected _initializeBackground(): void { protected _initializeBackground(): void {
const hass = this._hass; const hass = this._hass;
const config = this._getConfig(); const config = this._getConfig();
const needMediaPlayers = config.menu.buttons.media_player.enabled; if (!hass || !config) {
if (!hass || !config || !needMediaPlayers) {
return; 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; return;
} }
this._initializer this._initializer
.initializeMultipleIfNecessary({ .initializeMultipleIfNecessary({
[InitializationAspect.MEDIA_PLAYERS]: async () => ...(config.menu.buttons.media_player.enabled && {
await this._initializeMediaPlayers(hass), [InitializationAspect.MEDIA_PLAYERS]: async () =>
await this._initializeMediaPlayers(hass),
}),
...(config.live.microphone.connect_on_load && {
[InitializationAspect.MICROPHONE]: async () =>
await this._initializeMicrophone(),
}),
}) })
.then((initialized) => { .then((initialized) => {
if (initialized) { if (initialized) {
@@ -1601,7 +1621,7 @@ class FrigateCard extends LitElement {
// Must requestUpdate to show the correct microphone state in the // Must requestUpdate to show the correct microphone state in the
// menu. // menu.
this._microphoneController?.connect().then(() => this.requestUpdate()); this._initializeMicrophone().then(() => this.requestUpdate());
} else if (this._microphoneController?.isConnected()) { } else if (this._microphoneController?.isConnected()) {
this._microphoneController.unmute(); this._microphoneController.unmute();
this.requestUpdate(); this.requestUpdate();
+1 -1
View File
@@ -40,7 +40,7 @@ class FrigateCardGo2RTCPlayer extends VideoRTC {
constructor(microphoneStream?: MediaStream) { constructor(microphoneStream?: MediaStream) {
super(); super();
if (microphoneStream) { if (microphoneStream) {
this._microphoneStream = this._microphoneStream; this._microphoneStream = microphoneStream;
} }
} }
+2
View File
@@ -194,6 +194,8 @@ export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD =
`${CONF_LIVE}.show_image_during_load` as const; `${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS = export const CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS =
`${CONF_LIVE}.microphone.disconnect_seconds` as const; `${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; export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const;
const CONF_IMAGE = 'image' as const; const CONF_IMAGE = 'image' as const;
+5
View File
@@ -156,6 +156,7 @@ import {
CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS, CONF_VIEW_UPDATE_SECONDS,
MEDIA_CHUNK_SIZE_MAX, MEDIA_CHUNK_SIZE_MAX,
CONF_LIVE_MICROPHONE_CONNECT_ON_LOAD,
} from './const.js'; } from './const.js';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import { setLowPerformanceProfile } from './performance.js'; import { setLowPerformanceProfile } from './performance.js';
@@ -1878,6 +1879,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ name: 'mdi:microphone' }, { name: 'mdi:microphone' },
html` html`
${this._renderNumberInput(CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS)} ${this._renderNumberInput(CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS)}
${this._renderSwitch(
CONF_LIVE_MICROPHONE_CONNECT_ON_LOAD,
this._defaults.live.microphone.connect_on_load,
)}
`, `,
)} )}
</div> </div>
+1
View File
@@ -220,6 +220,7 @@
"lazy_load": "Live cameras are lazily loaded", "lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded", "lazy_unload": "Live cameras are lazily unloaded",
"microphone": { "microphone": {
"connect_on_load": "Connect microphone on card load",
"disconnect_seconds": "Seconds after which to disconnect microphone (0=never)", "disconnect_seconds": "Seconds after which to disconnect microphone (0=never)",
"editor_label": "Microphone", "editor_label": "Microphone",
"enabled": "Microphone enabled" "enabled": "Microphone enabled"
+1
View File
@@ -220,6 +220,7 @@
"lazy_load": "Le telecamere dal vivo sono pigramente cariche", "lazy_load": "Le telecamere dal vivo sono pigramente cariche",
"lazy_unload": "Le telecamere dal vivo sono pigramente non caricate", "lazy_unload": "Le telecamere dal vivo sono pigramente non caricate",
"microphone": { "microphone": {
"connect_on_load": "",
"disconnect_seconds": "", "disconnect_seconds": "",
"editor_label": "", "editor_label": "",
"enabled": "" "enabled": ""
+1
View File
@@ -220,6 +220,7 @@
"lazy_load": "As câmeras ao vivo são carregadas lentamente", "lazy_load": "As câmeras ao vivo são carregadas lentamente",
"lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente", "lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente",
"microphone": { "microphone": {
"connect_on_load": "",
"disconnect_seconds": "", "disconnect_seconds": "",
"editor_label": "", "editor_label": "",
"enabled": "" "enabled": ""
+6
View File
@@ -212,6 +212,12 @@
"layout": "layout", "layout": "layout",
"lazy_load": "As câmeras ao vivo são carregadas lentamente", "lazy_load": "As câmeras ao vivo são carregadas lentamente",
"lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente", "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", "preload": "Pré-carregar a visualização ao vivo em segundo plano",
"show_image_during_load": "Mostar imagem durante o carregamento", "show_image_during_load": "Mostar imagem durante o carregamento",
"transition_effect": "Efeito de transição de câmera ao vivo", "transition_effect": "Efeito de transição de câmera ao vivo",
+2
View File
@@ -415,6 +415,7 @@ const imageBaseConfigSchema = z.object({
const microphoneConfigDefault = { const microphoneConfigDefault = {
disconnect_seconds: 60, disconnect_seconds: 60,
connect_on_load: false,
}; };
const microphoneConfigSchema = z const microphoneConfigSchema = z
@@ -423,6 +424,7 @@ const microphoneConfigSchema = z
.number() .number()
.min(0) .min(0)
.default(microphoneConfigDefault.disconnect_seconds), .default(microphoneConfigDefault.disconnect_seconds),
connect_on_load: z.boolean().default(microphoneConfigDefault.connect_on_load),
}) })
.default(microphoneConfigDefault); .default(microphoneConfigDefault);
export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>; export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>;
+5 -3
View File
@@ -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'; import { log } from '../../src/utils/debug.js';
describe('log', () => { describe('log', () => {
const spy = vi.spyOn(global.console, 'debug').mockReturnValue(undefined);
afterAll(() => {
vi.resetAllMocks();
});
it('should do nothing without debug logging set', () => { it('should do nothing without debug logging set', () => {
const spy = vi.spyOn(global.console, 'debug');
log({}, 'foo'); log({}, 'foo');
expect(spy).not.toBeCalled(); expect(spy).not.toBeCalled();
}); });
it('should log debug when appropriately configured', () => { it('should log debug when appropriately configured', () => {
const spy = vi.spyOn(global.console, 'debug');
log({ debug: { logging: true } }, 'foo'); log({ debug: { logging: true } }, 'foo');
expect(spy).toBeCalledWith('foo'); expect(spy).toBeCalledWith('foo');
}); });
+3
View File
@@ -15,6 +15,7 @@ describe('MicrophoneController', () => {
}); });
afterEach(() => { afterEach(() => {
vi.resetAllMocks();
vi.unstubAllGlobals; vi.unstubAllGlobals;
}); });
@@ -48,6 +49,8 @@ describe('MicrophoneController', () => {
}); });
it('should be forbidden when permission denied', async () => { 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(); const controller = new MicrophoneController();
navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error()); navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error());
await controller.connect(); await controller.connect();