Rework autoplay/pause into a plugin, apply to viewer & live.

This commit is contained in:
Dermot Duffy
2022-01-21 22:44:30 -08:00
parent 77a8127848
commit 05bcbbb16b
6 changed files with 235 additions and 70 deletions
+10 -2
View File
@@ -1,5 +1,5 @@
import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; 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'; import { dispatchFrigateCardEvent } from '../common';
@@ -53,6 +53,14 @@ export class FrigateCardCarousel extends LitElement {
return undefined; 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 { protected _destroyCarousel(): void {
if (this._carousel) { if (this._carousel) {
this._carousel.destroy(); this._carousel.destroy();
@@ -69,7 +77,7 @@ export class FrigateCardCarousel extends LitElement {
) as HTMLElement; ) as HTMLElement;
if (carouselNode) { 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('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('select', () => { this._carousel.on('select', () => {
const selected = this.carouselSelected(); const selected = this.carouselSelected();
@@ -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<MediaAutoplayOptionsType> = {
autoplay: true,
autopause: true,
};
export type MediaAutoplayType = EmblaPluginType<MediaAutoplayOptionsType>;
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;
}
+90 -3
View File
@@ -19,16 +19,17 @@ import {
LiveProvider, LiveProvider,
frigateCardConfigDefaults, frigateCardConfigDefaults,
} from '../types.js'; } from '../types.js';
import { EmblaOptionsType } from 'embla-carousel'; import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { ref } from 'lit/directives/ref';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
import { BrowseMediaUtil } from '../browse-media-util.js'; import { BrowseMediaUtil } from '../browse-media-util.js';
import { ConditionState, getOverriddenConfig } from '../card-condition.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js';
import { FrigateCardMediaCarousel } from './media-carousel.js'; import { FrigateCardMediaCarousel } from './media-carousel.js';
import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import { MediaAutoplay } from './embla-plugins/media-autoplay.js';
import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
import { View } from '../view.js'; import { View } from '../view.js';
import { localize } from '../localize/localize.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 * 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 * 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 }) @property({ attribute: false })
public label = ''; 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 { protected _getResolvedProvider(): LiveProvider {
if (this.cameraConfig?.live_provider === 'auto') { if (this.cameraConfig?.live_provider === 'auto') {
if (this.cameraConfig?.webrtc?.entity || this.cameraConfig?.webrtc?.url) { if (this.cameraConfig?.webrtc?.entity || this.cameraConfig?.webrtc?.url) {
@@ -545,18 +576,21 @@ export class FrigateCardLiveProvider extends LitElement {
return html` return html`
${provider == 'frigate' ${provider == 'frigate'
? html` <frigate-card-live-frigate ? html` <frigate-card-live-frigate
${ref(this._providerRef)}
.hass=${this.hass} .hass=${this.hass}
.cameraEntity=${this.cameraConfig.camera_entity} .cameraEntity=${this.cameraConfig.camera_entity}
> >
</frigate-card-live-frigate>` </frigate-card-live-frigate>`
: provider == 'webrtc' : provider == 'webrtc'
? html`<frigate-card-live-webrtc ? html`<frigate-card-live-webrtc
${ref(this._providerRef)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.webRTCConfig=${this.liveConfig.webrtc} .webRTCConfig=${this.liveConfig.webrtc}
> >
</frigate-card-live-webrtc>` </frigate-card-live-webrtc>`
: html` <frigate-card-live-jsmpeg : html` <frigate-card-live-jsmpeg
${ref(this._providerRef)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.jsmpegConfig=${this.liveConfig.jsmpeg} .jsmpegConfig=${this.liveConfig.jsmpeg}
@@ -574,6 +608,22 @@ export class FrigateCardLiveFrigate extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
protected cameraEntity?: string; protected cameraEntity?: string;
protected _playerRef: Ref<FrigateCardLiveFrigate> = createRef();
/**
* Play the video.
*/
public play(): void {
this._playerRef.value?.play();
}
/**
* Pause the video.
*/
public pause(): void {
this._playerRef.value?.pause();
}
/** /**
* Master render method. * Master render method.
* @returns A rendered template. * @returns A rendered template.
@@ -591,6 +641,7 @@ export class FrigateCardLiveFrigate extends LitElement {
); );
} }
return html` <frigate-card-ha-camera-stream return html` <frigate-card-ha-camera-stream
${ref(this._playerRef)}
.hass=${this.hass} .hass=${this.hass}
.stateObj=${this.hass.states[this.cameraEntity]} .stateObj=${this.hass.states[this.cameraEntity]}
.controls=${true} .controls=${true}
@@ -619,6 +670,28 @@ export class FrigateCardLiveWebRTC extends LitElement {
protected hass?: HomeAssistant & ExtendedHomeAssistant; protected hass?: HomeAssistant & ExtendedHomeAssistant;
/**
* Play the video.
*/
public play(): void {
this._getPlayer()?.play();
}
/**
* Pause the video.
*/
public pause(): void {
this._getPlayer()?.pause();
}
/**
* Get the underlying video player.
* @returns The player or `null` if not found.
*/
protected _getPlayer(): HTMLVideoElement | null {
return this.renderRoot.querySelector('#video') as HTMLVideoElement | null;
}
/** /**
* Create the WebRTC element. May throw. * Create the WebRTC element. May throw.
*/ */
@@ -675,7 +748,7 @@ export class FrigateCardLiveWebRTC extends LitElement {
// Extract the video component after it has been rendered and generate the // Extract the video component after it has been rendered and generate the
// media load event. // media load event.
this.updateComplete.then(() => { this.updateComplete.then(() => {
const video = this.renderRoot.querySelector('#video') as HTMLVideoElement; const video = this._getPlayer();
if (video) { if (video) {
const onloadedmetadata = video.onloadedmetadata; const onloadedmetadata = video.onloadedmetadata;
const onplay = video.onplay; const onplay = video.onplay;
@@ -725,6 +798,20 @@ export class FrigateCardLiveJSMPEG extends LitElement {
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement; protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
protected _refreshPlayerTimerID?: number; 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. * Get a signed player URL.
* @returns A URL or null. * @returns A URL or null.
+12 -65
View File
@@ -7,7 +7,7 @@ import {
unsafeCSS, unsafeCSS,
} from 'lit'; } from 'lit';
import { BrowseMediaUtil } from '../browse-media-util.js'; 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 { HomeAssistant } from 'custom-card-helpers';
import { Task } from '@lit-labs/task'; import { Task } from '@lit-labs/task';
import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { createRef, Ref, ref } from 'lit/directives/ref.js';
@@ -29,6 +29,7 @@ import {
FrigateCardThumbnailCarousel, FrigateCardThumbnailCarousel,
ThumbnailCarouselTap, ThumbnailCarouselTap,
} from './thumbnail-carousel.js'; } from './thumbnail-carousel.js';
import { MediaAutoplay } from './embla-plugins/media-autoplay.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js'; import { View } from '../view.js';
import { 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 * 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 * 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. * Get slides to include in the render.
* @returns The slides to include in the render and an index keyed by slide * @returns The slides to include in the render and an index keyed by slide
+23
View File
@@ -9,6 +9,7 @@
// available as compilation time. // available as compilation time.
// ==================================================================== // ====================================================================
import { Ref, createRef, ref } from 'lit/directives/ref';
import { TemplateResult, css, html } from 'lit'; import { TemplateResult, css, html } from 'lit';
import { customElement } from 'lit/decorators.js'; import { customElement } from 'lit/decorators.js';
import { dispatchMediaShowEvent } from '../common.js'; import { dispatchMediaShowEvent } from '../common.js';
@@ -34,10 +35,31 @@ customElements.whenDefined('ha-camera-stream').then(() => {
@customElement('frigate-card-ha-camera-stream') @customElement('frigate-card-ha-camera-stream')
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
class FrigateCardHaCameraStream extends customElements.get('ha-camera-stream') { class FrigateCardHaCameraStream extends customElements.get('ha-camera-stream') {
protected _playerRef: Ref<HTMLElement> = createRef();
// ======================================================================================== // ========================================================================================
// Minor modifications from: // Minor modifications from:
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-camera-stream.ts // - 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 { protected render(): TemplateResult {
if (!this.stateObj) { if (!this.stateObj) {
return html``; return html``;
@@ -59,6 +81,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
: this._url : this._url
? html` ? html`
<frigate-card-ha-hls-player <frigate-card-ha-hls-player
${ref(this._playerRef)}
autoplay autoplay
playsinline playsinline
.allowExoPlayer=${this.allowExoPlayer} .allowExoPlayer=${this.allowExoPlayer}
+5
View File
@@ -764,6 +764,11 @@ export interface StateParameters {
style?: StyleInfo; style?: StyleInfo;
} }
export interface FrigateCardMediaPlayer {
play(): void;
pause(): void;
}
/** /**
* Home Assistant API types. * Home Assistant API types.
*/ */