Merge pull request #1177 from dermotduffy/media-info
Refactor media info handling into a controller
This commit is contained in:
+21
-21
@@ -95,6 +95,7 @@ import {
|
||||
import { Timer } from './utils/timer';
|
||||
import { getParseErrorPaths } from './utils/zod.js';
|
||||
import { View } from './view/view.js';
|
||||
import { MediaLoadedInfoController } from './utils/media-info-controller';
|
||||
|
||||
/** A note on media callbacks:
|
||||
*
|
||||
@@ -187,6 +188,7 @@ class FrigateCard extends LitElement {
|
||||
protected _conditionController?: ConditionController;
|
||||
protected _automationsController?: AutomationsController;
|
||||
protected _menuButtonController = new MenuButtonController();
|
||||
protected _mediaLoadedInfoController = new MediaLoadedInfoController();
|
||||
|
||||
protected _refMenu: Ref<FrigateCardMenu> = createRef();
|
||||
protected _refMain: Ref<HTMLElement> = createRef();
|
||||
@@ -197,10 +199,6 @@ class FrigateCard extends LitElement {
|
||||
protected _updateTimer = new Timer();
|
||||
protected _untriggerTimer = new Timer();
|
||||
|
||||
// Information about loaded media items.
|
||||
protected _currentMediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||
protected _lastValidMediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||
|
||||
// Error/info message to render.
|
||||
protected _message: Message | null = null;
|
||||
|
||||
@@ -341,7 +339,6 @@ class FrigateCard extends LitElement {
|
||||
return this._cameraManager.getStore().getCameraConfig(this._view.camera);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the card configuration.
|
||||
* @param inputConfig The card configuration.
|
||||
@@ -414,7 +411,7 @@ class FrigateCard extends LitElement {
|
||||
this._conditionController?.hasHAStateConditions && {
|
||||
state: this._hass.states,
|
||||
}),
|
||||
media_loaded: !!this._currentMediaLoadedInfo,
|
||||
media_loaded: this._mediaLoadedInfoController.has(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -446,7 +443,7 @@ class FrigateCard extends LitElement {
|
||||
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
|
||||
const changeView = (view: View): void => {
|
||||
if (View.isMajorMediaChange(this._view, view)) {
|
||||
this._currentMediaLoadedInfo = null;
|
||||
this._mediaLoadedInfoController.clear();
|
||||
}
|
||||
if (this._view?.view !== view.view) {
|
||||
this._resetMainScroll();
|
||||
@@ -1151,16 +1148,16 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
break;
|
||||
case 'mute':
|
||||
this._currentMediaLoadedInfo?.player?.mute();
|
||||
this._mediaLoadedInfoController.get()?.player?.mute();
|
||||
break;
|
||||
case 'unmute':
|
||||
this._currentMediaLoadedInfo?.player?.unmute();
|
||||
this._mediaLoadedInfoController.get()?.player?.unmute();
|
||||
break;
|
||||
case 'play':
|
||||
this._currentMediaLoadedInfo?.player?.play();
|
||||
this._mediaLoadedInfoController.get()?.player?.play();
|
||||
break;
|
||||
case 'pause':
|
||||
this._currentMediaLoadedInfo?.player?.pause();
|
||||
this._mediaLoadedInfoController.get()?.player?.pause();
|
||||
break;
|
||||
default:
|
||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||
@@ -1371,7 +1368,7 @@ class FrigateCard extends LitElement {
|
||||
this._view,
|
||||
this._expand,
|
||||
{
|
||||
currentMediaLoadedInfo: this._currentMediaLoadedInfo,
|
||||
currentMediaLoadedInfo: this._mediaLoadedInfoController.get(),
|
||||
mediaPlayers: this._mediaPlayers,
|
||||
cameraURL: this._getCameraURLFromContext(),
|
||||
microphoneController: this._microphoneController,
|
||||
@@ -1438,12 +1435,12 @@ class FrigateCard extends LitElement {
|
||||
|
||||
log(this._cardWideConfig, `Frigate Card media load: `, mediaLoadedInfo);
|
||||
|
||||
this._lastValidMediaLoadedInfo = this._currentMediaLoadedInfo = mediaLoadedInfo;
|
||||
this._mediaLoadedInfoController.set(mediaLoadedInfo);
|
||||
|
||||
this._setPropertiesForExpandedMode();
|
||||
|
||||
this._conditionController?.setState({
|
||||
media_loaded: !!this._currentMediaLoadedInfo,
|
||||
media_loaded: this._mediaLoadedInfoController.has(),
|
||||
});
|
||||
|
||||
this.requestUpdate();
|
||||
@@ -1453,10 +1450,11 @@ class FrigateCard extends LitElement {
|
||||
// When a new media loads, set the aspect ratio for when the card is
|
||||
// expanded/popped-up. This is based exclusively on last media content,
|
||||
// as dimension configuration does not apply in fullscreen or expanded mode.
|
||||
const lastKnown = this._mediaLoadedInfoController.getLastKnown();
|
||||
this.style.setProperty(
|
||||
'--frigate-card-expand-aspect-ratio',
|
||||
this._view?.isAnyMediaView() && this._lastValidMediaLoadedInfo
|
||||
? `${this._lastValidMediaLoadedInfo.width} / ${this._lastValidMediaLoadedInfo.height}`
|
||||
this._view?.isAnyMediaView() && lastKnown
|
||||
? `${lastKnown.width} / ${lastKnown.height}`
|
||||
: 'unset',
|
||||
);
|
||||
// Non-media mays have no intrinsic dimensions and so we need to explicit
|
||||
@@ -1475,7 +1473,7 @@ class FrigateCard extends LitElement {
|
||||
* Unload a media item.
|
||||
*/
|
||||
protected _mediaUnloadedHandler(): void {
|
||||
this._currentMediaLoadedInfo = null;
|
||||
this._mediaLoadedInfoController.clear();
|
||||
this._conditionController?.setState({ media_loaded: false });
|
||||
}
|
||||
|
||||
@@ -1542,8 +1540,9 @@ class FrigateCard extends LitElement {
|
||||
|
||||
const aspectRatioMode = this._getConfig().dimensions.aspect_ratio_mode;
|
||||
|
||||
if (this._lastValidMediaLoadedInfo && aspectRatioMode === 'dynamic') {
|
||||
return `${this._lastValidMediaLoadedInfo.width} / ${this._lastValidMediaLoadedInfo.height}`;
|
||||
const lastKnown = this._mediaLoadedInfoController.getLastKnown();
|
||||
if (lastKnown && aspectRatioMode === 'dynamic') {
|
||||
return `${lastKnown.width} / ${lastKnown.height}`;
|
||||
}
|
||||
|
||||
const defaultAspectRatio = this._getConfig().dimensions.aspect_ratio;
|
||||
@@ -1756,8 +1755,9 @@ class FrigateCard extends LitElement {
|
||||
* @returns The Lovelace card size in units of 50px.
|
||||
*/
|
||||
public getCardSize(): number {
|
||||
if (this._lastValidMediaLoadedInfo) {
|
||||
return this._lastValidMediaLoadedInfo.height / 50;
|
||||
const lastKnown = this._mediaLoadedInfoController.getLastKnown();
|
||||
if (lastKnown) {
|
||||
return lastKnown.height / 50;
|
||||
}
|
||||
return 6;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { MediaLoadedInfo } from '../types';
|
||||
|
||||
export class MediaLoadedInfoController {
|
||||
protected _current: MediaLoadedInfo | null = null;
|
||||
protected _lastKnown: MediaLoadedInfo | null = null;
|
||||
|
||||
public set(current: MediaLoadedInfo): void {
|
||||
this._current = current;
|
||||
this._lastKnown = current;
|
||||
}
|
||||
|
||||
public get(): MediaLoadedInfo | null {
|
||||
return this._current;
|
||||
}
|
||||
|
||||
public getLastKnown(): MediaLoadedInfo | null {
|
||||
return this._lastKnown;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._current = null;
|
||||
}
|
||||
|
||||
public has(): boolean {
|
||||
return !!this._current;
|
||||
}
|
||||
}
|
||||
+20
-20
@@ -69,18 +69,6 @@ export function dispatchMediaLoadedEvent(
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchMediaVolumeChangeEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:volumechange');
|
||||
}
|
||||
|
||||
export function dispatchMediaPlayEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:play');
|
||||
}
|
||||
|
||||
export function dispatchMediaPauseEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:pause');
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a pre-existing MediaLoadedInfo object as an event.
|
||||
* @param element The element to send the event.
|
||||
@@ -93,6 +81,26 @@ export function dispatchExistingMediaLoadedInfoAsEvent(
|
||||
dispatchFrigateCardEvent<MediaLoadedInfo>(target, 'media:loaded', MediaLoadedInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a media unloaded event.
|
||||
* @param element The element to send the event.
|
||||
*/
|
||||
export function dispatchMediaUnloadedEvent(element: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(element, 'media:unloaded');
|
||||
}
|
||||
|
||||
export function dispatchMediaVolumeChangeEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:volumechange');
|
||||
}
|
||||
|
||||
export function dispatchMediaPlayEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:play');
|
||||
}
|
||||
|
||||
export function dispatchMediaPauseEvent(target: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(target, 'media:pause');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a MediaLoadedInfo object is valid/acceptable.
|
||||
* @param info The MediaLoadedInfo object.
|
||||
@@ -103,11 +111,3 @@ export function isValidMediaLoadedInfo(info: MediaLoadedInfo): boolean {
|
||||
info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a media unloaded event.
|
||||
* @param element The element to send the event.
|
||||
*/
|
||||
export function dispatchMediaUnloadedEvent(element: HTMLElement): void {
|
||||
dispatchFrigateCardEvent(element, 'media:unloaded');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { MediaLoadedInfoController } from '../../src/utils/media-info-controller';
|
||||
import { createMediaLoadedInfo } from '../test-utils.js';
|
||||
|
||||
describe('MediaLoadedInfoController', () => {
|
||||
let controller: MediaLoadedInfoController;
|
||||
beforeEach(() => {
|
||||
controller = new MediaLoadedInfoController();
|
||||
});
|
||||
|
||||
it('should set', () => {
|
||||
const info = createMediaLoadedInfo();
|
||||
controller.set(info);
|
||||
expect(controller.has());
|
||||
expect(controller.get()).toBe(info);
|
||||
});
|
||||
|
||||
it('should get last known', () => {
|
||||
const info = createMediaLoadedInfo();
|
||||
controller.set(info);
|
||||
expect(controller.has()).toBeTruthy();
|
||||
controller.clear();
|
||||
expect(controller.has()).toBeFalsy();
|
||||
expect(controller.getLastKnown()).toBe(info);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { FrigateCardMediaPlayer, MediaLoadedCapabilities } from '../../src/types';
|
||||
import {
|
||||
createMediaLoadedInfo,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaUnloadedEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
isValidMediaLoadedInfo,
|
||||
} from '../../src/utils/media-info';
|
||||
import { createMediaLoadedInfo as createTestMediaLoadedInfo } from '../test-utils.js';
|
||||
|
||||
const options = {
|
||||
player: mock<FrigateCardMediaPlayer>(),
|
||||
capabilities: mock<MediaLoadedCapabilities>(),
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('createMediaLoadedInfo', () => {
|
||||
it('should create from image', () => {
|
||||
const img = document.createElement('img');
|
||||
|
||||
// Need to write readonly properties.
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 10 });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 20 });
|
||||
|
||||
expect(createMediaLoadedInfo(img, options)).toEqual({
|
||||
width: 10,
|
||||
height: 20,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create from video', () => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
// Need to write readonly properties.
|
||||
Object.defineProperty(video, 'videoWidth', { value: 30 });
|
||||
Object.defineProperty(video, 'videoHeight', { value: 40 });
|
||||
|
||||
expect(createMediaLoadedInfo(video, options)).toEqual({
|
||||
width: 30,
|
||||
height: 40,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create from canvas', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 50;
|
||||
canvas.height = 60;
|
||||
|
||||
expect(createMediaLoadedInfo(canvas, options)).toEqual({
|
||||
width: 50,
|
||||
height: 60,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create from unknown', () => {
|
||||
const div = document.createElement('div');
|
||||
expect(createMediaLoadedInfo(div, options)).toBeNull();
|
||||
});
|
||||
|
||||
it('should create from event', () => {
|
||||
const img = document.createElement('img');
|
||||
|
||||
// Need to write readonly properties.
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 70 });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 80 });
|
||||
|
||||
const event = new Event('foo');
|
||||
Object.defineProperty(event, 'composedPath', { value: () => [img] });
|
||||
|
||||
expect(createMediaLoadedInfo(event, options)).toEqual({
|
||||
width: 70,
|
||||
height: 80,
|
||||
...options,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaLoadedEvent', () => {
|
||||
const options = {
|
||||
player: mock<FrigateCardMediaPlayer>(),
|
||||
capabilities: mock<MediaLoadedCapabilities>(),
|
||||
};
|
||||
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:loaded', handler);
|
||||
|
||||
// Need to write readonly properties.
|
||||
const img = document.createElement('img');
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 10 });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 20 });
|
||||
|
||||
dispatchMediaLoadedEvent(div, img, options);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: {
|
||||
width: 10,
|
||||
height: 20,
|
||||
...options,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:loaded', handler);
|
||||
|
||||
dispatchMediaLoadedEvent(div, div, options);
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchExistingMediaLoadedInfoAsEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:loaded', handler);
|
||||
const info = createTestMediaLoadedInfo();
|
||||
|
||||
dispatchExistingMediaLoadedInfoAsEvent(div, info);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: info,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaUnloadedEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:unloaded', handler);
|
||||
|
||||
dispatchMediaUnloadedEvent(div);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaVolumeChangeEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:volumechange', handler);
|
||||
|
||||
dispatchMediaVolumeChangeEvent(div);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaPlayEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:play', handler);
|
||||
|
||||
dispatchMediaPlayEvent(div);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('dispatchMediaPauseEvent', () => {
|
||||
it('should dispatch', () => {
|
||||
const handler = vi.fn();
|
||||
const div = document.createElement('div');
|
||||
div.addEventListener('frigate-card:media:pause', handler);
|
||||
|
||||
dispatchMediaPauseEvent(div);
|
||||
expect(handler).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('isValidMediaLoadedInfo', () => {
|
||||
it('should be valid with correct dimensions', () => {
|
||||
expect(
|
||||
isValidMediaLoadedInfo(createTestMediaLoadedInfo({ width: 100, height: 100 })),
|
||||
).toBeTruthy();
|
||||
});
|
||||
it('should be invalid with unlikely dimensions', () => {
|
||||
expect(
|
||||
isValidMediaLoadedInfo(createTestMediaLoadedInfo({ width: 0, height: 0 })),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user