Initial media_player (cast) support.

This commit is contained in:
Dermot Duffy
2022-05-01 22:34:48 -07:00
parent 0eeab6e04f
commit d733498445
7 changed files with 145 additions and 11 deletions
+5 -1
View File
@@ -200,6 +200,8 @@ menu:
| `download` | :white_check_mark: | The `download` menu button: allow direct download of the media being displayed.|
| `frigate_ui` | :white_check_mark: | The `frigate_ui` menu button: brings the user to a context-appropriate page on the Frigate UI (e.g. the camera homepage). Will only appear if the `frigate.url` option is set.|
| `fullscreen` | :white_check_mark: | The `fullscreen` menu button: expand the card to consume the fullscreen. |
| `timeline` | :white_check_mark: | The `timeline` menu button: show the event timeline. |
| `media_player` | :white_check_mark: | The `media_player` menu button: sends the visible media to a remote media player. Supports Frigate clips, snapshots and live camera (only for cameras that specify a `camera_entity` and only using the default HA stream (equivalent to the `ha` live provider). `jsmpeg` or `webrtc-card` are not supported, although live can still be played as long as `camera_entity` is specified. In the player list, a `tap` will send the media to the player, a `hold` will stop the media on the player. |
##### Configuration on each button
@@ -680,7 +682,7 @@ Parameters for the `custom:frigate-card-conditional` element:
| Action name | Description |
| - | - |
| `custom:frigate-card-action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots`, `download`, `frigate_ui`, `fullscreen`, `camera_select`, `menu_toggle`.|
| `custom:frigate-card-action` | Call a Frigate Card action. Acceptable values are `default`, `clip`, `clips`, `image`, `live`, `snapshot`, `snapshots`, `download`, `frigate_ui`, `fullscreen`, `camera_select`, `menu_toggle`, `media_player`.|
| Value | Description |
| - | - |
@@ -691,6 +693,8 @@ Parameters for the `custom:frigate-card-conditional` element:
|`fullscreen`|Toggle fullscreen.|
|`camera_select`|Select a given camera. Takes a single additional `camera` parameter with the [camera ID](#camera-ids) of the camera to select. Respects the value of `view.camera_select` to choose the appropriate view on the new camera.|
|`menu_toggle` | Show/hide the menu (for the `hidden` mode style). |
|`media_player`| Perform a media player action. Takes a `media_player` parameter with the entity ID of the media_player on which to perform the action, and a `media_player_action` parameter which should be either `play` or `stop` to play or stop the media in question. |
<a name="views"></a>
+92 -1
View File
@@ -51,6 +51,8 @@ import {
getCameraIcon,
getCameraID,
getCameraTitle,
getEntityIcon,
getEntityTitle,
homeAssistantSignPath,
homeAssistantWSRequest,
isValidMediaShowInfo,
@@ -345,7 +347,7 @@ export class FrigateCard extends LitElement {
state_color: true,
title: getCameraTitle(this._hass, config),
selected: this._view?.camera === camera,
tap_action: createFrigateCardCustomAction('camera_select', camera),
tap_action: createFrigateCardCustomAction('camera_select', { camera: camera }),
};
});
@@ -465,6 +467,44 @@ export class FrigateCard extends LitElement {
});
}
const mediaPlayers = Object.keys(this._hass?.states || {}).filter((entity) =>
entity.startsWith('media_player.'),
);
if (
mediaPlayers.length &&
(this._view?.isViewerView() ||
(this._view?.is('live') && cameraConfig?.camera_entity))
) {
const mediaPlayerItems = mediaPlayers.map((playerEntityID) => {
const title = getEntityTitle(this._hass, playerEntityID) || playerEntityID;
const state = this._hass?.states[playerEntityID];
return {
icon: getEntityIcon(this._hass, playerEntityID) || 'mdi:cast',
entity: playerEntityID,
state_color: false,
title: title,
subtitle: title === playerEntityID ? undefined : playerEntityID,
disabled: !state || state.state === 'unavailable',
tap_action: createFrigateCardCustomAction('media_player', {
media_player: playerEntityID,
media_player_action: 'play',
}),
hold_action: createFrigateCardCustomAction('media_player', {
media_player: playerEntityID,
media_player_action: 'stop',
}),
};
});
buttons.push({
icon: 'mdi:cast',
...this._getConfig().menu.buttons.media_player,
type: 'custom:frigate-card-menu-submenu',
title: localize('config.menu.buttons.media_player'),
items: mediaPlayerItems,
});
}
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
style: this._getStyleFromActions(button),
...button,
@@ -886,6 +926,51 @@ export class FrigateCard extends LitElement {
}
}
/**
* Take a media player action.
* @param mediaPlayer The entity ID of the media player.
* @param action The action to take (currently only 'play' is supported).
* @returns
*/
protected _mediaPlayerAction(mediaPlayer: string, action: 'play' | 'stop'): void {
if (!['play', 'stop'].includes(action)) {
return;
}
let media_content_id: string;
let media_content_type: string;
let thumbnail: string | null = null;
const cameraEntity = this._getSelectedCameraConfig()?.camera_entity ?? null;
if (this._view?.isViewerView() && this._view.media) {
media_content_id = this._view.media.media_content_id;
media_content_type = this._view.media.media_content_type;
thumbnail = this._view.media.thumbnail;
} else if (this._view?.is('live') && cameraEntity) {
if (this._hass?.states && cameraEntity in this._hass.states) {
thumbnail = this._hass.states[cameraEntity].attributes.entity_picture ?? null;
}
media_content_id = `media-source://camera/${cameraEntity}`;
media_content_type = 'application/vnd.apple.mpegurl';
} else {
return;
}
if (action === 'play') {
this._hass?.callService('media_player', 'play_media', {
entity_id: mediaPlayer,
media_content_id: media_content_id,
media_content_type: media_content_type,
extra: thumbnail ? { thumb: thumbnail } : {},
});
} else if (action === 'stop') {
console.info('stopping');
this._hass?.callService('media_player', 'media_stop', {
entity_id: mediaPlayer,
});
}
}
/**
* Handle a request for a card action.
* @param ev The action requested.
@@ -955,6 +1040,12 @@ export class FrigateCard extends LitElement {
});
}
break;
case 'media_player':
this._mediaPlayerAction(
frigateCardAction.media_player,
frigateCardAction.media_player_action,
);
break;
default:
console.warn(`Frigate card received unknown card action: ${action}`);
}
+22 -7
View File
@@ -285,20 +285,35 @@ export function convertActionToFrigateCardCustomAction(
/**
* Create a Frigate card custom action.
* @param action The Frigate card action string (e.g. 'fullscreen')
* @returns A FrigateCardCustomAction for that action string.
* @returns A FrigateCardCustomAction for that action string or null.
*/
export function createFrigateCardCustomAction(
action: FrigateCardAction,
camera?: string,
): FrigateCardCustomAction | undefined {
if (action == 'camera_select') {
if (!camera) {
return undefined;
args?: {
camera?: string,
media_player?: string,
media_player_action?: 'play' | 'stop',
}
): FrigateCardCustomAction | null {
if (action === 'camera_select') {
if (!args?.camera) {
return null;
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
camera: camera,
camera: args.camera as string,
};
}
if (action === 'media_player') {
if (!args?.media_player || !args.media_player_action) {
return null;
}
return {
action: 'fire-dom-event',
frigate_card_action: action,
media_player: args.media_player,
media_player_action: args.media_player_action,
};
}
return {
+6 -1
View File
@@ -36,8 +36,10 @@ export class FrigateCardSubmenu extends LitElement {
<mwc-list-item
style="${styleMap(stateParameters.style || {})}"
graphic="icon"
?twoline=${!!item.subtitle}
?selected=${item.selected}
?activated=${item.selected}
?disabled=${!!item.disabled}
aria-label="${stateParameters.title || ''}"
@action=${(ev) => {
// Attach the action config so ascendants have access to it.
@@ -48,7 +50,10 @@ export class FrigateCardSubmenu extends LitElement {
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
})}
>
${stateParameters.title || ''}
<span>${stateParameters.title || ''}</span>
${item.subtitle
? html`<span slot="secondary">${item.subtitle}</span>`
: ''}
${stateParameters.icon
? html` <ha-icon
data-domain=${ifDefined(stateParameters.data_domain)}
+1
View File
@@ -978,6 +978,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderMenuButton('frigate_ui')}
${this._renderMenuButton('fullscreen')}
${this._renderMenuButton('timeline')}
${this._renderMenuButton('media_player')}
</div>
</div>
`
+1
View File
@@ -160,6 +160,7 @@
"buttons": {
"frigate": "Frigate menu / Default view",
"live": "Live",
"media_player": "Media Player",
"clips": "Clips",
"snapshots": "Snapshots",
"image": "Image",
+18 -1
View File
@@ -179,7 +179,11 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
'fullscreen',
'menu_toggle',
] as const;
const FRIGATE_CARD_ACTIONS = [...FRIGATE_CARD_GENERAL_ACTIONS, 'camera_select'] as const;
const FRIGATE_CARD_ACTIONS = [
...FRIGATE_CARD_GENERAL_ACTIONS,
'camera_select',
'media_player',
] as const;
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
@@ -189,9 +193,16 @@ const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.e
frigate_card_action: z.literal('camera_select'),
camera: z.string(),
});
const frigateCarMediaPlayerPlayActionSchema = frigateCardCustomactionsBaseSchema.extend({
frigate_card_action: z.literal('media_player'),
media_player: z.string(),
media_player_action: z.enum(['play', 'stop']),
});
export const frigateCardCustomActionSchema = z.union([
frigateCardGeneralActionSchema,
frigateCardCameraSelectActionSchema,
frigateCarMediaPlayerPlayActionSchema,
]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -401,6 +412,8 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
icon: z.string().optional(),
state_color: z.boolean().default(true).optional(),
selected: z.boolean().default(false).optional(),
subtitle: z.string().optional(),
disabled: z.boolean().optional(),
});
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
@@ -689,6 +702,7 @@ const menuConfigDefault = {
download: visibleButtonDefault,
frigate_ui: visibleButtonDefault,
fullscreen: visibleButtonDefault,
media_player: visibleButtonDefault,
},
button_size: 40,
};
@@ -719,6 +733,9 @@ const menuConfigSchema = z
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
frigate_ui: visibleButtonSchema.default(menuConfigDefault.buttons.frigate_ui),
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
media_player: visibleButtonSchema.default(
menuConfigDefault.buttons.media_player,
),
})
.default(menuConfigDefault.buttons),
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),