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.|
|
| `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.|
|
| `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. |
|
| `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
|
##### Configuration on each button
|
||||||
|
|
||||||
@@ -680,7 +682,7 @@ Parameters for the `custom:frigate-card-conditional` element:
|
|||||||
|
|
||||||
| Action name | Description |
|
| 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 |
|
| Value | Description |
|
||||||
| - | - |
|
| - | - |
|
||||||
@@ -691,6 +693,8 @@ Parameters for the `custom:frigate-card-conditional` element:
|
|||||||
|`fullscreen`|Toggle fullscreen.|
|
|`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.|
|
|`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). |
|
|`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>
|
<a name="views"></a>
|
||||||
|
|
||||||
|
|||||||
+92
-1
@@ -51,6 +51,8 @@ import {
|
|||||||
getCameraIcon,
|
getCameraIcon,
|
||||||
getCameraID,
|
getCameraID,
|
||||||
getCameraTitle,
|
getCameraTitle,
|
||||||
|
getEntityIcon,
|
||||||
|
getEntityTitle,
|
||||||
homeAssistantSignPath,
|
homeAssistantSignPath,
|
||||||
homeAssistantWSRequest,
|
homeAssistantWSRequest,
|
||||||
isValidMediaShowInfo,
|
isValidMediaShowInfo,
|
||||||
@@ -345,7 +347,7 @@ export class FrigateCard extends LitElement {
|
|||||||
state_color: true,
|
state_color: true,
|
||||||
title: getCameraTitle(this._hass, config),
|
title: getCameraTitle(this._hass, config),
|
||||||
selected: this._view?.camera === camera,
|
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) => ({
|
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||||
style: this._getStyleFromActions(button),
|
style: this._getStyleFromActions(button),
|
||||||
...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.
|
* Handle a request for a card action.
|
||||||
* @param ev The action requested.
|
* @param ev The action requested.
|
||||||
@@ -955,6 +1040,12 @@ export class FrigateCard extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'media_player':
|
||||||
|
this._mediaPlayerAction(
|
||||||
|
frigateCardAction.media_player,
|
||||||
|
frigateCardAction.media_player_action,
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-6
@@ -285,20 +285,35 @@ export function convertActionToFrigateCardCustomAction(
|
|||||||
/**
|
/**
|
||||||
* Create a Frigate card custom action.
|
* Create a Frigate card custom action.
|
||||||
* @param action The Frigate card action string (e.g. 'fullscreen')
|
* @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(
|
export function createFrigateCardCustomAction(
|
||||||
action: FrigateCardAction,
|
action: FrigateCardAction,
|
||||||
|
args?: {
|
||||||
camera?: string,
|
camera?: string,
|
||||||
): FrigateCardCustomAction | undefined {
|
media_player?: string,
|
||||||
if (action == 'camera_select') {
|
media_player_action?: 'play' | 'stop',
|
||||||
if (!camera) {
|
}
|
||||||
return undefined;
|
): FrigateCardCustomAction | null {
|
||||||
|
if (action === 'camera_select') {
|
||||||
|
if (!args?.camera) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
action: 'fire-dom-event',
|
action: 'fire-dom-event',
|
||||||
frigate_card_action: action,
|
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 {
|
return {
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
<mwc-list-item
|
<mwc-list-item
|
||||||
style="${styleMap(stateParameters.style || {})}"
|
style="${styleMap(stateParameters.style || {})}"
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
|
?twoline=${!!item.subtitle}
|
||||||
?selected=${item.selected}
|
?selected=${item.selected}
|
||||||
?activated=${item.selected}
|
?activated=${item.selected}
|
||||||
|
?disabled=${!!item.disabled}
|
||||||
aria-label="${stateParameters.title || ''}"
|
aria-label="${stateParameters.title || ''}"
|
||||||
@action=${(ev) => {
|
@action=${(ev) => {
|
||||||
// Attach the action config so ascendants have access to it.
|
// 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),
|
hasDoubleClick: frigateCardHasAction(item.double_tap_action),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${stateParameters.title || ''}
|
<span>${stateParameters.title || ''}</span>
|
||||||
|
${item.subtitle
|
||||||
|
? html`<span slot="secondary">${item.subtitle}</span>`
|
||||||
|
: ''}
|
||||||
${stateParameters.icon
|
${stateParameters.icon
|
||||||
? html` <ha-icon
|
? html` <ha-icon
|
||||||
data-domain=${ifDefined(stateParameters.data_domain)}
|
data-domain=${ifDefined(stateParameters.data_domain)}
|
||||||
|
|||||||
@@ -978,6 +978,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderMenuButton('frigate_ui')}
|
${this._renderMenuButton('frigate_ui')}
|
||||||
${this._renderMenuButton('fullscreen')}
|
${this._renderMenuButton('fullscreen')}
|
||||||
${this._renderMenuButton('timeline')}
|
${this._renderMenuButton('timeline')}
|
||||||
|
${this._renderMenuButton('media_player')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"frigate": "Frigate menu / Default view",
|
"frigate": "Frigate menu / Default view",
|
||||||
"live": "Live",
|
"live": "Live",
|
||||||
|
"media_player": "Media Player",
|
||||||
"clips": "Clips",
|
"clips": "Clips",
|
||||||
"snapshots": "Snapshots",
|
"snapshots": "Snapshots",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
|
|||||||
+18
-1
@@ -179,7 +179,11 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
|||||||
'fullscreen',
|
'fullscreen',
|
||||||
'menu_toggle',
|
'menu_toggle',
|
||||||
] as const;
|
] 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];
|
export type FrigateCardAction = typeof FRIGATE_CARD_ACTIONS[number];
|
||||||
|
|
||||||
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
||||||
@@ -189,9 +193,16 @@ const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.e
|
|||||||
frigate_card_action: z.literal('camera_select'),
|
frigate_card_action: z.literal('camera_select'),
|
||||||
camera: z.string(),
|
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([
|
export const frigateCardCustomActionSchema = z.union([
|
||||||
frigateCardGeneralActionSchema,
|
frigateCardGeneralActionSchema,
|
||||||
frigateCardCameraSelectActionSchema,
|
frigateCardCameraSelectActionSchema,
|
||||||
|
frigateCarMediaPlayerPlayActionSchema,
|
||||||
]);
|
]);
|
||||||
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
||||||
|
|
||||||
@@ -401,6 +412,8 @@ const menuSubmenuItemSchema = elementsBaseSchema.extend({
|
|||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
state_color: z.boolean().default(true).optional(),
|
state_color: z.boolean().default(true).optional(),
|
||||||
selected: z.boolean().default(false).optional(),
|
selected: z.boolean().default(false).optional(),
|
||||||
|
subtitle: z.string().optional(),
|
||||||
|
disabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
|
export type MenuSubmenuItem = z.infer<typeof menuSubmenuItemSchema>;
|
||||||
|
|
||||||
@@ -689,6 +702,7 @@ const menuConfigDefault = {
|
|||||||
download: visibleButtonDefault,
|
download: visibleButtonDefault,
|
||||||
frigate_ui: visibleButtonDefault,
|
frigate_ui: visibleButtonDefault,
|
||||||
fullscreen: visibleButtonDefault,
|
fullscreen: visibleButtonDefault,
|
||||||
|
media_player: visibleButtonDefault,
|
||||||
},
|
},
|
||||||
button_size: 40,
|
button_size: 40,
|
||||||
};
|
};
|
||||||
@@ -719,6 +733,9 @@ const menuConfigSchema = z
|
|||||||
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
|
download: visibleButtonSchema.default(menuConfigDefault.buttons.download),
|
||||||
frigate_ui: visibleButtonSchema.default(menuConfigDefault.buttons.frigate_ui),
|
frigate_ui: visibleButtonSchema.default(menuConfigDefault.buttons.frigate_ui),
|
||||||
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
|
fullscreen: visibleButtonSchema.default(menuConfigDefault.buttons.fullscreen),
|
||||||
|
media_player: visibleButtonSchema.default(
|
||||||
|
menuConfigDefault.buttons.media_player,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.default(menuConfigDefault.buttons),
|
.default(menuConfigDefault.buttons),
|
||||||
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
button_size: z.number().min(BUTTON_SIZE_MIN).default(menuConfigDefault.button_size),
|
||||||
|
|||||||
Reference in New Issue
Block a user