diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 11e4bab0..ae8f2c7c 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,5 +1,5 @@ import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; -import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel'; +import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { dispatchFrigateCardEvent } from '../common'; @@ -53,6 +53,14 @@ export class FrigateCardCarousel extends LitElement { return undefined; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return undefined; + } + protected _destroyCarousel(): void { if (this._carousel) { this._carousel.destroy(); @@ -69,7 +77,7 @@ export class FrigateCardCarousel extends LitElement { ) as HTMLElement; if (carouselNode) { - this._carousel = EmblaCarousel(carouselNode, this._getOptions()); + this._carousel = EmblaCarousel(carouselNode, this._getOptions(), this._getPlugins()); this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init')); this._carousel.on('select', () => { const selected = this.carouselSelected(); diff --git a/src/components/embla-plugins/media-autoplay.ts b/src/components/embla-plugins/media-autoplay.ts new file mode 100644 index 00000000..d4a1a7e8 --- /dev/null +++ b/src/components/embla-plugins/media-autoplay.ts @@ -0,0 +1,95 @@ +import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel'; +import { FrigateCardMediaPlayer } from '../../types'; + +export type MediaAutoplayOptionsType = { + autoplay?: boolean; + autopause?: boolean; + playerSelector: string; +}; + +export const defaultOptions: Partial = { + autoplay: true, + autopause: true, +}; + +export type MediaAutoplayType = EmblaPluginType; + +export function MediaAutoplay( + userOptions?: MediaAutoplayOptionsType, +): MediaAutoplayType { + const options = Object.assign({}, defaultOptions, userOptions); + + let carousel: EmblaCarouselType; + let slides: HTMLElement[]; + + /** + * Initialize the plugin. + */ + function init(embla: EmblaCarouselType): void { + carousel = embla; + slides = carousel.slideNodes(); + + if (options.autopause) { + carousel.on('destroy', pauseAllHandler); + carousel.on('select', autopausePreviousHandler); + } + + if (options.autoplay) { + carousel.on('select', autoplayCurrentHandler); + carousel.on('init', autoplayCurrentHandler); + } + } + + /** + * Destroy the plugin. + */ + function destroy(): void { + if (options.autopause) { + carousel.off('destroy', pauseAllHandler); + carousel.off('select', autopausePreviousHandler); + } + + if (options.autoplay) { + carousel.off('select', autoplayCurrentHandler); + carousel.off('init', autoplayCurrentHandler); + } + } + + /** + * Get the media player from a slide. + * @param slide + * @returns A FrigateCardMediaPlayer object or `null`. + */ + function getPlayer(slide: HTMLElement): FrigateCardMediaPlayer | null { + return slide.querySelector(options.playerSelector) as FrigateCardMediaPlayer | null; + } + + /** + * Pause all clips. + */ + function pauseAllHandler(): void { + slides.forEach((slide) => getPlayer(slide)?.pause()); + } + + /** + * Autoplay the current slide. + */ + function autoplayCurrentHandler(): void { + getPlayer(slides[carousel.selectedScrollSnap()])?.play(); + } + + /** + * Autopause the previous slide. + */ + function autopausePreviousHandler(): void { + getPlayer(slides[carousel.previousScrollSnap()])?.pause(); + } + + const self: MediaAutoplayType = { + name: 'MediaAutoplay', + options, + init, + destroy, + }; + return self; +} diff --git a/src/components/live.ts b/src/components/live.ts index 3f05c191..718ae0ac 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -19,16 +19,17 @@ import { LiveProvider, frigateCardConfigDefaults, } from '../types.js'; -import { EmblaOptionsType } from 'embla-carousel'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; +import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { customElement, property, state } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref'; import { until } from 'lit/directives/until.js'; import { BrowseMediaUtil } from '../browse-media-util.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js'; import { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { View } from '../view.js'; import { localize } from '../localize/localize.js'; @@ -291,6 +292,18 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel { }; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return [ + MediaAutoplay({ + playerSelector: 'frigate-card-live-provider', + }), + ]; + } + /** * 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 @@ -511,6 +524,24 @@ export class FrigateCardLiveProvider extends LitElement { @property({ attribute: false }) public label = ''; + protected _providerRef: Ref< + FrigateCardLiveFrigate | FrigateCardLiveJSMPEG | FrigateCardLiveWebRTC + > = createRef(); + + /** + * Play the video. + */ + public play(): void { + this._providerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._providerRef.value?.pause(); + } + protected _getResolvedProvider(): LiveProvider { if (this.cameraConfig?.live_provider === 'auto') { if (this.cameraConfig?.webrtc?.entity || this.cameraConfig?.webrtc?.url) { @@ -545,18 +576,21 @@ export class FrigateCardLiveProvider extends LitElement { return html` ${provider == 'frigate' ? html` ` : provider == 'webrtc' ? html` ` : html` = createRef(); + + /** + * Play the video. + */ + public play(): void { + this._playerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._playerRef.value?.pause(); + } + /** * Master render method. * @returns A rendered template. @@ -591,6 +641,7 @@ export class FrigateCardLiveFrigate extends LitElement { ); } return html` { - const video = this.renderRoot.querySelector('#video') as HTMLVideoElement; + const video = this._getPlayer(); if (video) { const onloadedmetadata = video.onloadedmetadata; const onplay = video.onplay; @@ -725,6 +798,20 @@ export class FrigateCardLiveJSMPEG extends LitElement { protected _jsmpegVideoPlayer?: JSMpeg.VideoElement; protected _refreshPlayerTimerID?: number; + /** + * Play the video. + */ + public play(): void { + this._jsmpegVideoPlayer?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._jsmpegVideoPlayer?.stop(); + } + /** * Get a signed player URL. * @returns A URL or null. diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 1d4686d8..920d331e 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -7,7 +7,7 @@ import { unsafeCSS, } from 'lit'; import { BrowseMediaUtil } from '../browse-media-util.js'; -import { EmblaOptionsType } from 'embla-carousel'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { HomeAssistant } from 'custom-card-helpers'; import { Task } from '@lit-labs/task'; import { createRef, Ref, ref } from 'lit/directives/ref.js'; @@ -29,6 +29,7 @@ import { FrigateCardThumbnailCarousel, ThumbnailCarouselTap, } from './thumbnail-carousel.js'; +import { MediaAutoplay } from './embla-plugins/media-autoplay.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { View } from '../view.js'; import { @@ -293,6 +294,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { }; } + /** + * Get the Embla plugins to use. + * @returns An EmblaOptionsType object or undefined for no options. + */ + protected _getPlugins(): EmblaPluginType[] | undefined { + return [MediaAutoplay({ + autoplay: this.viewerConfig?.autoplay_clip, + playerSelector: 'frigate-card-ha-hls-player' })]; + } + /** * 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 @@ -532,70 +543,6 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { } } - protected _playOrPauseClip(action: 'play' | 'pause', slide: HTMLElement): void { - const player = slide.querySelector('frigate-card-ha-hls-player') as - | (HTMLElement & { play: () => void; pause: () => void }) - | undefined; - if (player) { - if (action === 'play') { - player.play(); - } else if (action === 'pause') { - player.pause(); - } - } - } - - /** - * Pause all clips. - */ - protected _pauseAllHandler(): void { - if (this._carousel) { - this._carousel - .slideNodes() - .forEach((slide) => this._playOrPauseClip('pause', slide)); - } - } - - /** - * Play the clip being shown to the user and pause the prior. - */ - protected _autoplayPauseHandler(pausePrevious: boolean): void { - if (!this._carousel) { - return; - } - - const slides = this._carousel.slideNodes(); - - // Pause the previous/current slide. - if (pausePrevious) { - this._carousel - .slidesInView(false) - .forEach((slide) => { - this._playOrPauseClip('pause', slides[slide]) - }); - } - - // Play the target slide. - this._carousel - .slidesInView(true) - .forEach((slide) => { - this._playOrPauseClip('play', slides[slide]) - }); - } - - /** - * Initialize the carousel. - */ - protected _initCarousel(): void { - super._initCarousel(); - - if (this._carousel && this.viewerConfig && this.viewerConfig.autoplay_clip) { - this._carousel.on('destroy', () => this._pauseAllHandler()); - this._carousel.on('init', () => this._autoplayPauseHandler(false)); - this._carousel.on('select', () => this._autoplayPauseHandler(true)); - } - } - /** * Get slides to include in the render. * @returns The slides to include in the render and an index keyed by slide diff --git a/src/patches/ha-camera-stream.ts b/src/patches/ha-camera-stream.ts index 83408a43..a94874d9 100644 --- a/src/patches/ha-camera-stream.ts +++ b/src/patches/ha-camera-stream.ts @@ -9,6 +9,7 @@ // available as compilation time. // ==================================================================== +import { Ref, createRef, ref } from 'lit/directives/ref'; import { TemplateResult, css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { dispatchMediaShowEvent } from '../common.js'; @@ -34,10 +35,31 @@ customElements.whenDefined('ha-camera-stream').then(() => { @customElement('frigate-card-ha-camera-stream') // eslint-disable-next-line @typescript-eslint/no-unused-vars class FrigateCardHaCameraStream extends customElements.get('ha-camera-stream') { + protected _playerRef: Ref = createRef(); + // ======================================================================================== // Minor modifications from: // - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-camera-stream.ts // ======================================================================================== + + /** + * Play the video. + */ + public play(): void { + this._playerRef.value?.play(); + } + + /** + * Pause the video. + */ + public pause(): void { + this._playerRef.value?.pause(); + } + + /** + * Master render method. + * @returns A rendered template. + */ protected render(): TemplateResult { if (!this.stateObj) { return html``; @@ -59,6 +81,7 @@ customElements.whenDefined('ha-camera-stream').then(() => { : this._url ? html`