Implement downloading and correct MediaShow events in carousel.

This commit is contained in:
Dermot Duffy
2021-10-29 16:36:36 -07:00
parent 715810dcc9
commit 66ae262654
13 changed files with 582 additions and 275 deletions
+129 -79
View File
@@ -2,20 +2,34 @@ import { ZodSchema, z } from 'zod';
import { MessageBase } from 'home-assistant-js-websocket';
import { HomeAssistant } from 'custom-card-helpers';
import { localize } from './localize/localize.js';
import type {
BrowseMediaQueryParameters,
BrowseMediaSource,
import {
ExtendedHomeAssistant,
MediaLoadInfo,
MediaShowInfo,
Message,
SignedPath,
signedPathSchema,
} from './types.js';
import { browseMediaSourceSchema } from './types.js';
const MEDIA_INFO_HEIGHT_CUTOFF = 50;
const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF;
/**
* Get the keys that didn't parse from a ZodError.
* @param error The zoderror to extract the keys from.
* @returns An array of error keys.
*/
export function getParseErrorKeys<T>(error: z.ZodError<T>): string[] {
const errors = error.format();
return Object.keys(errors).filter((v) => !v.startsWith('_'));
}
/**
* Make a HomeAssistant websocket request. May throw.
* @param hass The HomeAssistant object to send the request with.
* @param schema The expected Zod schema of the response.
* @param request The request to make.
* @returns The parsed valid response or null on malformed.
*/
export async function homeAssistantWSRequest<T>(
hass: HomeAssistant & ExtendedHomeAssistant,
schema: ZodSchema<T>,
@@ -43,66 +57,41 @@ export async function homeAssistantWSRequest<T>(
return parseResult.data;
}
export function isTrueMedia(media: BrowseMediaSource): boolean {
return !media.can_expand;
}
// From a BrowseMediaSource item extract the first true media item (i.e. a
// clip/snapshot, not a folder).
export function getFirstTrueMediaChildIndex(
media: BrowseMediaSource | null,
): number | null {
if (!media || !media.children) {
return null;
}
for (let i = 0; i < media.children.length; i++) {
if (isTrueMedia(media.children[i])) {
return i;
}
}
return null;
}
// Browse Frigate media with a media content id.
export async function browseMedia(
hass: (HomeAssistant & ExtendedHomeAssistant) | null,
media_content_id: string,
): Promise<BrowseMediaSource | null> {
if (!hass) {
return null;
}
const request = {
type: 'media_source/browse_media',
media_content_id: media_content_id,
};
return homeAssistantWSRequest(hass, browseMediaSourceSchema, request);
}
// Browse Frigate media with query parameters.
export async function browseMediaQuery(
/**
* Request that HA sign a path. May throw.
* @param hass The HomeAssistant object used to request the signature.
* @param path The path to sign.
* @param expires An optional number of seconds to sign the path for.
* @returns The signed URL, or null if the response was malformed.
*/
export async function homeAssistantSignPath(
hass: HomeAssistant & ExtendedHomeAssistant,
params: BrowseMediaQueryParameters,
): Promise<BrowseMediaSource | null> {
return browseMedia(
path: string,
expires?: number,
): Promise<string | null> {
const request = {
type: 'auth/sign_path',
path: path,
expires: expires,
};
const response = await homeAssistantWSRequest<SignedPath>(
hass,
// Defined in:
// https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py
[
'media-source://frigate',
params.clientId,
'event-search',
params.mediaType,
'', // Name/Title to render (not necessary here)
params.after ? String(params.after) : '',
params.before ? String(params.before) : '',
params.cameraName,
params.label,
params.zone,
].join('/'),
signedPathSchema,
request,
);
if (!response) {
return null;
}
return hass.hassUrl(response.path);
}
export function dispatchEvent<T>(element: HTMLElement, name: string, detail?: T): void {
/**
* Dispatch a Frigate Card event.
* @param element The element to send the event.
* @param name The name of the Frigate card event to send.
* @param detail An optional detail object to attach.
*/
export function dispatchFrigateCardEvent<T>(element: HTMLElement, name: string, detail?: T): void {
element.dispatchEvent(
new CustomEvent<T>(`frigate-card:${name}`, {
bubbles: true,
@@ -112,18 +101,28 @@ export function dispatchEvent<T>(element: HTMLElement, name: string, detail?: T)
);
}
/**
* Dispatch a Frigate card play event.
* @param element The element to send the event.
*/
export function dispatchPlayEvent(element: HTMLElement): void {
dispatchEvent(element, 'play');
dispatchFrigateCardEvent(element, 'play');
}
/**
* Dispatch a Frigate card pause event.
* @param element The element to send the event.
*/
export function dispatchPauseEvent(element: HTMLElement): void {
dispatchEvent(element, 'pause');
dispatchFrigateCardEvent(element, 'pause');
}
export function dispatchMediaLoadEvent(
element: HTMLElement,
source: Event | HTMLElement,
): void {
/**
* Create a MediaShowInfo object.
* @param source An event or HTMLElement that should be used as a source.
* @returns A new MediaShowInfo object or null if one could not be created.
*/
export function createMediaShowInfo(source: Event | HTMLElement): MediaShowInfo | null {
let target: HTMLElement | EventTarget;
if (source instanceof Event) {
target = source.composedPath()[0];
@@ -132,46 +131,88 @@ export function dispatchMediaLoadEvent(
}
if (target instanceof HTMLImageElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
return {
width: (target as HTMLImageElement).naturalWidth,
height: (target as HTMLImageElement).naturalHeight,
});
};
} else if (target instanceof HTMLVideoElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
return {
width: (target as HTMLVideoElement).videoWidth,
height: (target as HTMLVideoElement).videoHeight,
});
};
} else if (target instanceof HTMLCanvasElement) {
dispatchEvent<MediaLoadInfo>(element, 'media-load', {
return {
width: (target as HTMLCanvasElement).width,
height: (target as HTMLCanvasElement).height,
});
};
}
return null;
}
/**
* Dispatch a Frigate card media show event.
* @param element The element to send the event.
* @param source An event or HTMLElement that should be used as a source.
*/
export function dispatchMediaShowEvent(
element: HTMLElement,
source: Event | HTMLElement,
): void {
const mediaShowInfo = createMediaShowInfo(source);
if (mediaShowInfo) {
dispatchExistingMediaShowInfoAsEvent(element, mediaShowInfo);
}
}
/**
* Dispatch a pre-existing MediaShowInfo object as an event.
* @param element The element to send the event.
* @param mediaShowInfo The MediaShowInfo object to send.
*/
export function dispatchExistingMediaShowInfoAsEvent(
element: HTMLElement,
mediaShowInfo: MediaShowInfo,
): void {
dispatchFrigateCardEvent<MediaShowInfo>(element, 'media-show', mediaShowInfo);
}
/**
* Dispatch an event with a message to show to the user.
* @param element The element to send the event.
* @param message The message to show.
* @param icon An optional icon to attach to the message.
*/
export function dispatchMessageEvent(
element: HTMLElement,
message: string,
icon?: string,
): void {
dispatchEvent<Message>(element, 'message', {
dispatchFrigateCardEvent<Message>(element, 'message', {
message: message,
type: 'info',
icon: icon,
});
}
export function dispatchErrorMessageEvent(
element: HTMLElement,
message: string,
): void {
dispatchEvent<Message>(element, 'message', {
/**
* Dispatch an event with an error message to show to the user.
* @param element The element to send the event.
* @param message The message to show.
*/
export function dispatchErrorMessageEvent(element: HTMLElement, message: string): void {
dispatchFrigateCardEvent<Message>(element, 'message', {
message: message,
type: 'error',
});
}
// Determine whether the card should be updated based on Home Assistant changes.
/**
* Determine whether the card should be updated based on Home Assistant changes.
* @param newHass The new HA object.
* @param oldHass The old HA object.
* @param entities The entities to examine for changes.
* @returns A boolean indicating whether or not to allow an update.
*/
export function shouldUpdateBasedOnHass(
newHass: HomeAssistant | null,
oldHass: HomeAssistant | undefined,
@@ -198,3 +239,12 @@ export function shouldUpdateBasedOnHass(
}
return false;
}
/**
* Determine if a MediaShowInfo object is valid/acceptable.
* @param info The MediaShowInfo object.
* @returns True if the object is valid, false otherwise.
*/
export function isValidMediaShowInfo(info: MediaShowInfo): boolean {
return info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF;
}