From 632569b1eff17b39c95071192a3c6704002c5ef2 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Tue, 1 Feb 2022 20:40:34 -0800 Subject: [PATCH 1/3] Add auto unmute support. --- src/components/embla-plugins/automedia.ts | 69 +++++++++---- src/components/live.ts | 117 +++++++++++++++++++--- src/components/media-carousel.ts | 55 +++++----- src/components/viewer.ts | 19 +++- src/const.ts | 2 + src/editor.ts | 30 ++++-- src/localize/languages/en.json | 2 + src/patches/ha-camera-stream.ts | 15 ++- src/patches/ha-hls-player.ts | 15 ++- src/types.ts | 6 ++ 10 files changed, 260 insertions(+), 70 deletions(-) diff --git a/src/components/embla-plugins/automedia.ts b/src/components/embla-plugins/automedia.ts index 1ee510a8..ce407027 100644 --- a/src/components/embla-plugins/automedia.ts +++ b/src/components/embla-plugins/automedia.ts @@ -3,15 +3,20 @@ import { FrigateCardMediaPlayer } from '../../types.js'; export type AutoMediaPluginOptionsType = { playerSelector: string; - autoplayWhenVisible?: boolean; + autoPlayWhenVisible?: boolean; + autoUnmuteWhenVisible?: boolean; }; export const defaultOptions: Partial = { - autoplayWhenVisible: true, + autoPlayWhenVisible: true, + autoUnmuteWhenVisible: true, }; export type AutoMediaPluginType = EmblaPluginType & { play: () => void; + pause: () => void; + mute: () => void; + unmute: () => void; } /** @@ -35,9 +40,12 @@ export function AutoMediaPlugin( slides = carousel.slideNodes(); // Frigate card media autoplays when the media loads not necessarily when the - // slide is selected, so only pause based on carousel events. - carousel.on('destroy', pauseAllHandler); + // slide is selected, so only pause (and not play/unmute) based on carousel + // events. + carousel.on('destroy', pause); carousel.on('select', pausePrevious); + carousel.on('destroy', mute); + carousel.on('select', mutePrevious); document.addEventListener('visibilitychange', visibilityHandler); } @@ -46,8 +54,10 @@ export function AutoMediaPlugin( * Destroy the plugin. */ function destroy(): void { - carousel.off('destroy', pauseAllHandler); + carousel.off('destroy', pause); carousel.off('select', pausePrevious); + carousel.off('destroy', mute); + carousel.off('select', mutePrevious); document.removeEventListener('visibilitychange', visibilityHandler); } @@ -58,8 +68,14 @@ export function AutoMediaPlugin( function visibilityHandler(): void { if (document.visibilityState == 'hidden') { pause(); - } else if (document.visibilityState == 'visible' && options.autoplayWhenVisible) { - play(); + mute(); + } else if (document.visibilityState == 'visible') { + if (options.autoPlayWhenVisible) { + play(); + } + if (options.autoUnmuteWhenVisible) { + unmute(); + } } } @@ -73,39 +89,56 @@ export function AutoMediaPlugin( } /** - * Pause all slides. - */ - function pauseAllHandler(): void { - slides.forEach((slide) => getPlayer(slide)?.pause()); - } - - /** - * Autoplay the current slide. + * Play the current slide. */ function play(): void { getPlayer(slides[carousel.selectedScrollSnap()])?.play(); } /** - * Autopause the current slide. + * Pause the current slide. */ - function pause(): void { + function pause(): void { getPlayer(slides[carousel.selectedScrollSnap()])?.pause(); } /** - * Autopause the previous slide. + * Pause the previous slide. */ function pausePrevious(): void { getPlayer(slides[carousel.previousScrollSnap()])?.pause(); } + /** + * Unmute the current slide. + */ + function unmute(): void { + getPlayer(slides[carousel.selectedScrollSnap()])?.unmute(); + } + + /** + * Mute the current slide. + */ + function mute(): void { + getPlayer(slides[carousel.selectedScrollSnap()])?.mute(); + } + + /** + * Mute the previous slide. + */ + function mutePrevious(): void { + getPlayer(slides[carousel.previousScrollSnap()])?.mute(); + } + const self: AutoMediaPluginType = { name: 'AutoMediaPlugin', options, init, destroy, play, + pause, + mute, + unmute, }; return self; } diff --git a/src/components/live.ts b/src/components/live.ts index ded0049f..7b665ab6 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -15,6 +15,7 @@ import { MediaShowInfo, WebRTCConfig, FrigateCardError, + FrigateCardMediaPlayer, LiveOverrides, LiveProvider, frigateCardConfigDefaults, @@ -27,7 +28,7 @@ import { Task } from '@lit-labs/task'; import { customElement, property, state } from 'lit/decorators.js'; import { until } from 'lit/directives/until.js'; -import { AutoMediaPlugin } from './embla-plugins/automedia.js'; +import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js'; import { BrowseMediaUtil } from '../browse-media-util.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js'; import { FrigateCardMediaCarousel } from './media-carousel.js'; @@ -105,7 +106,7 @@ export class FrigateCardLive extends LitElement { protected _mediaShowHandler(e: CustomEvent): void { this._savedMediaShowInfo = e.detail; if (this._preloaded) { - // If live is being pre-loaded, don't let the event propogate upwards yet + // If live is being pre-loaded, don't let the event propagate upwards yet // as the media is not really being shown. e.stopPropagation(); } @@ -251,15 +252,16 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { */ updated(changedProperties: PropertyValues): void { if ( - changedProperties.has('cameras') || - changedProperties.has('liveConfig') || - changedProperties.has('preloaded') + this._carousel && + (changedProperties.has('cameras') || changedProperties.has('liveConfig')) ) { // All of these properties may fundamentally change the contents/size of // the DOM, and the carousel should be reset when they change. this._destroyCarousel(); } + super.updated(changedProperties); + if (changedProperties.has('view')) { const oldView = changedProperties.get('view') as View | undefined; if ( @@ -275,7 +277,22 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { } } - super.updated(changedProperties); + if (changedProperties.has('preloaded')) { + const automedia = this._plugins['AutoMediaPlugin'] as + | AutoMediaPluginType + | undefined; + if (automedia) { + // If this has changed to preloaded then pause & mute, otherwise play + // and potentially unmute (depending on configuration). + if (this.preloaded) { + automedia.pause(); + automedia.mute(); + } else { + automedia.play(); + this._autoUnmuteHandler(); + } + } + } } /** @@ -310,10 +327,20 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { }), AutoMediaPlugin({ playerSelector: 'frigate-card-live-provider', + autoUnmuteWhenVisible: !!this.liveConfig?.auto_unmute, }), ]; } + /** + * Unmute the media on the selected slide. + */ + protected _autoUnmuteHandler(): void { + if (this.liveConfig?.auto_unmute) { + super._autoUnmuteHandler(); + } + } + /** * Returns the number of slides to lazily load. 0 means all slides are lazy * loaded, 1 means that 1 slide on each side of the currently selected slide @@ -519,9 +546,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { @@ -549,9 +574,7 @@ export class FrigateCardLiveProvider extends LitElement { @property({ attribute: false }) public label = ''; - protected _providerRef: Ref< - FrigateCardLiveFrigate | FrigateCardLiveJSMPEG | FrigateCardLiveWebRTC - > = createRef(); + protected _providerRef: Ref = createRef(); /** * Play the video. @@ -567,6 +590,20 @@ export class FrigateCardLiveProvider extends LitElement { this._providerRef.value?.pause(); } + /** + * Mute the video. + */ + public mute(): void { + this._providerRef.value?.mute(); + } + + /** + * Unmute the video. + */ + public unmute(): void { + this._providerRef.value?.unmute(); + } + protected _getResolvedProvider(): LiveProvider { if (this.cameraConfig?.live_provider === 'auto') { if (this.cameraConfig?.webrtc?.entity || this.cameraConfig?.webrtc?.url) { @@ -649,6 +686,20 @@ export class FrigateCardLiveFrigate extends LitElement { this._playerRef.value?.pause(); } + /** + * Mute the video. + */ + public mute(): void { + this._playerRef.value?.mute(); + } + + /** + * Unmute the video. + */ + public unmute(): void { + this._playerRef.value?.unmute(); + } + /** * Master render method. * @returns A rendered template. @@ -718,6 +769,26 @@ export class FrigateCardLiveWebRTC extends LitElement { this._getPlayer()?.pause(); } + /** + * Mute the video. + */ + public mute(): void { + const player = this._getPlayer(); + if (player) { + player.muted = true; + } + } + + /** + * Unmute the video. + */ + public unmute(): void { + const player = this._getPlayer(); + if (player) { + player.muted = false; + } + } + /** * Get the underlying video player. * @returns The player or `null` if not found. @@ -848,6 +919,28 @@ export class FrigateCardLiveJSMPEG extends LitElement { this._jsmpegVideoPlayer?.stop(); } + /** + * Mute the video (included for completeness, JSMPEG live disables audio as + * Frigate does not encode it). + */ + public mute(): void { + const player = this._jsmpegVideoPlayer?.player; + if (player) { + player.volume = 0; + } + } + + /** + * Unmute the video (included for completeness, JSMPEG live disables audio as + * Frigate does not encode it). + */ + public unmute(): void { + const player = this._jsmpegVideoPlayer?.player; + if (player) { + player.volume = 1; + } + } + /** * Get a signed player URL. * @returns A URL or null. diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index b7614676..7d6fdd14 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -3,7 +3,10 @@ import { EmblaCarouselType } from 'embla-carousel'; import { createRef, Ref } from 'lit/directives/ref'; import { customElement } from 'lit/decorators.js'; +import { AutoMediaPluginType } from './embla-plugins/automedia.js'; import { FrigateCardCarousel } from './carousel.js'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import { FrigateCardTitleControl } from './title-control.js'; import type { MediaShowInfo } from '../types.js'; import { dispatchExistingMediaShowInfoAsEvent, @@ -14,10 +17,6 @@ import './next-prev-control.js'; import mediaCarouselStyle from '../scss/media-carousel.scss'; -import { FrigateCardNextPreviousControl } from './next-prev-control.js'; -import { FrigateCardTitleControl } from './title-control.js'; -import { AutoMediaPluginType } from './embla-plugins/automedia.js'; - const getEmptyImageSrc = (width: number, height: number) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`; export const IMG_EMPTY = getEmptyImageSrc(16, 9); @@ -30,12 +29,21 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { protected _previousControlRef: Ref = createRef(); protected _titleControlRef: Ref = createRef(); protected _titleTimerID: number | null = null; + /** * Play the media on the selected slide. May be overridden to control when * autoplay should happen. */ - protected _autoplayHandler(): void { - (this._plugins['MediaAutoPlayPause'] as AutoMediaPluginType | undefined)?.play(); + protected _autoPlayHandler(): void { + (this._plugins['AutoMediaPlugin'] as AutoMediaPluginType | undefined)?.play(); + } + + /** + * Play the media on the selected slide. May be overridden to control when + * autoplay should happen. + */ + protected _autoUnmuteHandler(): void { + (this._plugins['AutoMediaPlugin'] as AutoMediaPluginType | undefined)?.unmute(); } /** @@ -69,7 +77,8 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { */ connectedCallback(): void { super.connectedCallback(); - this.addEventListener('frigate-card:media-show', this._autoplayHandler); + this.addEventListener('frigate-card:media-show', this._autoPlayHandler); + this.addEventListener('frigate-card:media-show', this._autoUnmuteHandler); this.addEventListener('frigate-card:media-show', this._adaptiveHeightHandler); this.addEventListener('frigate-card:media-show', this._titleHandler); } @@ -79,7 +88,8 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { */ disconnectedCallback(): void { super.disconnectedCallback(); - this.removeEventListener('frigate-card:media-show', this._autoplayHandler); + this.removeEventListener('frigate-card:media-show', this._autoPlayHandler); + this.removeEventListener('frigate-card:media-show', this._autoUnmuteHandler); this.removeEventListener('frigate-card:media-show', this._adaptiveHeightHandler); this.removeEventListener('frigate-card:media-show', this._titleHandler); } @@ -128,15 +138,15 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { if (!this._carousel) { return; } - const slides = this._carousel.slideNodes(); - const heights = this._carousel.slidesInView(true).map((index) => { - return slides[index].getBoundingClientRect().height; - }); - const targetHeight = Math.max(...heights); - if (targetHeight > 0) { - this._carousel.containerNode().style.maxHeight = `${targetHeight}px`; - } else { - this._carousel.containerNode().style.removeProperty('max-height'); + const slide = this._carousel?.selectedScrollSnap() + if (slide !== undefined) { + const slides = this._carousel.slideNodes(); + const height = slides[slide].getBoundingClientRect().height; + if (height > 0) { + this._carousel.containerNode().style.maxHeight = `${height}px`; + } else { + this._carousel.containerNode().style.removeProperty('max-height'); + } } }; @@ -195,11 +205,10 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { return; } - this._carousel.slidesInView(true).forEach((slideIndex) => { - if (slideIndex in this._mediaShowInfo) { - dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]); - } - }); + const slideIndex = this._carousel.selectedScrollSnap(); + if (slideIndex in this._mediaShowInfo) { + dispatchExistingMediaShowInfoAsEvent(this, this._mediaShowInfo[slideIndex]); + } } /** @@ -232,7 +241,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel { // rejected upstream (empty 1x1 images will be rejected here). if (mediaShowInfo && isValidMediaShowInfo(mediaShowInfo)) { this._mediaShowInfo[slideIndex] = mediaShowInfo; - if (this._carousel && this._carousel?.slidesInView(true).includes(slideIndex)) { + if (this._carousel && this._carousel?.selectedScrollSnap() == slideIndex) { dispatchExistingMediaShowInfoAsEvent(this, mediaShowInfo); } diff --git a/src/components/viewer.ts b/src/components/viewer.ts index eaece065..d5ef16de 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -267,12 +267,20 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { } /** - * Play the media on the selected slide. May be overridden to control when - * autoplay should happen. + * Play the media on the selected slide. */ - protected _autoplayHandler(): void { + protected _autoPlayHandler(): void { if (this.viewerConfig?.autoplay_clip) { - super._autoplayHandler(); + super._autoPlayHandler(); + } + } + + /** + * Unmute the media on the selected slide. + */ + protected _autoUnmuteHandler(): void { + if (this.viewerConfig?.auto_unmute) { + super._autoUnmuteHandler(); } } @@ -322,7 +330,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { ? [ AutoMediaPlugin({ playerSelector: 'frigate-card-ha-hls-player', - autoplayWhenVisible: !!this.viewerConfig?.autoplay_clip, + autoPlayWhenVisible: !!this.viewerConfig?.autoplay_clip, + autoUnmuteWhenVisible: !!this.viewerConfig?.auto_unmute, }), ] : []), diff --git a/src/const.ts b/src/const.ts index 81e50ada..860db7e8 100644 --- a/src/const.ts +++ b/src/const.ts @@ -33,6 +33,7 @@ export const CONF_EVENT_GALLERY_MIN_COLUMNS = `${CONF_EVENT_GALLERY}.min_columns export const CONF_EVENT_VIEWER = 'event_viewer' as const; export const CONF_EVENT_VIEWER_AUTOPLAY_CLIP = `${CONF_EVENT_VIEWER}.autoplay_clip` as const; +export const CONF_EVENT_VIEWER_AUTO_UNMUTE = `${CONF_EVENT_VIEWER}.auto_unmute` as const; export const CONF_EVENT_VIEWER_DRAGGABLE = `${CONF_EVENT_VIEWER}.draggable` as const; export const CONF_EVENT_VIEWER_LAZY_LOAD = `${CONF_EVENT_VIEWER}.lazy_load` as const; export const CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE = @@ -49,6 +50,7 @@ export const CONF_EVENT_VIEWER_CONTROLS_TITLE_DURATION_SECONDS = `${CONF_EVENT_VIEWER}.controls.title.duration_seconds` as const; export const CONF_LIVE = 'live' as const; +export const CONF_LIVE_AUTO_UNMUTE = `${CONF_LIVE}.auto_unmute` as const; export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE = `${CONF_LIVE}.controls.next_previous.style` as const; export const CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE = diff --git a/src/editor.ts b/src/editor.ts index 328f377c..86b79491 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -29,6 +29,7 @@ import { CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_EVENT_GALLERY_MIN_COLUMNS, CONF_EVENT_VIEWER_AUTOPLAY_CLIP, + CONF_EVENT_VIEWER_AUTO_UNMUTE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_EVENT_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_EVENT_VIEWER_CONTROLS_THUMBNAILS_MODE, @@ -39,6 +40,7 @@ import { CONF_EVENT_VIEWER_LAZY_LOAD, CONF_IMAGE_REFRESH_SECONDS, CONF_IMAGE_SRC, + CONF_LIVE_AUTO_UNMUTE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA, @@ -240,8 +242,14 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor protected _titleModes = new Map([ ['', ''], ['none', localize('config.event_viewer.controls.title.modes.none')], - ['popup-top-left', localize('config.event_viewer.controls.title.modes.popup-top-left')], - ['popup-top-right', localize('config.event_viewer.controls.title.modes.popup-top-right')], + [ + 'popup-top-left', + localize('config.event_viewer.controls.title.modes.popup-top-left'), + ], + [ + 'popup-top-right', + localize('config.event_viewer.controls.title.modes.popup-top-right'), + ], [ 'popup-bottom-left', localize('config.event_viewer.controls.title.modes.popup-bottom-left'), @@ -757,6 +765,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderSwitch(CONF_LIVE_DRAGGABLE, defaults.live.draggable)} ${this._renderSwitch(CONF_LIVE_LAZY_LOAD, defaults.live.lazy_load)} ${this._renderSwitch(CONF_LIVE_LAZY_UNLOAD, defaults.live.lazy_unload)} + ${this._renderSwitch(CONF_LIVE_AUTO_UNMUTE, defaults.live.auto_unmute)} ${this._renderDropdown( CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, this._liveNextPreviousControlStyles, @@ -771,14 +780,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor this._thumbnailMedias, )} ${this._renderStringInput(CONF_LIVE_CONTROLS_THUMBNAILS_SIZE)} - ${this._renderDropdown( - CONF_LIVE_CONTROLS_TITLE_MODE, - this._titleModes, - )} - ${this._renderStringInput( - CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, - 'number', - )} + ${this._renderDropdown(CONF_LIVE_CONTROLS_TITLE_MODE, this._titleModes)} + ${this._renderStringInput( + CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, + 'number', + )} ` : ''} @@ -801,6 +807,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_EVENT_VIEWER_AUTOPLAY_CLIP, defaults.event_viewer.autoplay_clip, )} + ${this._renderSwitch( + CONF_EVENT_VIEWER_AUTO_UNMUTE, + defaults.event_viewer.auto_unmute, + )} ${this._renderSwitch( CONF_EVENT_VIEWER_DRAGGABLE, defaults.event_viewer.draggable, diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 9fd0174e..e39b1393 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -54,6 +54,7 @@ }, "event_viewer": { "autoplay_clip": "Autoplay clips", + "auto_unmute": "Automatically unmute media", "draggable": "Event Viewer can be dragged/swiped", "lazy_load": "Event Viewer media is lazily loaded in carousel", "controls": { @@ -93,6 +94,7 @@ "draggable": "Live cameras view can be dragged/swiped", "lazy_load": "Live cameras are lazily loaded", "lazy_unload": "Live cameras are lazily unloaded", + "auto_unmute": "Automatically unmute live cameras", "controls": { "next_previous": { "style": "Live view next & previous control style", diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index a94874d9..54b22bb9 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -56,6 +56,20 @@ customElements.whenDefined('ha-camera-stream').then(() => { this._playerRef.value?.pause(); } + /** + * Mute the video. + */ + public mute(): void { + this.muted = true; + } + + /** + * Unmute the video. + */ + public unmute(): void { + this.muted = false; + } + /** * Master render method. * @returns A rendered template. @@ -82,7 +96,6 @@ customElements.whenDefined('ha-camera-stream').then(() => { ? html` { this._videoRef.value?.pause(); } + /** + * Mute the video. + */ + public mute(): void { + this.muted = true; + } + + /** + * Unmute the video. + */ + public unmute(): void { + this.muted = false; + } + // ===================================================================================== // Minor modifications from: // - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts @@ -42,7 +56,6 @@ customElements.whenDefined('ha-hls-player').then(() => { return html`