Skip to content

Commit

Permalink
Merge pull request #1296 from dermotduffy/new-way-of-casting
Browse files Browse the repository at this point in the history
Add new method of casting that supports low latency live streams
  • Loading branch information
dermotduffy authored Oct 11, 2023
2 parents aa9d061 + ac0eab4 commit fa43d2e
Show file tree
Hide file tree
Showing 13 changed files with 525 additions and 63 deletions.
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
| `dependencies` | | :white_check_mark: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
| `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
| `webrtc_card` | | :white_check_mark: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
| `cast` | | :white_check_mark: | Configuration that controls how this camera is "casted" / sent to media players. See below. |

<a name="live-providers"></a>

Expand Down Expand Up @@ -332,6 +333,35 @@ cameras:
| `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera by automatically detecting and using the occupancy `binary_sensor` for this camera and its configured zones and labels. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant. If this camera has configured zones, only occupancy sensors for those zones are used -- if the overall _camera_ occupancy sensor is also required, it can be manually added to `entities`. If this camera has configured labels, only occupancy sensors for those labels are used.|
| `entities` | | :white_check_mark: | Whether to not to trigger the camera when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.|

#### Camera Cast Configuration

The `cast` block configures what how a camera is cast / sent to media players.

```yaml
cameras:
- cast:
```

| Option | Default | Overridable | Description |
| - | - | - | - |
| `method` | `standard` | :white_check_mark: | Whether to use `standard` media casting to send the live view to your media player, or to instead cast a `dashboard` you have manually setup. Casting a dashboard supports a much wider variety of video media, including low latency video providers (e.g. `go2rtc`). This setting has no effect on casting non-live media. |
| `dashboard` | | :white_check_mark: | Configuration for the dashboard to cast. See below. |

See the [dashboard method cast example](#cast-dashboard-example).

#### Camera Cast Dashboard Configuration

```yaml
cameras:
- cast:
dashboard:
```

| Option | Default | Overridable | Description |
| - | - | - | - |
| `dashboard_path` | | :white_check_mark: | A required field that specifies the name of the dashboard to cast. You can see this name in your HA URL when you visit the dashboard. |
| `view_path` | | :white_check_mark: | A required field that specifies view/"tab" on that dashboard to cast. This is the value you have specified in the `url` field of the view configuration on the dashboard. |

<a name="camera-ids"></a>

#### Camera IDs: Referring to cameras in card configuration
Expand Down Expand Up @@ -1673,8 +1703,6 @@ Pan around a large camera view to only show part of the video feed in the card a

## Examples

## Examples

### Illustrative Expanded Configuration Reference

**Caution**: 🚩 Just copying this full reference into your configuration will cause you a significant maintenance burden. Don't do it! Please only specify what you need as defaults can / do change continually as this card develops. Almost all the values shown here are the defaults (except in cases where is no default, parameters are added here for illustrative purposes).
Expand Down Expand Up @@ -1710,6 +1738,8 @@ cameras:
occupancy: true
entities:
- binary_sensor.front_door_sensor
cast:
method: standard
- camera_entity: camera.entrance
live_provider: webrtc-card
engine: auto
Expand Down Expand Up @@ -1747,6 +1777,11 @@ cameras:
- mjpeg
stream: sitting_room
url: 'https://my.custom.go2rtc.backend'
cast:
method: dashboard
dashboard:
dashboard_path: cast
view_path: front-door
- camera_entity: camera.sitting_room_webrtc_card
live_provider: webrtc_card
webrtc_card:
Expand Down Expand Up @@ -3942,6 +3977,49 @@ overrides:
```
</details>

<a name="cast-dashboard-example"></a>

### `dashboard` cast method

<details>
<summary>Expand: Using the `dashboard` cast method to cast a low latency live provider</summary>

This example will configure a Frigate card that can cast a dashboard view to a media player, which has a second Frigate card in panel mode with a low-latency live provider.

#### Source card

```yaml
type: custom:frigate-card
cameras:
- camera_entity: camera.front_door
cast:
method: dashboard
dashboard:
dashboard_path: cast
view_path: front-door
```

#### Dashboard configuration

This dashboard is configured at the path `/cast/` (path referred to in `dashboard_path` above).

```yaml
title: Frigate Card Casting
views:
- title: Casting
# This path is referred to in `view_path` above.
path: front-door
# Ensure the video is "maximized"
type: panel
cards:
- type: custom:frigate-card
cameras:
- camera_entity: camera.front_door
live_provider: go2rtc
```
</details>
<a name="media-layout-examples"></a>
## Card Refreshes
Expand Down
52 changes: 49 additions & 3 deletions src/card-controller/media-player-manager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { CameraConfig } from '../config/types';
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
import { ViewMedia } from '../view/media';
import { ViewMediaClassifier } from '../view/media-classifier';
import { localize } from '../localize/localize';
import { errorToConsole } from '../utils/basic';
import { Entity } from '../utils/ha/entity-registry/types';
import { supportsFeature } from '../utils/ha/update';
import { ViewMedia } from '../view/media';
import { ViewMediaClassifier } from '../view/media-classifier';
import { CardMediaPlayerAPI } from './types';

export class MediaPlayerManager {
Expand Down Expand Up @@ -76,11 +78,27 @@ export class MediaPlayerManager {
}

public async playLive(mediaPlayer: string, cameraID: string): Promise<void> {
const hass = this._api.getHASSManager().getHASS();
const cameraConfig = this._api
.getCameraManager()
.getStore()
.getCameraConfig(cameraID);
if (!cameraConfig) {
return;
}

if (cameraConfig.cast?.method === 'dashboard') {
await this._playLiveDashboard(mediaPlayer, cameraConfig);
} else {
await this._playLiveStandard(mediaPlayer, cameraID, cameraConfig);
}
}

protected async _playLiveStandard(
mediaPlayer: string,
cameraID: string,
cameraConfig: CameraConfig,
): Promise<void> {
const hass = this._api.getHASSManager().getHASS();
const cameraEntity = cameraConfig?.camera_entity ?? null;

if (!hass || !cameraEntity) {
Expand All @@ -102,6 +120,34 @@ export class MediaPlayerManager {
});
}

protected async _playLiveDashboard(
mediaPlayer: string,
cameraConfig: CameraConfig,
): Promise<void> {
const hass = this._api.getHASSManager().getHASS();
if (!hass) {
return;
}

const dashboardConfig = cameraConfig.cast?.dashboard;
if (!dashboardConfig?.dashboard_path || !dashboardConfig?.view_path) {
this._api.getMessageManager().setMessageIfHigherPriority({
type: 'error',
icon: 'mdi:cast',
message: localize('error.no_dashboard_or_view'),
});
return;
}

// When this bug is closed, a query string could be included:
// https://github.com/home-assistant/core/issues/98316
await hass.callService('cast', 'show_lovelace_view', {
entity_id: mediaPlayer,
dashboard_path: dashboardConfig.dashboard_path,
view_path: dashboardConfig.view_path,
});
}

public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise<void> {
const hass = this._api.getHASSManager().getHASS();

Expand Down
41 changes: 39 additions & 2 deletions src/card-controller/query-string-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,53 @@ export class QueryStringManager {

public executeNonViewRelated = (): void => {
this._executeNonViewRelated(this._calculateIntent());
}
};

public executeViewRelated = (): void => {
this._executeViewRelated(this._calculateIntent());
}
};

public executeAll = (): void => {
const intent = this._calculateIntent();
this._executeViewRelated(intent);
this._executeNonViewRelated(intent);
};

public generateQueryString(action: FrigateCardCustomAction): string | null {
const baseKey =
'frigate-card-action.' + (action.card_id ? `${action.card_id}.` : '');

switch (action.frigate_card_action) {
case 'camera_select':
case 'live_substream_select':
return new URLSearchParams([
[baseKey + action.frigate_card_action, action.camera],
]).toString();
case 'camera_ui':
case 'clip':
case 'clips':
case 'default':
case 'diagnostics':
case 'download':
case 'expand':
case 'image':
case 'live':
case 'menu_toggle':
case 'recording':
case 'recordings':
case 'snapshot':
case 'snapshots':
case 'timeline':
return new URLSearchParams([
[baseKey + action.frigate_card_action, ''],
]).toString();
default:
console.warn(
`Frigate card cannot convert unsupported action to query string:`,
action,
);
}
return null;
}

protected _executeViewRelated(intent: QueryStringViewIntent): void {
Expand Down
2 changes: 2 additions & 0 deletions src/card-controller/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export interface CardMediaPlayerAPI {
getHASSManager(): HASSManager;
getCameraManager(): CameraManager;
getEntityRegistryManager(): EntityRegistryManager;
getMessageManager(): MessageManager;
getQueryStringManager(): QueryStringManager;
}

export interface CardMessageAPI {
Expand Down
26 changes: 26 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const FRIGATE_CARD_VIEWS_USER_SPECIFIED = [
'image',
'timeline',
] as const;
export type FrigateCardUserSpecifiedView =
(typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED)[number];

const FRIGATE_CARD_VIEWS = [
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
Expand Down Expand Up @@ -213,6 +215,7 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
'screenshot',
'unmute',
] as const;
export type FrigateCardGeneralAction = (typeof FRIGATE_CARD_GENERAL_ACTIONS)[number];

const FRIGATE_CARD_ACTIONS = [
...FRIGATE_CARD_VIEWS_USER_SPECIFIED,
Expand All @@ -237,6 +240,9 @@ const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.e
frigate_card_action: z.literal('camera_select'),
camera: z.string(),
});
export type FrigateCardCameraSelectAction = z.infer<
typeof frigateCardCameraSelectActionSchema
>;

const frigateCardLiveDependencySelectActionSchema =
frigateCardCustomActionsBaseSchema.extend({
Expand Down Expand Up @@ -920,6 +926,24 @@ const liveOverridesSchema = z
.optional();
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;

// *************************************************************************
// Cast Configuration
// *************************************************************************

const castConfigDefault = {
method: 'standard' as const,
};

export const castSchema = z.object({
method: z.enum(['standard', 'dashboard']).default(castConfigDefault.method).optional(),
dashboard: z
.object({
dashboard_path: z.string().optional(),
view_path: z.string().optional(),
})
.optional(),
});

// *************************************************************************
// Camera Configuration
// *************************************************************************
Expand Down Expand Up @@ -1035,6 +1059,8 @@ export const cameraConfigSchema = z
image: liveImageConfigSchema.default(cameraConfigDefault.image),
jsmpeg: jsmpegConfigSchema.optional(),
webrtc_card: webrtcCardConfigSchema.optional(),

cast: castSchema.optional(),
})
.default(cameraConfigDefault);
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
Expand Down
5 changes: 5 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export const CONF_CAMERAS_ARRAY_CAMERA_ENTITY =
`${CONF_CAMERAS}.#.camera_entity` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_CAMERA_NAME =
`${CONF_CAMERAS}.#.frigate.camera_name` as const;
export const CONF_CAMERAS_ARRAY_CAST_METHOD = `${CONF_CAMERAS}.#.cast.method` as const;
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH =
`${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const;
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH =
`${CONF_CAMERAS}.#.cast.dashboard.view_path` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_CLIENT_ID =
`${CONF_CAMERAS}.#.frigate.client_id` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_LABELS =
Expand Down
Loading

0 comments on commit fa43d2e

Please sign in to comment.