Rework autoplay/pause into a plugin, apply to viewer & live.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
@@ -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` <frigate-card-live-frigate
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraEntity=${this.cameraConfig.camera_entity}
|
||||
>
|
||||
</frigate-card-live-frigate>`
|
||||
: provider == 'webrtc'
|
||||
? html`<frigate-card-live-webrtc
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.webRTCConfig=${this.liveConfig.webrtc}
|
||||
>
|
||||
</frigate-card-live-webrtc>`
|
||||
: html` <frigate-card-live-jsmpeg
|
||||
${ref(this._providerRef)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.jsmpegConfig=${this.liveConfig.jsmpeg}
|
||||
@@ -574,6 +608,22 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
@property({ attribute: false })
|
||||
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.
|
||||
* @returns A rendered template.
|
||||
@@ -591,6 +641,7 @@ export class FrigateCardLiveFrigate extends LitElement {
|
||||
);
|
||||
}
|
||||
return html` <frigate-card-ha-camera-stream
|
||||
${ref(this._playerRef)}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass.states[this.cameraEntity]}
|
||||
.controls=${true}
|
||||
@@ -619,6 +670,28 @@ export class FrigateCardLiveWebRTC extends LitElement {
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -675,7 +748,7 @@ export class FrigateCardLiveWebRTC extends LitElement {
|
||||
// Extract the video component after it has been rendered and generate the
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
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.
|
||||
|
||||
+12
-65
@@ -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
|
||||
|
||||
@@ -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<HTMLElement> = 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`
|
||||
<frigate-card-ha-hls-player
|
||||
${ref(this._playerRef)}
|
||||
autoplay
|
||||
playsinline
|
||||
.allowExoPlayer=${this.allowExoPlayer}
|
||||
|
||||
@@ -764,6 +764,11 @@ export interface StateParameters {
|
||||
style?: StyleInfo;
|
||||
}
|
||||
|
||||
export interface FrigateCardMediaPlayer {
|
||||
play(): void;
|
||||
pause(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Home Assistant API types.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user