diff --git a/src/camera-manager/frigate/engine-frigate.ts b/src/camera-manager/frigate/engine-frigate.ts index 8917c99a..d0cdaf99 100644 --- a/src/camera-manager/frigate/engine-frigate.ts +++ b/src/camera-manager/frigate/engine-frigate.ts @@ -1081,7 +1081,6 @@ export class FrigateCameraManagerEngine supportsSnapshots: !isBirdseye, supportsRecordings: !isBirdseye, supportsTimeline: !isBirdseye, - supports2WayAudio: cameraConfig.live_provider === 'go2rtc', }; } diff --git a/src/camera-manager/generic/engine-generic.ts b/src/camera-manager/generic/engine-generic.ts index 0f3fe130..9e9207ba 100644 --- a/src/camera-manager/generic/engine-generic.ts +++ b/src/camera-manager/generic/engine-generic.ts @@ -182,7 +182,6 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { supportsRecordings: false, supportsSnapshots: false, supportsTimeline: false, - supports2WayAudio: false, }; } diff --git a/src/camera-manager/manager.ts b/src/camera-manager/manager.ts index f3b1545b..b686082f 100644 --- a/src/camera-manager/manager.ts +++ b/src/camera-manager/manager.ts @@ -755,7 +755,6 @@ export class CameraManager { supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings), supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots), supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline), - supports2WayAudio: perCameraCapabilities.some((cap) => cap?.supports2WayAudio), }; } } diff --git a/src/camera-manager/types.ts b/src/camera-manager/types.ts index 5810567e..19b508cc 100644 --- a/src/camera-manager/types.ts +++ b/src/camera-manager/types.ts @@ -100,7 +100,6 @@ interface BaseCapabilities { supportsRecordings: boolean; supportsSnapshots: boolean; supportsTimeline: boolean; - supports2WayAudio: boolean; } export type CameraManagerCapabilities = BaseCapabilities; diff --git a/src/card.ts b/src/card.ts index 81a1b1da..99d0b991 100644 --- a/src/card.ts +++ b/src/card.ts @@ -619,7 +619,10 @@ class FrigateCard extends LitElement { }); } - if (this._microphoneController && cameraCapabilities?.supports2WayAudio) { + if ( + this._microphoneController && + this._currentMediaLoadedInfo?.capabilities?.supports2WayAudio + ) { const muted = this._microphoneController.isMuted(); const buttonType = this._getConfig().menu.buttons.microphone.type; buttons.push({ diff --git a/src/components/image.ts b/src/components/image.ts index 9a9f3e99..9fe081de 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -11,27 +11,32 @@ import { import { customElement, property } from 'lit/decorators.js'; import { live } from 'lit/directives/live.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; +import isEqual from 'lodash-es/isEqual'; import { CachedValueController } from '../cached-value-controller.js'; import defaultImage from '../images/frigate-bird-in-sky.jpg'; import { localize } from '../localize/localize.js'; import imageStyle from '../scss/image.scss'; -import { CameraConfig, ImageViewConfig, MediaLoadedInfo } from '../types.js'; +import { + CameraConfig, + FrigateCardMediaPlayer, + ImageViewConfig, + MediaLoadedInfo, +} from '../types.js'; +import { contentsChanged } from '../utils/basic.js'; import { isHassDifferent } from '../utils/ha'; -import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js'; import { createMediaLoadedInfo, dispatchExistingMediaLoadedInfoAsEvent, } from '../utils/media-info.js'; +import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js'; import { View } from '../view/view.js'; import { dispatchErrorMessageEvent } from './message.js'; -import { contentsChanged } from '../utils/basic.js'; -import isEqual from 'lodash-es/isEqual'; // See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py . const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000; @customElement('frigate-card-image') -export class FrigateCardImage extends LitElement { +export class FrigateCardImage extends LitElement implements FrigateCardMediaPlayer { @property({ attribute: false }) public hass?: HomeAssistant; @@ -54,6 +59,36 @@ export class FrigateCardImage extends LitElement { protected _mediaLoadedInfo: MediaLoadedInfo | null = null; + public async play(): Promise { + this._cachedValueController?.startTimer(); + } + + public async pause(): Promise { + this._cachedValueController?.stopTimer(); + } + + public async mute(): Promise { + // Not implemented. + } + + public async unmute(): Promise { + // Not implemented. + } + + public isMuted(): boolean { + return true; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async seek(_seconds: number): Promise { + // Not implemented. + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async setControls(_controls: boolean): Promise { + // Not implemented. + } + /** * Get the camera entity for the current camera configuration. * @returns The entity or undefined if no camera entity is available. @@ -250,7 +285,7 @@ export class FrigateCardImage extends LitElement { ${ref(this._refImage)} src=${live(src)} @load=${(ev: Event) => { - const mediaLoadedInfo = createMediaLoadedInfo(ev); + const mediaLoadedInfo = createMediaLoadedInfo(ev, { player: this }); // Avoid the media being reported as repeatedly loading unless the // media info changes. if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) { diff --git a/src/components/live/live-go2rtc.ts b/src/components/live/live-go2rtc.ts index e050824d..27b8effd 100644 --- a/src/components/live/live-go2rtc.ts +++ b/src/components/live/live-go2rtc.ts @@ -36,9 +36,11 @@ const GO2RTC_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; @customElement('frigate-card-live-go2rtc-player') class FrigateCardGo2RTCPlayer extends VideoRTC { protected _microphoneStream?: MediaStream; + protected _containingPlayer?: FrigateCardMediaPlayer; - constructor(microphoneStream?: MediaStream) { + constructor(containingPlayer: FrigateCardMediaPlayer, microphoneStream?: MediaStream) { super(); + this._containingPlayer = containingPlayer; if (microphoneStream) { this._microphoneStream = microphoneStream; } @@ -79,7 +81,16 @@ class FrigateCardGo2RTCPlayer extends VideoRTC { onloadeddata.call(this.video, e); } hideMediaControlsTemporarily(this.video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS); - dispatchMediaLoadedEvent(this, this.video); + dispatchMediaLoadedEvent(this, this.video, { + player: this._containingPlayer, + capabilities: { + // 2-way audio is only supported on WebRTC connections. The state of + // `this._microphoneStream` is not taken into account here since + // that can be created after the fact -- this is purely saying that + // were a microphone stream available it could be used usefully. + supports2WayAudio: !!this.pc, + }, + }); }; // Always started muted. Media may be unmuted in accordance with user @@ -256,7 +267,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla return; } - this._player = new FrigateCardGo2RTCPlayer(this.microphoneStream); + this._player = new FrigateCardGo2RTCPlayer(this, this.microphoneStream); this._player.src = address; this._player.visibilityCheck = false; diff --git a/src/components/live/live-image.ts b/src/components/live/live-image.ts index 4bc38d15..136097f5 100644 --- a/src/components/live/live-image.ts +++ b/src/components/live/live-image.ts @@ -1,10 +1,11 @@ import { HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; +import { createRef, ref, Ref } from 'lit/directives/ref.js'; import liveImageStyle from '../../scss/live-image.scss'; import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js'; -import { getStateObjOrDispatchError } from './live.js'; import '../image.js'; +import { getStateObjOrDispatchError } from './live.js'; @customElement('frigate-card-live-image') export class FrigateCardLiveImage extends LitElement implements FrigateCardMediaPlayer { @@ -14,37 +15,34 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia @property({ attribute: false }) public cameraConfig?: CameraConfig; - @state() - protected _playing = true; + protected _refImage: Ref = createRef(); public async play(): Promise { - this._playing = true; + await this._refImage.value?.play(); } public async pause(): Promise { - this._playing = false; + await this._refImage.value?.pause(); } public async mute(): Promise { - // Not implemented. + await this._refImage.value?.mute(); } public async unmute(): Promise { - // Not implemented. + await this._refImage.value?.unmute(); } public isMuted(): boolean { - return true; + return !!this._refImage.value?.isMuted(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async seek(_seconds: number): Promise { - // Not implemented. + public async seek(seconds: number): Promise { + await this._refImage.value?.seek(seconds); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async setControls(_controls: boolean): Promise { - // Not implemented. + public async setControls(controls: boolean): Promise { + await this._refImage.value?.setControls(controls); } protected render(): TemplateResult | void { @@ -55,16 +53,14 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia getStateObjOrDispatchError(this, this.hass, this.cameraConfig); return html` { - dispatchMediaLoadedEvent(this, ev); + dispatchMediaLoadedEvent(this, ev, { player: this }); }} > { - dispatchMediaLoadedEvent(this, e); + @load=${(ev: Event) => { + dispatchMediaLoadedEvent(this, ev, { player: this }); }} />`} `); diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index 9e513291..82dd6a49 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -90,7 +90,7 @@ customElements.whenDefined('ha-camera-stream').then(() => { return html` { - dispatchMediaLoadedEvent(this, ev); + dispatchMediaLoadedEvent(this, ev, { player: this }); }} .src=${typeof this._connected == 'undefined' || this._connected ? computeMJPEGStreamUrl(this.stateObj) diff --git a/src/patches/ha-hls-player.ts b/src/patches/ha-hls-player.ts index 2ef2ac38..72fd4720 100644 --- a/src/patches/ha-hls-player.ts +++ b/src/patches/ha-hls-player.ts @@ -97,7 +97,7 @@ customElements.whenDefined('ha-hls-player').then(() => { hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS); }} @loadeddata=${(e) => { - dispatchMediaLoadedEvent(this, e); + dispatchMediaLoadedEvent(this, e, { player: this }); }} > `; diff --git a/src/patches/ha-web-rtc-player.ts b/src/patches/ha-web-rtc-player.ts index b7f07988..bcc42d0d 100644 --- a/src/patches/ha-web-rtc-player.ts +++ b/src/patches/ha-web-rtc-player.ts @@ -93,7 +93,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => { hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS); }} @loadeddata=${(e) => { - dispatchMediaLoadedEvent(this, e); + dispatchMediaLoadedEvent(this, e, { player: this }); }} > `; diff --git a/src/types.ts b/src/types.ts index c32672f6..db5a2c8c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1471,9 +1471,15 @@ export interface ExtendedHomeAssistant extends HomeAssistant { }; } +export interface MediaLoadedCapabilities { + supports2WayAudio?: boolean; +} + export interface MediaLoadedInfo { width: number; height: number; + player?: FrigateCardMediaPlayer; + capabilities?: MediaLoadedCapabilities; } export const MESSAGE_TYPE_PRIORITIES = { diff --git a/src/utils/media-info.ts b/src/utils/media-info.ts index de7db2f8..4c75e0d3 100644 --- a/src/utils/media-info.ts +++ b/src/utils/media-info.ts @@ -1,4 +1,8 @@ -import { MediaLoadedInfo } from '../types.js'; +import { + FrigateCardMediaPlayer, + MediaLoadedCapabilities, + MediaLoadedInfo, +} from '../types.js'; import { dispatchFrigateCardEvent } from './basic.js'; const MEDIA_INFO_HEIGHT_CUTOFF = 50; @@ -11,6 +15,10 @@ const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; */ export function createMediaLoadedInfo( source: Event | HTMLElement, + options?: { + player?: FrigateCardMediaPlayer; + capabilities?: MediaLoadedCapabilities; + }, ): MediaLoadedInfo | null { let target: HTMLElement | EventTarget; if (source instanceof Event) { @@ -23,16 +31,20 @@ export function createMediaLoadedInfo( return { width: (target as HTMLImageElement).naturalWidth, height: (target as HTMLImageElement).naturalHeight, + ...options, }; } else if (target instanceof HTMLVideoElement) { return { width: (target as HTMLVideoElement).videoWidth, height: (target as HTMLVideoElement).videoHeight, + ...options, }; } else if (target instanceof HTMLCanvasElement) { return { width: (target as HTMLCanvasElement).width, height: (target as HTMLCanvasElement).height, + player: options?.player, + ...options, }; } return null; @@ -46,8 +58,12 @@ export function createMediaLoadedInfo( export function dispatchMediaLoadedEvent( target: HTMLElement, source: Event | HTMLElement, + options?: { + player?: FrigateCardMediaPlayer; + capabilities?: MediaLoadedCapabilities; + }, ): void { - const mediaLoadedInfo = createMediaLoadedInfo(source); + const mediaLoadedInfo = createMediaLoadedInfo(source, options); if (mediaLoadedInfo) { dispatchExistingMediaLoadedInfoAsEvent(target, mediaLoadedInfo); }