Merge pull request #274 from dermotduffy/pause-clips

Support autoplay & autopause for live views and event viewer
This commit is contained in:
Dermot Duffy
2022-01-22 19:41:06 -08:00
committed by GitHub
9 changed files with 403 additions and 159 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
"@material/rtl": "^13.0.0",
"custom-card-helpers": "^1.8.0",
"dayjs": "^1.10.7",
"embla-carousel": "^6.1.0",
"embla-carousel": "^6.1.1",
"home-assistant-js-websocket": "^5.11.1",
"lit": "^2.0.2",
"lodash-es": "^4.17.21",
+22 -2
View File
@@ -1,5 +1,9 @@
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';
@@ -11,6 +15,7 @@ export interface CarouselSelect {
export class FrigateCardCarousel extends LitElement {
protected _carousel?: EmblaCarouselType;
protected _plugins: Record<string, EmblaPluginType> = {};
/**
* Scroll to a particular slide.
@@ -53,10 +58,19 @@ 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();
}
this._plugins = {};
this._carousel = undefined;
}
@@ -69,7 +83,13 @@ export class FrigateCardCarousel extends LitElement {
) as HTMLElement;
if (carouselNode) {
this._carousel = EmblaCarousel(carouselNode, this._getOptions());
const plugins = this._getPlugins() ?? [];
this._plugins = plugins.reduce((acc, cur) => {
acc[cur.name] = cur;
return acc;
}, {});
this._carousel = EmblaCarousel(carouselNode, this._getOptions(), plugins);
this._carousel.on('init', () => dispatchFrigateCardEvent(this, 'carousel:init'));
this._carousel.on('select', () => {
const selected = this.carouselSelected();
+91
View File
@@ -0,0 +1,91 @@
import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel';
export type LazyloadOptionsType = {
count?: number;
lazyloadCallback: (index: number, slide: HTMLElement) => void;
};
export const defaultOptions: Partial<LazyloadOptionsType> = {
count: 0,
};
export type LazyloadType = EmblaPluginType<LazyloadOptionsType> & {
hasLazyloaded: (index: number) => boolean;
};
export function Lazyload(userOptions?: LazyloadOptionsType): LazyloadType {
const options = Object.assign({}, defaultOptions, userOptions);
let carousel: EmblaCarouselType;
let slides: HTMLElement[];
const isSlideLazyloaded: Record<number, boolean> = {};
/**
* Initialize the plugin.
*/
function init(embla: EmblaCarouselType): void {
carousel = embla;
slides = carousel.slideNodes();
carousel.on('init', lazyLoadHandler);
carousel.on('select', lazyLoadHandler);
carousel.on('resize', lazyLoadHandler);
}
/**
* Destroy the plugin.
*/
function destroy(): void {
carousel.off('init', lazyLoadHandler);
carousel.off('select', lazyLoadHandler);
carousel.off('resize', lazyLoadHandler);
}
/**
* Determine if a slide index has been lazily loaded.
* @param index Slide index.
* @returns `true` if the slide has been lazily loaded.
*/
function hasLazyloaded(index: number): boolean {
return !!isSlideLazyloaded[index];
}
/**
* Lazily load media in the carousel.
*/
function lazyLoadHandler(): void {
const lazyLoadCount = options.count ?? 0;
const slidesInView = carousel.slidesInView(true);
const slidesToLoad = new Set<number>();
const minSlide = Math.min(...slidesInView);
const maxSlide = Math.max(...slidesInView);
// Lazily load 'count' slides on either side of the slides in view.
for (let i = 1; i <= lazyLoadCount && minSlide - i >= 0; i++) {
slidesToLoad.add(minSlide - i);
}
slidesInView.forEach((index) => slidesToLoad.add(index));
for (let i = 1; i <= lazyLoadCount && maxSlide + i < slides.length; i++) {
slidesToLoad.add(maxSlide + i);
}
slidesToLoad.forEach((index) => {
// Only lazy load slides that are not already loaded.
if (isSlideLazyloaded[index]) {
return;
}
isSlideLazyloaded[index] = true;
options.lazyloadCallback(index, slides[index]);
});
}
const self: LazyloadType = {
name: 'Lazyload',
options,
init,
destroy,
hasLazyloaded,
};
return self;
}
@@ -0,0 +1,100 @@
import { EmblaCarouselType, EmblaPluginType } from 'embla-carousel';
import { FrigateCardMediaPlayer } from '../../types';
export type MediaAutoPlayPauseOptionsType = {
autoplay?: boolean;
autopause?: boolean;
playerSelector: string;
};
export const defaultOptions: Partial<MediaAutoPlayPauseOptionsType> = {
// Frigate card media autoplays when the media loads, not necessarily when the
// slide is selected.
autoplay: false,
autopause: true,
};
export type MediaAutoPlayPauseType = EmblaPluginType<MediaAutoPlayPauseOptionsType> & {
play: () => void;
}
export function MediaAutoPlayPause(
userOptions?: MediaAutoPlayPauseOptionsType,
): MediaAutoPlayPauseType {
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', pausePrevious);
}
if (options.autoplay) {
carousel.on('select', play);
carousel.on('init', play);
}
}
/**
* Destroy the plugin.
*/
function destroy(): void {
if (options.autopause) {
carousel.off('destroy', pauseAllHandler);
carousel.off('select', pausePrevious);
}
if (options.autoplay) {
carousel.off('select', play);
carousel.off('init', play);
}
}
/**
* Get the media player from a slide.
* @param slide
* @returns A FrigateCardMediaPlayer object or `null`.
*/
function getPlayer(slide: HTMLElement | undefined): 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 play(): void {
getPlayer(slides[carousel.selectedScrollSnap()])?.play();
}
/**
* Autopause the previous slide.
*/
function pausePrevious(): void {
getPlayer(slides[carousel.previousScrollSnap()])?.pause();
}
const self: MediaAutoPlayPauseType = {
name: 'MediaAutoPlayPause',
options,
init,
destroy,
play,
};
return self;
}
+103 -4
View File
@@ -19,16 +19,18 @@ 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 { Lazyload } from './embla-plugins/lazyload.js';
import { MediaAutoPlayPause } 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 +293,25 @@ 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 [
...(this.liveConfig?.lazy_load
? [
Lazyload({
lazyloadCallback: this._lazyLoadSlide.bind(this),
}),
]
: []),
MediaAutoPlayPause({
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
@@ -381,7 +402,7 @@ export class FrigateCardLiveCarousel extends FrigateCardMediaCarousel {
return html` <div class="embla__slide">
<frigate-card-live-provider
?disabled=${this._isLazyLoading()}
?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig}
.label=${getCameraTitle(this.hass, cameraConfig)}
.liveConfig=${config}
@@ -511,6 +532,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 +584,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 +616,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 +649,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 +678,32 @@ export class FrigateCardLiveWebRTC extends LitElement {
protected hass?: HomeAssistant & ExtendedHomeAssistant;
/**
* Play the video.
*/
public play(): void {
this._getPlayer()?.play().catch(() => {
// WebRTC appears to generate additional spurious load events, which may
// result in loads after a play() call, which causes the browser to spam
// the logs unless the promise rejection is handled here.
})
}
/**
* 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 +760,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 +810,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.
+18 -75
View File
@@ -15,6 +15,7 @@ import './next-prev-control.js';
import mediaCarouselStyle from '../scss/media-carousel.scss';
import { FrigateCardNextPreviousControl } from './next-prev-control.js';
import { MediaAutoPlayPauseType } from './embla-plugins/media-autoplay.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`;
@@ -24,31 +25,31 @@ export const IMG_EMPTY = getEmptyImageSrc(16, 9);
export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// A "map" from slide number to MediaShowInfo object.
protected _mediaShowInfo: Record<number, MediaShowInfo> = {};
// Whether or not a given slide has been successfully lazily loaded.
protected _slideHasBeenLazyLoaded: Record<number, boolean> = {};
protected _nextControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
protected _previousControlRef: Ref<FrigateCardNextPreviousControl> = createRef();
/**
* 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
* should lazy load, etc. `null` means lazy loading is disabled and everything
* should load simultaneously.
* @returns
* Play the media on the selected slide. May be overridden to control when
* autoplay should happen.
*/
protected _getLazyLoadCount(): number | null {
// Defaults to fully-lazy loading.
return 0;
protected _autoplayHandler(): void {
(this._plugins['MediaAutoPlayPause'] as MediaAutoPlayPauseType | undefined)?.play();
}
/**
* Determine if lazy loading is being used.
* @returns `true` is lazy loading is in use.
* Component connected callback.
*/
protected _isLazyLoading(): boolean {
return this._getLazyLoadCount() !== null;
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('frigate-card:media-show', this._autoplayHandler);
}
/**
* Component disconnected callback.
*/
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener('frigate-card:media-show', this._autoplayHandler);
}
protected _destroyCarousel(): void {
@@ -61,9 +62,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
// media would not reload) -- as such, leave this alone on carousel
// destroy. New media in that slide will replace the prior contents on
// load.
// * this._slideHasBeenLazyLoaded: This is a performance optimization and
// can be safely reset.
this._slideHasBeenLazyLoaded = {};
}
/**
@@ -92,13 +90,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
carousel?.on('init', this._adaptiveHeightSetHandler.bind(this));
carousel?.on('select', this._adaptiveHeightSetHandler.bind(this));
carousel?.on('resize', this._adaptiveHeightSetHandler.bind(this));
if (this._getLazyLoadCount() != null) {
// Load media as the carousel is moved (if lazy loading is in use).
carousel?.on('init', this._lazyLoadMediaHandler.bind(this));
carousel?.on('select', this._lazyLoadMediaHandler.bind(this));
carousel?.on('resize', this._lazyLoadMediaHandler.bind(this));
}
}
/**
@@ -165,54 +156,6 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
}
}
/**
* Lazily load media in the carousel.
*/
protected _lazyLoadMediaHandler(): void {
if (!this._carousel) {
return;
}
const lazyLoadCount = this._getLazyLoadCount();
if (lazyLoadCount === null) {
return;
}
const slides = this._carousel.slideNodes();
const slidesInView = this._carousel.slidesInView(true);
const slidesToLoad = new Set<number>();
const minSlide = Math.min(...slidesInView);
const maxSlide = Math.max(...slidesInView);
// Lazily load 'lazyLoadCount' slides on either side of the slides in view.
for (let i = 1; i <= lazyLoadCount && minSlide - i >= 0; i++) {
slidesToLoad.add(minSlide - i);
}
slidesInView.forEach((index) => slidesToLoad.add(index));
for (let i = 1; i <= lazyLoadCount && maxSlide + i < slides.length; i++) {
slidesToLoad.add(maxSlide + i);
}
slidesToLoad.forEach((index) => {
// Only lazy load slides that are not already loaded.
if (this._slideHasBeenLazyLoaded[index]) {
return;
}
this._slideHasBeenLazyLoaded[index] = true;
this._lazyLoadSlide(index, slides[index]);
});
}
/**
* Lazy load a slide.
* @param _index The index of the slide to lazy load.
* @param _slide The slide to lazy load.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _lazyLoadSlide(_index: number, _slide: HTMLElement): void {
// To be overridden in children.
}
/**
* Fire a media show event when a slide is selected.
*/
@@ -282,7 +225,7 @@ export class FrigateCardMediaCarousel extends FrigateCardCarousel {
* available.
*/
const firstMediaLoad = !Object.keys(this._mediaShowInfo).length;
if (firstMediaLoad && this._getLazyLoadCount() != null) {
if (firstMediaLoad) {
const replacementImageSrc = getEmptyImageSrc(
mediaShowInfo.width,
mediaShowInfo.height,
+40 -77
View File
@@ -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,8 @@ import {
FrigateCardThumbnailCarousel,
ThumbnailCarouselTap,
} from './thumbnail-carousel.js';
import { MediaAutoPlayPause } from './embla-plugins/media-autoplay.js';
import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js';
import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js';
import { View } from '../view.js';
import {
@@ -215,7 +217,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
async ([target]: (BrowseMediaSource | undefined)[]): Promise<void> => {
for (
let i = 0;
!this._isLazyLoading() &&
!this.viewerConfig?.lazy_load &&
this.hass &&
target &&
target.children &&
@@ -263,6 +265,16 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
super.updated(changedProperties);
}
/**
* Play the media on the selected slide. May be overridden to control when
* autoplay should happen.
*/
protected _autoplayHandler(): void {
if (this.viewerConfig?.autoplay_clip) {
super._autoplayHandler();
}
}
protected _destroyCarousel(): void {
super._destroyCarousel();
@@ -294,15 +306,28 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
}
/**
* 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
* should lazy load, etc. `null` means lazy loading is disabled and everything
* should load simultaneously.
* @returns
* Get the Embla plugins to use.
* @returns An EmblaOptionsType object or undefined for no options.
*/
protected _getLazyLoadCount(): number | null {
// Defaults to fully-lazy loading.
return this.viewerConfig?.lazy_load === false ? null : 0;
protected _getPlugins(): EmblaPluginType[] | undefined {
return [
...(this.viewerConfig?.lazy_load
? [
Lazyload({
lazyloadCallback: this._lazyLoadSlide.bind(this),
}),
]
: []),
// Don't need autoplay/pause for snapshots.
...(this.view?.is('clip')
? [
MediaAutoPlayPause({
playerSelector: 'frigate-card-ha-hls-player',
}),
]
: []),
];
}
/**
@@ -532,70 +557,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
@@ -644,7 +605,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
// If lazy loading is not enabled, wait for the media resolver task to
// complete and show a progress indictator until this.
if (!this._isLazyLoading() && !this._isMediaFullyResolved()) {
if (!this.viewerConfig?.lazy_load && !this._isMediaFullyResolved()) {
return html`${this._mediaResolutionTask.render({
initial: () => renderProgressIndicator(),
pending: () => renderProgressIndicator(),
@@ -711,7 +672,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
return;
}
const lazyLoad = this._isLazyLoading();
const lazyLoad = this.viewerConfig.lazy_load;
const resolvedMedia = this.resolvedMediaCache?.get(mediaToRender.media_content_id);
if (!resolvedMedia && !lazyLoad) {
return;
@@ -754,7 +715,9 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
// images in media-carousel.ts). Here we need to only call the
// media load handler on a 'real' load.
!lazyLoad ||
this._slideHasBeenLazyLoaded[slideIndex]
(this._plugins['Lazyload'] as LazyloadType | undefined)?.hasLazyloaded(
slideIndex,
)
) {
this._mediaLoadedHandler(slideIndex, createMediaShowInfo(e));
}
+23
View File
@@ -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}
+5
View File
@@ -764,6 +764,11 @@ export interface StateParameters {
style?: StyleInfo;
}
export interface FrigateCardMediaPlayer {
play(): void;
pause(): void;
}
/**
* Home Assistant API types.
*/