@matterbridge/core 3.10.10-dev-20260914-784e019 → 3.10.10-dev-20260917-9fdf275

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,36 @@
1
1
  import { CameraAvSettingsUserLevelManagementServer } from '@matter/node/behaviors/camera-av-settings-user-level-management';
2
+ import { type Viewport } from '@matter/types';
2
3
  import { CameraAvSettingsUserLevelManagement } from '@matter/types/clusters/camera-av-settings-user-level-management';
4
+ import type { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
3
5
  import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
4
6
  declare const MatterbridgeCameraAvSettingsUserLevelManagementServer_base: import("@matter/node").ClusterBehavior.Type<typeof CameraAvSettingsUserLevelManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvSettingsUserLevelManagement, {
5
- digitalPtz: false;
7
+ digitalPtz: true;
6
8
  mechanicalPan: true;
7
- mechanicalPresets: false;
9
+ mechanicalPresets: true;
8
10
  mechanicalTilt: true;
9
11
  mechanicalZoom: true;
10
12
  }>, import("@matter/types").ClusterType.Concrete, new () => {}, "cameraAvSettingsUserLevelManagement">;
11
13
  export declare class MatterbridgeCameraAvSettingsUserLevelManagementServer extends MatterbridgeCameraAvSettingsUserLevelManagementServer_base {
12
14
  readonly endpoint: MatterbridgeEndpoint;
15
+ protected internal: MatterbridgeCameraAvSettingsUserLevelManagementServer.Internal;
16
+ initialize(): void;
17
+ protected syncDptzStreams(allocatedVideoStreams: CameraAvStreamManagement.VideoStream[]): void;
18
+ protected syncDptzViewport(viewport: Viewport): void;
13
19
  mptzSetPosition(request: CameraAvSettingsUserLevelManagement.MptzSetPositionRequest): void;
14
20
  mptzRelativeMove(request: CameraAvSettingsUserLevelManagement.MptzRelativeMoveRequest): void;
21
+ mptzMoveToPreset(request: CameraAvSettingsUserLevelManagement.MptzMoveToPresetRequest): void;
22
+ mptzSavePreset(request: CameraAvSettingsUserLevelManagement.MptzSavePresetRequest): void;
23
+ mptzRemovePreset(request: CameraAvSettingsUserLevelManagement.MptzRemovePresetRequest): void;
24
+ private generatePresetId;
25
+ private dptzStreamEntry;
26
+ private applyDptzViewport;
27
+ dptzSetViewport(request: CameraAvSettingsUserLevelManagement.DptzSetViewportRequest): void;
28
+ dptzRelativeMove(request: CameraAvSettingsUserLevelManagement.DptzRelativeMoveRequest): void;
29
+ }
30
+ export declare namespace MatterbridgeCameraAvSettingsUserLevelManagementServer {
31
+ class Internal {
32
+ lastGeneratedPresetId: number;
33
+ }
15
34
  }
16
35
  export interface CameraAvSettingsUserLevelManagementClusterOptions {
17
36
  panMin: number;
@@ -20,6 +39,7 @@ export interface CameraAvSettingsUserLevelManagementClusterOptions {
20
39
  tiltMax: number;
21
40
  zoomMax: number;
22
41
  mptzPosition: CameraAvSettingsUserLevelManagement.Mptz;
42
+ maxPresets: number;
23
43
  }
24
44
  export declare function createDefaultCameraAvSettingsUserLevelManagementClusterServer(endpoint: MatterbridgeEndpoint, options: CameraAvSettingsUserLevelManagementClusterOptions): MatterbridgeEndpoint;
25
45
  export {};
@@ -1,11 +1,45 @@
1
1
  import { CameraAvSettingsUserLevelManagementServer } from '@matter/node/behaviors/camera-av-settings-user-level-management';
2
2
  import { Status, StatusResponseError } from '@matter/types';
3
3
  import { CameraAvSettingsUserLevelManagement } from '@matter/types/clusters/camera-av-settings-user-level-management';
4
+ import { MatterbridgeCameraAvStreamManagementServer } from './cameraAvStreamManagementServer.js';
4
5
  import { MatterbridgeServer } from './matterbridgeServer.js';
5
6
  function clamp(value, min, max) {
6
7
  return Math.min(Math.max(value, min), max);
7
8
  }
8
- export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends CameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom) {
9
+ function viewportWidth(viewport) {
10
+ return viewport.x2 - viewport.x1;
11
+ }
12
+ function viewportHeight(viewport) {
13
+ return viewport.y2 - viewport.y1;
14
+ }
15
+ function sameAspectRatio(width, height, referenceWidth, referenceHeight) {
16
+ return width * referenceHeight === referenceWidth * height;
17
+ }
18
+ function viewportAt(x1, y1, width, height, sensorWidth, sensorHeight) {
19
+ const left = clamp(x1, 0, sensorWidth - width);
20
+ const top = clamp(y1, 0, sensorHeight - height);
21
+ return { x1: left, y1: top, x2: left + width, y2: top + height };
22
+ }
23
+ export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends CameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom, CameraAvSettingsUserLevelManagement.Feature.MechanicalPresets, CameraAvSettingsUserLevelManagement.Feature.DigitalPtz) {
24
+ initialize() {
25
+ const streamManagement = this.agent.get(MatterbridgeCameraAvStreamManagementServer);
26
+ this.syncDptzStreams(streamManagement.state.allocatedVideoStreams);
27
+ this.reactTo(streamManagement.events.allocatedVideoStreams$Changed, this.syncDptzStreams);
28
+ this.reactTo(streamManagement.events.viewport$Changed, this.syncDptzViewport);
29
+ }
30
+ syncDptzStreams(allocatedVideoStreams) {
31
+ const allocatedIds = allocatedVideoStreams.map((stream) => stream.videoStreamId);
32
+ const currentIds = this.state.dptzStreams.map((entry) => entry.videoStreamId);
33
+ if (allocatedIds.length === currentIds.length && allocatedIds.every((id, index) => id === currentIds[index]))
34
+ return;
35
+ const viewport = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state.viewport;
36
+ this.state.dptzStreams = allocatedIds.map((videoStreamId) => this.state.dptzStreams.find((entry) => entry.videoStreamId === videoStreamId) ?? { videoStreamId, viewport });
37
+ }
38
+ syncDptzViewport(viewport) {
39
+ if (this.state.dptzStreams.length === 0)
40
+ return;
41
+ this.state.dptzStreams = this.state.dptzStreams.map((entry) => ({ videoStreamId: entry.videoStreamId, viewport }));
42
+ }
9
43
  mptzSetPosition(request) {
10
44
  const device = this.endpoint.stateOf(MatterbridgeServer);
11
45
  const { pan, tilt, zoom } = request;
@@ -42,11 +76,147 @@ export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends Camer
42
76
  device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved mechanical PTZ position by pan ${request.panDelta ?? 0}°, tilt ${request.tiltDelta ?? 0}°, zoom ${request.zoomDelta ?? 0} to pan ${pan}°, tilt ${tilt}°, zoom ${zoom} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
43
77
  this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzRelativeMove', request, this.context);
44
78
  }
79
+ mptzMoveToPreset(request) {
80
+ const device = this.endpoint.stateOf(MatterbridgeServer);
81
+ const { presetId } = request;
82
+ if (presetId < 1 || presetId > this.state.maxPresets) {
83
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
84
+ }
85
+ const preset = this.state.mptzPresets.find((entry) => entry.presetId === presetId);
86
+ if (preset === undefined) {
87
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is not present in mptzPresets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
88
+ }
89
+ this.state.mptzPosition = preset.settings;
90
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved mechanical PTZ position to preset ${presetId} "${preset.name}": pan ${preset.settings.pan}°, tilt ${preset.settings.tilt}°, zoom ${preset.settings.zoom} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
91
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzMoveToPreset', request, this.context);
92
+ }
93
+ mptzSavePreset(request) {
94
+ const device = this.endpoint.stateOf(MatterbridgeServer);
95
+ const { presetId, name } = request;
96
+ if (name.length > 32) {
97
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: name "${name}" exceeds the maximum length of 32 characters (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
98
+ }
99
+ if (presetId !== undefined && (presetId < 1 || presetId > this.state.maxPresets)) {
100
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
101
+ }
102
+ if (presetId !== undefined && this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
103
+ this.state.mptzPresets = this.state.mptzPresets.map((entry) => (entry.presetId === presetId ? { presetId, name, settings: this.state.mptzPosition } : entry));
104
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: updated preset ${presetId} "${name}" with the current mechanical PTZ position (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
105
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzSavePreset', request, this.context);
106
+ return;
107
+ }
108
+ if (this.state.mptzPresets.length >= this.state.maxPresets) {
109
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: mptzPresets already holds the maximum of ${this.state.maxPresets} presets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ResourceExhausted);
110
+ }
111
+ const newPresetId = presetId ?? this.generatePresetId();
112
+ this.state.mptzPresets = [...this.state.mptzPresets, { presetId: newPresetId, name, settings: this.state.mptzPosition }];
113
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: saved preset ${newPresetId} "${name}" with the current mechanical PTZ position (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
114
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzSavePreset', request, this.context);
115
+ }
116
+ mptzRemovePreset(request) {
117
+ const device = this.endpoint.stateOf(MatterbridgeServer);
118
+ const { presetId } = request;
119
+ if (presetId < 1 || presetId > this.state.maxPresets) {
120
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
121
+ }
122
+ if (!this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
123
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is not present in mptzPresets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
124
+ }
125
+ this.state.mptzPresets = this.state.mptzPresets.filter((entry) => entry.presetId !== presetId);
126
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: removed preset ${presetId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
127
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzRemovePreset', request, this.context);
128
+ }
129
+ generatePresetId() {
130
+ let presetId = (this.internal.lastGeneratedPresetId % this.state.maxPresets) + 1;
131
+ while (this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
132
+ presetId = (presetId % this.state.maxPresets) + 1;
133
+ }
134
+ this.internal.lastGeneratedPresetId = presetId;
135
+ return presetId;
136
+ }
137
+ dptzStreamEntry(videoStreamId) {
138
+ const entry = this.state.dptzStreams.find((stream) => stream.videoStreamId === videoStreamId);
139
+ if (entry === undefined) {
140
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: video stream ${videoStreamId} is not present in dptzStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
141
+ }
142
+ return entry;
143
+ }
144
+ applyDptzViewport(videoStreamId, viewport) {
145
+ this.state.dptzStreams = this.state.dptzStreams.map((entry) => (entry.videoStreamId === videoStreamId ? { videoStreamId, viewport } : entry));
146
+ }
147
+ dptzSetViewport(request) {
148
+ const device = this.endpoint.stateOf(MatterbridgeServer);
149
+ const { videoStreamId, viewport } = request;
150
+ const entry = this.dptzStreamEntry(videoStreamId);
151
+ const { videoSensorParams, minViewportResolution } = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state;
152
+ const width = viewportWidth(viewport);
153
+ const height = viewportHeight(viewport);
154
+ if (width < minViewportResolution.width || height < minViewportResolution.height) {
155
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} is smaller than the minimum viewport resolution ${minViewportResolution.width}x${minViewportResolution.height} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
156
+ }
157
+ if (viewport.x2 > videoSensorParams.sensorWidth ||
158
+ viewport.y2 > videoSensorParams.sensorHeight ||
159
+ width > videoSensorParams.sensorWidth ||
160
+ height > videoSensorParams.sensorHeight) {
161
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} at (${viewport.x1}, ${viewport.y1}) does not fit the sensor ${videoSensorParams.sensorWidth}x${videoSensorParams.sensorHeight} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
162
+ }
163
+ if (!sameAspectRatio(width, height, viewportWidth(entry.viewport), viewportHeight(entry.viewport))) {
164
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} does not match the aspect ratio of video stream ${videoStreamId} (${viewportWidth(entry.viewport)}x${viewportHeight(entry.viewport)}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
165
+ }
166
+ this.applyDptzViewport(videoStreamId, viewport);
167
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: set the viewport of video stream ${videoStreamId} to (${viewport.x1}, ${viewport.y1})-(${viewport.x2}, ${viewport.y2}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
168
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'dptzSetViewport', request, this.context);
169
+ }
170
+ dptzRelativeMove(request) {
171
+ const device = this.endpoint.stateOf(MatterbridgeServer);
172
+ const { videoStreamId, deltaX, deltaY, zoomDelta } = request;
173
+ const entry = this.dptzStreamEntry(videoStreamId);
174
+ const { videoSensorParams, minViewportResolution } = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state;
175
+ const { sensorWidth, sensorHeight } = videoSensorParams;
176
+ const current = entry.viewport;
177
+ let x1 = current.x1 + (deltaX ?? 0);
178
+ let y1 = current.y1 + (deltaY ?? 0);
179
+ let width = viewportWidth(current);
180
+ let height = viewportHeight(current);
181
+ if (zoomDelta !== undefined && zoomDelta !== 0) {
182
+ const scaledWidth = Math.round((width * (100 - zoomDelta)) / 100);
183
+ const scaledHeight = Math.round((height * (100 - zoomDelta)) / 100);
184
+ x1 += Math.round((width - scaledWidth) / 2);
185
+ y1 += Math.round((height - scaledHeight) / 2);
186
+ width = scaledWidth;
187
+ height = scaledHeight;
188
+ }
189
+ const streamWidth = viewportWidth(current);
190
+ const streamHeight = viewportHeight(current);
191
+ if (!sameAspectRatio(width, height, streamWidth, streamHeight)) {
192
+ const clippedHeight = Math.round((width * streamHeight) / streamWidth);
193
+ if (clippedHeight <= height)
194
+ height = clippedHeight;
195
+ else
196
+ width = Math.round((height * streamWidth) / streamHeight);
197
+ }
198
+ width = Math.max(width, minViewportResolution.width);
199
+ height = Math.max(height, minViewportResolution.height);
200
+ width = Math.min(width, sensorWidth);
201
+ height = Math.min(height, sensorHeight);
202
+ const viewport = viewportAt(x1, y1, width, height, sensorWidth, sensorHeight);
203
+ this.applyDptzViewport(videoStreamId, viewport);
204
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved the viewport of video stream ${videoStreamId} by deltaX ${deltaX ?? 0}, deltaY ${deltaY ?? 0}, zoomDelta ${zoomDelta ?? 0} to (${viewport.x1}, ${viewport.y1})-(${viewport.x2}, ${viewport.y2}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
205
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'dptzRelativeMove', request, this.context);
206
+ }
45
207
  }
208
+ (function (MatterbridgeCameraAvSettingsUserLevelManagementServer) {
209
+ class Internal {
210
+ lastGeneratedPresetId = 0;
211
+ }
212
+ MatterbridgeCameraAvSettingsUserLevelManagementServer.Internal = Internal;
213
+ })(MatterbridgeCameraAvSettingsUserLevelManagementServer || (MatterbridgeCameraAvSettingsUserLevelManagementServer = {}));
46
214
  export function createDefaultCameraAvSettingsUserLevelManagementClusterServer(endpoint, options) {
47
- endpoint.behaviors.require(MatterbridgeCameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom), {
215
+ endpoint.behaviors.require(MatterbridgeCameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom, CameraAvSettingsUserLevelManagement.Feature.MechanicalPresets, CameraAvSettingsUserLevelManagement.Feature.DigitalPtz), {
48
216
  ...options,
49
217
  movementState: CameraAvSettingsUserLevelManagement.PhysicalMovement.Idle,
218
+ mptzPresets: [],
219
+ dptzStreams: [],
50
220
  });
51
221
  return endpoint;
52
222
  }
@@ -3,12 +3,8 @@ import { StreamUsage } from '@matter/types';
3
3
  import type { Viewport } from '@matter/types';
4
4
  import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
5
5
  import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
6
- export interface CameraColorTestJpeg {
7
- data: Buffer;
8
- resolution: CameraAvStreamManagement.VideoResolution;
9
- }
10
- export declare function cameraColorTestJpegForResolution(requestedResolution: CameraAvStreamManagement.VideoResolution): CameraColorTestJpeg;
11
- declare const MatterbridgeCameraAvStreamManagementServer_base: import("@matter/node").ClusterBehavior.Type<typeof CameraAvStreamManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvStreamManagement, {
6
+ export type SnapshotSource = 'test' | 'webcam' | 'rtsp';
7
+ declare const MatterbridgeCameraAvStreamManagementServerBase: import("@matter/node").ClusterBehavior.Type<typeof CameraAvStreamManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvStreamManagement, {
12
8
  audio: true;
13
9
  highDynamicRange: false;
14
10
  imageControl: true;
@@ -21,9 +17,10 @@ declare const MatterbridgeCameraAvStreamManagementServer_base: import("@matter/n
21
17
  video: true;
22
18
  watermark: false;
23
19
  }>, import("@matter/types").ClusterType.Concrete, new () => {}, "cameraAvStreamManagement">;
24
- export declare class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCameraAvStreamManagementServer_base {
20
+ export declare class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCameraAvStreamManagementServerBase {
25
21
  #private;
26
22
  readonly endpoint: MatterbridgeEndpoint;
23
+ readonly state: MatterbridgeCameraAvStreamManagementServer.State;
27
24
  initialize(): void;
28
25
  setStreamPriorities(request: CameraAvStreamManagement.SetStreamPrioritiesRequest): void;
29
26
  videoStreamAllocate(request: CameraAvStreamManagement.VideoStreamAllocateRequest): CameraAvStreamManagement.VideoStreamAllocateResponse;
@@ -32,7 +29,13 @@ export declare class MatterbridgeCameraAvStreamManagementServer extends Matterbr
32
29
  audioStreamDeallocate(request: CameraAvStreamManagement.AudioStreamDeallocateRequest): void;
33
30
  snapshotStreamAllocate(request: CameraAvStreamManagement.SnapshotStreamAllocateRequest): Promise<CameraAvStreamManagement.SnapshotStreamAllocateResponse>;
34
31
  snapshotStreamDeallocate(request: CameraAvStreamManagement.SnapshotStreamDeallocateRequest): Promise<void>;
35
- captureSnapshot(request: CameraAvStreamManagement.CaptureSnapshotRequest): CameraAvStreamManagement.CaptureSnapshotResponse;
32
+ captureSnapshot(request: CameraAvStreamManagement.CaptureSnapshotRequest): Promise<CameraAvStreamManagement.CaptureSnapshotResponse>;
33
+ }
34
+ export declare namespace MatterbridgeCameraAvStreamManagementServer {
35
+ class State extends MatterbridgeCameraAvStreamManagementServerBase.State {
36
+ snapshotSource?: SnapshotSource;
37
+ snapshotSourceDevice?: string;
38
+ }
36
39
  }
37
40
  export interface CameraAvStreamManagementClusterOptions {
38
41
  maxContentBufferSize: number;
@@ -76,6 +79,8 @@ export interface SnapshotCameraAvStreamManagementClusterOptions {
76
79
  maxNetworkBandwidth: number;
77
80
  supportedStreamUsages: StreamUsage[];
78
81
  streamUsagePriorities: StreamUsage[];
82
+ snapshotSource?: SnapshotSource;
83
+ snapshotSourceDevice?: string;
79
84
  }
80
85
  export declare function createDefaultSnapshotCameraAvStreamManagementClusterServer(endpoint: MatterbridgeEndpoint, options: SnapshotCameraAvStreamManagementClusterOptions): MatterbridgeEndpoint;
81
86
  export {};
@@ -1,24 +1,20 @@
1
- import { readFileSync } from 'node:fs';
2
1
  import { CameraAvStreamManagementServer } from '@matter/node/behaviors/camera-av-stream-management';
2
+ import { WebRtcTransportProviderServer } from '@matter/node/behaviors/web-rtc-transport-provider';
3
3
  import { Status, StatusResponseError, StreamUsage, ThreeLevelAuto } from '@matter/types';
4
4
  import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
5
5
  import { MatterbridgeServer } from './matterbridgeServer.js';
6
- const DEFAULT_CAMERA_COLOR_TEST_RESOLUTION = '640x480';
6
+ import { captureSnapshot, TEST_SNAPSHOT_SOURCE } from './snapshot.js';
7
7
  const AUDIO_STREAM_BIT_DEPTHS = [8, 16, 24, 32];
8
8
  function numericEnumValues(enumObject) {
9
9
  return Object.values(enumObject).filter((value) => typeof value === 'number');
10
10
  }
11
11
  const AUDIO_CODECS = numericEnumValues(CameraAvStreamManagement.AudioCodec);
12
12
  const VIDEO_CODECS = numericEnumValues(CameraAvStreamManagement.VideoCodec);
13
- const cameraColorTestJpegs = {
14
- '640x480': { data: readFileSync(new URL('../../assets/camera-color-test-640-480.jpeg', import.meta.url)), resolution: { width: 640, height: 480 } },
15
- '1280x720': { data: readFileSync(new URL('../../assets/camera-color-test-1280-720.jpeg', import.meta.url)), resolution: { width: 1280, height: 720 } },
16
- '1920x1080': { data: readFileSync(new URL('../../assets/camera-color-test-1920-1080.jpeg', import.meta.url)), resolution: { width: 1920, height: 1080 } },
17
- };
18
- export function cameraColorTestJpegForResolution(requestedResolution) {
19
- return cameraColorTestJpegs[`${requestedResolution.width}x${requestedResolution.height}`] ?? cameraColorTestJpegs[DEFAULT_CAMERA_COLOR_TEST_RESOLUTION];
13
+ function hasWeriftOfferOptions(state) {
14
+ return 'weriftOfferOptions' in state && typeof state.weriftOfferOptions === 'object' && state.weriftOfferOptions !== null;
20
15
  }
21
- export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl) {
16
+ const MatterbridgeCameraAvStreamManagementServerBase = CameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl);
17
+ export class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCameraAvStreamManagementServerBase {
22
18
  #isAutoAllocateSkipped() {
23
19
  return process.env.MATTERBRIDGE_SKIP_AUTO_ALLOCATE_CAMERA_AV_STREAM_MANAGEMENT === '1';
24
20
  }
@@ -189,6 +185,9 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
189
185
  if (!this.state.allocatedVideoStreams.some((stream) => stream.videoStreamId === request.videoStreamId)) {
190
186
  throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: video stream ${request.videoStreamId} is not present in allocatedVideoStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
191
187
  }
188
+ if (this.state.allocatedVideoStreams.some((stream) => stream.videoStreamId === request.videoStreamId && stream.referenceCount > 0)) {
189
+ throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: video stream ${request.videoStreamId} is in use (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
190
+ }
192
191
  this.state.allocatedVideoStreams = this.state.allocatedVideoStreams.filter((stream) => stream.videoStreamId !== request.videoStreamId);
193
192
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated video stream ${request.videoStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
194
193
  this.endpoint.emitCommand(CameraAvStreamManagement, 'videoStreamDeallocate', request, this.context);
@@ -252,6 +251,9 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
252
251
  if (!this.state.allocatedAudioStreams.some((stream) => stream.audioStreamId === request.audioStreamId)) {
253
252
  throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: audio stream ${request.audioStreamId} is not present in allocatedAudioStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
254
253
  }
254
+ if (this.state.allocatedAudioStreams.some((stream) => stream.audioStreamId === request.audioStreamId && stream.referenceCount > 0)) {
255
+ throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: audio stream ${request.audioStreamId} is in use (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
256
+ }
255
257
  this.state.allocatedAudioStreams = this.state.allocatedAudioStreams.filter((stream) => stream.audioStreamId !== request.audioStreamId);
256
258
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated audio stream ${request.audioStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
257
259
  this.endpoint.emitCommand(CameraAvStreamManagement, 'audioStreamDeallocate', request, this.context);
@@ -328,7 +330,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
328
330
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated snapshot stream ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
329
331
  this.endpoint.emitCommand(CameraAvStreamManagement, 'snapshotStreamDeallocate', request, this.context);
330
332
  }
331
- captureSnapshot(request) {
333
+ #resolveSnapshotSource() {
334
+ const endpoint = `(endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
335
+ const fail = (reason) => {
336
+ this.endpoint.stateOf(MatterbridgeServer).log.error(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`);
337
+ return new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`, Status.Failure);
338
+ };
339
+ const sourceFor = (source, device, sourceOption, deviceOption) => {
340
+ if (source === 'test')
341
+ return TEST_SNAPSHOT_SOURCE;
342
+ if (device)
343
+ return device;
344
+ throw fail(`${sourceOption}=${source} requires ${deviceOption} to be set`);
345
+ };
346
+ const { snapshotSource, snapshotSourceDevice } = this.state;
347
+ if (snapshotSource !== undefined)
348
+ return sourceFor(snapshotSource, snapshotSourceDevice, 'snapshotSource', 'snapshotSourceDevice');
349
+ if (!this.endpoint.behaviors.has(WebRtcTransportProviderServer)) {
350
+ throw fail('no snapshotSource is configured and the endpoint has no WebRtcTransportProvider cluster to read the video source from');
351
+ }
352
+ const state = this.endpoint.stateOf(WebRtcTransportProviderServer);
353
+ if (!hasWeriftOfferOptions(state))
354
+ throw fail('the WebRtcTransportProvider cluster state has no weriftOfferOptions');
355
+ const { videoSource, videoSourceDevice } = state.weriftOfferOptions;
356
+ if (videoSource === 'none')
357
+ throw fail(`weriftOfferOptions.videoSource=${videoSource} provides no video source`);
358
+ return sourceFor(videoSource, videoSourceDevice, 'weriftOfferOptions.videoSource', 'videoSourceDevice');
359
+ }
360
+ async captureSnapshot(request) {
332
361
  const device = this.endpoint.stateOf(MatterbridgeServer);
333
362
  const { snapshotStreamId } = request;
334
363
  const stream = this.state.allocatedSnapshotStreams.find((s) => snapshotStreamId === null || s.snapshotStreamId === snapshotStreamId);
@@ -337,15 +366,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
337
366
  }
338
367
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: capturing snapshot ${snapshotStreamId ?? 'auto'} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
339
368
  device.log.debug(`MatterbridgeCameraAvStreamManagementServer: captureSnapshot called with snapshotStreamId ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
340
- const { data, resolution } = cameraColorTestJpegForResolution(request.requestedResolution);
369
+ const src = this.#resolveSnapshotSource();
370
+ const { width, height } = request.requestedResolution;
371
+ let captured;
372
+ try {
373
+ captured = await captureSnapshot({ src, width, height });
374
+ }
375
+ catch (error) {
376
+ const message = `MatterbridgeCameraAvStreamManagementServer: snapshot capture failed: ${error instanceof Error ? error.message : String(error)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
377
+ device.log.error(message);
378
+ throw new StatusResponseError(message, Status.Failure);
379
+ }
380
+ const resolution = { width: captured.width, height: captured.height === -1 ? Math.round((captured.width * height) / width) : captured.height };
381
+ device.log.info(`MatterbridgeCameraAvStreamManagementServer: captured snapshot ${captured.byteLength}B at ${resolution.width}x${resolution.height}, quality ${captured.quality}${captured.downgraded ? ' (downgraded)' : ''} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
341
382
  this.endpoint.emitCommand(CameraAvStreamManagement, 'captureSnapshot', request, this.context);
342
383
  return {
343
- data,
384
+ data: captured.data,
344
385
  imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg,
345
386
  resolution,
346
387
  };
347
388
  }
348
389
  }
390
+ (function (MatterbridgeCameraAvStreamManagementServer) {
391
+ class State extends MatterbridgeCameraAvStreamManagementServerBase.State {
392
+ snapshotSource = undefined;
393
+ snapshotSourceDevice = undefined;
394
+ }
395
+ MatterbridgeCameraAvStreamManagementServer.State = State;
396
+ })(MatterbridgeCameraAvStreamManagementServer || (MatterbridgeCameraAvStreamManagementServer = {}));
349
397
  export function createDefaultCameraAvStreamManagementClusterServer(endpoint, options) {
350
398
  endpoint.behaviors.require(MatterbridgeCameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl), {
351
399
  ...options,
@@ -408,6 +456,8 @@ export function createDefaultSnapshotCameraAvStreamManagementClusterServer(endpo
408
456
  maxEncodedPixelRate: options.maxEncodedPixelRate,
409
457
  snapshotCapabilities: options.snapshotCapabilities,
410
458
  allocatedSnapshotStreams: [],
459
+ snapshotSource: options.snapshotSource,
460
+ snapshotSourceDevice: options.snapshotSourceDevice,
411
461
  });
412
462
  return endpoint;
413
463
  }
@@ -1,11 +1,25 @@
1
1
  import { ColorControlServer } from '@matter/node/behaviors/color-control';
2
2
  import { ColorControl } from '@matter/types/clusters/color-control';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeColorControlServer extends ColorControlServer.with(ColorControl.Feature.HueSaturation, ColorControl.Feature.Xy, ColorControl.Feature.ColorTemperature, ColorControl.Feature.EnhancedHue) {
5
5
  initialize() {
6
6
  if (process.env.MATTERBRIDGE_CHIP_TEST)
7
7
  this.state.managedTransitionTimeHandling = true;
8
- return super.initialize();
8
+ const result = super.initialize();
9
+ if (this.features.colorTemperature && !isSoftwareUpdateBoot(this.env)) {
10
+ const startUpMireds = this.state.startUpColorTemperatureMireds ?? null;
11
+ if (startUpMireds !== null) {
12
+ const crop = (mireds) => Math.min(Math.max(mireds, this.minimumColorTemperatureMireds), this.maximumColorTemperatureMireds);
13
+ const currentMireds = crop(this.state.colorTemperatureMireds);
14
+ const targetMireds = crop(startUpMireds);
15
+ if (targetMireds !== currentMireds) {
16
+ this.state.colorMode = ColorControl.ColorMode.ColorTemperatureMireds;
17
+ this.state.enhancedColorMode = ColorControl.EnhancedColorMode.ColorTemperatureMireds;
18
+ this.state.colorTemperatureMireds = targetMireds;
19
+ }
20
+ }
21
+ }
22
+ return result;
9
23
  }
10
24
  async moveToHue(request) {
11
25
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
@@ -1,11 +1,20 @@
1
1
  import { LevelControlServer } from '@matter/node/behaviors/level-control';
2
2
  import { LevelControl } from '@matter/types/clusters/level-control';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeLevelControlServer extends LevelControlServer {
5
5
  initialize() {
6
6
  if (process.env.MATTERBRIDGE_CHIP_TEST)
7
7
  this.state.managedTransitionTimeHandling = true;
8
- return super.initialize();
8
+ const result = super.initialize();
9
+ if (this.features.lighting && !isSoftwareUpdateBoot(this.env)) {
10
+ const state = this.state;
11
+ const startUpCurrentLevel = state.startUpCurrentLevel ?? null;
12
+ const currentLevel = state.currentLevel;
13
+ const targetLevel = startUpCurrentLevel === 0 ? this.minLevel : (startUpCurrentLevel ?? currentLevel);
14
+ if (targetLevel !== currentLevel)
15
+ state.currentLevel = targetLevel;
16
+ }
17
+ return result;
9
18
  }
10
19
  async moveToLevel(request) {
11
20
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
@@ -1,6 +1,8 @@
1
+ import type { Environment } from '@matter/general';
1
2
  import { Behavior } from '@matter/node';
2
3
  import type { AnsiLogger } from 'node-ansi-logger';
3
4
  import type { CommandHandler } from '../matterbridgeEndpointCommandHandler.js';
5
+ export declare function isSoftwareUpdateBoot(env: Environment): boolean;
4
6
  export declare class MatterbridgeServer extends Behavior {
5
7
  static readonly id = "matterbridge";
6
8
  state: MatterbridgeServer.State;
@@ -1,4 +1,12 @@
1
- import { Behavior } from '@matter/node';
1
+ import { Behavior, ServerNode } from '@matter/node';
2
+ import { GeneralDiagnosticsBehavior } from '@matter/node/behaviors/general-diagnostics';
3
+ import { GeneralDiagnostics } from '@matter/types/clusters/general-diagnostics';
4
+ export function isSoftwareUpdateBoot(env) {
5
+ const rootEndpoint = env.get(ServerNode);
6
+ if (!rootEndpoint.behaviors.has(GeneralDiagnosticsBehavior))
7
+ return false;
8
+ return rootEndpoint.stateOf(GeneralDiagnosticsBehavior).bootReason === GeneralDiagnostics.BootReason.SoftwareUpdateCompleted;
9
+ }
2
10
  export class MatterbridgeServer extends Behavior {
3
11
  static id = 'matterbridge';
4
12
  initialize() {
@@ -1,3 +1,4 @@
1
+ import type { MaybePromise } from '@matter/general';
1
2
  import { OnOffServer } from '@matter/node/behaviors/on-off';
2
3
  import { OnOff } from '@matter/types/clusters/on-off';
3
4
  import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
@@ -16,6 +17,7 @@ declare const MatterbridgeOnOffServer_base: import("@matter/node").ClusterBehavi
16
17
  }>, import("@matter/types").ClusterType.Concrete, typeof import("@matter/node/behaviors/on-off").OnOffBaseServer.Internal, "onOff">;
17
18
  export declare class MatterbridgeOnOffServer extends MatterbridgeOnOffServer_base {
18
19
  readonly endpoint: MatterbridgeEndpoint;
20
+ initialize(): MaybePromise;
19
21
  on(): Promise<void>;
20
22
  off(): Promise<void>;
21
23
  toggle(): Promise<void>;
@@ -1,7 +1,20 @@
1
1
  import { OnOffServer } from '@matter/node/behaviors/on-off';
2
2
  import { OnOff } from '@matter/types/clusters/on-off';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeOnOffServer extends OnOffServer.with(OnOff.Feature.Lighting) {
5
+ initialize() {
6
+ const result = super.initialize();
7
+ if (this.features.lighting && !isSoftwareUpdateBoot(this.env)) {
8
+ const startUpOnOff = this.state.startUpOnOff ?? null;
9
+ if (startUpOnOff !== null) {
10
+ const currentOnOff = this.state.onOff;
11
+ const targetOnOff = startUpOnOff === OnOff.StartUpOnOff.Toggle ? !currentOnOff : startUpOnOff === OnOff.StartUpOnOff.On;
12
+ if (targetOnOff !== currentOnOff)
13
+ this.state.onOff = targetOnOff;
14
+ }
15
+ }
16
+ return result;
17
+ }
5
18
  async on() {
6
19
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
7
20
  await super.on();
@@ -1,3 +1,4 @@
1
+ export declare const TEST_SNAPSHOT_SOURCE = "test";
1
2
  export interface SnapshotRequest {
2
3
  src: string;
3
4
  width: number;
@@ -7,6 +7,7 @@ const MAX_BYTES = 64000;
7
7
  const QUALITIES = [6, 10, 14, 18, 24, 31];
8
8
  const FALLBACK_WIDTH = 640;
9
9
  const WEBCAM_WARMUP_SECONDS = 1;
10
+ export const TEST_SNAPSHOT_SOURCE = 'test';
10
11
  const CAPTURE_TIMEOUT_MS = 10_000;
11
12
  const TERMINATION_GRACE_MS = 1_000;
12
13
  const log = new AnsiLogger({ logName: 'Snapshot', logLevel: "debug", logNameColor: MAGENTA, logTimestampFormat: 4 });
@@ -56,6 +57,10 @@ export async function captureSnapshot(request) {
56
57
  throw new Error(`Snapshot capture from ${redactedSrc} exceeds ${maxBytes}B at minimum quality/resolution (${last.length}B at quality ${lastQuality})`);
57
58
  }
58
59
  function buildInputArgs(src, width, height) {
60
+ if (src === TEST_SNAPSHOT_SOURCE) {
61
+ const size = height === -1 ? '' : `=size=${width}x${height}`;
62
+ return ['-f', 'lavfi', '-i', `testsrc${size}`];
63
+ }
59
64
  if (src.startsWith('rtsp://')) {
60
65
  return ['-rtsp_transport', 'tcp', '-i', src];
61
66
  }
@@ -24,6 +24,7 @@ export declare namespace MatterbridgeWebRtcTransportProviderServer {
24
24
  class Internal {
25
25
  nextWebRtcSessionId: number;
26
26
  sessions: Map<number, WeriftWebRtcSession>;
27
+ trickledSessions: Set<number>;
27
28
  }
28
29
  }
29
30
  export declare function createDefaultWebRtcTransportProviderClusterServer(endpoint: MatterbridgeEndpoint, weriftOfferOptions?: WeriftOfferOptions): MatterbridgeEndpoint;