fix: Increase reliability of media auto actions (e.g. play) (#1934)
Refactors media player handling entirely to make the code more consistent and less boilerplate. Add testing for media player actions. - Closes #1921 - Closes #1916
This commit is contained in:
@@ -4,6 +4,6 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
await api.getMediaLoadedInfoManager().get()?.player?.mute();
|
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.mute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class PauseAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class PauseAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
await api.getMediaLoadedInfoManager().get()?.player?.pause();
|
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class PlayAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class PlayAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
await api.getMediaLoadedInfoManager().get()?.player?.play();
|
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class UnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class UnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
await api.getMediaLoadedInfoManager().get()?.player?.unmute();
|
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.unmute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class DownloadManager {
|
|||||||
const url = await this._api
|
const url = await this._api
|
||||||
.getMediaLoadedInfoManager()
|
.getMediaLoadedInfoManager()
|
||||||
.get()
|
.get()
|
||||||
?.player?.getScreenshotURL();
|
?.mediaPlayerController?.getScreenshotURL();
|
||||||
if (url) {
|
if (url) {
|
||||||
downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView()));
|
downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,16 @@ export class WebkitFullScreenProvider
|
|||||||
|
|
||||||
protected _stateChangeHandler = (change: ConditionStateChange): void => {
|
protected _stateChangeHandler = (change: ConditionStateChange): void => {
|
||||||
if (
|
if (
|
||||||
change.old.mediaLoadedInfo?.player?.getFullscreenElement() !==
|
change.old.mediaLoadedInfo?.mediaPlayerController?.getFullscreenElement() !==
|
||||||
change.new.mediaLoadedInfo?.player?.getFullscreenElement()
|
change.new.mediaLoadedInfo?.mediaPlayerController?.getFullscreenElement()
|
||||||
) {
|
) {
|
||||||
const oldElement = change.old.mediaLoadedInfo?.player?.getFullscreenElement();
|
const oldElement =
|
||||||
|
change.old.mediaLoadedInfo?.mediaPlayerController?.getFullscreenElement();
|
||||||
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
||||||
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
||||||
|
|
||||||
const newElement = change.new.mediaLoadedInfo?.player?.getFullscreenElement();
|
const newElement =
|
||||||
|
change.new.mediaLoadedInfo?.mediaPlayerController?.getFullscreenElement();
|
||||||
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
||||||
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
||||||
}
|
}
|
||||||
@@ -46,7 +48,7 @@ export class WebkitFullScreenProvider
|
|||||||
const element = this._api
|
const element = this._api
|
||||||
.getMediaLoadedInfoManager()
|
.getMediaLoadedInfoManager()
|
||||||
.get()
|
.get()
|
||||||
?.player?.getFullscreenElement();
|
?.mediaPlayerController?.getFullscreenElement();
|
||||||
return element instanceof HTMLVideoElement ? element : null;
|
return element instanceof HTMLVideoElement ? element : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
AutoPlayCondition,
|
AutoPlayCondition,
|
||||||
AutoUnmuteCondition,
|
AutoUnmuteCondition,
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../types.js';
|
import { MediaPlayerElement } from '../types.js';
|
||||||
import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info.js';
|
import { AdvancedCameraCardMediaLoadedEventTarget } from '../utils/media-info.js';
|
||||||
import { Timer } from '../utils/timer.js';
|
import { Timer } from '../utils/timer.js';
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ export interface MediaActionsControllerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RenderRoot = HTMLElement & AdvancedCameraCardMediaLoadedEventTarget;
|
type RenderRoot = HTMLElement & AdvancedCameraCardMediaLoadedEventTarget;
|
||||||
type PlayerElement = HTMLElement & AdvancedCameraCardMediaPlayer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General note: Always unmute before playing, since Chrome may pause a piece of
|
* General note: Always unmute before playing, since Chrome may pause a piece of
|
||||||
@@ -43,13 +42,17 @@ export class MediaActionsController {
|
|||||||
protected _root: RenderRoot | null = null;
|
protected _root: RenderRoot | null = null;
|
||||||
|
|
||||||
protected _eventListeners = new Map<HTMLElement, () => void>();
|
protected _eventListeners = new Map<HTMLElement, () => void>();
|
||||||
protected _children: PlayerElement[] = [];
|
protected _children: MediaPlayerElement[] = [];
|
||||||
protected _target: MediaActionsTarget | null = null;
|
protected _target: MediaActionsTarget | null = null;
|
||||||
protected _mutationObserver = new MutationObserver(this._mutationHandler.bind(this));
|
protected _mutationObserver = new MutationObserver(this._mutationHandler.bind(this));
|
||||||
protected _intersectionObserver = new IntersectionObserver(
|
protected _intersectionObserver = new IntersectionObserver(
|
||||||
this._intersectionHandler.bind(this),
|
this._intersectionHandler.bind(this),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
document.addEventListener('visibilitychange', this._visibilityHandler);
|
||||||
|
}
|
||||||
|
|
||||||
public setOptions(options: MediaActionsControllerOptions): void {
|
public setOptions(options: MediaActionsControllerOptions): void {
|
||||||
if (this._options?.microphoneState !== options.microphoneState) {
|
if (this._options?.microphoneState !== options.microphoneState) {
|
||||||
this._microphoneStateChangeHandler(
|
this._microphoneStateChangeHandler(
|
||||||
@@ -116,7 +119,7 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _play(index: number): Promise<void> {
|
protected async _play(index: number): Promise<void> {
|
||||||
await this._children[index]?.play();
|
await (await this._children[index]?.getMediaPlayerController())?.play();
|
||||||
}
|
}
|
||||||
protected async _unmuteTargetIfConfigured(
|
protected async _unmuteTargetIfConfigured(
|
||||||
condition: AutoUnmuteCondition,
|
condition: AutoUnmuteCondition,
|
||||||
@@ -129,7 +132,7 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _unmute(index: number): Promise<void> {
|
protected async _unmute(index: number): Promise<void> {
|
||||||
await this._children[index]?.unmute();
|
await (await this._children[index]?.getMediaPlayerController())?.unmute();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _pauseAllIfConfigured(condition: AutoPauseCondition): Promise<void> {
|
protected async _pauseAllIfConfigured(condition: AutoPauseCondition): Promise<void> {
|
||||||
@@ -150,7 +153,7 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _pause(index: number): Promise<void> {
|
protected async _pause(index: number): Promise<void> {
|
||||||
await this._children[index]?.pause();
|
await (await this._children[index]?.getMediaPlayerController())?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _muteAllIfConfigured(condition: AutoMuteCondition): Promise<void> {
|
protected async _muteAllIfConfigured(condition: AutoMuteCondition): Promise<void> {
|
||||||
@@ -169,7 +172,7 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _mute(index: number): Promise<void> {
|
protected async _mute(index: number): Promise<void> {
|
||||||
await this._children[index]?.mute();
|
await (await this._children[index]?.getMediaPlayerController())?.mute();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _mutationHandler(
|
protected _mutationHandler(
|
||||||
@@ -196,17 +199,21 @@ export class MediaActionsController {
|
|||||||
this._eventListeners.clear();
|
this._eventListeners.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public initialize(root: RenderRoot): void {
|
public setRoot(root: RenderRoot): boolean {
|
||||||
|
if (root === this._root) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._target = null;
|
||||||
this._root = root;
|
this._root = root;
|
||||||
this._initializeRoot();
|
this._initializeRoot();
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this._visibilityHandler);
|
|
||||||
|
|
||||||
this._intersectionObserver.disconnect();
|
this._intersectionObserver.disconnect();
|
||||||
this._intersectionObserver.observe(root);
|
this._intersectionObserver.observe(this._root);
|
||||||
|
|
||||||
this._mutationObserver.disconnect();
|
this._mutationObserver.disconnect();
|
||||||
this._mutationObserver.observe(this._root, { childList: true, subtree: true });
|
this._mutationObserver.observe(this._root, { childList: true, subtree: true });
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _initializeRoot(): void {
|
protected _initializeRoot(): void {
|
||||||
@@ -217,7 +224,7 @@ export class MediaActionsController {
|
|||||||
this._removeChildHandlers();
|
this._removeChildHandlers();
|
||||||
|
|
||||||
this._children = [
|
this._children = [
|
||||||
...this._root.querySelectorAll<PlayerElement>(this._options.playerSelector),
|
...this._root.querySelectorAll<MediaPlayerElement>(this._options.playerSelector),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [index, child] of this._children.entries()) {
|
for (const [index, child] of this._children.entries()) {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { LitElement } from 'lit';
|
||||||
|
import { FullscreenElement, MediaPlayerController } from '../../types';
|
||||||
|
import { screenshotImage } from '../../utils/screenshot';
|
||||||
|
|
||||||
|
export class ImageMediaPlayerController implements MediaPlayerController {
|
||||||
|
private _host: LitElement;
|
||||||
|
private _getImageCallback: () => HTMLImageElement | null;
|
||||||
|
|
||||||
|
constructor(host: LitElement, getImageCallback: () => HTMLImageElement | null) {
|
||||||
|
this._host = host;
|
||||||
|
this._getImageCallback = getImageCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pause(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mute(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unmute(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public isMuted(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async seek(_seconds: number): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async setControls(_controls: boolean): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
// The image could be an MJPEG, so it is always reported unpaused.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const image = this._getImageCallback();
|
||||||
|
|
||||||
|
// It might an MJPEG so still need to screenshot it.
|
||||||
|
return image ? screenshotImage(image) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._getImageCallback() ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||||
|
import { LitElement } from 'lit';
|
||||||
|
import { FullscreenElement, MediaPlayerController } from '../../types';
|
||||||
|
|
||||||
|
export class JSMPEGMediaPlayerController implements MediaPlayerController {
|
||||||
|
private _host: LitElement;
|
||||||
|
private _getJSMPEGVideoElementCallback: () => JSMpeg.VideoElement | null;
|
||||||
|
private _getCanvasElementCallback: () => HTMLCanvasElement | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: LitElement,
|
||||||
|
_getJSMPEGVideoElementCallback: () => JSMpeg.VideoElement | null,
|
||||||
|
_getCanvasElementCallback: () => HTMLCanvasElement | null,
|
||||||
|
) {
|
||||||
|
this._host = host;
|
||||||
|
this._getJSMPEGVideoElementCallback = _getJSMPEGVideoElementCallback;
|
||||||
|
this._getCanvasElementCallback = _getCanvasElementCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
return this._getJSMPEGVideoElementCallback()?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pause(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
return this._getJSMPEGVideoElementCallback()?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mute(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
const player = this._getJSMPEGVideoElementCallback()?.player;
|
||||||
|
if (player) {
|
||||||
|
player.volume = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unmute(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
const player = this._getJSMPEGVideoElementCallback()?.player;
|
||||||
|
if (player) {
|
||||||
|
player.volume = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isMuted(): boolean {
|
||||||
|
const player = this._getJSMPEGVideoElementCallback()?.player;
|
||||||
|
return player ? player.volume === 0 : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async seek(_seconds: number): Promise<void> {
|
||||||
|
// JSMPEG does not support seeking.
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async setControls(_controls: boolean): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._getJSMPEGVideoElementCallback()?.player?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
return this._getCanvasElementCallback()?.toDataURL('image/jpeg') ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._getCanvasElementCallback() ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { LitElement } from 'lit';
|
||||||
|
import { FullscreenElement, MediaPlayerController } from '../../types';
|
||||||
|
import { CachedValueController } from '../cached-value-controller';
|
||||||
|
|
||||||
|
export class UpdatingImageMediaPlayerController implements MediaPlayerController {
|
||||||
|
private _host: LitElement;
|
||||||
|
private _getImageCallback: () => HTMLImageElement | null;
|
||||||
|
private _getCachedValueController: () => CachedValueController<string> | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: LitElement,
|
||||||
|
getImageCallback: () => HTMLImageElement | null,
|
||||||
|
getCachedValueController: () => CachedValueController<string> | null,
|
||||||
|
) {
|
||||||
|
this._host = host;
|
||||||
|
this._getImageCallback = getImageCallback;
|
||||||
|
this._getCachedValueController = getCachedValueController;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
this._getCachedValueController()?.startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pause(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
this._getCachedValueController()?.stopTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mute(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unmute(): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public isMuted(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async seek(_seconds: number): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async setControls(_controls: boolean): Promise<void> {
|
||||||
|
// Not implemented.
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return !this._getCachedValueController()?.hasTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
return this._getCachedValueController()?.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._getImageCallback() ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { LitElement } from 'lit';
|
||||||
|
import { FullscreenElement, MediaPlayerController } from '../../types';
|
||||||
|
import { hideMediaControlsTemporarily, setControlsOnVideo } from '../../utils/controls';
|
||||||
|
import { screenshotVideo } from '../../utils/screenshot';
|
||||||
|
|
||||||
|
export class VideoMediaPlayerController implements MediaPlayerController {
|
||||||
|
private _host: LitElement;
|
||||||
|
private _getVideoCallback: () => HTMLVideoElement | null;
|
||||||
|
private _getControlsDefaultCallback: (() => boolean) | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: LitElement,
|
||||||
|
getVideoCallback: () => HTMLVideoElement | null,
|
||||||
|
getControlsDefaultCallback?: () => boolean,
|
||||||
|
) {
|
||||||
|
this._host = host;
|
||||||
|
this._getVideoCallback = getVideoCallback;
|
||||||
|
this._getControlsDefaultCallback = getControlsDefaultCallback ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async play(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
if (!video?.play) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the play call fails, and the media is not already muted, mute it first
|
||||||
|
// and then try again. This works around some browsers that prevent
|
||||||
|
// auto-play unless the video is muted.
|
||||||
|
try {
|
||||||
|
await video.play();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if ((err as Error).name === 'NotAllowedError' && !this.isMuted()) {
|
||||||
|
await this.mute();
|
||||||
|
try {
|
||||||
|
await video.play();
|
||||||
|
} catch (_) {
|
||||||
|
// Pass.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pause(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
this._getVideoCallback()?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mute(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
// The muted property is only for the initial muted state. Must explicitly
|
||||||
|
// set the muted on the video player to make the change dynamic.
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
if (video) {
|
||||||
|
video.muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unmute(): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
if (video) {
|
||||||
|
video.muted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isMuted(): boolean {
|
||||||
|
return this._getVideoCallback()?.muted ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async seek(seconds: number): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
if (video) {
|
||||||
|
hideMediaControlsTemporarily(video);
|
||||||
|
video.currentTime = seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setControls(controls?: boolean): Promise<void> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
const value = controls ?? this._getControlsDefaultCallback?.();
|
||||||
|
if (video && value !== undefined) {
|
||||||
|
setControlsOnVideo(video, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPaused(): boolean {
|
||||||
|
return this._getVideoCallback()?.paused ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
|
await this._host.updateComplete;
|
||||||
|
|
||||||
|
const video = this._getVideoCallback();
|
||||||
|
return video ? screenshotVideo(video) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._getVideoCallback() ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -489,10 +489,10 @@ export class MenuButtonController {
|
|||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
if (
|
if (
|
||||||
currentMediaLoadedInfo &&
|
currentMediaLoadedInfo &&
|
||||||
currentMediaLoadedInfo.player &&
|
currentMediaLoadedInfo.mediaPlayerController &&
|
||||||
currentMediaLoadedInfo.capabilities?.supportsPause
|
currentMediaLoadedInfo.capabilities?.supportsPause
|
||||||
) {
|
) {
|
||||||
const paused = currentMediaLoadedInfo.player.isPaused();
|
const paused = currentMediaLoadedInfo.mediaPlayerController?.isPaused();
|
||||||
return {
|
return {
|
||||||
icon: paused ? 'mdi:play' : 'mdi:pause',
|
icon: paused ? 'mdi:play' : 'mdi:pause',
|
||||||
...config.menu.buttons.play,
|
...config.menu.buttons.play,
|
||||||
@@ -512,10 +512,10 @@ export class MenuButtonController {
|
|||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
if (
|
if (
|
||||||
currentMediaLoadedInfo &&
|
currentMediaLoadedInfo &&
|
||||||
currentMediaLoadedInfo.player &&
|
currentMediaLoadedInfo.mediaPlayerController &&
|
||||||
currentMediaLoadedInfo?.capabilities?.hasAudio
|
currentMediaLoadedInfo?.capabilities?.hasAudio
|
||||||
) {
|
) {
|
||||||
const muted = currentMediaLoadedInfo.player.isMuted();
|
const muted = currentMediaLoadedInfo.mediaPlayerController?.isMuted();
|
||||||
return {
|
return {
|
||||||
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
|
icon: muted ? 'mdi:volume-off' : 'mdi:volume-high',
|
||||||
...config.menu.buttons.mute,
|
...config.menu.buttons.mute,
|
||||||
@@ -533,7 +533,7 @@ export class MenuButtonController {
|
|||||||
config: AdvancedCameraCardConfig,
|
config: AdvancedCameraCardConfig,
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null,
|
currentMediaLoadedInfo?: MediaLoadedInfo | null,
|
||||||
): MenuItem | null {
|
): MenuItem | null {
|
||||||
if (currentMediaLoadedInfo && currentMediaLoadedInfo.player) {
|
if (currentMediaLoadedInfo && currentMediaLoadedInfo.mediaPlayerController) {
|
||||||
return {
|
return {
|
||||||
icon: 'mdi:monitor-screenshot',
|
icon: 'mdi:monitor-screenshot',
|
||||||
...config.menu.buttons.screenshot,
|
...config.menu.buttons.screenshot,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { ImageMediaPlayerController } from '../components-lib/media-player/image';
|
||||||
|
import imagePlayerStyle from '../scss/image-player.scss';
|
||||||
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types';
|
||||||
|
import { dispatchMediaLoadedEvent } from '../utils/media-info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple media player to wrap a single static image.
|
||||||
|
*/
|
||||||
|
@customElement('advanced-camera-card-image-player')
|
||||||
|
export class AdvancedCameraCardImagePlayer extends LitElement implements MediaPlayer {
|
||||||
|
@property()
|
||||||
|
public url?: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
public filetype?: string;
|
||||||
|
|
||||||
|
protected _refImage: Ref<MediaPlayerElement<HTMLImageElement>> = createRef();
|
||||||
|
protected _mediaPlayerController = new ImageMediaPlayerController(
|
||||||
|
this,
|
||||||
|
() => this._refImage.value ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
|
return this._mediaPlayerController;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`<img
|
||||||
|
${ref(this._refImage)}
|
||||||
|
src="${ifDefined(this.url)}"
|
||||||
|
@load=${(ev: Event) => {
|
||||||
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
|
...(this._mediaPlayerController && {
|
||||||
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
|
}),
|
||||||
|
technology: [this.filetype ?? 'jpg'],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(imagePlayerStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'advanced-camera-card-image-player': AdvancedCameraCardImagePlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,14 +15,15 @@ import isEqual from 'lodash-es/isEqual';
|
|||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js';
|
import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js';
|
||||||
import { CachedValueController } from '../components-lib/cached-value-controller.js';
|
import { CachedValueController } from '../components-lib/cached-value-controller.js';
|
||||||
|
import { UpdatingImageMediaPlayerController } from '../components-lib/media-player/updating-image.js';
|
||||||
import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
|
import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
|
||||||
import defaultImage from '../images/iris-screensaver.jpg';
|
import defaultImage from '../images/iris-screensaver.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageUpdatingPlayerStyle from '../scss/image-updating-player.scss';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
FullscreenElement,
|
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
Message,
|
Message,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
@@ -60,10 +61,13 @@ export const resolveImageMode = (options?: {
|
|||||||
return 'screensaver';
|
return 'screensaver';
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement('advanced-camera-card-image-base')
|
/**
|
||||||
export class AdvancedCameraCardImageBase
|
* A media player to wrap a image that updates continuously.
|
||||||
|
*/
|
||||||
|
@customElement('advanced-camera-card-image-updating-player')
|
||||||
|
export class AdvancedCameraCardImageUpdatingPlayer
|
||||||
extends LitElement
|
extends LitElement
|
||||||
implements AdvancedCameraCardMediaPlayer
|
implements MediaPlayer
|
||||||
{
|
{
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
@@ -93,46 +97,14 @@ export class AdvancedCameraCardImageBase
|
|||||||
|
|
||||||
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
|
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
protected _mediaPlayerController = new UpdatingImageMediaPlayerController(
|
||||||
this._cachedValueController?.startTimer();
|
this,
|
||||||
}
|
() => this._refImage.value ?? null,
|
||||||
|
() => this._cachedValueController ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
this._cachedValueController?.stopTimer();
|
return this._mediaPlayerController;
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public async seek(_seconds: number): Promise<void> {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public async setControls(_controls: boolean): Promise<void> {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return !this._cachedValueController?.hasTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._cachedValueController?.value ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._refImage.value ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,7 +338,7 @@ export class AdvancedCameraCardImageBase
|
|||||||
src=${live(src)}
|
src=${live(src)}
|
||||||
@load=${(ev: Event) => {
|
@load=${(ev: Event) => {
|
||||||
const mediaLoadedInfo = createMediaLoadedInfo(ev, {
|
const mediaLoadedInfo = createMediaLoadedInfo(ev, {
|
||||||
player: this,
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: !!this.imageConfig?.refresh_seconds,
|
supportsPause: !!this.imageConfig?.refresh_seconds,
|
||||||
},
|
},
|
||||||
@@ -406,12 +378,12 @@ export class AdvancedCameraCardImageBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(imageStyle);
|
return unsafeCSS(imageUpdatingPlayerStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'advanced-camera-card-image-base': AdvancedCameraCardImageBase;
|
'advanced-camera-card-image-updating-player': AdvancedCameraCardImageUpdatingPlayer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-50
@@ -16,19 +16,16 @@ import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
|||||||
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
||||||
import { CameraConfig, ImageViewConfig } from '../config/types';
|
import { CameraConfig, ImageViewConfig } from '../config/types';
|
||||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||||
import { aspectRatioToString } from '../utils/basic';
|
import { aspectRatioToString } from '../utils/basic';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import './image-base';
|
import './image-updating-player';
|
||||||
import { resolveImageMode } from './image-base';
|
import { resolveImageMode } from './image-updating-player';
|
||||||
import './zoomer.js';
|
import './zoomer.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-image')
|
@customElement('advanced-camera-card-image')
|
||||||
export class AdvancedCameraCardImage
|
export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@@ -44,47 +41,12 @@ export class AdvancedCameraCardImage
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public imageConfig?: ImageViewConfig;
|
public imageConfig?: ImageViewConfig;
|
||||||
|
|
||||||
protected _refImage: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
|
await this.updateComplete;
|
||||||
public async play(): Promise<void> {
|
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||||
await this._refImage.value?.play();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||||
await this._refImage.value?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
await this._refImage.value?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
await this._refImage.value?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return !!this._refImage.value?.isMuted();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
await this._refImage.value?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
await this._refImage.value?.setControls(controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._refImage.value?.isPaused() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
||||||
@@ -147,19 +109,19 @@ export class AdvancedCameraCardImage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this._useZoomIfRequired(html`
|
return this._useZoomIfRequired(html`
|
||||||
<advanced-camera-card-image-base
|
<advanced-camera-card-image-updating-player
|
||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.viewManagerEpoch?.manager.getView()}
|
.view=${this.viewManagerEpoch?.manager.getView()}
|
||||||
.imageConfig=${this.imageConfig}
|
.imageConfig=${this.imageConfig}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-image-base>
|
</advanced-camera-card-image-updating-player>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return unsafeCSS(basicBlockStyle);
|
return unsafeCSS(imageStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -429,16 +429,14 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
|||||||
public updated(changedProperties: PropertyValues): void {
|
public updated(changedProperties: PropertyValues): void {
|
||||||
super.updated(changedProperties);
|
super.updated(changedProperties);
|
||||||
|
|
||||||
let initialized = false;
|
const rootChanged = this._refCarousel.value
|
||||||
if (!this._mediaActionsController.hasRoot() && this._refCarousel.value) {
|
? this._mediaActionsController.setRoot(this._refCarousel.value)
|
||||||
this._mediaActionsController.initialize(this._refCarousel.value);
|
: false;
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the view has changed, or if the media actions controller has just been
|
// If the view has changed, or if the media actions controller has just been
|
||||||
// initialized, then call the necessary media action.
|
// initialized, then call the necessary media action.
|
||||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1626
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1626
|
||||||
if (initialized || changedProperties.has('viewManagerEpoch')) {
|
if (rootChanged || changedProperties.has('viewManagerEpoch')) {
|
||||||
this._setMediaTarget();
|
this._setMediaTarget();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
|||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FullscreenElement,
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
|
MediaPlayerElement,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { aspectRatioToString } from '../../utils/basic.js';
|
import { aspectRatioToString } from '../../utils/basic.js';
|
||||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
|
||||||
import '../icon.js';
|
import '../icon.js';
|
||||||
import { renderMessage } from '../message.js';
|
import { renderMessage } from '../message.js';
|
||||||
import '../next-prev-control.js';
|
import '../next-prev-control.js';
|
||||||
@@ -40,10 +40,7 @@ import '../ptz.js';
|
|||||||
import '../surround.js';
|
import '../surround.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-provider')
|
@customElement('advanced-camera-card-live-provider')
|
||||||
export class AdvancedCameraCardLiveProvider
|
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@@ -84,7 +81,7 @@ export class AdvancedCameraCardLiveProvider
|
|||||||
@state()
|
@state()
|
||||||
protected _showStreamTroubleshooting = false;
|
protected _showStreamTroubleshooting = false;
|
||||||
|
|
||||||
protected _refProvider: Ref<LitElement & AdvancedCameraCardMediaPlayer> = createRef();
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
|
|
||||||
// A note on dynamic imports:
|
// A note on dynamic imports:
|
||||||
//
|
//
|
||||||
@@ -98,58 +95,9 @@ export class AdvancedCameraCardLiveProvider
|
|||||||
// background. These calls fail without waiting for loading here.
|
// background. These calls fail without waiting for loading here.
|
||||||
protected _importPromises: Promise<unknown>[] = [];
|
protected _importPromises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._refProvider.value?.updateComplete;
|
return (await this._refProvider.value?.getMediaPlayerController()) ?? null;
|
||||||
await playMediaMutingIfNecessary(this, this._refProvider.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
await this._refProvider.value?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
await this._refProvider.value?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
await this._refProvider.value?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._refProvider.value?.isMuted() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
await this._refProvider.value?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
await this._refProvider.value?.setControls(controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._refProvider.value?.isPaused() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._refProvider.value?.updateComplete;
|
|
||||||
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._refProvider.value?.getFullscreenElement() ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,8 +213,10 @@ export class AdvancedCameraCardLiveProvider
|
|||||||
: undefined,
|
: undefined,
|
||||||
)}
|
)}
|
||||||
.settings=${this.zoomSettings}
|
.settings=${this.zoomSettings}
|
||||||
@advanced-camera-card:zoom:zoomed=${() => this.setControls(false)}
|
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||||
@advanced-camera-card:zoom:unzoomed=${() => this.setControls()}
|
(await this.getMediaPlayerController())?.setControls(false)}
|
||||||
|
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||||
|
(await this.getMediaPlayerController())?.setControls()}
|
||||||
>
|
>
|
||||||
${template}
|
${template}
|
||||||
</advanced-camera-card-zoomer>`
|
</advanced-camera-card-zoomer>`
|
||||||
|
|||||||
@@ -10,18 +10,17 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
||||||
import { MicrophoneState } from '../../../../card-controller/types.js';
|
import { MicrophoneState } from '../../../../card-controller/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js';
|
||||||
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
||||||
import { localize } from '../../../../localize/localize.js';
|
import { localize } from '../../../../localize/localize.js';
|
||||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||||
import {
|
import {
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
AdvancedCameraCardMediaPlayer,
|
MediaPlayer,
|
||||||
FullscreenElement,
|
MediaPlayerController,
|
||||||
Message,
|
Message,
|
||||||
} from '../../../../types.js';
|
} from '../../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
||||||
import { setControlsOnVideo } from '../../../../utils/media.js';
|
|
||||||
import { screenshotMedia } from '../../../../utils/screenshot.js';
|
|
||||||
import { renderMessage } from '../../../message.js';
|
import { renderMessage } from '../../../message.js';
|
||||||
import { VideoRTC } from './video-rtc.js';
|
import { VideoRTC } from './video-rtc.js';
|
||||||
|
|
||||||
@@ -35,10 +34,7 @@ customElements.define('advanced-camera-card-live-go2rtc-player', VideoRTC);
|
|||||||
const GO2RTC_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const GO2RTC_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-go2rtc')
|
@customElement('advanced-camera-card-live-go2rtc')
|
||||||
export class AdvancedCameraCardGo2RTC
|
export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
// Not an reactive property to avoid resetting the video.
|
// Not an reactive property to avoid resetting the video.
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@@ -62,52 +58,14 @@ export class AdvancedCameraCardGo2RTC
|
|||||||
|
|
||||||
protected _player?: VideoRTC;
|
protected _player?: VideoRTC;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||||
return this._player?.video?.play();
|
this,
|
||||||
}
|
() => this._player?.video ?? null,
|
||||||
|
() => this.controls,
|
||||||
|
);
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
this._player?.video?.pause();
|
return this._mediaPlayerController;
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
if (this._player?.video) {
|
|
||||||
this._player.video.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
if (this._player?.video) {
|
|
||||||
this._player.video.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._player?.video?.muted ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
if (this._player?.video) {
|
|
||||||
this._player.video.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
if (this._player?.video) {
|
|
||||||
setControlsOnVideo(this._player.video, controls ?? this.controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._player?.video?.paused ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._player?.video ? screenshotMedia(this._player.video) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._player?.video ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
@@ -155,7 +113,7 @@ export class AdvancedCameraCardGo2RTC
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._player = new VideoRTC();
|
this._player = new VideoRTC();
|
||||||
this._player.containingPlayer = this;
|
this._player.mediaPlayerController = this._mediaPlayerController;
|
||||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||||
this._player.src = address;
|
this._player.src = address;
|
||||||
this._player.visibilityCheck = false;
|
this._player.visibilityCheck = false;
|
||||||
|
|||||||
+3
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { MediaPlayerController } from '../../../../types';
|
||||||
|
|
||||||
export class VideoRTC extends HTMLElement {
|
export class VideoRTC extends HTMLElement {
|
||||||
DISCONNECT_TIMEOUT: number;
|
DISCONNECT_TIMEOUT: number;
|
||||||
RECONNECT_TIMEOUT: number;
|
RECONNECT_TIMEOUT: number;
|
||||||
@@ -29,9 +31,8 @@ export class VideoRTC extends HTMLElement {
|
|||||||
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
||||||
|
|
||||||
// Custom methods/members.
|
// Custom methods/members.
|
||||||
containingPlayer: AdvancedCameraCardMediaPlayer | null;
|
mediaPlayerController: MediaPlayerController | null;
|
||||||
microphoneStream: MediaStream | null;
|
microphoneStream: MediaStream | null;
|
||||||
reconnect();
|
reconnect();
|
||||||
|
|
||||||
setControls(controls: boolean): void;
|
setControls(controls: boolean): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
hideMediaControlsTemporarily,
|
hideMediaControlsTemporarily,
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
setControlsOnVideo,
|
setControlsOnVideo,
|
||||||
} from '../../../../utils/media';
|
} from '../../../../utils/controls.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
@@ -162,13 +162,13 @@ export class VideoRTC extends HTMLElement {
|
|||||||
this.microphoneStream = null;
|
this.microphoneStream = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reference to a containing AdvancedCameraCardMediaPlayer object.
|
* A reference to a MediaPlayerController for this video
|
||||||
* @type {AdvancedCameraCardMediaPlayer}}
|
* @type {MediaPlayerController | null}
|
||||||
*/
|
*/
|
||||||
this.containingPlayer = null;
|
this.mediaPlayerController = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show or hide video controls for videos created in future.
|
* Whether to show or hide video controls for videos created *in future*.
|
||||||
* @type {boolean}}
|
* @type {boolean}}
|
||||||
*/
|
*/
|
||||||
this.controls = true;
|
this.controls = true;
|
||||||
@@ -353,7 +353,9 @@ export class VideoRTC extends HTMLElement {
|
|||||||
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,
|
...(this.mediaPlayerController && {
|
||||||
|
mediaPlayerController: this.mediaPlayerController,
|
||||||
|
}),
|
||||||
capabilities: {
|
capabilities: {
|
||||||
// 2-way audio is only supported on WebRTC connections. The state of
|
// 2-way audio is only supported on WebRTC connections. The state of
|
||||||
// `this.microphoneStream` is not taken into account here since
|
// `this.microphoneStream` is not taken into account here since
|
||||||
@@ -734,7 +736,9 @@ export class VideoRTC extends HTMLElement {
|
|||||||
if (!receivedFirstFrame) {
|
if (!receivedFirstFrame) {
|
||||||
receivedFirstFrame = true;
|
receivedFirstFrame = true;
|
||||||
dispatchMediaLoadedEvent(this, this.video, {
|
dispatchMediaLoadedEvent(this, this.video, {
|
||||||
player: this.containingPlayer,
|
...(this.mediaPlayerController && {
|
||||||
|
mediaPlayerController: this.mediaPlayerController,
|
||||||
|
}),
|
||||||
technology: ['mjpeg'],
|
technology: ['mjpeg'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -779,7 +783,9 @@ export class VideoRTC extends HTMLElement {
|
|||||||
context = canvas.getContext('2d');
|
context = canvas.getContext('2d');
|
||||||
|
|
||||||
dispatchMediaLoadedEvent(this, video2, {
|
dispatchMediaLoadedEvent(this, video2, {
|
||||||
player: this.containingPlayer,
|
...(this.mediaPlayerController && {
|
||||||
|
mediaPlayerController: this.mediaPlayerController,
|
||||||
|
}),
|
||||||
technology: ['mp4'],
|
technology: ['mp4'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import '../../../patches/ha-camera-stream';
|
|||||||
import '../../../patches/ha-hls-player.js';
|
import '../../../patches/ha-hls-player.js';
|
||||||
import '../../../patches/ha-web-rtc-player.js';
|
import '../../../patches/ha-web-rtc-player.js';
|
||||||
import liveHAStyle from '../../../scss/live-ha.scss';
|
import liveHAStyle from '../../../scss/live-ha.scss';
|
||||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
import {
|
||||||
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
|
MediaPlayerElement,
|
||||||
|
} from '../../../types.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-ha')
|
@customElement('advanced-camera-card-live-ha')
|
||||||
export class AdvancedCameraCardLiveHA
|
export class AdvancedCameraCardLiveHA extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@@ -23,46 +24,11 @@ export class AdvancedCameraCardLiveHA
|
|||||||
@property({ attribute: true, type: Boolean })
|
@property({ attribute: true, type: Boolean })
|
||||||
public controls = false;
|
public controls = false;
|
||||||
|
|
||||||
protected _playerRef: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
protected _playerRef: Ref<MediaPlayerElement> = createRef();
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
return this._playerRef.value?.play();
|
await this.updateComplete;
|
||||||
}
|
return (await this._playerRef.value?.getMediaPlayerController()) ?? null;
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
this._playerRef.value?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
this._playerRef.value?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
this._playerRef.value?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._playerRef.value?.isMuted() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
this._playerRef.value?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
this._playerRef.value?.setControls(controls ?? this.controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._playerRef.value?.isPaused() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._playerRef.value?.getFullscreenElement() ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
|||||||
@@ -4,60 +4,26 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { CameraConfig } from '../../../config/types';
|
import { CameraConfig } from '../../../config/types';
|
||||||
import basicBlockStyle from '../../../scss/basic-block.scss';
|
import basicBlockStyle from '../../../scss/basic-block.scss';
|
||||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
import {
|
||||||
import '../../image-base.js';
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
|
MediaPlayerElement,
|
||||||
|
} from '../../../types.js';
|
||||||
|
import '../../image-updating-player.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-image')
|
@customElement('advanced-camera-card-live-image')
|
||||||
export class AdvancedCameraCardLiveImage
|
export class AdvancedCameraCardLiveImage extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraConfig?: CameraConfig;
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
protected _refImage: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
await this._refImage.value?.play();
|
await this.updateComplete;
|
||||||
}
|
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
await this._refImage.value?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
await this._refImage.value?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
await this._refImage.value?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return !!this._refImage.value?.isMuted();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
await this._refImage.value?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
await this._refImage.value?.setControls(controls);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._refImage.value?.isPaused() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
@@ -66,13 +32,13 @@ export class AdvancedCameraCardLiveImage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<advanced-camera-card-image-base
|
<advanced-camera-card-image-updating-player
|
||||||
${ref(this._refImage)}
|
${ref(this._refImage)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.imageConfig=${this.cameraConfig.image}
|
.imageConfig=${this.cameraConfig.image}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-image-base>
|
</advanced-camera-card-image-updating-player>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import { customElement, property, state } 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 { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
||||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
import { CameraConfig, CardWideConfig } from '../../../config/types.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';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FullscreenElement,
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
Message,
|
Message,
|
||||||
} from '../../../types.js';
|
} from '../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
||||||
@@ -39,10 +40,9 @@ const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
|||||||
const JSMPEG_URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
const JSMPEG_URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
||||||
|
|
||||||
@customElement('advanced-camera-card-live-jsmpeg')
|
@customElement('advanced-camera-card-live-jsmpeg')
|
||||||
export class AdvancedCameraCardLiveJSMPEG
|
export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
protected hass?: ExtendedHomeAssistant;
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraConfig?: CameraConfig;
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
@@ -52,61 +52,21 @@ export class AdvancedCameraCardLiveJSMPEG
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
protected hass?: ExtendedHomeAssistant;
|
@state()
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||||
protected _refreshPlayerTimer = new Timer();
|
protected _refreshPlayerTimer = new Timer();
|
||||||
|
|
||||||
@state()
|
protected _mediaPlayerController = new JSMPEGMediaPlayerController(
|
||||||
protected _message: Message | null = null;
|
this,
|
||||||
|
() => this._jsmpegVideoPlayer ?? null,
|
||||||
|
() => this._jsmpegCanvasElement ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
return this._jsmpegVideoPlayer?.play();
|
return this._mediaPlayerController;
|
||||||
}
|
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
this._jsmpegVideoPlayer?.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
|
||||||
if (player) {
|
|
||||||
player.volume = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
const player = this._jsmpegVideoPlayer?.player;
|
|
||||||
if (player) {
|
|
||||||
player.volume = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._jsmpegVideoPlayer ? this._jsmpegVideoPlayer.player.volume === 0 : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public async seek(_seconds: number): Promise<void> {
|
|
||||||
// JSMPEG does not support seeking.
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public async setControls(_controls: boolean): Promise<void> {
|
|
||||||
// Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._jsmpegVideoPlayer?.player?.paused ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._jsmpegCanvasElement ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
@@ -170,7 +130,7 @@ export class AdvancedCameraCardLiveJSMPEG
|
|||||||
// calls back to the player to check for pause status for menu buttons.
|
// calls back to the player to check for pause status for menu buttons.
|
||||||
if (this._jsmpegCanvasElement) {
|
if (this._jsmpegCanvasElement) {
|
||||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement, {
|
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement, {
|
||||||
player: this,
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,28 +12,28 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||||
|
import { VideoMediaPlayerController } from '../../../components-lib/media-player/video.js';
|
||||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||||
import { localize } from '../../../localize/localize.js';
|
import { localize } from '../../../localize/localize.js';
|
||||||
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardError,
|
AdvancedCameraCardError,
|
||||||
AdvancedCameraCardMediaPlayer,
|
MediaPlayer,
|
||||||
FullscreenElement,
|
MediaPlayerController,
|
||||||
Message,
|
Message,
|
||||||
} from '../../../types.js';
|
} from '../../../types.js';
|
||||||
import { mayHaveAudio } from '../../../utils/audio.js';
|
import { mayHaveAudio } from '../../../utils/audio.js';
|
||||||
|
import {
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
setControlsOnVideo,
|
||||||
|
} from '../../../utils/controls.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
dispatchMediaVolumeChangeEvent,
|
dispatchMediaVolumeChangeEvent,
|
||||||
} from '../../../utils/media-info.js';
|
} from '../../../utils/media-info.js';
|
||||||
import {
|
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
setControlsOnVideo,
|
|
||||||
} from '../../../utils/media.js';
|
|
||||||
import { screenshotMedia } from '../../../utils/screenshot.js';
|
|
||||||
import { renderTask } from '../../../utils/task.js';
|
import { renderTask } from '../../../utils/task.js';
|
||||||
import '../../message.js';
|
import '../../message.js';
|
||||||
import { renderMessage } from '../../message.js';
|
import { renderMessage } from '../../message.js';
|
||||||
@@ -44,10 +44,7 @@ import { VideoRTC } from './go2rtc/video-rtc.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
|
||||||
@customElement('advanced-camera-card-live-webrtc-card')
|
@customElement('advanced-camera-card-live-webrtc-card')
|
||||||
export class AdvancedCameraCardLiveWebRTCCard
|
export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraConfig?: CameraConfig;
|
public cameraConfig?: CameraConfig;
|
||||||
|
|
||||||
@@ -65,62 +62,19 @@ export class AdvancedCameraCardLiveWebRTCCard
|
|||||||
|
|
||||||
protected hass?: HomeAssistant;
|
protected hass?: HomeAssistant;
|
||||||
|
|
||||||
|
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||||
|
this,
|
||||||
|
() => this._getVideo(),
|
||||||
|
() => this.controls,
|
||||||
|
);
|
||||||
|
|
||||||
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
|
return this._mediaPlayerController;
|
||||||
|
}
|
||||||
|
|
||||||
// A task to await the load of the WebRTC component.
|
// A task to await the load of the WebRTC component.
|
||||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
|
||||||
return this._getVideo()?.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
this._getVideo()?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
const player = this._getVideo();
|
|
||||||
if (player) {
|
|
||||||
player.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
const player = this._getVideo();
|
|
||||||
if (player) {
|
|
||||||
player.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._getVideo()?.muted ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
const player = this._getVideo();
|
|
||||||
if (player) {
|
|
||||||
player.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
const player = this._getVideo();
|
|
||||||
if (player) {
|
|
||||||
setControlsOnVideo(player, controls ?? this.controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._getVideo()?.paused ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
const video = this._getVideo();
|
|
||||||
return video ? screenshotMedia(video) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._getVideo();
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -253,7 +207,7 @@ export class AdvancedCameraCardLiveWebRTCCard
|
|||||||
hideMediaControlsTemporarily(video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
hideMediaControlsTemporarily(video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||||
}
|
}
|
||||||
dispatchMediaLoadedEvent(this, video, {
|
dispatchMediaLoadedEvent(this, video, {
|
||||||
player: this,
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
hasAudio: mayHaveAudio(video),
|
hasAudio: mayHaveAudio(video),
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { VideoMediaPlayerController } from '../components-lib/media-player/video';
|
||||||
|
import videoPlayerStyle from '../scss/video-player.scss';
|
||||||
|
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types';
|
||||||
|
import { mayHaveAudio } from '../utils/audio';
|
||||||
|
import {
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
} from '../utils/controls';
|
||||||
|
import {
|
||||||
|
dispatchMediaLoadedEvent,
|
||||||
|
dispatchMediaPauseEvent,
|
||||||
|
dispatchMediaPlayEvent,
|
||||||
|
dispatchMediaVolumeChangeEvent,
|
||||||
|
} from '../utils/media-info';
|
||||||
|
|
||||||
|
@customElement('advanced-camera-card-video-player')
|
||||||
|
export class AdvancedCameraCardVideoPlayer extends LitElement implements MediaPlayer {
|
||||||
|
@property()
|
||||||
|
public url?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public controls = false;
|
||||||
|
|
||||||
|
protected _refVideo: Ref<MediaPlayerElement<HTMLVideoElement>> = createRef();
|
||||||
|
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||||
|
this,
|
||||||
|
() => this._refVideo.value ?? null,
|
||||||
|
() => this.controls,
|
||||||
|
);
|
||||||
|
|
||||||
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
|
return this._mediaPlayerController;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
return html`
|
||||||
|
<video
|
||||||
|
${ref(this._refVideo)}
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
crossorigin="anonymous"
|
||||||
|
?autoplay=${false}
|
||||||
|
?controls=${this.controls}
|
||||||
|
@loadedmetadata=${(ev: Event) => {
|
||||||
|
if (ev.target && this.controls) {
|
||||||
|
hideMediaControlsTemporarily(
|
||||||
|
ev.target as HTMLVideoElement,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
@loadeddata=${(ev: Event) => {
|
||||||
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
|
...(this._mediaPlayerController && {
|
||||||
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
|
}),
|
||||||
|
capabilities: {
|
||||||
|
supportsPause: true,
|
||||||
|
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
||||||
|
},
|
||||||
|
technology: ['mp4'],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||||
|
@play=${() => dispatchMediaPlayEvent(this)}
|
||||||
|
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||||
|
>
|
||||||
|
<source src="${ifDefined(this.url)}" type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(videoPlayerStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'advanced-camera-card-video-player': AdvancedCameraCardVideoPlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,9 @@ import { localize } from '../../localize/localize.js';
|
|||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import viewerCarouselStyle from '../../scss/viewer-carousel.scss';
|
import viewerCarouselStyle from '../../scss/viewer-carousel.scss';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
|
MediaPlayerController,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js';
|
import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js';
|
||||||
@@ -92,7 +92,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
|||||||
|
|
||||||
protected _media: ViewMedia[] | null = null;
|
protected _media: ViewMedia[] | null = null;
|
||||||
protected _mediaActionsController = new MediaActionsController();
|
protected _mediaActionsController = new MediaActionsController();
|
||||||
protected _player: AdvancedCameraCardMediaPlayer | null = null;
|
protected _loadedMediaPlayerController: MediaPlayerController | null = null;
|
||||||
protected _refCarousel: Ref<HTMLElement> = createRef();
|
protected _refCarousel: Ref<HTMLElement> = createRef();
|
||||||
|
|
||||||
updated(changedProperties: PropertyValues): void {
|
updated(changedProperties: PropertyValues): void {
|
||||||
@@ -110,8 +110,8 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._mediaActionsController.hasRoot() && this._refCarousel.value) {
|
if (this._refCarousel.value) {
|
||||||
this._mediaActionsController.initialize(this._refCarousel.value);
|
this._mediaActionsController.setRoot(this._refCarousel.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,11 +383,11 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
|||||||
this._setViewSelectedIndex(ev.detail.index);
|
this._setViewSelectedIndex(ev.detail.index);
|
||||||
}}
|
}}
|
||||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
this._player = ev.detail.player ?? null;
|
this._loadedMediaPlayerController = ev.detail.mediaPlayerController ?? null;
|
||||||
this._seekHandler();
|
this._seekHandler();
|
||||||
}}
|
}}
|
||||||
@advanced-camera-card:media:unloaded=${() => {
|
@advanced-camera-card:media:unloaded=${() => {
|
||||||
this._player = null;
|
this._loadedMediaPlayerController = null;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${this.showControls ? this._renderNextPrevious('left', neighbors) : ''}
|
${this.showControls ? this._renderNextPrevious('left', neighbors) : ''}
|
||||||
@@ -418,7 +418,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
|||||||
protected async _seekHandler(): Promise<void> {
|
protected async _seekHandler(): Promise<void> {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
const seek = view?.context?.mediaViewer?.seek;
|
const seek = view?.context?.mediaViewer?.seek;
|
||||||
if (!this.hass || !seek || !this._media || !this._player) {
|
if (!this.hass || !seek || !this._media || !this._loadedMediaPlayerController) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedMedia = this._media[this._selected];
|
const selectedMedia = this._media[this._selected];
|
||||||
@@ -428,17 +428,17 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
|||||||
|
|
||||||
const seekTimeInMedia = selectedMedia.includesTime(seek);
|
const seekTimeInMedia = selectedMedia.includesTime(seek);
|
||||||
setOrRemoveAttribute(this, !seekTimeInMedia, 'unseekable');
|
setOrRemoveAttribute(this, !seekTimeInMedia, 'unseekable');
|
||||||
if (!seekTimeInMedia && !this._player.isPaused()) {
|
if (!seekTimeInMedia && !this._loadedMediaPlayerController.isPaused()) {
|
||||||
this._player.pause();
|
this._loadedMediaPlayerController.pause();
|
||||||
} else if (seekTimeInMedia && this._player.isPaused()) {
|
} else if (seekTimeInMedia && this._loadedMediaPlayerController.isPaused()) {
|
||||||
this._player.play();
|
this._loadedMediaPlayerController.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
const seekTime =
|
const seekTime =
|
||||||
(await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
|
(await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
|
||||||
|
|
||||||
if (seekTime !== null) {
|
if (seekTime !== null) {
|
||||||
this._player.seek(seekTime);
|
this._loadedMediaPlayerController.seek(seekTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import { CardWideConfig, ViewerConfig } from '../../config/types.js';
|
|||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FullscreenElement,
|
MediaPlayer,
|
||||||
|
MediaPlayerController,
|
||||||
|
MediaPlayerElement,
|
||||||
ResolvedMedia,
|
ResolvedMedia,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { mayHaveAudio } from '../../utils/audio.js';
|
|
||||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
||||||
import {
|
import {
|
||||||
canonicalizeHAURL,
|
canonicalizeHAURL,
|
||||||
@@ -35,30 +35,16 @@ import {
|
|||||||
getWebProxiedURL,
|
getWebProxiedURL,
|
||||||
shouldUseWebProxy,
|
shouldUseWebProxy,
|
||||||
} from '../../utils/ha/web-proxy.js';
|
} from '../../utils/ha/web-proxy.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 {
|
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
playMediaMutingIfNecessary,
|
|
||||||
setControlsOnVideo,
|
|
||||||
} from '../../utils/media.js';
|
|
||||||
import { screenshotMedia } from '../../utils/screenshot.js';
|
|
||||||
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
||||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
||||||
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
||||||
|
import '../image-player.js';
|
||||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||||
|
import '../video-player.js';
|
||||||
|
|
||||||
@customElement('advanced-camera-card-viewer-provider')
|
@customElement('advanced-camera-card-viewer-provider')
|
||||||
export class AdvancedCameraCardViewerProvider
|
export class AdvancedCameraCardViewerProvider extends LitElement implements MediaPlayer {
|
||||||
extends LitElement
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
|
|
||||||
@@ -86,100 +72,14 @@ export class AdvancedCameraCardViewerProvider
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
protected _refAdvancedCameraCardMediaPlayer: Ref<
|
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||||
Element & AdvancedCameraCardMediaPlayer
|
|
||||||
> = createRef();
|
|
||||||
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
|
||||||
protected _refImageProvider: Ref<HTMLImageElement> = createRef();
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected _url: string | null = null;
|
protected _url: string | null = null;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
await playMediaMutingIfNecessary(
|
await this.updateComplete;
|
||||||
this,
|
return (await this._refProvider.value?.getMediaPlayerController()) ?? null;
|
||||||
this._refAdvancedCameraCardMediaPlayer.value ?? this._refVideoProvider.value,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
(
|
|
||||||
this._refAdvancedCameraCardMediaPlayer.value || this._refVideoProvider.value
|
|
||||||
)?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
this._refAdvancedCameraCardMediaPlayer.value?.mute();
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
this._refVideoProvider.value.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
this._refAdvancedCameraCardMediaPlayer.value?.mute();
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
this._refVideoProvider.value.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return this._refAdvancedCameraCardMediaPlayer.value?.isMuted() ?? true;
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
return this._refVideoProvider.value.muted;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return this._refAdvancedCameraCardMediaPlayer.value.seek(seconds);
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
hideMediaControlsTemporarily(this._refVideoProvider.value);
|
|
||||||
this._refVideoProvider.value.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return this._refAdvancedCameraCardMediaPlayer.value.setControls(controls);
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
setControlsOnVideo(
|
|
||||||
this._refVideoProvider.value,
|
|
||||||
controls ?? this.viewerConfig?.controls.builtin ?? true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return this._refAdvancedCameraCardMediaPlayer.value.isPaused();
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
return this._refVideoProvider.value.paused;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return await this._refAdvancedCameraCardMediaPlayer.value.getScreenshotURL();
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
return screenshotMedia(this._refVideoProvider.value);
|
|
||||||
} else if (this._refImageProvider.value) {
|
|
||||||
return this._refImageProvider.value.src;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
|
||||||
return this._refAdvancedCameraCardMediaPlayer.value.getFullscreenElement();
|
|
||||||
} else if (this._refVideoProvider.value) {
|
|
||||||
return this._refVideoProvider.value;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _switchToRelatedClipView(): Promise<void> {
|
protected async _switchToRelatedClipView(): Promise<void> {
|
||||||
@@ -332,8 +232,10 @@ export class AdvancedCameraCardViewerProvider
|
|||||||
: undefined,
|
: undefined,
|
||||||
)}
|
)}
|
||||||
.settings=${mediaID ? view?.context?.zoom?.[mediaID]?.requested : undefined}
|
.settings=${mediaID ? view?.context?.zoom?.[mediaID]?.requested : undefined}
|
||||||
@advanced-camera-card:zoom:zoomed=${() => this.setControls(false)}
|
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||||
@advanced-camera-card:zoom:unzoomed=${() => this.setControls()}
|
(await this.getMediaPlayerController())?.setControls(false)}
|
||||||
|
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||||
|
(await this.getMediaPlayerController())?.setControls()}
|
||||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||||
handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
|
handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
|
||||||
>
|
>
|
||||||
@@ -359,7 +261,7 @@ export class AdvancedCameraCardViewerProvider
|
|||||||
${ViewMediaClassifier.isVideo(this.media)
|
${ViewMediaClassifier.isVideo(this.media)
|
||||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||||
? html`<advanced-camera-card-ha-hls-player
|
? html`<advanced-camera-card-ha-hls-player
|
||||||
${ref(this._refAdvancedCameraCardMediaPlayer)}
|
${ref(this._refProvider)}
|
||||||
allow-exoplayer
|
allow-exoplayer
|
||||||
aria-label="${this.media.getTitle() ?? ''}"
|
aria-label="${this.media.getTitle() ?? ''}"
|
||||||
?autoplay=${false}
|
?autoplay=${false}
|
||||||
@@ -373,54 +275,26 @@ export class AdvancedCameraCardViewerProvider
|
|||||||
>
|
>
|
||||||
</advanced-camera-card-ha-hls-player>`
|
</advanced-camera-card-ha-hls-player>`
|
||||||
: html`
|
: html`
|
||||||
<video
|
<advanced-camera-card-video-player
|
||||||
${ref(this._refVideoProvider)}
|
${ref(this._refProvider)}
|
||||||
|
url=${this._url}
|
||||||
aria-label="${this.media.getTitle() ?? ''}"
|
aria-label="${this.media.getTitle() ?? ''}"
|
||||||
title="${this.media.getTitle() ?? ''}"
|
title="${this.media.getTitle() ?? ''}"
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
crossorigin="anonymous"
|
|
||||||
?autoplay=${false}
|
|
||||||
?controls=${this.viewerConfig.controls.builtin}
|
?controls=${this.viewerConfig.controls.builtin}
|
||||||
@loadedmetadata=${(ev: Event) => {
|
|
||||||
if (ev.target && !!this.viewerConfig?.controls.builtin) {
|
|
||||||
hideMediaControlsTemporarily(
|
|
||||||
ev.target as HTMLVideoElement,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
@loadeddata=${(ev: Event) => {
|
|
||||||
dispatchMediaLoadedEvent(this, ev, {
|
|
||||||
player: this,
|
|
||||||
capabilities: {
|
|
||||||
supportsPause: true,
|
|
||||||
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
|
||||||
},
|
|
||||||
technology: ['hls'],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
|
||||||
@play=${() => dispatchMediaPlayEvent(this)}
|
|
||||||
@pause=${() => dispatchMediaPauseEvent(this)}
|
|
||||||
>
|
>
|
||||||
<source src=${this._url} type="video/mp4" />
|
</advanced-camera-card-video-player>
|
||||||
</video>
|
|
||||||
`
|
`
|
||||||
: html`<img
|
: html`<advanced-camera-card-image-player
|
||||||
${ref(this._refImageProvider)}
|
${ref(this._refProvider)}
|
||||||
|
url="${this._url}"
|
||||||
aria-label="${this.media.getTitle() ?? ''}"
|
aria-label="${this.media.getTitle() ?? ''}"
|
||||||
src="${this._url}"
|
|
||||||
title="${this.media.getTitle() ?? ''}"
|
title="${this.media.getTitle() ?? ''}"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||||
this._switchToRelatedClipView();
|
this._switchToRelatedClipView();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@load=${(ev: Event) => {
|
></advanced-camera-card-image-player>`}
|
||||||
dispatchMediaLoadedEvent(this, ev, { player: this, technology: ['jpg'] });
|
|
||||||
}}
|
|
||||||
/>`}
|
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,18 +12,12 @@
|
|||||||
import { css, CSSResultGroup, html, nothing, PropertyValues, unsafeCSS } from 'lit';
|
import { css, CSSResultGroup, html, nothing, PropertyValues, 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 '../components/image-player.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import {
|
import { MediaLoadedInfo, MediaPlayer, MediaPlayerController } from '../types.js';
|
||||||
AdvancedCameraCardMediaPlayer,
|
import { dispatchExistingMediaLoadedInfoAsEvent } from '../utils/media-info.js';
|
||||||
FullscreenElement,
|
import './ha-hls-player.js';
|
||||||
MediaLoadedInfo,
|
import './ha-web-rtc-player.js';
|
||||||
} from '../types.js';
|
|
||||||
import {
|
|
||||||
createMediaLoadedInfo,
|
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
|
||||||
} from '../utils/media-info.js';
|
|
||||||
import './ha-hls-player';
|
|
||||||
import './ha-web-rtc-player';
|
|
||||||
|
|
||||||
customElements.whenDefined('ha-camera-stream').then(() => {
|
customElements.whenDefined('ha-camera-stream').then(() => {
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
@@ -44,12 +38,12 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
class AdvancedCameraCardHaCameraStream
|
class AdvancedCameraCardHaCameraStream
|
||||||
extends customElements.get('ha-camera-stream')
|
extends customElements.get('ha-camera-stream')
|
||||||
implements AdvancedCameraCardMediaPlayer
|
implements MediaPlayer
|
||||||
{
|
{
|
||||||
// Due to an obscure behavior when this card is casted, this element needs
|
// Due to an obscure behavior when this card is casted, this element needs
|
||||||
// to use query rather than the ref directive to find the player.
|
// to use query rather than the ref directive to find the player.
|
||||||
@query('.player:not(.hidden)')
|
@query('.player:not(.hidden)')
|
||||||
protected _player: AdvancedCameraCardMediaPlayer;
|
protected _player: MediaPlayer;
|
||||||
|
|
||||||
protected _mediaLoadedInfoPerStream: Record<StreamType, MediaLoadedInfo> = {};
|
protected _mediaLoadedInfoPerStream: Record<StreamType, MediaLoadedInfo> = {};
|
||||||
protected _mediaLoadedInfoDispatched: MediaLoadedInfo | null = null;
|
protected _mediaLoadedInfoDispatched: MediaLoadedInfo | null = null;
|
||||||
@@ -59,46 +53,9 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
// - 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
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
return this._player?.play();
|
await this.updateComplete;
|
||||||
}
|
return (await this._player?.getMediaPlayerController()) ?? null;
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
|
||||||
this._player?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
this._player?.mute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
this._player?.unmute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._player?.isMuted() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
this._player?.seek(seconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
if (this._player) {
|
|
||||||
this._player.setControls(controls ?? this.controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._player?.isPaused() ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._player ? await this._player.getScreenshotURL() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._player?.getFullscreenElement() ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _storeMediaLoadedInfoHandler(
|
protected _storeMediaLoadedInfoHandler(
|
||||||
@@ -123,16 +80,17 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
}
|
}
|
||||||
if (stream.type === STREAM_TYPE_MJPEG) {
|
if (stream.type === STREAM_TYPE_MJPEG) {
|
||||||
return html`
|
return html`
|
||||||
<img
|
<advanced-camera-card-image-player
|
||||||
@load=${(ev) =>
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
this._storeMediaLoadedInfo(
|
this._storeMediaLoadedInfo(STREAM_TYPE_MJPEG, ev.detail);
|
||||||
STREAM_TYPE_MJPEG,
|
ev.stopPropagation();
|
||||||
createMediaLoadedInfo(ev, { player: this, technology: ['mjpeg'] }),
|
}}
|
||||||
)}
|
src=${typeof this._connected == 'undefined' || this._connected
|
||||||
.src=${typeof this._connected == 'undefined' || this._connected
|
|
||||||
? computeMJPEGStreamUrl(this.stateObj)
|
? computeMJPEGStreamUrl(this.stateObj)
|
||||||
: this._posterUrl || ''}
|
: this._posterUrl || ''}
|
||||||
/>
|
filetype="mjpeg"
|
||||||
|
class="player"
|
||||||
|
></advanced-camera-card-image-player>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,8 +104,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityid=${this.stateObj.entity_id}
|
.entityid=${this.stateObj.entity_id}
|
||||||
.posterUrl=${this._posterUrl}
|
.posterUrl=${this._posterUrl}
|
||||||
@advanced-camera-card:media:loaded=${(ev) =>
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
this._storeMediaLoadedInfoHandler(STREAM_TYPE_HLS, ev)}
|
this._storeMediaLoadedInfoHandler(STREAM_TYPE_HLS, ev);
|
||||||
|
ev.stopPropagation();
|
||||||
|
}}
|
||||||
@streams=${this._handleHlsStreams}
|
@streams=${this._handleHlsStreams}
|
||||||
class="player ${stream.visible ? '' : 'hidden'}"
|
class="player ${stream.visible ? '' : 'hidden'}"
|
||||||
></advanced-camera-card-ha-hls-player>`;
|
></advanced-camera-card-ha-hls-player>`;
|
||||||
@@ -162,8 +122,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityid=${this.stateObj.entity_id}
|
.entityid=${this.stateObj.entity_id}
|
||||||
.posterUrl=${this._posterUrl}
|
.posterUrl=${this._posterUrl}
|
||||||
@advanced-camera-card:media:loaded=${(ev) =>
|
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||||
this._storeMediaLoadedInfoHandler(STREAM_TYPE_WEB_RTC, ev)}
|
this._storeMediaLoadedInfoHandler(STREAM_TYPE_WEB_RTC, ev);
|
||||||
|
ev.stopPropagation();
|
||||||
|
}}
|
||||||
@streams=${this._handleWebRtcStreams}
|
@streams=${this._handleWebRtcStreams}
|
||||||
class="player ${stream.visible ? '' : 'hidden'}"
|
class="player ${stream.visible ? '' : 'hidden'}"
|
||||||
></advanced-camera-card-ha-web-rtc-player>`;
|
></advanced-camera-card-ha-web-rtc-player>`;
|
||||||
|
|||||||
@@ -13,86 +13,43 @@ import { css, CSSResultGroup, html, TemplateResult, 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 { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { VideoMediaPlayerController } from '../components-lib/media-player/video.js';
|
||||||
import { renderMessage } from '../components/message.js';
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController } from '../types.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
import { errorToConsole } from '../utils/basic.js';
|
import { errorToConsole } from '../utils/basic.js';
|
||||||
|
import {
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
} from '../utils/controls.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
dispatchMediaVolumeChangeEvent,
|
dispatchMediaVolumeChangeEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import {
|
import { ConstructableLitElement } from './types.js';
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
setControlsOnVideo,
|
|
||||||
} from '../utils/media.js';
|
|
||||||
import { screenshotMedia } from '../utils/screenshot.js';
|
|
||||||
|
|
||||||
customElements.whenDefined('ha-hls-player').then(() => {
|
customElements.whenDefined('ha-hls-player').then(() => {
|
||||||
|
const HaHlsPlayer = customElements.get('ha-hls-player') as ConstructableLitElement;
|
||||||
|
|
||||||
@customElement('advanced-camera-card-ha-hls-player')
|
@customElement('advanced-camera-card-ha-hls-player')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
class AdvancedCameraCardHaHlsPlayer
|
class AdvancedCameraCardHaHlsPlayer extends HaHlsPlayer implements MediaPlayer {
|
||||||
extends customElements.get('ha-hls-player')
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
// Due to an obscure behavior when this card is casted, this element needs
|
// Due to an obscure behavior when this card is casted, this element needs
|
||||||
// to use query rather than the ref directive to find the player.
|
// to use query rather than the ref directive to find the player.
|
||||||
@query('#video')
|
@query('#video')
|
||||||
protected _video: HTMLVideoElement;
|
protected _video: HTMLVideoElement;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||||
return this._video?.play();
|
this,
|
||||||
}
|
() => this._video,
|
||||||
|
() => this.controls,
|
||||||
|
);
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
this._video?.pause();
|
return this._mediaPlayerController;
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
// The muted property is only for the initial muted state. Must explicitly
|
|
||||||
// set the muted on the video player to make the change dynamic.
|
|
||||||
if (this._video) {
|
|
||||||
this._video.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
// See note in mute().
|
|
||||||
if (this._video) {
|
|
||||||
this._video.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._video?.muted ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
if (this._video) {
|
|
||||||
hideMediaControlsTemporarily(this._video);
|
|
||||||
this._video.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
if (this._video) {
|
|
||||||
setControlsOnVideo(this._video, controls ?? this.controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._video?.paused ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._video ? screenshotMedia(this._video) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._video;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
@@ -141,7 +98,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
private _loadedDataHandler(ev: Event) {
|
private _loadedDataHandler(ev: Event) {
|
||||||
super._loadedData();
|
super._loadedData();
|
||||||
dispatchMediaLoadedEvent(this, ev, {
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
player: this,
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
hasAudio: mayHaveAudio(this._video),
|
hasAudio: mayHaveAudio(this._video),
|
||||||
|
|||||||
@@ -14,84 +14,44 @@ import { customElement } from 'lit/decorators.js';
|
|||||||
import { query } from 'lit/decorators/query.js';
|
import { query } from 'lit/decorators/query.js';
|
||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
|
import { VideoMediaPlayerController } from '../components-lib/media-player/video.js';
|
||||||
import { renderMessage } from '../components/message.js';
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../types.js';
|
import { MediaPlayer, MediaPlayerController } from '../types.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
|
import {
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
} from '../utils/controls.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
dispatchMediaVolumeChangeEvent,
|
dispatchMediaVolumeChangeEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import {
|
import { ConstructableLitElement } from './types.js';
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
setControlsOnVideo,
|
|
||||||
} from '../utils/media.js';
|
|
||||||
import { screenshotMedia } from '../utils/screenshot.js';
|
|
||||||
|
|
||||||
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||||
|
const HaWebRtcPlayer = customElements.get(
|
||||||
|
'ha-web-rtc-player',
|
||||||
|
) as ConstructableLitElement;
|
||||||
|
|
||||||
@customElement('advanced-camera-card-ha-web-rtc-player')
|
@customElement('advanced-camera-card-ha-web-rtc-player')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
class AdvancedCameraCardHaWebRtcPlayer
|
class AdvancedCameraCardHaWebRtcPlayer extends HaWebRtcPlayer implements MediaPlayer {
|
||||||
extends customElements.get('ha-web-rtc-player')
|
|
||||||
implements AdvancedCameraCardMediaPlayer
|
|
||||||
{
|
|
||||||
// Due to an obscure behavior when this card is casted, this element needs
|
// Due to an obscure behavior when this card is casted, this element needs
|
||||||
// to use query rather than the ref directive to find the player.
|
// to use query rather than the ref directive to find the player.
|
||||||
@query('#remote-stream')
|
@query('#remote-stream')
|
||||||
protected _video: HTMLVideoElement;
|
protected _video: HTMLVideoElement;
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||||
return this._video?.play();
|
this,
|
||||||
}
|
() => this._video,
|
||||||
|
() => this.controls,
|
||||||
|
);
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||||
this._video?.pause();
|
return this._mediaPlayerController;
|
||||||
}
|
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
|
||||||
// The muted property is only for the initial muted state. Must explicitly
|
|
||||||
// set the muted on the video player to make the change dynamic.
|
|
||||||
if (this._video) {
|
|
||||||
this._video.muted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
// See note in mute().
|
|
||||||
if (this._video) {
|
|
||||||
this._video.muted = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
return this._video?.muted ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
|
||||||
if (this._video) {
|
|
||||||
this._video.currentTime = seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
|
||||||
if (this._video) {
|
|
||||||
setControlsOnVideo(this._video, controls ?? this.controls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPaused(): boolean {
|
|
||||||
return this._video?.paused ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
|
||||||
return this._video ? screenshotMedia(this._video) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenElement(): FullscreenElement | null {
|
|
||||||
return this._video ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
@@ -136,7 +96,7 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
private _loadedDataHandler(ev: Event) {
|
private _loadedDataHandler(ev: Event) {
|
||||||
super._loadedData();
|
super._loadedData();
|
||||||
dispatchMediaLoadedEvent(this, ev, {
|
dispatchMediaLoadedEvent(this, ev, {
|
||||||
player: this,
|
mediaPlayerController: this._mediaPlayerController,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
hasAudio: mayHaveAudio(this._video),
|
hasAudio: mayHaveAudio(this._video),
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { LitElement } from 'lit';
|
||||||
|
|
||||||
|
export type ConstructableLitElement = { new (...args: unknown[]): LitElement };
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@use 'basic-block.scss';
|
||||||
|
@use 'media-layout.scss';
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@include media-layout.media-layout();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@use 'basic-block.scss';
|
||||||
|
@use 'media-background.scss';
|
||||||
|
@use 'media-layout.scss';
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@include media-layout.media-layout();
|
||||||
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
@use 'media-background.scss';
|
@use 'basic-block.scss';
|
||||||
@use 'media-layout.scss';
|
@use 'media-layout.scss';
|
||||||
|
@use 'media-background.scss';
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
@use 'basic-block.scss';
|
||||||
@use 'media-background.scss';
|
@use 'media-background.scss';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@use 'basic-block.scss';
|
||||||
|
@use 'media-layout.scss';
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@include media-layout.media-layout();
|
||||||
|
}
|
||||||
@@ -1,22 +1,14 @@
|
|||||||
|
@use 'basic-block.scss';
|
||||||
@use 'media-background.scss';
|
@use 'media-background.scss';
|
||||||
@use 'media-layout.scss';
|
|
||||||
|
|
||||||
:host {
|
advanced-camera-card-ha-hls-player,
|
||||||
|
advanced-camera-card-image-player,
|
||||||
|
advanced-camera-card-video-player {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
|
||||||
video,
|
|
||||||
advanced-camera-card-ha-hls-player {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
@include media-layout.media-layout();
|
|
||||||
}
|
|
||||||
|
|
||||||
advanced-camera-card-progress-indicator {
|
advanced-camera-card-progress-indicator {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
+9
-2
@@ -39,7 +39,8 @@ export interface MediaLoadedInfo {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
technology?: MediaTechnology[];
|
technology?: MediaTechnology[];
|
||||||
player?: AdvancedCameraCardMediaPlayer;
|
|
||||||
|
mediaPlayerController?: MediaPlayerController;
|
||||||
capabilities?: MediaLoadedCapabilities;
|
capabilities?: MediaLoadedCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ export type WebkitHTMLVideoElement = HTMLVideoElement & {
|
|||||||
|
|
||||||
export type FullscreenElement = HTMLElement;
|
export type FullscreenElement = HTMLElement;
|
||||||
|
|
||||||
export interface AdvancedCameraCardMediaPlayer {
|
export interface MediaPlayerController {
|
||||||
play(): Promise<void>;
|
play(): Promise<void>;
|
||||||
pause(): Promise<void>;
|
pause(): Promise<void>;
|
||||||
mute(): Promise<void>;
|
mute(): Promise<void>;
|
||||||
@@ -77,6 +78,12 @@ export interface AdvancedCameraCardMediaPlayer {
|
|||||||
getFullscreenElement(): FullscreenElement | null;
|
getFullscreenElement(): FullscreenElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MediaPlayer {
|
||||||
|
getMediaPlayerController(): Promise<MediaPlayerController | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MediaPlayerElement<T extends HTMLElement = HTMLElement> = T & MediaPlayer;
|
||||||
|
|
||||||
export type LovelaceCardWithEditor = LovelaceCard & {
|
export type LovelaceCardWithEditor = LovelaceCard & {
|
||||||
constructor: {
|
constructor: {
|
||||||
getConfigElement(): Promise<LovelaceCardEditor>;
|
getConfigElement(): Promise<LovelaceCardEditor>;
|
||||||
|
|||||||
+5
-4
@@ -4,10 +4,11 @@ export interface AudioProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// There is currently no consistent cross-browser modern way to determine if a
|
// 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
|
// video element has audio tracks. The below will work in ~24% of browsers, but
|
||||||
// not in Chrome. There used to be a usable `webkitAudioDecodedByteCount`
|
// notably not in Chrome. There used to be a usable
|
||||||
// property, but this now seems to be consistently 0 in Chrome. This generously
|
// `webkitAudioDecodedByteCount` property, but this now seems to be consistently
|
||||||
// defaults to assuming there is audio when we cannot rule it out.
|
// 0 in Chrome. This generously defaults to assuming there is audio when we
|
||||||
|
// cannot rule it out.
|
||||||
export const mayHaveAudio = (video: HTMLVideoElement & AudioProperties): boolean => {
|
export const mayHaveAudio = (video: HTMLVideoElement & AudioProperties): boolean => {
|
||||||
if (video.mozHasAudio !== undefined) {
|
if (video.mozHasAudio !== undefined) {
|
||||||
return video.mozHasAudio;
|
return video.mozHasAudio;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AdvancedCameraCardMediaPlayer } from '../types';
|
|
||||||
import { Timer } from './timer';
|
import { Timer } from './timer';
|
||||||
|
|
||||||
// The number of seconds to hide the video controls for after loading (in order
|
// The number of seconds to hide the video controls for after loading (in order
|
||||||
@@ -59,30 +58,3 @@ export const hideMediaControlsTemporarily = (
|
|||||||
setControlsOnVideo(video, oldValue);
|
setControlsOnVideo(video, oldValue);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @param player The Advanced Camera Card Media Player object.
|
|
||||||
* @param video An underlying video or media player upon which to call play.
|
|
||||||
*/
|
|
||||||
export const playMediaMutingIfNecessary = async (
|
|
||||||
player: AdvancedCameraCardMediaPlayer,
|
|
||||||
video?: HTMLVideoElement | AdvancedCameraCardMediaPlayer,
|
|
||||||
): Promise<void> => {
|
|
||||||
// If the play call fails, and the media is not already muted, mute it first
|
|
||||||
// and then try again. This works around some browsers that prevent
|
|
||||||
// auto-play unless the video is muted.
|
|
||||||
if (video?.play) {
|
|
||||||
try {
|
|
||||||
await video.play();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if ((err as Error).name === 'NotAllowedError' && !player.isMuted()) {
|
|
||||||
await player.mute();
|
|
||||||
try {
|
|
||||||
await video.play();
|
|
||||||
} catch (_) {
|
|
||||||
// Pass.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
MediaLoadedCapabilities,
|
MediaLoadedCapabilities,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
|
MediaPlayerController,
|
||||||
MediaTechnology,
|
MediaTechnology,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { dispatchAdvancedCameraCardEvent } from './basic.js';
|
import { dispatchAdvancedCameraCardEvent } from './basic.js';
|
||||||
@@ -17,7 +17,7 @@ const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF;
|
|||||||
export function createMediaLoadedInfo(
|
export function createMediaLoadedInfo(
|
||||||
source: Event | HTMLElement,
|
source: Event | HTMLElement,
|
||||||
options?: {
|
options?: {
|
||||||
player?: AdvancedCameraCardMediaPlayer;
|
mediaPlayerController?: MediaPlayerController;
|
||||||
capabilities?: MediaLoadedCapabilities;
|
capabilities?: MediaLoadedCapabilities;
|
||||||
technology?: MediaTechnology[];
|
technology?: MediaTechnology[];
|
||||||
},
|
},
|
||||||
@@ -45,7 +45,7 @@ export function createMediaLoadedInfo(
|
|||||||
return {
|
return {
|
||||||
width: (target as HTMLCanvasElement).width,
|
width: (target as HTMLCanvasElement).width,
|
||||||
height: (target as HTMLCanvasElement).height,
|
height: (target as HTMLCanvasElement).height,
|
||||||
player: options?.player,
|
mediaPlayerController: options?.mediaPlayerController,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ export function dispatchMediaLoadedEvent(
|
|||||||
target: HTMLElement,
|
target: HTMLElement,
|
||||||
source: Event | HTMLElement,
|
source: Event | HTMLElement,
|
||||||
options?: {
|
options?: {
|
||||||
player?: AdvancedCameraCardMediaPlayer;
|
mediaPlayerController?: MediaPlayerController;
|
||||||
capabilities?: MediaLoadedCapabilities;
|
capabilities?: MediaLoadedCapabilities;
|
||||||
technology?: MediaTechnology[];
|
technology?: MediaTechnology[];
|
||||||
},
|
},
|
||||||
|
|||||||
+16
-4
@@ -1,16 +1,28 @@
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
|
|
||||||
export const screenshotMedia = (video: HTMLVideoElement): string | null => {
|
export const screenshotImage = (image: HTMLImageElement): string | null => {
|
||||||
|
return screenshotElement(image, image.naturalWidth, image.naturalHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const screenshotVideo = (video: HTMLVideoElement): string | null => {
|
||||||
|
return screenshotElement(video, video.videoWidth, video.videoHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
const screenshotElement = (
|
||||||
|
src: CanvasImageSource,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): string | null => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = video.videoWidth;
|
canvas.width = width;
|
||||||
canvas.height = video.videoHeight;
|
canvas.height = height;
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
ctx.drawImage(src, 0, 0, canvas.width, canvas.height);
|
||||||
return canvas.toDataURL('image/jpeg');
|
return canvas.toDataURL('image/jpeg');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { expect, it, vi } from 'vitest';
|
import { expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { MuteAction } from '../../../../src/card-controller/actions/actions/mute';
|
import { MuteAction } from '../../../../src/card-controller/actions/actions/mute';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../../../src/types';
|
import { MediaPlayerController } from '../../../../src/types';
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||||
|
|
||||||
it('should handle mute action', async () => {
|
it('should handle mute action', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const action = new MuteAction(
|
const action = new MuteAction(
|
||||||
@@ -22,5 +22,5 @@ it('should handle mute action', async () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(player.mute).toBeCalled();
|
expect(mediaPlayerController.mute).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { expect, it, vi } from 'vitest';
|
import { expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { PauseAction } from '../../../../src/card-controller/actions/actions/pause';
|
import { PauseAction } from '../../../../src/card-controller/actions/actions/pause';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../../../src/types';
|
import { MediaPlayerController } from '../../../../src/types';
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||||
|
|
||||||
it('should handle pause action', async () => {
|
it('should handle pause action', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const action = new PauseAction(
|
const action = new PauseAction(
|
||||||
@@ -22,5 +22,5 @@ it('should handle pause action', async () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(player.pause).toBeCalled();
|
expect(mediaPlayerController.pause).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { expect, it, vi } from 'vitest';
|
import { expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { PlayAction } from '../../../../src/card-controller/actions/actions/play';
|
import { PlayAction } from '../../../../src/card-controller/actions/actions/play';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../../../src/types';
|
import { MediaPlayerController } from '../../../../src/types';
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||||
|
|
||||||
it('should handle play action', async () => {
|
it('should handle play action', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const action = new PlayAction(
|
const action = new PlayAction(
|
||||||
@@ -22,5 +22,5 @@ it('should handle play action', async () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(player.play).toBeCalled();
|
expect(mediaPlayerController.play).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { expect, it, vi } from 'vitest';
|
import { expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { UnmuteAction } from '../../../../src/card-controller/actions/actions/unmute';
|
import { UnmuteAction } from '../../../../src/card-controller/actions/actions/unmute';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../../../src/types';
|
import { MediaPlayerController } from '../../../../src/types';
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||||
|
|
||||||
it('should handle unmute action', async () => {
|
it('should handle unmute action', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const action = new UnmuteAction(
|
const action = new UnmuteAction(
|
||||||
@@ -22,5 +22,5 @@ it('should handle unmute action', async () => {
|
|||||||
|
|
||||||
await action.execute(api);
|
await action.execute(api);
|
||||||
|
|
||||||
expect(player.unmute).toBeCalled();
|
expect(mediaPlayerController.unmute).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { DownloadManager } from '../../src/card-controller/download-manager';
|
import { DownloadManager } from '../../src/card-controller/download-manager';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../src/types';
|
import { MediaPlayerController } from '../../src/types';
|
||||||
import { downloadMedia, downloadURL } from '../../src/utils/download.js';
|
import { downloadMedia, downloadURL } from '../../src/utils/download.js';
|
||||||
import {
|
import {
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
@@ -62,12 +62,12 @@ describe('DownloadManager.downloadScreenshot', () => {
|
|||||||
|
|
||||||
it('with url', async () => {
|
it('with url', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
player.getScreenshotURL.mockResolvedValue('http://screenshot');
|
mediaPlayerController.getScreenshotURL.mockResolvedValue('http://screenshot');
|
||||||
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const manager = new DownloadManager(api);
|
const manager = new DownloadManager(api);
|
||||||
@@ -78,12 +78,12 @@ describe('DownloadManager.downloadScreenshot', () => {
|
|||||||
|
|
||||||
it('without url', async () => {
|
it('without url', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
player.getScreenshotURL.mockResolvedValue(null);
|
mediaPlayerController.getScreenshotURL.mockResolvedValue(null);
|
||||||
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const manager = new DownloadManager(api);
|
const manager = new DownloadManager(api);
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
||||||
import { ConditionStateManager } from '../../../../src/conditions/state-manager';
|
import { ConditionStateManager } from '../../../../src/conditions/state-manager';
|
||||||
import {
|
import { MediaPlayerController, WebkitHTMLVideoElement } from '../../../../src/types';
|
||||||
AdvancedCameraCardMediaPlayer,
|
|
||||||
WebkitHTMLVideoElement,
|
|
||||||
} from '../../../../src/types';
|
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||||
|
|
||||||
const createWebkitVideoElement = (): HTMLVideoElement &
|
const createWebkitVideoElement = (): HTMLVideoElement &
|
||||||
@@ -13,10 +10,10 @@ const createWebkitVideoElement = (): HTMLVideoElement &
|
|||||||
return document.createElement('video');
|
return document.createElement('video');
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlayer = (element: HTMLElement): AdvancedCameraCardMediaPlayer => {
|
const createMediaPlayerController = (element: HTMLElement): MediaPlayerController => {
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
player.getFullscreenElement.mockReturnValue(element);
|
mediaPlayerController.getFullscreenElement.mockReturnValue(element);
|
||||||
return player;
|
return mediaPlayerController;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -57,10 +54,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
const element = createWebkitVideoElement();
|
const element = createWebkitVideoElement();
|
||||||
element.webkitDisplayingFullscreen = fullscreen;
|
element.webkitDisplayingFullscreen = fullscreen;
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,10 +73,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
const element = createWebkitVideoElement();
|
const element = createWebkitVideoElement();
|
||||||
element.webkitSupportsFullscreen = supported;
|
element.webkitSupportsFullscreen = supported;
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -96,10 +93,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
element.webkitEnterFullscreen = vi.fn();
|
element.webkitEnterFullscreen = vi.fn();
|
||||||
element.webkitSupportsFullscreen = true;
|
element.webkitSupportsFullscreen = true;
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -116,10 +113,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
element.webkitExitFullscreen = vi.fn();
|
element.webkitExitFullscreen = vi.fn();
|
||||||
element.webkitSupportsFullscreen = true;
|
element.webkitSupportsFullscreen = true;
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -137,10 +134,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
element.webkitExitFullscreen = vi.fn();
|
element.webkitExitFullscreen = vi.fn();
|
||||||
element.webkitSupportsFullscreen = false;
|
element.webkitSupportsFullscreen = false;
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -160,10 +157,10 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
element.webkitEnterFullscreen = vi.fn();
|
element.webkitEnterFullscreen = vi.fn();
|
||||||
element.webkitExitFullscreen = vi.fn();
|
element.webkitExitFullscreen = vi.fn();
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -190,9 +187,11 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
provider.connect();
|
provider.connect();
|
||||||
|
|
||||||
const element_1 = createWebkitVideoElement();
|
const element_1 = createWebkitVideoElement();
|
||||||
const player_1 = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController_1 = mock<MediaPlayerController>();
|
||||||
player_1.getFullscreenElement.mockReturnValue(element_1);
|
mediaPlayerController_1.getFullscreenElement.mockReturnValue(element_1);
|
||||||
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
|
const mediaLoadedInfo_1 = createMediaLoadedInfo({
|
||||||
|
mediaPlayerController: mediaPlayerController_1,
|
||||||
|
});
|
||||||
|
|
||||||
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_1 });
|
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_1 });
|
||||||
|
|
||||||
@@ -201,9 +200,11 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
expect(handler).toBeCalledTimes(1);
|
expect(handler).toBeCalledTimes(1);
|
||||||
|
|
||||||
const element_2 = createWebkitVideoElement();
|
const element_2 = createWebkitVideoElement();
|
||||||
const player_2 = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController_2 = mock<MediaPlayerController>();
|
||||||
player_2.getFullscreenElement.mockReturnValue(element_2);
|
mediaPlayerController_2.getFullscreenElement.mockReturnValue(element_2);
|
||||||
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
|
const mediaLoadedInfo_2 = createMediaLoadedInfo({
|
||||||
|
mediaPlayerController: mediaPlayerController_2,
|
||||||
|
});
|
||||||
|
|
||||||
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_2 });
|
stateManager.setState({ mediaLoadedInfo: mediaLoadedInfo_2 });
|
||||||
|
|
||||||
@@ -243,14 +244,14 @@ describe('WebkitFullScreenProvider', () => {
|
|||||||
const element = createWebkitVideoElement();
|
const element = createWebkitVideoElement();
|
||||||
element.play = vi.fn();
|
element.play = vi.fn();
|
||||||
|
|
||||||
const player = createPlayer(element);
|
const mediaPlayerController = createMediaPlayerController(element);
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
createMediaLoadedInfo({
|
createMediaLoadedInfo({
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaLoadedInfo = createMediaLoadedInfo({ player });
|
const mediaLoadedInfo = createMediaLoadedInfo({ mediaPlayerController });
|
||||||
|
|
||||||
stateManager.setState({ mediaLoadedInfo });
|
stateManager.setState({ mediaLoadedInfo });
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { MicrophoneState } from '../../src/card-controller/types';
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
import {
|
import {
|
||||||
MediaActionsController,
|
MediaActionsController,
|
||||||
MediaActionsControllerOptions,
|
MediaActionsControllerOptions,
|
||||||
} from '../../src/components-lib/media-actions-controller';
|
} from '../../src/components-lib/media-actions-controller';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../src/types';
|
import { MediaPlayerController, MediaPlayerElement } from '../../src/types';
|
||||||
import {
|
import {
|
||||||
IntersectionObserverMock,
|
IntersectionObserverMock,
|
||||||
MutationObserverMock,
|
MutationObserverMock,
|
||||||
@@ -18,30 +19,22 @@ import { callVisibilityHandler, createTestSlideNodes } from '../utils/embla/test
|
|||||||
const getPlayer = (
|
const getPlayer = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
selector: string,
|
selector: string,
|
||||||
): (HTMLElement & AdvancedCameraCardMediaPlayer) | null => {
|
): MediaPlayerElement | null => {
|
||||||
return element.querySelector(selector);
|
return element.querySelector(selector);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlayer = (): HTMLElement & AdvancedCameraCardMediaPlayer => {
|
const createPlayerElement = (controller?: MediaPlayerController): MediaPlayerElement => {
|
||||||
const player = document.createElement('video');
|
const player = document.createElement('video');
|
||||||
|
player['getMediaPlayerController'] = vi
|
||||||
player['play'] = vi.fn();
|
.fn()
|
||||||
player['pause'] = vi.fn();
|
.mockResolvedValue(controller ?? mock<MediaPlayerController>());
|
||||||
player['mute'] = vi.fn();
|
return player as unknown as MediaPlayerElement;
|
||||||
player['unmute'] = vi.fn();
|
|
||||||
player['isMuted'] = vi.fn().mockReturnValue(true);
|
|
||||||
player['seek'] = vi.fn();
|
|
||||||
player['getScreenshotURL'] = vi.fn();
|
|
||||||
player['setControls'] = vi.fn();
|
|
||||||
player['isPaused'] = vi.fn();
|
|
||||||
|
|
||||||
return player as unknown as HTMLElement & AdvancedCameraCardMediaPlayer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
||||||
const divs = createTestSlideNodes({ n: n });
|
const divs = createTestSlideNodes({ n: n });
|
||||||
for (const div of divs) {
|
for (const div of divs) {
|
||||||
div.appendChild(createPlayer());
|
div.appendChild(createPlayerElement());
|
||||||
}
|
}
|
||||||
return divs;
|
return divs;
|
||||||
};
|
};
|
||||||
@@ -61,11 +54,11 @@ describe('MediaActionsController', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should initialize', () => {
|
describe('should set root', () => {
|
||||||
it('should have root', async () => {
|
it('should have root', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
|
|
||||||
controller.initialize(createParent());
|
controller.setRoot(createParent());
|
||||||
|
|
||||||
expect(controller.hasRoot()).toBeTruthy();
|
expect(controller.hasRoot()).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -76,13 +69,23 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
const parent = createParent({ children: children });
|
const parent = createParent({ children: children });
|
||||||
|
|
||||||
controller.initialize(parent);
|
controller.setRoot(parent);
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should re-initialize after mutation', async () => {
|
it('should do nothing on resetting same root', () => {
|
||||||
|
const controller = new MediaActionsController();
|
||||||
|
const parent = createParent({ children: createPlayerSlideNodes() });
|
||||||
|
|
||||||
|
expect(controller.setRoot(parent)).toBeTruthy();
|
||||||
|
expect(controller.setRoot(parent)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-setRoot after mutation', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
playerSelector: 'video',
|
playerSelector: 'video',
|
||||||
@@ -90,9 +93,11 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const parent = createParent({ children: createPlayerSlideNodes(1) });
|
const parent = createParent({ children: createPlayerSlideNodes(1) });
|
||||||
controller.initialize(parent);
|
controller.setRoot(parent);
|
||||||
|
|
||||||
const newPlayer = createPlayer();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
|
|
||||||
|
const newPlayer = createPlayerElement(mediaPlayerController);
|
||||||
const newChild = document.createElement('div');
|
const newChild = document.createElement('div');
|
||||||
newChild.appendChild(newPlayer);
|
newChild.appendChild(newPlayer);
|
||||||
parent.append(newChild);
|
parent.append(newChild);
|
||||||
@@ -101,7 +106,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await controller.setTarget(1, true);
|
await controller.setTarget(1, true);
|
||||||
|
|
||||||
expect(newPlayer.play).toBeCalled();
|
expect(mediaPlayerController.play).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,13 +120,15 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
const parent = createParent({ children: children });
|
const parent = createParent({ children: children });
|
||||||
controller.initialize(parent);
|
controller.setRoot(parent);
|
||||||
|
|
||||||
controller.destroy();
|
controller.destroy();
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,11 +153,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -162,13 +171,19 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unselect before selecting a new target', async () => {
|
it('should unselect before selecting a new target', async () => {
|
||||||
@@ -180,13 +195,17 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
await controller.setTarget(1, true);
|
await controller.setTarget(1, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.pause).toBeCalled();
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.pause,
|
||||||
|
).toBeCalled();
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
|
||||||
|
).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select after target was previously visible', async () => {
|
it('should select after target was previously visible', async () => {
|
||||||
@@ -198,17 +217,25 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, false);
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).not.toBeCalled();
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).not.toBeCalled();
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).not.toBeCalled();
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalled();
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalled();
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,12 +248,16 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
|
||||||
controller.unsetTarget();
|
controller.unsetTarget();
|
||||||
|
|
||||||
@@ -236,8 +267,12 @@ describe('MediaActionsController', () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
// Play/Mute will not have been called again.
|
// Play/Mute will not have been called again.
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to media loaded', () => {
|
describe('should respond to media loaded', () => {
|
||||||
@@ -249,10 +284,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
|
||||||
getPlayer(children[0], 'video')?.dispatchEvent(
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
new Event('advanced-camera-card:media:loaded'),
|
new Event('advanced-camera-card:media:loaded'),
|
||||||
@@ -260,7 +298,9 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(2);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unmute after media load', async () => {
|
it('should unmute after media load', async () => {
|
||||||
@@ -271,10 +311,12 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
|
||||||
getPlayer(children[0], 'video')?.dispatchEvent(
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
new Event('advanced-camera-card:media:loaded'),
|
new Event('advanced-camera-card:media:loaded'),
|
||||||
@@ -282,7 +324,9 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(2);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should take no action on unrelated media load', async () => {
|
it('should take no action on unrelated media load', async () => {
|
||||||
@@ -294,7 +338,7 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
@@ -304,8 +348,12 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getPlayer(children[9], 'video')?.play).not.toBeCalled();
|
expect(
|
||||||
expect(getPlayer(children[9], 'video')?.unmute).not.toBeCalled();
|
(await getPlayer(children[9], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).not.toBeCalled();
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[9], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should play and unmute on unselected but targeted media load', async () => {
|
it('should play and unmute on unselected but targeted media load', async () => {
|
||||||
@@ -317,12 +365,16 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, false);
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(1);
|
||||||
|
|
||||||
getPlayer(children[0], 'video')?.dispatchEvent(
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
new Event('advanced-camera-card:media:loaded'),
|
new Event('advanced-camera-card:media:loaded'),
|
||||||
@@ -330,8 +382,12 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(2);
|
expect(
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(2);
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.play,
|
||||||
|
).toBeCalledTimes(2);
|
||||||
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -356,12 +412,14 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
await controller.setTarget(0, false);
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -389,11 +447,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).not.toBeCalled();
|
||||||
|
|
||||||
Object.defineProperty(document, 'visibilityState', {
|
Object.defineProperty(document, 'visibilityState', {
|
||||||
value: 'visible',
|
value: 'visible',
|
||||||
@@ -402,7 +462,9 @@ describe('MediaActionsController', () => {
|
|||||||
await callVisibilityHandler();
|
await callVisibilityHandler();
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -432,11 +494,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).not.toBeCalled();
|
||||||
|
|
||||||
Object.defineProperty(document, 'visibilityState', {
|
Object.defineProperty(document, 'visibilityState', {
|
||||||
value: 'hidden',
|
value: 'hidden',
|
||||||
@@ -445,7 +509,9 @@ describe('MediaActionsController', () => {
|
|||||||
await callVisibilityHandler();
|
await callVisibilityHandler();
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -471,11 +537,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).not.toBeCalled();
|
||||||
|
|
||||||
// There's always a first call to an intersection observer handler. In
|
// There's always a first call to an intersection observer handler. In
|
||||||
// this case the MediaActionsController ignores it.
|
// this case the MediaActionsController ignores it.
|
||||||
@@ -484,7 +552,9 @@ describe('MediaActionsController', () => {
|
|||||||
await callIntersectionHandler(true);
|
await callIntersectionHandler(true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -510,11 +580,13 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).not.toBeCalled();
|
||||||
|
|
||||||
// There's always a first call to an intersection observer handler. In
|
// There's always a first call to an intersection observer handler. In
|
||||||
// this case the MediaActionsController ignores it.
|
// this case the MediaActionsController ignores it.
|
||||||
@@ -523,7 +595,9 @@ describe('MediaActionsController', () => {
|
|||||||
await callIntersectionHandler(true);
|
await callIntersectionHandler(true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.[func],
|
||||||
|
).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -558,7 +632,7 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
@@ -568,7 +642,9 @@ describe('MediaActionsController', () => {
|
|||||||
microphoneState: createMicrophoneState({ muted: false }),
|
microphoneState: createMicrophoneState({ muted: false }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.unmute,
|
||||||
|
).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should mute after delay after microphone muted', async () => {
|
it('should mute after delay after microphone muted', async () => {
|
||||||
@@ -581,7 +657,7 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
@@ -593,7 +669,9 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
|
||||||
|
).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not mute after delay after microphone muted', async () => {
|
it('should not mute after delay after microphone muted', async () => {
|
||||||
@@ -606,7 +684,7 @@ describe('MediaActionsController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.setRoot(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
@@ -618,7 +696,9 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.mute).not.toBeCalled();
|
expect(
|
||||||
|
(await getPlayer(children[0], 'video')?.getMediaPlayerController())?.mute,
|
||||||
|
).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { ImageMediaPlayerController } from '../../../src/components-lib/media-player/image';
|
||||||
|
import { createLitElement } from '../../test-utils';
|
||||||
|
import { screenshotImage } from '../../../src/utils/screenshot';
|
||||||
|
|
||||||
|
vi.mock('../../../src/utils/screenshot.js');
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('ImageMediaPlayerController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore play', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore pause', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore mute', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore unmute', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always report muted', () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore seek', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.seek(10);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore set controls', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
await controller.setControls(true);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always report unpaused', () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get screenshot URL', async () => {
|
||||||
|
it('should return screenshot URL with image', async () => {
|
||||||
|
const url = 'data:image/png;base64,';
|
||||||
|
vi.mocked(screenshotImage).mockReturnValue(url);
|
||||||
|
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBe(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without image', async () => {
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get fullscreen element', async () => {
|
||||||
|
it('should return fullscreen element with image', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
expect(await controller.getFullscreenElement()).toBe(image);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without image', async () => {
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(controller.getFullscreenElement()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import JSMpeg from '@cycjimmy/jsmpeg-player';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { JSMPEGMediaPlayerController } from '../../../src/components-lib/media-player/jsmpeg';
|
||||||
|
import { createLitElement } from '../../test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('JSMPEGMediaPlayerController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should play', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(videoElement.play).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
expect(videoElement.stop).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should mute', async () => {
|
||||||
|
it('should mute with player', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.volume = 1;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
expect(videoElement.player.volume).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without player', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => null,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should unmute', async () => {
|
||||||
|
it('should mute with player', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.volume = 0;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
expect(videoElement.player.volume).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without player', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => null,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return muted state', () => {
|
||||||
|
it('should return true when muted', () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.volume = 0;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when not muted', () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.volume = 1;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no player', () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => null,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore seek', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.seek(10);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore set controls', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
await controller.setControls(true);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return paused state', () => {
|
||||||
|
it('should return true when paused', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.paused = true;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when not paused', async () => {
|
||||||
|
const videoElement = mock<JSMpeg.VideoElement>();
|
||||||
|
videoElement.player = mock<JSMpeg.Player>();
|
||||||
|
videoElement.player.paused = false;
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => videoElement,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no video', () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => null,
|
||||||
|
() => mock<HTMLCanvasElement>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get screenshot URL', async () => {
|
||||||
|
it('should return screenshot URL with canvas', async () => {
|
||||||
|
const url = 'data:image/png;base64,';
|
||||||
|
const canvas = mock<HTMLCanvasElement>();
|
||||||
|
canvas.toDataURL.mockReturnValue(url);
|
||||||
|
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => canvas,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBe(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without canvas', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get fullscreen element', async () => {
|
||||||
|
it('should return fullscreen element with canvas', async () => {
|
||||||
|
const canvas = mock<HTMLCanvasElement>();
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => canvas,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getFullscreenElement()).toBe(canvas);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without cancas', async () => {
|
||||||
|
const controller = new JSMPEGMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<JSMpeg.VideoElement>(),
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.getFullscreenElement()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { CachedValueController } from '../../../src/components-lib/cached-value-controller';
|
||||||
|
import { ImageMediaPlayerController } from '../../../src/components-lib/media-player/image';
|
||||||
|
import { UpdatingImageMediaPlayerController } from '../../../src/components-lib/media-player/updating-image';
|
||||||
|
import { createLitElement } from '../../test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('UpdatingImageMediaPlayerController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should play', async () => {
|
||||||
|
const cachedValueController = mock<CachedValueController<string>>();
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => cachedValueController,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(cachedValueController.startTimer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause', async () => {
|
||||||
|
const cachedValueController = mock<CachedValueController<string>>();
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => cachedValueController,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
expect(cachedValueController.stopTimer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore mute', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore unmute', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always report muted', () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore seek', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.seek(10);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore set controls', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.setControls(true);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always report unpaused', () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
const controller = new ImageMediaPlayerController(createLitElement(), () => image);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get paused state', () => {
|
||||||
|
it('should return true when the cached value controller does not have a timer', () => {
|
||||||
|
const cachedValueController = mock<CachedValueController<string>>();
|
||||||
|
cachedValueController.hasTimer.mockReturnValue(false);
|
||||||
|
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => cachedValueController,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when the cached value controller has a timer', () => {
|
||||||
|
const cachedValueController = mock<CachedValueController<string>>();
|
||||||
|
cachedValueController.hasTimer.mockReturnValue(true);
|
||||||
|
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => cachedValueController,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true without cached value controller', () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get screenshot URL', () => {
|
||||||
|
it('should return screenshot URL with cached value controller', async () => {
|
||||||
|
const url = 'data:image/png;base64,';
|
||||||
|
const cachedValueController = mock<CachedValueController<string>>();
|
||||||
|
Object.defineProperty(cachedValueController, 'value', { value: url });
|
||||||
|
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => cachedValueController,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBe(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without cached value controller', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => mock<HTMLImageElement>(),
|
||||||
|
() => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get fullscreen element', async () => {
|
||||||
|
it('should return fullscreen element with image', async () => {
|
||||||
|
const image = mock<HTMLImageElement>();
|
||||||
|
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => image,
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await controller.getFullscreenElement()).toBe(image);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without image', async () => {
|
||||||
|
const controller = new UpdatingImageMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => null,
|
||||||
|
() => mock<CachedValueController<string>>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(controller.getFullscreenElement()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import { VideoMediaPlayerController } from '../../../src/components-lib/media-player/video';
|
||||||
|
import {
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
setControlsOnVideo,
|
||||||
|
} from '../../../src/utils/controls';
|
||||||
|
import { screenshotVideo } from '../../../src/utils/screenshot';
|
||||||
|
import { createLitElement } from '../../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../src/utils/controls.js');
|
||||||
|
vi.mock('../../../src/utils/screenshot.js');
|
||||||
|
|
||||||
|
class NotAllowedError extends Error {
|
||||||
|
name = 'NotAllowedError';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('VideoMediaPlayerController', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should play', () => {
|
||||||
|
it('should play when no error', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.play.mockResolvedValue();
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(video.play).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mute if not allowed to play and unmuted', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.play.mockRejectedValueOnce(new NotAllowedError()).mockResolvedValueOnce();
|
||||||
|
video.muted = false;
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(video.play).toBeCalledTimes(2);
|
||||||
|
expect(video.muted).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mute if not allowed to play and already unmuted', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.play.mockRejectedValueOnce(new NotAllowedError());
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(video.play).toBeCalledTimes(1);
|
||||||
|
expect(video.muted).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore exception if subsequent play call throws', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.play.mockRejectedValue(new NotAllowedError());
|
||||||
|
video.muted = false;
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
expect(video.play).toBeCalledTimes(2);
|
||||||
|
expect(video.muted).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without a video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
await controller.play();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pause', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
expect(video.pause).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should mute', async () => {
|
||||||
|
it('should mute with video', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.muted = false;
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
expect(video.muted).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without a video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
await controller.mute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should unmute', async () => {
|
||||||
|
it('should unmute with video', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.muted = true;
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
expect(video.muted).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without a video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
await controller.unmute();
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return muted state', () => {
|
||||||
|
it('should return true when muted', () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.muted = true;
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when not muted', () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
video.muted = false;
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no video', () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(controller.isMuted()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should seek', () => {
|
||||||
|
it('should seek', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.seek(10);
|
||||||
|
|
||||||
|
expect(hideMediaControlsTemporarily).toBeCalled();
|
||||||
|
expect(video.currentTime).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without a video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
await controller.seek(10);
|
||||||
|
|
||||||
|
// Currently no observable side effects.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should set controls', () => {
|
||||||
|
it('should set controls', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.setControls(true);
|
||||||
|
|
||||||
|
expect(setControlsOnVideo).toBeCalledWith(video, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set controls to default', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
const controller = new VideoMediaPlayerController(
|
||||||
|
createLitElement(),
|
||||||
|
() => video,
|
||||||
|
() => true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.setControls();
|
||||||
|
|
||||||
|
expect(setControlsOnVideo).toBeCalledWith(video, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore calls without a default or value', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
await controller.setControls(true);
|
||||||
|
|
||||||
|
expect(setControlsOnVideo).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should return paused state', () => {
|
||||||
|
it('should return true when paused', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
Object.defineProperty(video, 'paused', { value: true });
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when not paused', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
Object.defineProperty(video, 'paused', { value: false });
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
await controller.pause();
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when no video', () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(controller.isPaused()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get screenshot URL', async () => {
|
||||||
|
it('should return screenshot URL with video', async () => {
|
||||||
|
const url = 'data:image/png;base64,';
|
||||||
|
vi.mocked(screenshotVideo).mockReturnValue(url);
|
||||||
|
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBe(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(await controller.getScreenshotURL()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get fullscreen element', async () => {
|
||||||
|
it('should return fullscreen element with video', async () => {
|
||||||
|
const video = mock<HTMLVideoElement>();
|
||||||
|
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => video);
|
||||||
|
|
||||||
|
expect(await controller.getFullscreenElement()).toBe(video);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null without video', async () => {
|
||||||
|
const controller = new VideoMediaPlayerController(createLitElement(), () => null);
|
||||||
|
|
||||||
|
expect(controller.getFullscreenElement()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
ViewDisplayMode,
|
ViewDisplayMode,
|
||||||
} from '../../src/config/types';
|
} from '../../src/config/types';
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../src/types';
|
import { MediaPlayerController } from '../../src/types';
|
||||||
import { createGeneralAction, createViewAction } from '../../src/utils/action';
|
import { createGeneralAction, createViewAction } from '../../src/utils/action';
|
||||||
import { ViewMedia } from '../../src/view/media';
|
import { ViewMedia } from '../../src/view/media';
|
||||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
@@ -1349,13 +1349,13 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have pause button', () => {
|
it('should have pause button', () => {
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
currentMediaLoadedInfo: createMediaLoadedInfo({
|
currentMediaLoadedInfo: createMediaLoadedInfo({
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
},
|
},
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1370,14 +1370,14 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have play button', () => {
|
it('should have play button', () => {
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
player.isPaused.mockReturnValue(true);
|
mediaPlayerController.isPaused.mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
currentMediaLoadedInfo: createMediaLoadedInfo({
|
currentMediaLoadedInfo: createMediaLoadedInfo({
|
||||||
capabilities: {
|
capabilities: {
|
||||||
supportsPause: true,
|
supportsPause: true,
|
||||||
},
|
},
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1392,13 +1392,13 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have mute button', () => {
|
it('should have mute button', () => {
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
currentMediaLoadedInfo: createMediaLoadedInfo({
|
currentMediaLoadedInfo: createMediaLoadedInfo({
|
||||||
capabilities: {
|
capabilities: {
|
||||||
hasAudio: true,
|
hasAudio: true,
|
||||||
},
|
},
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1413,14 +1413,14 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have unmute button', () => {
|
it('should have unmute button', () => {
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
const mediaPlayerController = mock<MediaPlayerController>();
|
||||||
player.isMuted.mockReturnValue(true);
|
mediaPlayerController.isMuted.mockReturnValue(true);
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
currentMediaLoadedInfo: createMediaLoadedInfo({
|
currentMediaLoadedInfo: createMediaLoadedInfo({
|
||||||
capabilities: {
|
capabilities: {
|
||||||
hasAudio: true,
|
hasAudio: true,
|
||||||
},
|
},
|
||||||
player: player,
|
mediaPlayerController,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1437,7 +1437,7 @@ describe('MenuButtonController', () => {
|
|||||||
it('should have screenshot button', () => {
|
it('should have screenshot button', () => {
|
||||||
const buttons = calculateButtons(controller, {
|
const buttons = calculateButtons(controller, {
|
||||||
currentMediaLoadedInfo: createMediaLoadedInfo({
|
currentMediaLoadedInfo: createMediaLoadedInfo({
|
||||||
player: mock<AdvancedCameraCardMediaPlayer>(),
|
mediaPlayerController: mock<MediaPlayerController>(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -473,6 +473,15 @@ export const createLitElement = (): LitElement => {
|
|||||||
const element = document.createElement('div') as unknown as LitElement;
|
const element = document.createElement('div') as unknown as LitElement;
|
||||||
element.addController = vi.fn();
|
element.addController = vi.fn();
|
||||||
element.requestUpdate = vi.fn();
|
element.requestUpdate = vi.fn();
|
||||||
|
|
||||||
|
const promise: Promise<boolean> = new Promise((resolve) => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Need to overwrite a read-only property.
|
||||||
|
Object.defineProperty(element, 'updateComplete', {
|
||||||
|
value: promise,
|
||||||
|
});
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
AdvancedCameraCardHTMLVideoElement,
|
||||||
|
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||||
|
hideMediaControlsTemporarily,
|
||||||
|
setControlsOnVideo,
|
||||||
|
} from '../../src/utils/controls.js';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('setControlsOnVideo', () => {
|
||||||
|
it('should set controls', () => {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
|
||||||
|
setControlsOnVideo(video, false);
|
||||||
|
expect(video.controls).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should stop timer', () => {
|
||||||
|
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
||||||
|
hideMediaControlsTemporarily(video);
|
||||||
|
|
||||||
|
expect(video._controlsHideTimer).toBeTruthy();
|
||||||
|
expect(video._controlsHideTimer?.isRunning()).toBeTruthy();
|
||||||
|
|
||||||
|
setControlsOnVideo(video, false);
|
||||||
|
expect(video.controls).toBeFalsy();
|
||||||
|
expect(video._controlsHideTimer).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hideMediaControlsTemporarily', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set controls', () => {
|
||||||
|
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
||||||
|
video.controls = true;
|
||||||
|
hideMediaControlsTemporarily(video);
|
||||||
|
|
||||||
|
expect(video.controls).toBeFalsy();
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expect(video.controls).toBeTruthy();
|
||||||
|
expect(video._controlsHideTimer).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add event listener that resets controls', () => {
|
||||||
|
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
||||||
|
video.controls = true;
|
||||||
|
|
||||||
|
hideMediaControlsTemporarily(video);
|
||||||
|
expect(video.controls).toBeFalsy();
|
||||||
|
|
||||||
|
// After a new media starts to load, the controls should reset.
|
||||||
|
video.dispatchEvent(new Event('loadstart'));
|
||||||
|
expect(video.controls).toBeTruthy();
|
||||||
|
|
||||||
|
// ... but only once, future loadstart events without subsequent calls to
|
||||||
|
// hideMediaControlsTemporarily() should do nothing.
|
||||||
|
video.controls = false;
|
||||||
|
video.dispatchEvent(new Event('loadstart'));
|
||||||
|
expect(video._controlsHideTimer).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constants', () => {
|
||||||
|
it('MEDIA_LOAD_CONTROLS_HIDE_SECONDS', () => {
|
||||||
|
expect(MEDIA_LOAD_CONTROLS_HIDE_SECONDS).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { AdvancedCameraCardMediaPlayer } from '../../src/types.js';
|
|
||||||
import {
|
|
||||||
AdvancedCameraCardHTMLVideoElement,
|
|
||||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
|
||||||
hideMediaControlsTemporarily,
|
|
||||||
playMediaMutingIfNecessary,
|
|
||||||
setControlsOnVideo,
|
|
||||||
} from '../../src/utils/media.js';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('setControlsOnVideo', () => {
|
|
||||||
it('should set controls', () => {
|
|
||||||
const video = document.createElement('video');
|
|
||||||
|
|
||||||
setControlsOnVideo(video, false);
|
|
||||||
expect(video.controls).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop timer', () => {
|
|
||||||
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
|
||||||
hideMediaControlsTemporarily(video);
|
|
||||||
|
|
||||||
expect(video._controlsHideTimer).toBeTruthy();
|
|
||||||
expect(video._controlsHideTimer?.isRunning()).toBeTruthy();
|
|
||||||
|
|
||||||
setControlsOnVideo(video, false);
|
|
||||||
expect(video.controls).toBeFalsy();
|
|
||||||
expect(video._controlsHideTimer).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hideMediaControlsTemporarily', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set controls', () => {
|
|
||||||
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
|
||||||
video.controls = true;
|
|
||||||
hideMediaControlsTemporarily(video);
|
|
||||||
|
|
||||||
expect(video.controls).toBeFalsy();
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(video.controls).toBeTruthy();
|
|
||||||
expect(video._controlsHideTimer).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add event listener that resets controls', () => {
|
|
||||||
const video: AdvancedCameraCardHTMLVideoElement = document.createElement('video');
|
|
||||||
video.controls = true;
|
|
||||||
|
|
||||||
hideMediaControlsTemporarily(video);
|
|
||||||
expect(video.controls).toBeFalsy();
|
|
||||||
|
|
||||||
// After a new media starts to load, the controls should reset.
|
|
||||||
video.dispatchEvent(new Event('loadstart'));
|
|
||||||
expect(video.controls).toBeTruthy();
|
|
||||||
|
|
||||||
// ... but only once, future loadstart events without subsequent calls to
|
|
||||||
// hideMediaControlsTemporarily() should do nothing.
|
|
||||||
video.controls = false;
|
|
||||||
video.dispatchEvent(new Event('loadstart'));
|
|
||||||
expect(video._controlsHideTimer).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
class NotAllowedError extends Error {
|
|
||||||
name = 'NotAllowedError';
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('playMediaMutingIfNecessary', () => {
|
|
||||||
it('should play', async () => {
|
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
|
||||||
const video = mock<HTMLVideoElement>();
|
|
||||||
video.play.mockResolvedValue();
|
|
||||||
await playMediaMutingIfNecessary(player, video);
|
|
||||||
expect(video.play).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mute if not allowed to play and unmuted', async () => {
|
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
|
||||||
player.isMuted.mockReturnValue(false);
|
|
||||||
player.mute.mockResolvedValue();
|
|
||||||
|
|
||||||
const video = mock<HTMLVideoElement>();
|
|
||||||
video.play.mockRejectedValueOnce(new NotAllowedError()).mockResolvedValueOnce();
|
|
||||||
|
|
||||||
await playMediaMutingIfNecessary(player, video);
|
|
||||||
|
|
||||||
expect(video.play).toBeCalledTimes(2);
|
|
||||||
expect(player.isMuted).toBeCalled();
|
|
||||||
expect(player.mute).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not mute if not allowed to play and already unmuted', async () => {
|
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
|
||||||
player.isMuted.mockReturnValue(true);
|
|
||||||
|
|
||||||
const video = mock<HTMLVideoElement>();
|
|
||||||
video.play.mockRejectedValueOnce(new NotAllowedError());
|
|
||||||
|
|
||||||
await playMediaMutingIfNecessary(player, video);
|
|
||||||
|
|
||||||
expect(video.play).toBeCalledTimes(1);
|
|
||||||
expect(player.isMuted).toBeCalled();
|
|
||||||
expect(player.mute).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore exception if subsequent play call throws', async () => {
|
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
|
||||||
player.isMuted.mockReturnValue(false);
|
|
||||||
|
|
||||||
const video = mock<HTMLVideoElement>();
|
|
||||||
video.play.mockRejectedValue(new NotAllowedError());
|
|
||||||
|
|
||||||
await playMediaMutingIfNecessary(player, video);
|
|
||||||
|
|
||||||
expect(video.play).toBeCalledTimes(2);
|
|
||||||
expect(player.isMuted).toBeCalled();
|
|
||||||
expect(player.mute).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore calls without a video', async () => {
|
|
||||||
const player = mock<AdvancedCameraCardMediaPlayer>();
|
|
||||||
player.isMuted.mockReturnValue(false);
|
|
||||||
|
|
||||||
await playMediaMutingIfNecessary(player);
|
|
||||||
|
|
||||||
expect(player.isMuted).not.toBeCalled();
|
|
||||||
expect(player.mute).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constants', () => {
|
|
||||||
it('MEDIA_LOAD_CONTROLS_HIDE_SECONDS', () => {
|
|
||||||
expect(MEDIA_LOAD_CONTROLS_HIDE_SECONDS).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { generateScreenshotTitle, screenshotMedia } from '../../src/utils/screenshot';
|
import {
|
||||||
|
generateScreenshotTitle,
|
||||||
|
screenshotImage,
|
||||||
|
screenshotVideo,
|
||||||
|
} from '../../src/utils/screenshot';
|
||||||
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../../src/view/media-queries-results';
|
||||||
import { View } from '../../src/view/view';
|
import { View } from '../../src/view/view';
|
||||||
import { TestViewMedia, createView } from '../test-utils';
|
import { TestViewMedia, createView } from '../test-utils';
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('screenshotMedia', () => {
|
describe('screenshotVideo', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@@ -19,7 +23,7 @@ describe('screenshotMedia', () => {
|
|||||||
canvas.getContext = getContext;
|
canvas.getContext = getContext;
|
||||||
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
||||||
|
|
||||||
expect(screenshotMedia(video)).toBeNull();
|
expect(screenshotVideo(video)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should screenshot', () => {
|
it('should screenshot', () => {
|
||||||
@@ -31,7 +35,36 @@ describe('screenshotMedia', () => {
|
|||||||
canvas.toDataURL = vi.fn().mockReturnValue('data:image/jpeg;base64');
|
canvas.toDataURL = vi.fn().mockReturnValue('data:image/jpeg;base64');
|
||||||
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
||||||
|
|
||||||
expect(screenshotMedia(video)).toBe('data:image/jpeg;base64');
|
expect(screenshotVideo(video)).toBe('data:image/jpeg;base64');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('screenshotImage', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not screenshot without context', () => {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const getContext = vi.fn().mockReturnValue(null);
|
||||||
|
canvas.getContext = getContext;
|
||||||
|
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
||||||
|
|
||||||
|
expect(screenshotImage(image)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should screenshot', () => {
|
||||||
|
const image = document.createElement('img');
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const getContext = vi.fn().mockReturnValue(mock<CanvasRenderingContext2D>());
|
||||||
|
canvas.getContext = getContext;
|
||||||
|
canvas.toDataURL = vi.fn().mockReturnValue('data:image/jpeg;base64');
|
||||||
|
vi.spyOn(document, 'createElement').mockReturnValue(canvas);
|
||||||
|
|
||||||
|
expect(screenshotImage(image)).toBe('data:image/jpeg;base64');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user