Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to show media in a multi-camera grid #1251

Merged
merged 33 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b663e0b
Initial support for grid for live and media viewer.
dermotduffy Aug 9, 2023
e709471
Add documentation.
dermotduffy Aug 9, 2023
d9b2b4d
Minor fixes.
dermotduffy Aug 9, 2023
330f927
Fix controls issue.
dermotduffy Aug 10, 2023
fe2adb5
Reduce number of event listeners.
dermotduffy Aug 10, 2023
cc77caa
Reset the query on grid change in a smarter way.
dermotduffy Aug 12, 2023
8a0ba11
Reset query when changing to non-grid .
dermotduffy Aug 12, 2023
c61ceb9
Fix query/queryResults issue.
dermotduffy Aug 13, 2023
b459586
Avoid carousel init duplication.
dermotduffy Aug 13, 2023
c665f1b
Clean up seek handler.
dermotduffy Aug 13, 2023
5364c82
Show all cameras on timeline in grid mode.
dermotduffy Aug 13, 2023
17bfe4f
Recalculate grid for a slot change.
dermotduffy Aug 19, 2023
89c16b5
Remove old TODOs.
dermotduffy Aug 19, 2023
3c23894
Scrubbing performance improvements.
dermotduffy Aug 20, 2023
d23d412
Fix typos.
dermotduffy Aug 20, 2023
2df22b9
Fix various small issues.
dermotduffy Aug 20, 2023
0e5e994
Better timeline functionality for recordings.
dermotduffy Aug 20, 2023
ef7598c
Use usable time for in progress events.
dermotduffy Aug 20, 2023
74f00e1
Remove unused variable.
dermotduffy Aug 22, 2023
87f137b
Add support for timeline seeking locked to a camera.
dermotduffy Aug 26, 2023
af5b0f7
Be less eager with carousel resizes.
dermotduffy Aug 26, 2023
48ece1e
Improve display mode button text.
dermotduffy Aug 26, 2023
e830db1
Complete carousel refactor.
dermotduffy Sep 4, 2023
cd84e49
Watch grid DOM for id changes.
dermotduffy Sep 5, 2023
1554cb4
Add missing files changes on prior commit.
dermotduffy Sep 5, 2023
d425457
Resize the container based on media loads.
dermotduffy Sep 5, 2023
07bc639
Recalculate timeline cameras when display mode changes.
dermotduffy Sep 5, 2023
25150be
Set the container height on every resize.
dermotduffy Sep 5, 2023
1a4d38e
Fix init/select out of sync issues.
dermotduffy Sep 6, 2023
7173743
Fix test after carousel controller changes.
dermotduffy Sep 6, 2023
88563b5
Fix thumbnail fade issue.
dermotduffy Sep 6, 2023
1c27bc5
Add screenshot.
dermotduffy Sep 6, 2023
4cd0778
Fix editor label.
dermotduffy Sep 6, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"i18n-ally.keepFulfilled": true,
"i18n-ally.editor.preferEditor": true,
"i18n-ally.translate.saveAsCandidates": true,
"vitest.commandLine": "npx vitest --root ."
"vitest.commandLine": "npx vitest --root .",
"diffEditor.experimental.useVersion2": true
}
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ See the [fully expanded live configuration example](#config-expanded-live) for h
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
| `layout` | | :white_check_mark: | See [media layout](#media-layout) below.|
| `microphone` | | :white_check_mark: | See [microphone](#microphone) below.|
| `display` | | :white_check_mark: | See [display](#live-display) below.|

#### Live Controls

Expand Down Expand Up @@ -579,6 +580,35 @@ live:

See [Using 2-way audio](#using-2-way-audio) for more information about the very particular requirements that must be followed for 2-way audio to work.

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

#### Live: Display

All configuration is under:

```yaml
live:
display:
```

| Option | Default | Overridable | Description |
| - | - | - | - |
| `mode` | `single` | :white_check_mark: | Whether to display a `single` media item at a time, or a media item for all cameras in a `grid` configuration.|
| `grid_selected_width_factor` | `2` | :white_check_mark: | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
| `grid_columns` | | :white_check_mark: | If specified the grid will always have exactly this number of columns.|
| `grid_max_columns` | `4` | :white_check_mark: | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. The precise number will be calculated based on the [grid layout algorithm](#grid-layout-algorith). |

<a name="grid-layout-algorithm"></a>

##### Grid Layout Algorithm

The grid will lay out cameras roughly in the order they are specified in the config (items may be moved to optimize grid 'density'). The following algorithm is used to calculate the number of columns. This attempts to offers a balance between configurability, reasonable display in a typical Lovelace card width and reasonable display in a typical fullscreen display.

* Use `grid_columns` if specified.
* Otherwise, use the largest number of columns in the range `[2 - grid_max_columns]` that will fit at least a `600px` column width.
* Otherwise, use the largest number of columns in the range `[2 - grid_max_columns]` that will fit at least a `190px` column width.
* Otherwise, there will be `1` column only.

### Media Viewer Options

The `media_viewer` is used for viewing all `clip`, `snapshot` or recording media, in a media carousel.
Expand Down Expand Up @@ -689,6 +719,24 @@ media_viewer:
| `mode` | `popup-bottom-right` | :heavy_multiplication_x: | How to display the Media viewer media title. Acceptable values: `none`, `popup-top-left`, `popup-top-right`, `popup-bottom-left`, `popup-bottom-right` . |
| `duration_seconds` | `2` | :heavy_multiplication_x: | The number of seconds to display the title popup. `0` implies forever.|

<a name="media-viewer-display"></a>

#### Media Viewer: Display

All configuration is under:

```yaml
media_viewer:
display:
```

| Option | Default | Overridable | Description |
| - | - | - | - |
| `mode` | `single` | :white_check_mark: | Whether to display a `single` media item at a time, or a media item for all cameras in a `grid` configuration.|
| `grid_selected_width_factor` | `2` | :white_check_mark: | How much to scale up the selected media item in a grid. A value of `1` will not scale the selected item at all, the default value of `2` will scale the media item width to twice what it would otherwise be, etc. |
| `grid_columns` | | :white_check_mark: | If specified the grid will always have exactly this number of columns.|
| `grid_max_columns` | `4` | :white_check_mark: | If specified, and `grid_columns` is not specified, the grid will not render more than this number of columns. The precise number will be calculated based on the [grid layout algorithm](#grid-layout-algorith). |

<a name="media-gallery-options"></a>

### Media Gallery Options
Expand Down Expand Up @@ -806,6 +854,17 @@ timeline:

<a name="dimensions"></a>

#### Timeline Seek Behavior

The behavior of the timeline during seeking/dragging can be controlled by means of the icon on the bottom-right of the timeline.

| Option | Behavior |
| - | - |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>filmstrip-box-multiple</title><path d="M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M10,15H8V13H10V15M10,11H8V9H10V11M10,7H8V5H10V7M20,15H18V13H20V15M20,11H18V9H20V11M20,7H18V5H20V7Z" /></svg> | Dragging the timeline will seek / select across all available media from all cameras, selecting the media item with the longest duration whilst favoring (but not limited to) the currently selected camera. |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>play-box-lock</title><path d="M23 17.3V20.8C23 21.4 22.4 22 21.7 22H16.2C15.6 22 15 21.4 15 20.7V17.2C15 16.6 15.6 16 16.2 16V14.5C16.2 13.1 17.6 12 19 12C20.4 12 21.8 13.1 21.8 14.5V16C22.4 16 23 16.6 23 17.3M13 19V21H4C2.89 21 2 20.1 2 19V5C2 3.89 2.89 3 4 3H18C19.1 3 20 3.89 20 5V10.1L19 10L18 10.1C15.79 10.55 14.12 12.45 14 14.76C13.39 15.31 13 16.11 13 17V19M20.5 14.5C20.5 13.7 19.8 13.2 19 13.2C18.2 13.2 17.5 13.7 17.5 14.5V16H20.5V14.5M9 8V16L14 12L9 8Z" /></svg> | Dragging the timeline will seek within the selected media item only. |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>camera-lock</title><path d="M4 4H7L9 2H15L17 4H20C21.11 4 22 4.89 22 6V12C21.16 11.37 20.13 11 19 11C18.21 11 17.46 11.18 16.79 11.5C16.18 9.22 14.27 7 12 7C9.24 7 7 9.24 7 12C7 14.76 9.24 17 12 17C12.42 17 12.84 16.95 13.23 16.85C13.08 17.2 13 17.59 13 18V20H4C2.9 20 2 19.11 2 18V6C2 4.89 2.9 4 4 4M12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9M23 18.3V21.8C23 22.4 22.4 23 21.7 23H16.2C15.6 23 15 22.4 15 21.7V18.2C15 17.6 15.6 17 16.2 17V15.5C16.2 14.1 17.6 13 19 13C20.4 13 21.8 14.1 21.8 15.5V17C22.4 17 23 17.6 23 18.3M20.5 15.5C20.5 14.7 19.8 14.2 19 14.2C18.2 14.2 17.5 14.7 17.5 15.5V17H20.5V15.5Z" /></svg> | Dragging the timeline will seek / select across all available media from the selected camera only, selecting the media item with the longest duration. |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>pan-horizontal</title><path d="M7,8L2.5,12L7,16V8M17,8V16L21.5,12L17,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z" /></svg> | Dragging the timeline will pan only without selected or seeking any media. |

### Dimensions Options

These options control the aspect-ratio of the entire card to make placement in
Expand Down Expand Up @@ -1590,6 +1649,12 @@ Pan around a large camera view to only show part of the video feed in the card a

<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/dev/images/navigate-picture-elements.gif" alt="Taking card actions via the URL" width="400px">

### Interacting with a camera grid

<img src="https://raw.githubusercontent.com/dermotduffy/frigate-hass-card/dev/images/grid-small.gif" alt="Interacting with a camera grid" width="400px">

## Examples

## Examples

### Illustrative Expanded Configuration Reference
Expand Down Expand Up @@ -1946,6 +2011,10 @@ live:
microphone:
always_connected: false
disconnect_seconds: 60
display:
mode: single
grid_selected_width_factor: 2
grid_max_columns: 4
actions:
entity: light.office_main_lights
tap_action:
Expand Down Expand Up @@ -2007,6 +2076,10 @@ media_viewer:
position:
x: 50
y: 50
display:
mode: single
grid_selected_width_factor: 2
grid_max_columns: 4
actions:
entity: light.office_main_lights
tap_action:
Expand Down Expand Up @@ -3823,6 +3896,32 @@ automations:
```
</details>

### Grid display overrides

<details>
<summary>Expand: Change the grid layout configuration in fullscreen mode</summary>

This example will always render 5 columns in fullscreen mode in both the live
and media viewer views, and will not enlarge the selected item. The normal
auto-layout behavior will be used outside of fullscreen mode.

```yaml
overrides:
- conditions:
fullscreen: true
display_mode: grid
overrides:
live:
display:
grid_columns: 5
grid_selected_width_factor: 1
media_viewer:
display:
grid_columns: 5
grid_selected_width_factor: 1
```
</details>

<a name="media-layout-examples"></a>

## Card Refreshes
Expand Down
Binary file added images/grid-small.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@
"custom-card-helpers": "^1.9.0",
"date-fns": "^2.29.2",
"date-fns-tz": "^1.3.7",
"embla-carousel": "^7.0.9",
"embla-carousel-wheel-gestures": "^3.0.0",
"embla-carousel": "8.0.0-rc12",
"embla-carousel-wheel-gestures": "8.0.0-rc04",
"home-assistant-js-websocket": "^8.0.0",
"keycharm": "^0.4.0",
"lit": "^2.3.1",
"lit-flatpickr": "^0.4.0",
"lodash-es": "^4.17.21",
"masonry-layout": "^4.2.2",
"moment": "^2.29.4",
"propagating-hammerjs": "^2.0.1",
"quick-lru": "^6.1.0",
Expand All @@ -58,9 +59,10 @@
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-replace": "^4.0.0",
"@types/lodash-es": "^4.17.5",
"@types/masonry-layout": "^4.2.5",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"@vitest/coverage-c8": "^0.29.8",
"@vitest/coverage-istanbul": "^0.34.3",
"eslint": "^8.23.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.5.0",
Expand All @@ -78,8 +80,8 @@
"sass": "^1.54.9",
"ts-prune": "^0.10.3",
"typescript": "^4.9.5",
"vitest": "^0.29.8",
"vitest-mock-extended": "^1.1.3"
"vitest": "^0.34.3",
"vitest-mock-extended": "^1.2.1"
},
"scripts": {
"start": "rollup -c --watch",
Expand Down
7 changes: 5 additions & 2 deletions src/action-handler-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import type {
import { noChange } from 'lit';
import {
AttributePart,
directive,
Directive,
DirectiveParameters,
directive,
} from 'lit/directive.js';
import { stopEventFromActivatingCardWideActions } from './utils/action.js';
import { Timer } from './utils/timer.js';
Expand Down Expand Up @@ -53,7 +53,10 @@ class ActionHandler extends HTMLElement implements ActionHandler {
});
}

public bind(element: ActionHandlerElement, options): void {
public bind(
element: ActionHandlerElement,
options: FrigateCardActionHandlerOptions,
): void {
if (element.actionHandlerOptions) {
// Reset the options on an existing actionHandler.
element.actionHandlerOptions = options;
Expand Down
12 changes: 5 additions & 7 deletions src/cached-value-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,11 @@ export class CachedValueController<T> implements ReactiveController {
public startTimer(): void {
this.stopTimer();

if (this._timerSeconds > 0) {
this._timerStartCallback?.();
this._timer.startRepeated(this._timerSeconds, () => {
this.updateValue();
this._host.requestUpdate();
});
}
this._timerStartCallback?.();
this._timer.startRepeated(this._timerSeconds, () => {
this.updateValue();
this._host.requestUpdate();
});
}

public hasTimer(): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/camera-manager/engine-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class CameraManagerEngineFactory {
engine = Engine.MotionEye;
} else if (cameraConfig.engine === 'generic') {
engine = Engine.Generic;
} else if (cameraConfig.engine === 'auto') {
} else {
const cameraEntity = getCameraEntityFromConfig(cameraConfig);

if (cameraEntity) {
Expand Down
19 changes: 9 additions & 10 deletions src/camera-manager/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export class ExpiringMemoryRangeSet
}

public add(range: ExpiringRange<Date>): void {
this._expireOldRanges();
this._ranges.push(range);
this._expireOldRanges();
}

protected _expireOldRanges(): void {
Expand Down Expand Up @@ -95,26 +95,25 @@ export const compressRanges = <T extends Date | number>(
ranges = orderBy(ranges, (range) => range.start, 'asc');

let current: Range<T> | null = null;
for (let i = 0; i < ranges.length; ++i) {
const item = ranges[i];
const itemStartSeconds =
item.start instanceof Date ? item.start.getTime() : item.start;
for (const range of ranges) {
const rangeStartSeconds: number =
range.start instanceof Date ? range.start.getTime() : range.start;

if (!current) {
current = { ...item };
current = { ...range };
continue;
}

const currentEndSeconds =
current.end instanceof Date ? current.end.getTime() : (current.end as number);

if (currentEndSeconds + toleranceSeconds * 1000 >= itemStartSeconds) {
if (item.end > current.end) {
current.end = item.end;
if (currentEndSeconds + toleranceSeconds * 1000 >= rangeStartSeconds) {
if (range.end > current.end) {
current.end = range.end;
}
} else {
compressedRanges.push(current);
current = { ...item };
current = { ...range };
}
}
if (current) {
Expand Down
Loading
Loading