Add play/pause/mute/unmute Frigate card commands.
This commit is contained in:
@@ -5,11 +5,21 @@ export class CachedValueController<T> implements ReactiveController {
|
|||||||
protected _host: ReactiveControllerHost;
|
protected _host: ReactiveControllerHost;
|
||||||
protected _timerSeconds: number;
|
protected _timerSeconds: number;
|
||||||
protected _callback: () => T;
|
protected _callback: () => T;
|
||||||
|
protected _timerStartCallback?: () => void;
|
||||||
|
protected _timerStopCallback?: () => void;
|
||||||
protected _timerID?: number;
|
protected _timerID?: number;
|
||||||
|
|
||||||
constructor(host: ReactiveControllerHost, timerSeconds: number, callback: () => T) {
|
constructor(
|
||||||
|
host: ReactiveControllerHost,
|
||||||
|
timerSeconds: number,
|
||||||
|
callback: () => T,
|
||||||
|
timerStartCallback?: () => void,
|
||||||
|
timerStopCallback?: () => void,
|
||||||
|
) {
|
||||||
this._timerSeconds = timerSeconds;
|
this._timerSeconds = timerSeconds;
|
||||||
this._callback = callback;
|
this._callback = callback;
|
||||||
|
this._timerStartCallback = timerStartCallback;
|
||||||
|
this._timerStopCallback = timerStopCallback;
|
||||||
(this._host = host).addController(this);
|
(this._host = host).addController(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +58,7 @@ export class CachedValueController<T> implements ReactiveController {
|
|||||||
public stopTimer(): void {
|
public stopTimer(): void {
|
||||||
if (this._timerID !== undefined) {
|
if (this._timerID !== undefined) {
|
||||||
window.clearInterval(this._timerID);
|
window.clearInterval(this._timerID);
|
||||||
|
this._timerStopCallback?.();
|
||||||
}
|
}
|
||||||
this._timerID = undefined;
|
this._timerID = undefined;
|
||||||
}
|
}
|
||||||
@@ -59,6 +70,7 @@ export class CachedValueController<T> implements ReactiveController {
|
|||||||
this.stopTimer();
|
this.stopTimer();
|
||||||
|
|
||||||
if (this._timerSeconds > 0) {
|
if (this._timerSeconds > 0) {
|
||||||
|
this._timerStartCallback?.();
|
||||||
this._timerID = window.setInterval(() => {
|
this._timerID = window.setInterval(() => {
|
||||||
this.updateValue();
|
this.updateValue();
|
||||||
this._host.requestUpdate();
|
this._host.requestUpdate();
|
||||||
@@ -66,6 +78,10 @@ export class CachedValueController<T> implements ReactiveController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasTimer(): boolean {
|
||||||
|
return !!this._timerID;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Host has connected to the cache.
|
* Host has connected to the cache.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+49
@@ -714,6 +714,34 @@ class FrigateCard extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._currentMediaLoadedInfo && this._currentMediaLoadedInfo.player) {
|
||||||
|
if (this._currentMediaLoadedInfo.capabilities?.supportsPause) {
|
||||||
|
const paused = this._currentMediaLoadedInfo.player.isPaused();
|
||||||
|
buttons.push({
|
||||||
|
icon: paused ? 'mdi:play' : 'mdi:pause',
|
||||||
|
...this._getConfig().menu.buttons.play,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.play'),
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
paused ? 'play' : 'pause',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._currentMediaLoadedInfo.capabilities?.hasAudio) {
|
||||||
|
const muted = this._currentMediaLoadedInfo.player.isMuted();
|
||||||
|
buttons.push({
|
||||||
|
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
|
||||||
|
...this._getConfig().menu.buttons.mute,
|
||||||
|
type: 'custom:frigate-card-menu-icon',
|
||||||
|
title: localize('config.menu.buttons.mute'),
|
||||||
|
tap_action: createFrigateCardCustomAction(
|
||||||
|
muted ? 'unmute' : 'mute',
|
||||||
|
) as FrigateCardCustomAction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||||
style: this._getStyleFromActions(button),
|
style: this._getStyleFromActions(button),
|
||||||
...button,
|
...button,
|
||||||
@@ -1633,6 +1661,18 @@ class FrigateCard extends LitElement {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'mute':
|
||||||
|
this._currentMediaLoadedInfo?.player?.mute();
|
||||||
|
break;
|
||||||
|
case 'unmute':
|
||||||
|
this._currentMediaLoadedInfo?.player?.unmute();
|
||||||
|
break;
|
||||||
|
case 'play':
|
||||||
|
this._currentMediaLoadedInfo?.player?.play();
|
||||||
|
break;
|
||||||
|
case 'pause':
|
||||||
|
this._currentMediaLoadedInfo?.player?.pause();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||||
}
|
}
|
||||||
@@ -2157,6 +2197,15 @@ class FrigateCard extends LitElement {
|
|||||||
@frigate-card:view:change-context=${this._addViewContextHandler.bind(this)}
|
@frigate-card:view:change-context=${this._addViewContextHandler.bind(this)}
|
||||||
@frigate-card:media:loaded=${this._mediaLoadedHandler.bind(this)}
|
@frigate-card:media:loaded=${this._mediaLoadedHandler.bind(this)}
|
||||||
@frigate-card:media:unloaded=${this._mediaUnloadedHandler.bind(this)}
|
@frigate-card:media:unloaded=${this._mediaUnloadedHandler.bind(this)}
|
||||||
|
@frigate-card:media:volumechange=${
|
||||||
|
() => this.requestUpdate() /* Refresh mute menu button */
|
||||||
|
}
|
||||||
|
@frigate-card:media:play=${
|
||||||
|
() => this.requestUpdate() /* Refresh play/pause menu button */
|
||||||
|
}
|
||||||
|
@frigate-card:media:pause=${
|
||||||
|
() => this.requestUpdate() /* Refresh play/pause menu button */
|
||||||
|
}
|
||||||
@frigate-card:render=${() => this.requestUpdate()}
|
@frigate-card:render=${() => this.requestUpdate()}
|
||||||
>
|
>
|
||||||
${renderMenuAbove ? this._renderMenu() : ''}
|
${renderMenuAbove ? this._renderMenu() : ''}
|
||||||
|
|||||||
+16
-3
@@ -6,7 +6,7 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
@@ -20,13 +20,15 @@ import {
|
|||||||
CameraConfig,
|
CameraConfig,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
ImageViewConfig,
|
ImageViewConfig,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
import { isHassDifferent } from '../utils/ha';
|
import { isHassDifferent } from '../utils/ha';
|
||||||
import {
|
import {
|
||||||
createMediaLoadedInfo,
|
createMediaLoadedInfo,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
@@ -89,6 +91,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return !this._cachedValueController?.hasTimer() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the camera entity for the current camera configuration.
|
* Get the camera entity for the current camera configuration.
|
||||||
* @returns The entity or undefined if no camera entity is available.
|
* @returns The entity or undefined if no camera entity is available.
|
||||||
@@ -143,6 +149,8 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
this,
|
this,
|
||||||
this.imageConfig.refresh_seconds,
|
this.imageConfig.refresh_seconds,
|
||||||
this._getImageSource.bind(this),
|
this._getImageSource.bind(this),
|
||||||
|
() => dispatchMediaPlayEvent(this),
|
||||||
|
() => dispatchMediaPauseEvent(this),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout);
|
updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout);
|
||||||
@@ -285,7 +293,12 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
src=${live(src)}
|
src=${live(src)}
|
||||||
@load=${(ev: Event) => {
|
@load=${(ev: Event) => {
|
||||||
const mediaLoadedInfo = createMediaLoadedInfo(ev, { player: this });
|
const mediaLoadedInfo = createMediaLoadedInfo(ev, {
|
||||||
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: !!this.imageConfig?.refresh_seconds,
|
||||||
|
},
|
||||||
|
});
|
||||||
// Avoid the media being reported as repeatedly loading unless the
|
// Avoid the media being reported as repeatedly loading unless the
|
||||||
// media info changes.
|
// media info changes.
|
||||||
if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) {
|
if (mediaLoadedInfo && !isEqual(this._mediaLoadedInfo, mediaLoadedInfo)) {
|
||||||
|
|||||||
@@ -4,27 +4,33 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import { VideoRTC } from '../../external/go2rtc/video-rtc';
|
||||||
|
import { localize } from '../../localize/localize';
|
||||||
import liveMSEStyle from '../../scss/live-go2rtc.scss';
|
import liveMSEStyle from '../../scss/live-go2rtc.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
MicrophoneConfig,
|
MicrophoneConfig
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import '../image.js';
|
import { mayHaveAudio } from '../../utils/audio';
|
||||||
|
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
||||||
import {
|
import {
|
||||||
hideMediaControlsTemporarily,
|
hideMediaControlsTemporarily,
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS
|
||||||
} from '../../utils/media';
|
} from '../../utils/media';
|
||||||
import { dispatchMediaLoadedEvent } from '../../utils/media-info';
|
import {
|
||||||
import { localize } from '../../localize/localize';
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent,
|
||||||
|
dispatchMediaVolumeChangeEvent
|
||||||
|
} from '../../utils/media-info';
|
||||||
|
import '../image.js';
|
||||||
import { dispatchErrorMessageEvent } from '../message';
|
import { dispatchErrorMessageEvent } from '../message';
|
||||||
import { VideoRTC } from '../../external/go2rtc/video-rtc';
|
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
|
||||||
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
|
||||||
|
|
||||||
// Note (2023-02-18): Depending on the behavior of the player / browser is
|
// Note (2023-02-18): Depending on the behavior of the player / browser is
|
||||||
// possible this URL will need to be re-signed in order to avoid HA spamming
|
// possible this URL will need to be re-signed in order to avoid HA spamming
|
||||||
@@ -75,11 +81,7 @@ class FrigateCardGo2RTCPlayer extends VideoRTC {
|
|||||||
super.oninit();
|
super.oninit();
|
||||||
|
|
||||||
if (this.video) {
|
if (this.video) {
|
||||||
const onloadeddata = this.video.onloadeddata;
|
this.video.onloadeddata = () => {
|
||||||
this.video.onloadeddata = (e) => {
|
|
||||||
if (onloadeddata) {
|
|
||||||
onloadeddata.call(this.video, e);
|
|
||||||
}
|
|
||||||
hideMediaControlsTemporarily(this.video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
hideMediaControlsTemporarily(this.video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||||
dispatchMediaLoadedEvent(this, this.video, {
|
dispatchMediaLoadedEvent(this, this.video, {
|
||||||
player: this._containingPlayer,
|
player: this._containingPlayer,
|
||||||
@@ -89,10 +91,15 @@ class FrigateCardGo2RTCPlayer extends VideoRTC {
|
|||||||
// that can be created after the fact -- this is purely saying that
|
// that can be created after the fact -- this is purely saying that
|
||||||
// were a microphone stream available it could be used usefully.
|
// were a microphone stream available it could be used usefully.
|
||||||
supports2WayAudio: !!this.pc,
|
supports2WayAudio: !!this.pc,
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(this.video),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
this.video.onvolumechange = () => dispatchMediaVolumeChangeEvent(this),
|
||||||
|
this.video.onplay = () => dispatchMediaPlayEvent(this),
|
||||||
|
this.video.onpause = () => dispatchMediaPauseEvent(this),
|
||||||
|
|
||||||
// Always started muted. Media may be unmuted in accordance with user
|
// Always started muted. Media may be unmuted in accordance with user
|
||||||
// configuration.
|
// configuration.
|
||||||
this.video.muted = true;
|
this.video.muted = true;
|
||||||
@@ -233,6 +240,10 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._player?.video.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._player = undefined;
|
this._player = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
this._playerRef.value?.setControls(controls);
|
this._playerRef.value?.setControls(controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._playerRef.value?.isPaused() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
await this._refImage.value?.setControls(controls);
|
await this._refImage.value?.setControls(controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._refImage.value?.isPaused() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.cameraConfig) {
|
if (!this.hass || !this.cameraConfig) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import JSMpeg from '@cycjimmy/jsmpeg-player';
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import { renderProgressIndicator } from '../../components/message.js';
|
import { renderProgressIndicator } from '../../components/message.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
||||||
@@ -9,12 +10,15 @@ import {
|
|||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
|
||||||
import { dispatchErrorMessageEvent } from '../message.js';
|
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
|
||||||
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint.js';
|
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint.js';
|
||||||
|
import {
|
||||||
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent
|
||||||
|
} from '../../utils/media-info.js';
|
||||||
|
import { dispatchErrorMessageEvent } from '../message.js';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
@@ -75,6 +79,10 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
// Not implemented.
|
// Not implemented.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._jsmpegVideoPlayer?.player?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSMPEG player.
|
* Create a JSMPEG player.
|
||||||
* @param url The URL for the player to connect to.
|
* @param url The URL for the player to connect to.
|
||||||
@@ -113,10 +121,15 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
|||||||
videoDecoded = true;
|
videoDecoded = true;
|
||||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement, {
|
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement, {
|
||||||
player: this,
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
resolve(player);
|
resolve(player);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPlay: () => dispatchMediaPlayEvent(this),
|
||||||
|
onPause: () => dispatchMediaPauseEvent(this),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
FrigateCardError,
|
FrigateCardError,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
|
import {
|
||||||
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent,
|
||||||
|
dispatchMediaVolumeChangeEvent,
|
||||||
|
} from '../../utils/media-info.js';
|
||||||
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
|
import { dispatchErrorMessageEvent, renderProgressIndicator } from '../message.js';
|
||||||
import { renderTask } from '../../utils/task.js';
|
import { renderTask } from '../../utils/task.js';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +23,7 @@ import {
|
|||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
} from '../../utils/media.js';
|
} from '../../utils/media.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import { mayHaveAudio } from '../../utils/audio.js';
|
||||||
|
|
||||||
// Create a wrapper for AlexxIT's WebRTC card
|
// Create a wrapper for AlexxIT's WebRTC card
|
||||||
// - https://github.com/AlexxIT/WebRTC
|
// - https://github.com/AlexxIT/WebRTC
|
||||||
@@ -80,6 +86,10 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._getPlayer()?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -178,15 +188,19 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
const video = this._getPlayer();
|
const video = this._getPlayer();
|
||||||
if (video) {
|
if (video) {
|
||||||
const onloadeddata = video.onloadeddata;
|
video.onloadeddata = () => {
|
||||||
|
|
||||||
video.onloadeddata = (e) => {
|
|
||||||
if (onloadeddata) {
|
|
||||||
onloadeddata.call(video, e);
|
|
||||||
}
|
|
||||||
hideMediaControlsTemporarily(video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
hideMediaControlsTemporarily(video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||||
dispatchMediaLoadedEvent(this, video, { player: this });
|
dispatchMediaLoadedEvent(this, video, {
|
||||||
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(video),
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
video.onplay = () => dispatchMediaPlayEvent(this);
|
||||||
|
video.onpause = () => dispatchMediaPauseEvent(this);
|
||||||
|
video.onvolumechange = () => dispatchMediaVolumeChangeEvent(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -787,6 +787,10 @@ export class FrigateCardLiveProvider
|
|||||||
this._refProvider.value?.setControls(controls);
|
this._refProvider.value?.setControls(controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._refProvider.value?.isPaused() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the fully resolved live provider.
|
* Get the fully resolved live provider.
|
||||||
* @returns A live provider (that is not 'auto').
|
* @returns A live provider (that is not 'auto').
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS,
|
unsafeCSS
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { guard } from 'lit/directives/guard.js';
|
import { guard } from 'lit/directives/guard.js';
|
||||||
@@ -25,22 +25,23 @@ import {
|
|||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
TransitionEffect,
|
TransitionEffect,
|
||||||
ViewerConfig,
|
ViewerConfig
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
import { contentsChanged, errorToConsole } from '../utils/basic.js';
|
import { contentsChanged, errorToConsole } from '../utils/basic.js';
|
||||||
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
import { canonicalizeHAURL } from '../utils/ha/index.js';
|
||||||
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
import { ResolvedMediaCache, resolveMedia } from '../utils/ha/resolved-media.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
import { dispatchMediaLoadedEvent, dispatchMediaPauseEvent, dispatchMediaPlayEvent, dispatchMediaVolumeChangeEvent } from '../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import {
|
import {
|
||||||
changeViewToRecentEventsForCameraAndDependents,
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
changeViewToRecentRecordingForCameraAndDependents,
|
changeViewToRecentRecordingForCameraAndDependents
|
||||||
} from '../utils/media-to-view.js';
|
} from '../utils/media-to-view.js';
|
||||||
import {
|
import {
|
||||||
hideMediaControlsTemporarily,
|
hideMediaControlsTemporarily,
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
playMediaMutingIfNecessary,
|
playMediaMutingIfNecessary
|
||||||
} from '../utils/media.js';
|
} from '../utils/media.js';
|
||||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
@@ -52,7 +53,7 @@ import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
|||||||
import { Lazyload } from './embla-plugins/lazyload.js';
|
import { Lazyload } from './embla-plugins/lazyload.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardMediaCarousel,
|
FrigateCardMediaCarousel,
|
||||||
wrapMediaLoadedEventForCarousel,
|
wrapMediaLoadedEventForCarousel
|
||||||
} from './media-carousel.js';
|
} from './media-carousel.js';
|
||||||
import './next-prev-control.js';
|
import './next-prev-control.js';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
@@ -610,6 +611,15 @@ export class FrigateCardViewerProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
if (this._refFrigateCardMediaPlayer.value) {
|
||||||
|
return this._refFrigateCardMediaPlayer.value.isPaused();
|
||||||
|
} else if (this._refVideoProvider.value) {
|
||||||
|
return this._refVideoProvider.value.paused;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch a clip view that matches the current (snapshot) query.
|
* Dispatch a clip view that matches the current (snapshot) query.
|
||||||
*/
|
*/
|
||||||
@@ -750,8 +760,17 @@ export class FrigateCardViewerProvider
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@loadeddata=${(ev: Event) => {
|
@loadeddata=${(ev: Event) => {
|
||||||
dispatchMediaLoadedEvent(this, ev, { player: this });
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||||
|
@play=${() => dispatchMediaPlayEvent(this)}
|
||||||
|
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||||
>
|
>
|
||||||
<source
|
<source
|
||||||
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
src=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''}
|
||||||
|
|||||||
@@ -1782,6 +1782,8 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ label: localize('config.menu.buttons.type') },
|
{ label: localize('config.menu.buttons.type') },
|
||||||
)}`,
|
)}`,
|
||||||
)}
|
)}
|
||||||
|
${this._renderMenuButton('play')}
|
||||||
|
${this._renderMenuButton('mute')}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|||||||
@@ -277,6 +277,8 @@
|
|||||||
"live": "Live",
|
"live": "Live",
|
||||||
"media_player": "Send to media player",
|
"media_player": "Send to media player",
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
|
"mute": "Mute / Unmute",
|
||||||
|
"play": "Play / Pause",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"recordings": "Recordings",
|
"recordings": "Recordings",
|
||||||
"snapshots": "Snapshots",
|
"snapshots": "Snapshots",
|
||||||
|
|||||||
@@ -275,6 +275,8 @@
|
|||||||
"image": "Immagine",
|
"image": "Immagine",
|
||||||
"live": "Abitare",
|
"live": "Abitare",
|
||||||
"media_player": "Invia a Media Player",
|
"media_player": "Invia a Media Player",
|
||||||
|
"mute": "",
|
||||||
|
"play": "",
|
||||||
"priority": "Priorità",
|
"priority": "Priorità",
|
||||||
"snapshots": "Istantanee",
|
"snapshots": "Istantanee",
|
||||||
"substreams": "Flusso/i secondario/i",
|
"substreams": "Flusso/i secondario/i",
|
||||||
|
|||||||
@@ -276,6 +276,8 @@
|
|||||||
"image": "Imagem",
|
"image": "Imagem",
|
||||||
"live": "Ao vivo",
|
"live": "Ao vivo",
|
||||||
"media_player": "Enviar para o reprodutor de mídia",
|
"media_player": "Enviar para o reprodutor de mídia",
|
||||||
|
"mute": "",
|
||||||
|
"play": "",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
"recordings": "Gravações",
|
"recordings": "Gravações",
|
||||||
"snapshots": "Instantâneos",
|
"snapshots": "Instantâneos",
|
||||||
|
|||||||
@@ -268,6 +268,8 @@
|
|||||||
"image": "Imagem",
|
"image": "Imagem",
|
||||||
"live": "Ao vivo",
|
"live": "Ao vivo",
|
||||||
"media_player": "Enviar para o reprodutor de mídia",
|
"media_player": "Enviar para o reprodutor de mídia",
|
||||||
|
"mute": "",
|
||||||
|
"play": "",
|
||||||
"priority": "Prioridade",
|
"priority": "Prioridade",
|
||||||
"snapshots": "Instantâneos",
|
"snapshots": "Instantâneos",
|
||||||
"substreams": "substreams",
|
"substreams": "substreams",
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._player?.isPaused() ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
|
|||||||
@@ -9,17 +9,23 @@
|
|||||||
// available as compilation time.
|
// available as compilation time.
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
|
|
||||||
import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
|
import { CSSResultGroup, TemplateResult, css, html, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { dispatchErrorMessageEvent } from '../components/message.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import {
|
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
} from '../utils/media.js';
|
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
|
import {
|
||||||
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent,
|
||||||
|
dispatchMediaVolumeChangeEvent,
|
||||||
|
} from '../utils/media-info.js';
|
||||||
|
import {
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
} from '../utils/media.js';
|
||||||
|
|
||||||
customElements.whenDefined('ha-hls-player').then(() => {
|
customElements.whenDefined('ha-hls-player').then(() => {
|
||||||
@customElement('frigate-card-ha-hls-player')
|
@customElement('frigate-card-ha-hls-player')
|
||||||
@@ -73,6 +79,10 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._video?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
||||||
@@ -96,9 +106,18 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
@loadedmetadata=${() => {
|
@loadedmetadata=${() => {
|
||||||
hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||||
}}
|
}}
|
||||||
@loadeddata=${(e) => {
|
@loadeddata=${(ev) => {
|
||||||
dispatchMediaLoadedEvent(this, e, { player: this });
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(this._video),
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||||
|
@play=${() => dispatchMediaPlayEvent(this)}
|
||||||
|
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||||
></video>
|
></video>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,13 @@ import { query } from 'lit/decorators/query.js';
|
|||||||
import { dispatchErrorMessageEvent } from '../components/message.js';
|
import { dispatchErrorMessageEvent } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer } from '../types.js';
|
||||||
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
|
import {
|
||||||
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent,
|
||||||
|
dispatchMediaVolumeChangeEvent,
|
||||||
|
} from '../utils/media-info.js';
|
||||||
import {
|
import {
|
||||||
hideMediaControlsTemporarily,
|
hideMediaControlsTemporarily,
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
@@ -72,6 +78,10 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._video?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
||||||
@@ -92,9 +102,18 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
@loadedmetadata=${() => {
|
@loadedmetadata=${() => {
|
||||||
hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
hideMediaControlsTemporarily(this._video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||||
}}
|
}}
|
||||||
@loadeddata=${(e) => {
|
@loadeddata=${(ev) => {
|
||||||
dispatchMediaLoadedEvent(this, e, { player: this });
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
|
player: this,
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(this._video),
|
||||||
|
},
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||||
|
@play=${() => dispatchMediaPlayEvent(this)}
|
||||||
|
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||||
></video>
|
></video>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,15 +219,19 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'image',
|
'image',
|
||||||
'live',
|
'live',
|
||||||
'menu_toggle',
|
'menu_toggle',
|
||||||
|
'mute',
|
||||||
'live_substream_on',
|
'live_substream_on',
|
||||||
'live_substream_off',
|
'live_substream_off',
|
||||||
'microphone_mute',
|
'microphone_mute',
|
||||||
'microphone_unmute',
|
'microphone_unmute',
|
||||||
|
'play',
|
||||||
|
'pause',
|
||||||
'recording',
|
'recording',
|
||||||
'recordings',
|
'recordings',
|
||||||
'snapshot',
|
'snapshot',
|
||||||
'snapshots',
|
'snapshots',
|
||||||
'timeline',
|
'timeline',
|
||||||
|
'unmute',
|
||||||
] as const;
|
] as const;
|
||||||
const FRIGATE_CARD_ACTIONS = [
|
const FRIGATE_CARD_ACTIONS = [
|
||||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
...FRIGATE_CARD_GENERAL_ACTIONS,
|
||||||
@@ -1069,6 +1073,8 @@ const menuConfigDefault = {
|
|||||||
...hiddenButtonDefault,
|
...hiddenButtonDefault,
|
||||||
type: 'momentary' as const,
|
type: 'momentary' as const,
|
||||||
},
|
},
|
||||||
|
mute: hiddenButtonDefault,
|
||||||
|
play: hiddenButtonDefault,
|
||||||
recordings: hiddenButtonDefault,
|
recordings: hiddenButtonDefault,
|
||||||
},
|
},
|
||||||
button_size: 40,
|
button_size: 40,
|
||||||
@@ -1113,6 +1119,8 @@ const menuConfigSchema = z
|
|||||||
})
|
})
|
||||||
.default(menuConfigDefault.buttons.microphone),
|
.default(menuConfigDefault.buttons.microphone),
|
||||||
recordings: hiddenButtonSchema.default(menuConfigDefault.buttons.recordings),
|
recordings: hiddenButtonSchema.default(menuConfigDefault.buttons.recordings),
|
||||||
|
mute: hiddenButtonSchema.default(menuConfigDefault.buttons.mute),
|
||||||
|
play: hiddenButtonSchema.default(menuConfigDefault.buttons.play),
|
||||||
})
|
})
|
||||||
.default(menuConfigDefault.buttons),
|
.default(menuConfigDefault.buttons),
|
||||||
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
||||||
@@ -1473,6 +1481,8 @@ export interface ExtendedHomeAssistant extends HomeAssistant {
|
|||||||
|
|
||||||
export interface MediaLoadedCapabilities {
|
export interface MediaLoadedCapabilities {
|
||||||
supports2WayAudio?: boolean;
|
supports2WayAudio?: boolean;
|
||||||
|
supportsPause?: boolean;
|
||||||
|
hasAudio?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaLoadedInfo {
|
export interface MediaLoadedInfo {
|
||||||
@@ -1517,6 +1527,7 @@ export interface FrigateCardMediaPlayer {
|
|||||||
isMuted(): boolean;
|
isMuted(): boolean;
|
||||||
seek(seconds: number): Promise<void>;
|
seek(seconds: number): Promise<void>;
|
||||||
setControls(controls: boolean): Promise<void>;
|
setControls(controls: boolean): Promise<void>;
|
||||||
|
isPaused(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardHelpers {
|
export interface CardHelpers {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface AudioProperties {
|
||||||
|
mozHasAudio?: boolean;
|
||||||
|
audioTracks?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is currently no consistent cross-browser modern way to determine if a
|
||||||
|
// viden has audio tracks. The below will work in ~24% of browsers, but notably
|
||||||
|
// not in Chrome. There used to be a usable `webkitAudioDecodedByteCount`
|
||||||
|
// property, but this now seems to be consistently 0 in Chrome. This generously
|
||||||
|
// defaults to assuming there is audio when we cannot rule it out.
|
||||||
|
export const mayHaveAudio = (video: HTMLVideoElement & AudioProperties): boolean => {
|
||||||
|
if (video.mozHasAudio !== undefined) {
|
||||||
|
return video.mozHasAudio;
|
||||||
|
}
|
||||||
|
if (video.audioTracks !== undefined) {
|
||||||
|
return Boolean(video.audioTracks?.length);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -69,6 +69,18 @@ export function dispatchMediaLoadedEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dispatchMediaVolumeChangeEvent(target: HTMLElement): void {
|
||||||
|
dispatchFrigateCardEvent(target, 'media:volumechange');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dispatchMediaPlayEvent(target: HTMLElement): void {
|
||||||
|
dispatchFrigateCardEvent(target, 'media:play');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dispatchMediaPauseEvent(target: HTMLElement): void {
|
||||||
|
dispatchFrigateCardEvent(target, 'media:pause');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch a pre-existing MediaLoadedInfo object as an event.
|
* Dispatch a pre-existing MediaLoadedInfo object as an event.
|
||||||
* @param element The element to send the event.
|
* @param element The element to send the event.
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { ReactiveControllerHost } from 'lit';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CachedValueController } from '../src/cached-value-controller';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('CachedValueController', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should construct', () => {
|
||||||
|
const host = mock<ReactiveControllerHost>();
|
||||||
|
const callback = vi.fn();
|
||||||
|
const controller = new CachedValueController(host, 10, callback);
|
||||||
|
|
||||||
|
expect(controller).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove host', () => {
|
||||||
|
const host = mock<ReactiveControllerHost>();
|
||||||
|
const callback = vi.fn();
|
||||||
|
const controller = new CachedValueController(host, 10, callback);
|
||||||
|
|
||||||
|
controller.removeController();
|
||||||
|
expect(host.removeController).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have timer', () => {
|
||||||
|
const host = mock<ReactiveControllerHost>();
|
||||||
|
const callback = vi.fn();
|
||||||
|
const startCallback = vi.fn();
|
||||||
|
const stopCallback = vi.fn();
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const controller = new CachedValueController(
|
||||||
|
host,
|
||||||
|
10,
|
||||||
|
callback,
|
||||||
|
startCallback,
|
||||||
|
stopCallback,
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.startTimer();
|
||||||
|
expect(startCallback).toBeCalled();
|
||||||
|
|
||||||
|
callback.mockReturnValue(3);
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(callback).toBeCalled();
|
||||||
|
expect(host.requestUpdate).toBeCalled();
|
||||||
|
expect(controller.value).toBe(3);
|
||||||
|
|
||||||
|
callback.mockReturnValue(4);
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(callback).toBeCalled();
|
||||||
|
expect(host.requestUpdate).toBeCalled();
|
||||||
|
expect(controller.value).toBe(4);
|
||||||
|
|
||||||
|
expect(controller.hasTimer()).toBeTruthy();
|
||||||
|
|
||||||
|
controller.stopTimer();
|
||||||
|
expect(stopCallback).toBeCalled();
|
||||||
|
|
||||||
|
callback.mockReset();
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(callback).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear value', () => {
|
||||||
|
const host = mock<ReactiveControllerHost>();
|
||||||
|
const callback = vi.fn().mockReturnValue(42);
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const controller = new CachedValueController(host, 10, callback);
|
||||||
|
controller.startTimer();
|
||||||
|
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(controller.value).equal(42);
|
||||||
|
|
||||||
|
controller.clearValue();
|
||||||
|
expect(controller.value).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should connect and disconnect host', () => {
|
||||||
|
const host = mock<ReactiveControllerHost>();
|
||||||
|
const callback = vi.fn().mockReturnValue(43);
|
||||||
|
const startCallback = vi.fn();
|
||||||
|
const stopCallback = vi.fn();
|
||||||
|
|
||||||
|
const controller = new CachedValueController(
|
||||||
|
host,
|
||||||
|
10,
|
||||||
|
callback,
|
||||||
|
startCallback,
|
||||||
|
stopCallback,
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.hostConnected();
|
||||||
|
expect(controller.value).equal(43);
|
||||||
|
expect(startCallback).toBeCalled();
|
||||||
|
expect(host.requestUpdate).toBeCalled();
|
||||||
|
|
||||||
|
controller.hostDisconnected();
|
||||||
|
expect(controller.value).toBeUndefined();
|
||||||
|
expect(stopCallback).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { AudioProperties, mayHaveAudio } from '../../src/utils/audio';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('mayHaveAudio', () => {
|
||||||
|
it('should detect audio when mozHasAudio true', () => {
|
||||||
|
const element: HTMLVideoElement & AudioProperties = document.createElement('video');
|
||||||
|
element.mozHasAudio = true;
|
||||||
|
expect(mayHaveAudio(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect audio when mozHasAudio undefined', () => {
|
||||||
|
const element: HTMLVideoElement & AudioProperties = document.createElement('video');
|
||||||
|
element.mozHasAudio = undefined;
|
||||||
|
expect(mayHaveAudio(element)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect audio when audioTracks has length', () => {
|
||||||
|
// Workaround: "Cannot set property audioTracks of #<HTMLMediaElement> which has only a getter"
|
||||||
|
const element = {} as HTMLVideoElement & AudioProperties;
|
||||||
|
element.audioTracks = [1, 2, 3];
|
||||||
|
expect(mayHaveAudio(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect audio when audioTracks has no length', () => {
|
||||||
|
// Workaround: "Cannot set property audioTracks of #<HTMLMediaElement> which has only a getter"
|
||||||
|
const element = {} as HTMLVideoElement & AudioProperties;
|
||||||
|
element.audioTracks = [];
|
||||||
|
expect(mayHaveAudio(element)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect audio when no evidence to the contrary', () => {
|
||||||
|
const element = {} as HTMLVideoElement & AudioProperties;
|
||||||
|
expect(mayHaveAudio(element)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user