fix: Treat a un/selection of grid items as selected for auto media actions (#1623)
This is a slight behavioral change vs earlier v6 betas, but makes the media actions less surprising. This also allows for automated unmuting in a grid when a camera is selected, without having _every_ camera automatically unmuted.
This commit is contained in:
@@ -8,12 +8,12 @@ live:
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| ------------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `actions` | | [Actions](actions/README.md) to use for the `live` view. |
|
| `actions` | | [Actions](actions/README.md) to use for the `live` view. |
|
||||||
| `auto_mute` | `[unselected, hidden, microphone]` | A list of conditions in which live camera feeds are muted. `unselected` will automatically mute when a camera is unselected in the carousel, `hidden` will automatically mute when the browser/tab becomes hidden or `microphone` will automatically mute after the microphone is muted as long as the camera stays selected (see the `live.microphone.mute_after_microphone_mute_seconds` to control how long after). Use an empty list (`[]`) to never automatically mute. Note that if `auto_play` is enabled, the stream may mute itself automatically in order to honor the `auto_play` setting, as some browsers will not auto play media that is unmuted -- that is to say, where necessary, the `auto_play` parameter will take priority over the `auto_mute` parameter. |
|
| `auto_mute` | `[unselected, hidden, microphone]` | A list of conditions in which live camera feeds are muted. `unselected` will automatically mute when a camera is unselected in the carousel or grid. `hidden` will automatically mute when the camera becomes hidden (e.g. browser tab change) or `microphone` will automatically mute after the microphone is muted as long as the camera stays selected (see the `live.microphone.mute_after_microphone_mute_seconds` to control how long after). Use an empty list (`[]`) to never automatically mute. Note that if `auto_play` is enabled, the stream may mute itself automatically in order to honor the `auto_play` setting, as some browsers will not auto play media that is unmuted -- that is to say, where necessary, the `auto_play` parameter will take priority over the `auto_mute` parameter. |
|
||||||
| `auto_pause` | `[]` | A list of conditions in which live camera feeds are automatically paused. `unselected` will automatically pause when a camera is unselected in the carousel and `hidden` will automatically pause when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically pause. **Caution**: Some live providers (e.g. `jsmpeg`) may not offer human-accessible means to resume play if it is paused, unless the `auto_play` option is used. |
|
| `auto_pause` | `[]` | A list of conditions in which live camera feeds are automatically paused. `unselected` will automatically pause when a camera is unselected in the carousel or grid. `hidden` will automatically pause when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically pause. **Caution**: Some live providers (e.g. `jsmpeg`) may not offer human-accessible means to resume play if it is paused, unless the `auto_play` option is used. |
|
||||||
| `auto_play` | `[selected, visible]` | A list of conditions in which live camera feeds are automatically played.`selected` will automatically play when a camera is selected in the carousel and `visible` will automatically play when the browser/tab becomes visible. Use an empty list (`[]`) to never automatically play. Some live providers (e.g. `webrtc-card`, `jsmpeg`) do not support the prevention of automatic play on initial load, but should still respect the value of this flag on play-after-pause. |
|
| `auto_play` | `[selected, visible]` | A list of conditions in which live camera feeds are automatically played. `selected` will automatically play when a camera is selected in a carousel or grid. `visible` will automatically play when a camera becomes visible (e.g. browser tab change, or visible in a grid but not selected). Use an empty list (`[]`) to never automatically play. Some live providers (e.g. `webrtc-card`, `jsmpeg`) do not support the prevention of automatic play on initial load, but should still respect the value of this parameter on play-after-pause. |
|
||||||
| `auto_unmute` | `[microphone]` | A list of conditions in which live camera feeds are unmuted. `selected` will automatically unmute when a camera is unselected in the carousel, `visible` will automatically unmute when the browser/tab becomes visible or `microphone` will automatically unmute after the microphone is unmuted. Use an empty list (`[]`) to never automatically unmute. |
|
| `auto_unmute` | `[microphone]` | A list of conditions in which live camera feeds are unmuted. `selected` will automatically unmute when a camera is selected in a carousel or grid. `visible` will automatically unmute when a camera becomes visible (e.g. a browser/tab change, or visible in a grid but not selected). `microphone` will automatically unmute after the microphone is unmuted. Use an empty list (`[]`) to never automatically unmute. Some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead. |
|
||||||
| `controls` | | Configuration for the `live` view controls. See below. |
|
| `controls` | | Configuration for the `live` view controls. See below. |
|
||||||
| `display` | | Controls whether to show a single or grid `live` view. See below. |
|
| `display` | | Controls whether to show a single or grid `live` view. See below. |
|
||||||
| `draggable` | `true` | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ media_viewer:
|
|||||||
```
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `actions` | | [Actions](actions/README.md) to use for all views that use the `media_viewer` (e.g. `clip`, `snapshot`). |
|
| `actions` | | [Actions](actions/README.md) to use for all views that use the `media_viewer` (e.g. `clip`, `snapshot`). |
|
||||||
| `auto_mute` | `[unselected, hidden]` | A list of conditions in which media items are muted. `unselected` will automatically mute when a media item is unselected in the carousel and `hidden` will automatically mute when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically mute. |
|
| `auto_mute` | `[unselected, hidden]` | A list of conditions in which media items are muted. `unselected` will automatically mute when a media item is unselected in the carousel or grid. `hidden` will automatically mute when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically mute. |
|
||||||
| `auto_pause` | `[unselected, hidden]` | A list of conditions in which media items are automatically paused. `unselected` will automatically pause when a media item is unselected in the carousel and `hidden` will automatically pause when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically pause. |
|
| `auto_pause` | `[unselected, hidden]` | A list of conditions in which media items are automatically paused. `unselected` will automatically pause when a media item is unselected in the carousel or grid. `hidden` will automatically pause when the browser/tab becomes hidden. Use an empty list (`[]`) to never automatically pause. |
|
||||||
| `auto_play` | `[selected, visible]` | A list of conditions in which media items are automatically played.`selected` will automatically play when a media item is selected in the carousel and `visible` will automatically play when the browser/tab becomes visible. Use an empty list (`[]`) to never automatically play. |
|
| `auto_play` | `[selected, visible]` | A list of conditions in which media items are automatically played. `selected` will automatically play when a media item is selected in a carousel or grid. `visible` will automatically play when a media item becomes visible (e.g. browser tab change, or visible in a grid but not selected). Use an empty list (`[]`) to never automatically play. |
|
||||||
| `auto_unmute` | `[]` | A list of conditions in which media items are unmuted. `selected` will automatically unmute when a media item is unselected in the carousel and `visible` will automatically unmute when the browser/tab becomes visible. Use an empty list (`[]`) to never automatically unmute. Note that some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead. |
|
| `auto_unmute` | `[]` | A list of conditions in which media items are unmuted. `selected` will automatically unmute when a media item is selected in a carousel or grid. `visible` will automatically unmute when a media item becomes visible (e.g. a browser/tab change, or visible in a grid but not selected). Use an empty list (`[]`) to never automatically unmute. Some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead. |
|
||||||
| `controls` | | Configuration for the Media viewer controls. See below. |
|
| `controls` | | Configuration for the Media viewer controls. See below. |
|
||||||
| `draggable` | `true` | Whether or not the Media viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
| `draggable` | `true` | Whether or not the Media viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. |
|
||||||
| `lazy_load` | `true` | Whether or not to lazily load media in the Media viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
| `lazy_load` | `true` | Whether or not to lazily load media in the Media viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. |
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ type PlayerElement = HTMLElement & FrigateCardMediaPlayer;
|
|||||||
* start the video.
|
* start the video.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type MediaActionsTarget = {
|
||||||
|
selected: boolean;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class MediaActionsController {
|
export class MediaActionsController {
|
||||||
protected _options: MediaActionsControllerOptions | null = null;
|
protected _options: MediaActionsControllerOptions | null = null;
|
||||||
protected _viewportIntersecting: boolean | null = null;
|
protected _viewportIntersecting: boolean | null = null;
|
||||||
@@ -42,7 +47,7 @@ export class MediaActionsController {
|
|||||||
|
|
||||||
protected _eventListeners = new Map<HTMLElement, () => void>();
|
protected _eventListeners = new Map<HTMLElement, () => void>();
|
||||||
protected _children: PlayerElement[] = [];
|
protected _children: PlayerElement[] = [];
|
||||||
protected _selected: number | null = null;
|
protected _target: MediaActionsTarget | null = null;
|
||||||
protected _mutationObserver = new MutationObserver(this._mutationHandler.bind(this));
|
protected _mutationObserver = new MutationObserver(this._mutationHandler.bind(this));
|
||||||
protected _intersectionObserver = new IntersectionObserver(
|
protected _intersectionObserver = new IntersectionObserver(
|
||||||
this._intersectionHandler.bind(this),
|
this._intersectionHandler.bind(this),
|
||||||
@@ -67,59 +72,62 @@ export class MediaActionsController {
|
|||||||
this._root = null;
|
this._root = null;
|
||||||
this._removeChildHandlers();
|
this._removeChildHandlers();
|
||||||
this._children = [];
|
this._children = [];
|
||||||
this._selected = null;
|
this._target = null;
|
||||||
this._mutationObserver.disconnect();
|
this._mutationObserver.disconnect();
|
||||||
this._intersectionObserver.disconnect();
|
this._intersectionObserver.disconnect();
|
||||||
this._options?.microphoneManager?.removeListener(this._microphoneChangeHandler);
|
this._options?.microphoneManager?.removeListener(this._microphoneChangeHandler);
|
||||||
document.removeEventListener('visibilitychange', this._visibilityHandler);
|
document.removeEventListener('visibilitychange', this._visibilityHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async select(index: number): Promise<void> {
|
public async setTarget(index: number, selected: boolean): Promise<void> {
|
||||||
if (this._selected === index) {
|
if (this._target?.index === index && this._target?.selected === selected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._selected !== null) {
|
|
||||||
await this.unselect();
|
|
||||||
}
|
|
||||||
this._selected = index;
|
|
||||||
await this._unmuteSelectedIfConfigured('selected');
|
|
||||||
await this._playSelectedIfConfigured('selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unselect(): Promise<void> {
|
// If there's already a selected target, unselect it.
|
||||||
await this._pauseSelectedIfConfigured('unselected');
|
if (!!this._target?.selected) {
|
||||||
await this._muteSelectedIfConfigured('unselected');
|
await this._pauseTargetIfConfigured('unselected');
|
||||||
|
await this._muteTargetIfConfigured('unselected');
|
||||||
this._microphoneMuteTimer.stop();
|
this._microphoneMuteTimer.stop();
|
||||||
this._selected = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unselectAll(): Promise<void> {
|
this._target = {
|
||||||
this._selected = null;
|
selected,
|
||||||
await this._pauseAllIfConfigured('unselected');
|
index,
|
||||||
await this._muteAllIfConfigured('unselected');
|
};
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await this._unmuteTargetIfConfigured('selected');
|
||||||
|
await this._playTargetIfConfigured('selected');
|
||||||
|
} else {
|
||||||
|
await this._unmuteTargetIfConfigured('visible');
|
||||||
|
await this._playTargetIfConfigured('visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _playSelectedIfConfigured(
|
public unsetTarget(): void {
|
||||||
condition: AutoPlayCondition,
|
this._target = null;
|
||||||
): Promise<void> {
|
}
|
||||||
|
|
||||||
|
protected async _playTargetIfConfigured(condition: AutoPlayCondition): Promise<void> {
|
||||||
if (
|
if (
|
||||||
this._selected !== null &&
|
this._target !== null &&
|
||||||
this._options?.autoPlayConditions?.includes(condition)
|
this._options?.autoPlayConditions?.includes(condition)
|
||||||
) {
|
) {
|
||||||
await this._play(this._selected);
|
await this._play(this._target.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _play(index: number): Promise<void> {
|
protected async _play(index: number): Promise<void> {
|
||||||
await this._children[index]?.play();
|
await this._children[index]?.play();
|
||||||
}
|
}
|
||||||
protected async _unmuteSelectedIfConfigured(
|
protected async _unmuteTargetIfConfigured(
|
||||||
condition: AutoUnmuteCondition,
|
condition: AutoUnmuteCondition,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
this._selected !== null &&
|
this._target !== null &&
|
||||||
this._options?.autoUnmuteConditions?.includes(condition)
|
this._options?.autoUnmuteConditions?.includes(condition)
|
||||||
) {
|
) {
|
||||||
await this._unmute(this._selected);
|
await this._unmute(this._target.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _unmute(index: number): Promise<void> {
|
protected async _unmute(index: number): Promise<void> {
|
||||||
@@ -133,14 +141,14 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _pauseSelectedIfConfigured(
|
protected async _pauseTargetIfConfigured(
|
||||||
condition: AutoPauseCondition,
|
condition: AutoPauseCondition,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (
|
if (
|
||||||
this._selected !== null &&
|
this._target !== null &&
|
||||||
this._options?.autoPauseConditions?.includes(condition)
|
this._options?.autoPauseConditions?.includes(condition)
|
||||||
) {
|
) {
|
||||||
await this._pause(this._selected);
|
await this._pause(this._target.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _pause(index: number): Promise<void> {
|
protected async _pause(index: number): Promise<void> {
|
||||||
@@ -154,14 +162,12 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _muteSelectedIfConfigured(
|
protected async _muteTargetIfConfigured(condition: AutoMuteCondition): Promise<void> {
|
||||||
condition: AutoMuteCondition,
|
|
||||||
): Promise<void> {
|
|
||||||
if (
|
if (
|
||||||
this._selected !== null &&
|
this._target !== null &&
|
||||||
this._options?.autoMuteConditions?.includes(condition)
|
this._options?.autoMuteConditions?.includes(condition)
|
||||||
) {
|
) {
|
||||||
await this._mute(this._selected);
|
await this._mute(this._target.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected async _mute(index: number): Promise<void> {
|
protected async _mute(index: number): Promise<void> {
|
||||||
@@ -178,11 +184,11 @@ export class MediaActionsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _mediaLoadedHandler = async (index: number): Promise<void> => {
|
protected _mediaLoadedHandler = async (index: number): Promise<void> => {
|
||||||
if (this._selected !== index) {
|
if (this._target?.index !== index) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this._unmuteSelectedIfConfigured('selected');
|
await this._unmuteTargetIfConfigured(this._target.selected ? 'selected' : 'visible');
|
||||||
await this._playSelectedIfConfigured('selected');
|
await this._playTargetIfConfigured(this._target.selected ? 'selected' : 'visible');
|
||||||
};
|
};
|
||||||
|
|
||||||
protected _removeChildHandlers(): void {
|
protected _removeChildHandlers(): void {
|
||||||
@@ -242,8 +248,8 @@ export class MediaActionsController {
|
|||||||
|
|
||||||
protected _changeVisibility = async (visible: boolean): Promise<void> => {
|
protected _changeVisibility = async (visible: boolean): Promise<void> => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
await this._unmuteSelectedIfConfigured('visible');
|
await this._unmuteTargetIfConfigured('visible');
|
||||||
await this._playSelectedIfConfigured('visible');
|
await this._playTargetIfConfigured('visible');
|
||||||
} else {
|
} else {
|
||||||
await this._pauseAllIfConfigured('hidden');
|
await this._pauseAllIfConfigured('hidden');
|
||||||
await this._muteAllIfConfigured('hidden');
|
await this._muteAllIfConfigured('hidden');
|
||||||
@@ -253,7 +259,7 @@ export class MediaActionsController {
|
|||||||
change: MicrophoneManagerListenerChange,
|
change: MicrophoneManagerListenerChange,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (change === 'unmuted') {
|
if (change === 'unmuted') {
|
||||||
await this._unmuteSelectedIfConfigured('microphone');
|
await this._unmuteTargetIfConfigured('microphone');
|
||||||
} else if (
|
} else if (
|
||||||
change === 'muted' &&
|
change === 'muted' &&
|
||||||
this._options?.autoMuteConditions?.includes('microphone')
|
this._options?.autoMuteConditions?.includes('microphone')
|
||||||
@@ -261,7 +267,7 @@ export class MediaActionsController {
|
|||||||
this._microphoneMuteTimer.start(
|
this._microphoneMuteTimer.start(
|
||||||
this._options.microphoneMuteSeconds ?? 60,
|
this._options.microphoneMuteSeconds ?? 60,
|
||||||
async () => {
|
async () => {
|
||||||
await this._muteSelectedIfConfigured('microphone');
|
await this._muteTargetIfConfigured('microphone');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,13 +325,17 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getSelectedCameraIndex(): number {
|
protected _getSelectedCameraIndex(): number {
|
||||||
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
if (this.viewFilterCameraID) {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
|
||||||
if (!cameraIDs?.size || !view || this.viewFilterCameraID) {
|
|
||||||
// If the carousel is limited to a single cameraID, the first (only)
|
// If the carousel is limited to a single cameraID, the first (only)
|
||||||
// element is always the selected one.
|
// element is always the selected one.
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cameraIDs = this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||||
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
|
if (!cameraIDs?.size || !view) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return Math.max(0, Array.from(cameraIDs).indexOf(view.camera));
|
return Math.max(0, Array.from(cameraIDs).indexOf(view.camera));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,13 +368,19 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('viewManagerEpoch')) {
|
if (changedProps.has('viewManagerEpoch')) {
|
||||||
if (
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
this.viewFilterCameraID &&
|
const selectedCameraIndex = this._getSelectedCameraIndex();
|
||||||
this.viewManagerEpoch?.manager.getView()?.camera !== this.viewFilterCameraID
|
|
||||||
) {
|
if (this.viewFilterCameraID) {
|
||||||
this._mediaActionsController.unselectAll();
|
this._mediaActionsController.setTarget(
|
||||||
|
selectedCameraIndex,
|
||||||
|
// Camera in this carousel is only selected if the camera from the
|
||||||
|
// view matches the filtered camera.
|
||||||
|
view?.camera === this.viewFilterCameraID,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this._mediaActionsController.select(this._getSelectedCameraIndex());
|
// Carousel is not filtered, so the targeted camera is always selected.
|
||||||
|
this._mediaActionsController.setTarget(selectedCameraIndex, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,10 +386,21 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
this._selected = newSelected;
|
this._selected = newSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newMedia) {
|
if (!newMedia?.length) {
|
||||||
this._mediaActionsController.unselectAll();
|
// No media will be rendered.
|
||||||
|
this._mediaActionsController.unsetTarget();
|
||||||
} else {
|
} else {
|
||||||
this._mediaActionsController.select(newSelected);
|
if (this.viewFilterCameraID) {
|
||||||
|
this._mediaActionsController.setTarget(
|
||||||
|
newSelected,
|
||||||
|
// Camera in this carousel is only selected if the camera from the
|
||||||
|
// view matches the filtered camera.
|
||||||
|
view?.camera === this.viewFilterCameraID,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Carousel is not filtered, so the targeted camera is always selected.
|
||||||
|
this._mediaActionsController.setTarget(newSelected, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const deviceSchema = z.object({
|
const deviceSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
model: z.string().nullable(),
|
model: z.string().nullable(),
|
||||||
config_entries: z.string().array(),
|
config_entries: z.string().array(),
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe('MediaActionsController', () => {
|
|||||||
const parent = createParent({ children: children });
|
const parent = createParent({ children: children });
|
||||||
|
|
||||||
controller.initialize(parent);
|
controller.initialize(parent);
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -112,7 +112,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await callMutationHandler();
|
await callMutationHandler();
|
||||||
|
|
||||||
await controller.select(1);
|
await controller.setTarget(1, true);
|
||||||
|
|
||||||
expect(newPlayer.play).toBeCalled();
|
expect(newPlayer.play).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -132,13 +132,13 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
controller.destroy();
|
controller.destroy();
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to select', () => {
|
describe('should respond to setting target', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['should play', { autoPlayConditions: ['selected' as const] }, 'play', true],
|
['should play', { autoPlayConditions: ['selected' as const] }, 'play', true],
|
||||||
['should not play', { autoPlayConditions: [] }, 'play', false],
|
['should not play', { autoPlayConditions: [] }, 'play', false],
|
||||||
@@ -161,13 +161,13 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it('should not re-select', async () => {
|
it('should not reselect previously selected target', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoPlayConditions: ['selected' as const],
|
autoPlayConditions: ['selected' as const],
|
||||||
@@ -177,14 +177,14 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unselect first', async () => {
|
it('should unselect before selecting a new target', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoPauseConditions: ['unselected' as const],
|
autoPauseConditions: ['unselected' as const],
|
||||||
@@ -195,16 +195,66 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
await controller.select(1);
|
await controller.setTarget(1, true);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.pause).toBeCalled();
|
expect(getPlayer(children[0], 'video')?.pause).toBeCalled();
|
||||||
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should select after target was previously visible', async () => {
|
||||||
|
const controller = new MediaActionsController();
|
||||||
|
controller.setOptions({
|
||||||
|
autoPlayConditions: ['selected' as const],
|
||||||
|
autoUnmuteConditions: ['selected' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
|
});
|
||||||
|
|
||||||
|
const children = createPlayerSlideNodes();
|
||||||
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).not.toBeCalled();
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).not.toBeCalled();
|
||||||
|
|
||||||
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).toBeCalled();
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should take no action after target unset', async () => {
|
||||||
|
const controller = new MediaActionsController();
|
||||||
|
controller.setOptions({
|
||||||
|
autoPlayConditions: ['selected' as const, 'visible' as const],
|
||||||
|
autoUnmuteConditions: ['selected' as const, 'visible' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
|
});
|
||||||
|
|
||||||
|
const children = createPlayerSlideNodes();
|
||||||
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
controller.unsetTarget();
|
||||||
|
|
||||||
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
|
new Event('frigate-card:media:loaded'),
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
// Play/Mute will not have been called again.
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to media loaded', () => {
|
describe('should respond to media loaded', () => {
|
||||||
it('should play', async () => {
|
it('should play after media load', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoPlayConditions: ['selected' as const],
|
autoPlayConditions: ['selected' as const],
|
||||||
@@ -214,7 +264,7 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
|
|
||||||
getPlayer(children[0], 'video')?.dispatchEvent(
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
@@ -226,7 +276,7 @@ describe('MediaActionsController', () => {
|
|||||||
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(2);
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should unmute', async () => {
|
it('should unmute after media load', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoUnmuteConditions: ['selected' as const],
|
autoUnmuteConditions: ['selected' as const],
|
||||||
@@ -236,7 +286,7 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
||||||
|
|
||||||
getPlayer(children[0], 'video')?.dispatchEvent(
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
@@ -248,18 +298,18 @@ describe('MediaActionsController', () => {
|
|||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(2);
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should take no action on unselected media load', async () => {
|
it('should take no action on unrelated media load', async () => {
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoPlayConditions: ['selected' as const],
|
autoPlayConditions: ['selected' as const, 'visible' as const],
|
||||||
autoUnmuteConditions: ['selected' as const],
|
autoUnmuteConditions: ['selected' as const, 'visible' as const],
|
||||||
playerSelector: 'video',
|
playerSelector: 'video',
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
getPlayer(children[9], 'video')?.dispatchEvent(
|
getPlayer(children[9], 'video')?.dispatchEvent(
|
||||||
new Event('frigate-card:media:loaded'),
|
new Event('frigate-card:media:loaded'),
|
||||||
@@ -270,9 +320,35 @@ describe('MediaActionsController', () => {
|
|||||||
expect(getPlayer(children[9], 'video')?.play).not.toBeCalled();
|
expect(getPlayer(children[9], 'video')?.play).not.toBeCalled();
|
||||||
expect(getPlayer(children[9], 'video')?.unmute).not.toBeCalled();
|
expect(getPlayer(children[9], 'video')?.unmute).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should play and unmute on unselected but targeted media load', async () => {
|
||||||
|
const controller = new MediaActionsController();
|
||||||
|
controller.setOptions({
|
||||||
|
autoPlayConditions: ['visible' as const],
|
||||||
|
autoUnmuteConditions: ['visible' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to unselect', () => {
|
const children = createPlayerSlideNodes();
|
||||||
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(1);
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
getPlayer(children[0], 'video')?.dispatchEvent(
|
||||||
|
new Event('frigate-card:media:loaded'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getPlayer(children[0], 'video')?.play).toBeCalledTimes(2);
|
||||||
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should take action on unselect', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['should pause', { autoPauseConditions: ['unselected' as const] }, 'pause', true],
|
['should pause', { autoPauseConditions: ['unselected' as const] }, 'pause', true],
|
||||||
['should not pause', { autoPauseConditions: [] }, 'pause', false],
|
['should not pause', { autoPauseConditions: [] }, 'pause', false],
|
||||||
@@ -295,47 +371,15 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
await controller.unselect();
|
await controller.setTarget(0, false);
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
expect(getPlayer(children[0], 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to unselect all', () => {
|
describe('should take action on page being visible', () => {
|
||||||
it.each([
|
|
||||||
['should pause', { autoPauseConditions: ['unselected' as const] }, 'pause', true],
|
|
||||||
['should not pause', { autoPauseConditions: [] }, 'pause', false],
|
|
||||||
['should mute', { autoMuteConditions: ['unselected' as const] }, 'mute', true],
|
|
||||||
['should not mute', { autoMuteConditions: [] }, 'mute', false],
|
|
||||||
])(
|
|
||||||
'%s',
|
|
||||||
async (
|
|
||||||
_: string,
|
|
||||||
options: Partial<MediaActionsControllerOptions>,
|
|
||||||
func: string,
|
|
||||||
called: boolean,
|
|
||||||
) => {
|
|
||||||
const controller = new MediaActionsController();
|
|
||||||
controller.setOptions({
|
|
||||||
playerSelector: 'video',
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
|
||||||
controller.initialize(createParent({ children: children }));
|
|
||||||
|
|
||||||
await controller.unselectAll();
|
|
||||||
|
|
||||||
children.forEach((child) => {
|
|
||||||
expect(getPlayer(child, 'video')?.[func]).toBeCalledTimes(called ? 1 : 0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should respond to page being visible', () => {
|
|
||||||
it.each([
|
it.each([
|
||||||
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
||||||
['should not play', { autoPlayConditions: [] }, 'play', false],
|
['should not play', { autoPlayConditions: [] }, 'play', false],
|
||||||
@@ -359,7 +403,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
||||||
@@ -376,7 +420,7 @@ describe('MediaActionsController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to page being hiddne', () => {
|
describe('should take action on page being hiddne', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.spyOn(global.document, 'addEventListener');
|
vi.spyOn(global.document, 'addEventListener');
|
||||||
});
|
});
|
||||||
@@ -402,7 +446,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
||||||
@@ -419,7 +463,7 @@ describe('MediaActionsController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to page intersecting with viewport', () => {
|
describe('should take action on page intersecting with viewport', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
||||||
['should not play', { autoPlayConditions: [] }, 'play', false],
|
['should not play', { autoPlayConditions: [] }, 'play', false],
|
||||||
@@ -441,7 +485,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
||||||
@@ -458,7 +502,7 @@ describe('MediaActionsController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to page not intersecting with viewport', () => {
|
describe('should take action on page not intersecting with viewport', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
['should play', { autoPlayConditions: ['visible' as const] }, 'play', true],
|
||||||
['should not play', { autoPlayConditions: [] }, 'play', false],
|
['should not play', { autoPlayConditions: [] }, 'play', false],
|
||||||
@@ -480,7 +524,7 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
// Not configured to take action on selection.
|
// Not configured to take action on selection.
|
||||||
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
expect(getPlayer(children[0], 'video')?.[func]).not.toBeCalled();
|
||||||
@@ -497,7 +541,7 @@ describe('MediaActionsController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should respond to microphone changes', () => {
|
describe('should take action on microphone changes', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
@@ -519,7 +563,7 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'unmuted');
|
callMicrophoneListener(microphoneManager, 'unmuted');
|
||||||
|
|
||||||
@@ -539,7 +583,7 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'muted');
|
callMicrophoneListener(microphoneManager, 'muted');
|
||||||
|
|
||||||
@@ -561,7 +605,7 @@ describe('MediaActionsController', () => {
|
|||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
controller.initialize(createParent({ children: children }));
|
controller.initialize(createParent({ children: children }));
|
||||||
|
|
||||||
await controller.select(0);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'muted');
|
callMicrophoneListener(microphoneManager, 'muted');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user