Initial media_player (cast) support.
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"buttons": {
|
||||
"frigate": "Frigate menu / Default view",
|
||||
"live": "Live",
|
||||
"media_player": "Media Player",
|
||||
"clips": "Clips",
|
||||
"snapshots": "Snapshots",
|
||||
"image": "Image",
|
||||
|
||||
+18
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user