refactor: Unify substream actions into substream_{on,off} (#2497)

## Summary

- Collapse `live_substream_{on,off,select}` into a unified
`substream_{on,off}` pair, symmetric with `call_{start,end}`.
- Rename the `camera` field on the former `live_substream_select` to
`stream` (it always was a stream ID).
- Add optional `camera` field to both new actions for targeting a
non-selected base camera.
- YAML configs are migrated automatically; URL bookmarks must be updated
by hand.

## Migration

### Cycling between camera and substream (toggle button)

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_on
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_on
```

### Selecting a specific substream

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_select
  camera: camera.front_door_hd
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_on
  stream: camera.front_door_hd
```

### Turning the substream off

Before:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: live_substream_off
```

After:
```yaml
tap_action:
  action: custom:advanced-camera-card-action
  advanced_camera_card_action: substream_off
```

### URL querystrings (manual update required)

| Before | After |
| --- | --- |
|
`?advanced-camera-card-action.live_substream_select=camera.front_door_hd`
| `?advanced-camera-card-action.substream_on=camera.front_door_hd` |
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent 15e335a647
commit f18e4cd4b8
32 changed files with 878 additions and 386 deletions
+21 -13
View File
@@ -1,30 +1,38 @@
import { View } from '../../../view/view';
import { ViewModifier } from '../types';
// The single write path for the camera-keyed `live.overrides` map (the read
// path being `getStreamCameraID` in `view/substream`): sets a camera's
// substream override, or clears it when `substreamID` is absent so the
// camera's own stream is used. `cameraID` defaults to the selected camera.
export class SubstreamViewModifier implements ViewModifier {
private _substreamID?: string;
private _cameraID?: string;
interface SubstreamViewModifierOptions {
// The substream to engage. When absent, the override is cleared so the
// camera's own stream is used.
stream?: string;
constructor(substreamID?: string, cameraID?: string) {
this._substreamID = substreamID;
this._cameraID = cameraID;
// The camera whose override to write. Defaults to the selected camera.
camera?: string;
}
// The single write path for the camera-keyed `live.overrides` map (the read
// path being `getStreamCameraID` in `view/substream`).
export class SubstreamViewModifier implements ViewModifier {
private _options: SubstreamViewModifierOptions;
constructor(options?: SubstreamViewModifierOptions) {
this._options = options ?? {};
}
public modify(view: View): void {
const cameraID = this._cameraID ?? view.camera;
const cameraID = this._options.camera ?? view.camera;
if (!cameraID) {
return;
}
if (!this._substreamID) {
// A stream equal to the camera itself is semantically "no substream";
// normalise it to a cleared override so the map doesn't carry self-
// referential entries.
if (!this._options.stream || this._options.stream === cameraID) {
view.context?.live?.overrides?.delete(cameraID);
return;
}
const overrides = view.context?.live?.overrides ?? new Map<string, string>();
overrides.set(cameraID, this._substreamID);
overrides.set(cameraID, this._options.stream);
view.mergeInContext({ live: { overrides } });
}
}