feat: Implement support for gesture-based PTZ control (#2378)
- Closes: #1839
This commit is contained in:
@@ -15,8 +15,13 @@ import { MicrophoneState } from '../../card-controller/types.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
||||
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
|
||||
import { PTZDragController } from '../../components-lib/ptz/drag-controller.js';
|
||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import {
|
||||
ptzControlsDefaults,
|
||||
PTZControlType,
|
||||
} from '../../config/schema/common/controls/ptz.js';
|
||||
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
||||
import { LiveConfig } from '../../config/schema/live.js';
|
||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||
@@ -32,7 +37,6 @@ import '../carousel';
|
||||
import { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { AdvancedCameraCardPTZ } from '../ptz.js';
|
||||
import './provider.js';
|
||||
|
||||
const ADVANCED_CAMERA_CARD_LIVE_PROVIDER = 'advanced-camera-card-live-provider';
|
||||
@@ -70,14 +74,13 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public viewFilterCameraID?: string;
|
||||
|
||||
// Index between camera name and slide number.
|
||||
private _cameraToSlide: Record<string, number> = {};
|
||||
private _refPTZControl: Ref<AdvancedCameraCardPTZ> = createRef();
|
||||
private _refCarousel: Ref<HTMLElement> = createRef();
|
||||
|
||||
private _mediaActionsController = new MediaActionsController();
|
||||
private _mediaHeightController = new MediaHeightController(this, '.embla__slide');
|
||||
|
||||
private _ptzDragController = new PTZDragController(this);
|
||||
|
||||
@state()
|
||||
private _mediaHasLoaded = false;
|
||||
|
||||
@@ -96,10 +99,38 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _getTransitionEffect(): TransitionEffect {
|
||||
return this.liveConfig?.transition_effect ?? configDefaults.live.transition_effect;
|
||||
private _getDisplayPTZType(cameraID: string | null): PTZControlType {
|
||||
if (
|
||||
!cameraID ||
|
||||
// For cameras without physical PTZ, always display buttons so the user
|
||||
// can control digital zoom. Gesture type controls have no effect on those
|
||||
// cameras.
|
||||
!this.cameraManager?.getCameraCapabilities(cameraID)?.hasPTZCapability()
|
||||
) {
|
||||
return 'buttons';
|
||||
}
|
||||
return (
|
||||
this.viewManagerEpoch?.manager.getView()?.context?.ptzControls?.type ??
|
||||
this.liveConfig?.controls.ptz.type ??
|
||||
ptzControlsDefaults.type
|
||||
);
|
||||
}
|
||||
|
||||
private _isGesturesPTZActive(
|
||||
view: View | null | undefined,
|
||||
cameraID: string | null,
|
||||
): boolean {
|
||||
// _getDisplayPTZType returns 'buttons' for digital-only cameras, so this
|
||||
// implicitly guards against cameras without physical PTZ capability.
|
||||
return (
|
||||
this._getDisplayPTZType(cameraID) === 'gestures' &&
|
||||
view?.context?.ptzControls?.enabled !== false
|
||||
);
|
||||
}
|
||||
|
||||
private _getTransitionEffect = (): TransitionEffect =>
|
||||
this.liveConfig?.transition_effect ?? configDefaults.live.transition_effect;
|
||||
|
||||
private _getSelectedCameraIndex(): number {
|
||||
if (this.viewFilterCameraID) {
|
||||
// If the carousel is limited to a single cameraID, the first (only)
|
||||
@@ -144,21 +175,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
return [AutoMediaLoadedInfo()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of slides to lazily load. 0 means all slides are lazy
|
||||
* loaded, 1 means that 1 slide on each side of the currently selected slide
|
||||
* should lazy load, etc. `null` means lazy loading is disabled and everything
|
||||
* should load simultaneously.
|
||||
* @returns
|
||||
*/
|
||||
private _getLazyLoadCount(): number | null {
|
||||
// Defaults to fully-lazy loading.
|
||||
return this.liveConfig?.lazy_load === false ? null : 0;
|
||||
}
|
||||
|
||||
private _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||
private _getSlides(): TemplateResult[] {
|
||||
if (!this.cameraManager) {
|
||||
return [[], {}];
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
@@ -167,16 +186,13 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
: this.cameraManager?.getStore().getCameraIDsWithCapability('live');
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
|
||||
for (const cameraID of cameraIDs ?? []) {
|
||||
const slide = this._renderLive(this._getSubstreamCameraID(cameraID, view));
|
||||
if (slide) {
|
||||
cameraToSlide[cameraID] = slides.length;
|
||||
slides.push(slide);
|
||||
}
|
||||
}
|
||||
return [slides, cameraToSlide];
|
||||
return slides;
|
||||
}
|
||||
|
||||
private _setViewHandler(ev: CustomEvent<CarouselSelected>): void {
|
||||
@@ -221,6 +237,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
||||
.zoom=${!this._isGesturesPTZActive(view, cameraID)}
|
||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||
handleZoomSettingsObservedEvent(
|
||||
ev,
|
||||
@@ -312,8 +329,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const [slides, cameraToSlide] = this._getSlides();
|
||||
this._cameraToSlide = cameraToSlide;
|
||||
const slides = this._getSlides();
|
||||
if (!slides.length) {
|
||||
return;
|
||||
}
|
||||
@@ -321,6 +337,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
const hasMultipleCameras = slides.length > 1;
|
||||
const neighbors = this._getCameraNeighbors();
|
||||
|
||||
const streamAwareCameraID = getStreamCameraID(view, this.viewFilterCameraID);
|
||||
const gesturesPTZActive = this._isGesturesPTZActive(view, streamAwareCameraID);
|
||||
|
||||
const forcePTZVisibility =
|
||||
!this._mediaHasLoaded ||
|
||||
(!!this.viewFilterCameraID && this.viewFilterCameraID !== view.camera) ||
|
||||
@@ -328,6 +347,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
? false
|
||||
: view.context?.ptzControls?.enabled;
|
||||
|
||||
const dragEnabled =
|
||||
hasMultipleCameras && this.liveConfig?.draggable && !gesturesPTZActive;
|
||||
|
||||
// Notes on the below:
|
||||
// - guard() is used to avoid reseting the carousel unless the
|
||||
// options/plugins actually change.
|
||||
@@ -336,7 +358,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
<advanced-camera-card-carousel
|
||||
${ref(this._refCarousel)}
|
||||
.loop=${hasMultipleCameras}
|
||||
.dragEnabled=${hasMultipleCameras && this.liveConfig?.draggable}
|
||||
.dragEnabled=${dragEnabled}
|
||||
.plugins=${guard(
|
||||
[this.cameraManager, this.liveConfig],
|
||||
this._getPlugins.bind(this),
|
||||
@@ -363,8 +385,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.config=${this.liveConfig.controls.ptz}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cameraID=${getStreamCameraID(view, this.viewFilterCameraID)}
|
||||
.cameraID=${streamAwareCameraID}
|
||||
.forceVisibility=${forcePTZVisibility}
|
||||
.type=${this._getDisplayPTZType(streamAwareCameraID)}
|
||||
>
|
||||
</advanced-camera-card-ptz>
|
||||
`;
|
||||
@@ -402,6 +425,18 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
if (rootChanged || changedProperties.has('viewManagerEpoch')) {
|
||||
this._setMediaTarget();
|
||||
}
|
||||
|
||||
const carouselEl = this._refCarousel.value;
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const streamAwareCameraID = view
|
||||
? getStreamCameraID(view, this.viewFilterCameraID)
|
||||
: null;
|
||||
|
||||
if (this._isGesturesPTZActive(view, streamAwareCameraID) && carouselEl) {
|
||||
this._ptzDragController.activateIfNecessary(carouselEl);
|
||||
} else {
|
||||
this._ptzDragController.deactivateIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
Reference in New Issue
Block a user