@matterbridge/core 3.10.10-dev-20260912-8fddbe4 → 3.10.10-dev-20260915-57739b4

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.
@@ -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
  }
@@ -328,7 +324,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
328
324
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated snapshot stream ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
329
325
  this.endpoint.emitCommand(CameraAvStreamManagement, 'snapshotStreamDeallocate', request, this.context);
330
326
  }
331
- captureSnapshot(request) {
327
+ #resolveSnapshotSource() {
328
+ const endpoint = `(endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
329
+ const fail = (reason) => {
330
+ this.endpoint.stateOf(MatterbridgeServer).log.error(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`);
331
+ return new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`, Status.Failure);
332
+ };
333
+ const sourceFor = (source, device, sourceOption, deviceOption) => {
334
+ if (source === 'test')
335
+ return TEST_SNAPSHOT_SOURCE;
336
+ if (device)
337
+ return device;
338
+ throw fail(`${sourceOption}=${source} requires ${deviceOption} to be set`);
339
+ };
340
+ const { snapshotSource, snapshotSourceDevice } = this.state;
341
+ if (snapshotSource !== undefined)
342
+ return sourceFor(snapshotSource, snapshotSourceDevice, 'snapshotSource', 'snapshotSourceDevice');
343
+ if (!this.endpoint.behaviors.has(WebRtcTransportProviderServer)) {
344
+ throw fail('no snapshotSource is configured and the endpoint has no WebRtcTransportProvider cluster to read the video source from');
345
+ }
346
+ const state = this.endpoint.stateOf(WebRtcTransportProviderServer);
347
+ if (!hasWeriftOfferOptions(state))
348
+ throw fail('the WebRtcTransportProvider cluster state has no weriftOfferOptions');
349
+ const { videoSource, videoSourceDevice } = state.weriftOfferOptions;
350
+ if (videoSource === 'none')
351
+ throw fail(`weriftOfferOptions.videoSource=${videoSource} provides no video source`);
352
+ return sourceFor(videoSource, videoSourceDevice, 'weriftOfferOptions.videoSource', 'videoSourceDevice');
353
+ }
354
+ async captureSnapshot(request) {
332
355
  const device = this.endpoint.stateOf(MatterbridgeServer);
333
356
  const { snapshotStreamId } = request;
334
357
  const stream = this.state.allocatedSnapshotStreams.find((s) => snapshotStreamId === null || s.snapshotStreamId === snapshotStreamId);
@@ -337,15 +360,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
337
360
  }
338
361
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: capturing snapshot ${snapshotStreamId ?? 'auto'} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
339
362
  device.log.debug(`MatterbridgeCameraAvStreamManagementServer: captureSnapshot called with snapshotStreamId ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
340
- const { data, resolution } = cameraColorTestJpegForResolution(request.requestedResolution);
363
+ const src = this.#resolveSnapshotSource();
364
+ const { width, height } = request.requestedResolution;
365
+ let captured;
366
+ try {
367
+ captured = await captureSnapshot({ src, width, height });
368
+ }
369
+ catch (error) {
370
+ const message = `MatterbridgeCameraAvStreamManagementServer: snapshot capture failed: ${error instanceof Error ? error.message : String(error)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
371
+ device.log.error(message);
372
+ throw new StatusResponseError(message, Status.Failure);
373
+ }
374
+ const resolution = { width: captured.width, height: captured.height === -1 ? Math.round((captured.width * height) / width) : captured.height };
375
+ 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
376
  this.endpoint.emitCommand(CameraAvStreamManagement, 'captureSnapshot', request, this.context);
342
377
  return {
343
- data,
378
+ data: captured.data,
344
379
  imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg,
345
380
  resolution,
346
381
  };
347
382
  }
348
383
  }
384
+ (function (MatterbridgeCameraAvStreamManagementServer) {
385
+ class State extends MatterbridgeCameraAvStreamManagementServerBase.State {
386
+ snapshotSource = undefined;
387
+ snapshotSourceDevice = undefined;
388
+ }
389
+ MatterbridgeCameraAvStreamManagementServer.State = State;
390
+ })(MatterbridgeCameraAvStreamManagementServer || (MatterbridgeCameraAvStreamManagementServer = {}));
349
391
  export function createDefaultCameraAvStreamManagementClusterServer(endpoint, options) {
350
392
  endpoint.behaviors.require(MatterbridgeCameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl), {
351
393
  ...options,
@@ -408,6 +450,8 @@ export function createDefaultSnapshotCameraAvStreamManagementClusterServer(endpo
408
450
  maxEncodedPixelRate: options.maxEncodedPixelRate,
409
451
  snapshotCapabilities: options.snapshotCapabilities,
410
452
  allocatedSnapshotStreams: [],
453
+ snapshotSource: options.snapshotSource,
454
+ snapshotSourceDevice: options.snapshotSourceDevice,
411
455
  });
412
456
  return endpoint;
413
457
  }
@@ -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
  }
@@ -255,8 +255,8 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
255
255
  const webRtcPeer = new WeriftWebRtcSession(webRtcSessionId, this.state.weriftOfferOptions);
256
256
  this.internal.sessions.set(webRtcSessionId, webRtcPeer);
257
257
  const sdp = await webRtcPeer.createOffer({
258
- video: !!videoStreams?.length,
259
- audio: !!audioStreams?.length,
258
+ offerVideo: !!videoStreams?.length,
259
+ offerAudio: !!audioStreams?.length,
260
260
  videoResolution: this.state.weriftOfferOptions.videoResolution ?? this.#resolveVideoResolution(videoStreams),
261
261
  });
262
262
  if (requestorEndpoint) {
@@ -324,8 +324,8 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
324
324
  const webRtcPeer = this.internal.sessions.get(webRtcSessionId) ?? new WeriftWebRtcSession(webRtcSessionId, this.state.weriftOfferOptions);
325
325
  this.internal.sessions.set(webRtcSessionId, webRtcPeer);
326
326
  const sdp = await webRtcPeer.createAnswer(request.sdp, {
327
- video: !!session.videoStreams?.length,
328
- audio: !!session.audioStreams?.length,
327
+ offerVideo: !!session.videoStreams?.length,
328
+ offerAudio: !!session.audioStreams?.length,
329
329
  videoResolution: this.state.weriftOfferOptions.videoResolution ?? this.#resolveVideoResolution(session.videoStreams),
330
330
  });
331
331
  const requestorEndpoint = await this.#resolvePeerRequestorEndpoint(session.peerNodeId, session.fabricIndex, session.peerEndpointId);
@@ -407,7 +407,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
407
407
  }
408
408
  (function (MatterbridgeWebRtcTransportProviderServer) {
409
409
  class State extends WebRtcTransportProviderServer.State {
410
- weriftOfferOptions = { video: true, audio: true, videoSource: 'none', audioSource: 'none' };
410
+ weriftOfferOptions = { offerVideo: true, offerAudio: true, videoSource: 'none', audioSource: 'none' };
411
411
  }
412
412
  MatterbridgeWebRtcTransportProviderServer.State = State;
413
413
  class Internal {
@@ -416,7 +416,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
416
416
  }
417
417
  MatterbridgeWebRtcTransportProviderServer.Internal = Internal;
418
418
  })(MatterbridgeWebRtcTransportProviderServer || (MatterbridgeWebRtcTransportProviderServer = {}));
419
- export function createDefaultWebRtcTransportProviderClusterServer(endpoint, weriftOfferOptions = { video: true, audio: true, videoSource: 'none', audioSource: 'none' }) {
419
+ export function createDefaultWebRtcTransportProviderClusterServer(endpoint, weriftOfferOptions = { offerVideo: true, offerAudio: true, videoSource: 'none', audioSource: 'none' }) {
420
420
  endpoint.behaviors.require(MatterbridgeWebRtcTransportProviderServer, { currentSessions: [], weriftOfferOptions });
421
421
  return endpoint;
422
422
  }
@@ -2,8 +2,8 @@ import { RTCPeerConnection } from 'werift';
2
2
  type VideoSource = 'none' | 'test' | 'webcam' | 'rtsp';
3
3
  type AudioSource = 'none' | 'test' | 'microphone' | 'rtsp';
4
4
  export interface WeriftOfferOptions {
5
- video: boolean;
6
- audio: boolean;
5
+ offerVideo: boolean;
6
+ offerAudio: boolean;
7
7
  videoSource: VideoSource;
8
8
  videoSourceDevice?: string;
9
9
  videoResolution?: string;
@@ -29,6 +29,8 @@ export declare class WeriftWebRtcSession {
29
29
  private static readonly DIAGNOSTICS_INTERVAL_MS;
30
30
  private lastDiagnosticsCounters?;
31
31
  private readonly dtlsTransportsWithStateLogging;
32
+ private static parseIcePortRange;
33
+ private static parseIceAdditionalHostAddresses;
32
34
  constructor(webRtcSessionId: number, options: WeriftOfferOptions);
33
35
  private summarizeSdp;
34
36
  private getFreeUdpPort;
@@ -40,10 +42,11 @@ export declare class WeriftWebRtcSession {
40
42
  private getConfiguredVideoResolution;
41
43
  private getConfiguredVideoSource;
42
44
  private buildFfmpegVideoInputArgs;
45
+ private logFfmpegStderr;
43
46
  private generateVideoTrack;
44
47
  private preferVideoCodecOnTransceivers;
45
48
  private preferAudioCodecOnTransceivers;
46
- private static readonly TEST_VOICE_PATH;
49
+ private static readonly TEST_TONE_INPUT;
47
50
  private getConfiguredAudioSource;
48
51
  private buildFfmpegAudioInputArgs;
49
52
  private generateAudioTrack;
@@ -1,5 +1,4 @@
1
1
  import { createSocket } from 'node:dgram';
2
- import { fileURLToPath } from 'node:url';
3
2
  import { fireAndForget, getErrorMessage } from '@matterbridge/utils';
4
3
  import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
5
4
  import { RTCPeerConnection, RTCRtpCodecParameters, useH264, useOPUS, usePCMU, useVP8 } from 'werift';
@@ -22,11 +21,35 @@ export class WeriftWebRtcSession {
22
21
  static DIAGNOSTICS_INTERVAL_MS = 10_000;
23
22
  lastDiagnosticsCounters;
24
23
  dtlsTransportsWithStateLogging = new Set();
24
+ static parseIcePortRange() {
25
+ const raw = process.env.MATTERBRIDGE_ICE_PORT_RANGE?.trim();
26
+ if (!raw)
27
+ return undefined;
28
+ const match = /^(\d{1,5})-(\d{1,5})$/.exec(raw);
29
+ if (!match)
30
+ return undefined;
31
+ const min = Number(match[1]);
32
+ const max = Number(match[2]);
33
+ if (min < 1024 || max > 65_535 || min >= max)
34
+ return undefined;
35
+ return [min, max];
36
+ }
37
+ static parseIceAdditionalHostAddresses() {
38
+ const addresses = process.env.MATTERBRIDGE_ICE_HOST_ADDRESSES?.split(',')
39
+ .map((address) => address.trim())
40
+ .filter((address) => address.length > 0) ?? [];
41
+ return addresses.length > 0 ? addresses : undefined;
42
+ }
25
43
  constructor(webRtcSessionId, options) {
26
44
  this.webRtcSessionId = webRtcSessionId;
27
45
  this.options = { ...options };
28
- this.peerConnection = new RTCPeerConnection({ codecs: { audio: [useOPUS(), usePCMU()], video: [useH264(), useVP8()] } });
46
+ const icePortRange = WeriftWebRtcSession.parseIcePortRange();
47
+ const iceAdditionalHostAddresses = WeriftWebRtcSession.parseIceAdditionalHostAddresses();
48
+ this.peerConnection = new RTCPeerConnection({ codecs: { audio: [useOPUS(), usePCMU()], video: [useH264(), useVP8()] }, icePortRange, iceAdditionalHostAddresses });
29
49
  this.log = new AnsiLogger({ logName: `WebRTC session ${webRtcSessionId}`, logLevel: "debug", logNameColor: MAGENTA, logTimestampFormat: 4 });
50
+ if (icePortRange || iceAdditionalHostAddresses) {
51
+ this.log.debug(`ICE overrides: portRange=${icePortRange ? `${icePortRange[0]}-${icePortRange[1]}` : 'default'} additionalHostAddresses=${iceAdditionalHostAddresses?.join(', ') ?? 'none'}`);
52
+ }
30
53
  this.peerConnection.iceGatheringStateChange.subscribe((state) => {
31
54
  this.log.info(`ICE gathering state: ${state}`);
32
55
  });
@@ -154,19 +177,19 @@ export class WeriftWebRtcSession {
154
177
  }
155
178
  }
156
179
  buildFfmpegVideoInputArgs(videoSource) {
157
- const testPatternInput = {
158
- args: ['-re', '-f', 'lavfi', '-i', 'testsrc=size=640x480:rate=10'],
159
- description: 'synthetic moving test pattern',
160
- bitrateKbps: WeriftWebRtcSession.DEFAULT_BITRATE_KBPS,
161
- };
162
180
  if (videoSource === 'test') {
181
+ const testPatternInput = {
182
+ args: ['-re', '-f', 'lavfi', '-i', 'testsrc=size=640x480:rate=10'],
183
+ description: 'synthetic moving test pattern',
184
+ bitrateKbps: WeriftWebRtcSession.DEFAULT_BITRATE_KBPS,
185
+ };
163
186
  this.log.debug(`Test pattern params: resolution=640x480, description="${testPatternInput.description}", bitrateKbps=${testPatternInput.bitrateKbps}`);
164
187
  return testPatternInput;
165
188
  }
166
189
  const device = this.options.videoSourceDevice;
167
190
  if (!device) {
168
- this.log.warn(`options.videoSource=${videoSource} requires options.videoSourceDevice to be set; falling back to the synthetic test video`);
169
- return testPatternInput;
191
+ this.log.error(`options.videoSource=${videoSource} requires options.videoSourceDevice to be set; not injecting a video track`);
192
+ return undefined;
170
193
  }
171
194
  const resolution = this.getConfiguredVideoResolution();
172
195
  const bitrateKbps = this.getConfiguredVideoBitrate();
@@ -185,10 +208,17 @@ export class WeriftWebRtcSession {
185
208
  case 'win32':
186
209
  return { args: ['-f', 'dshow', '-video_size', resolution, '-framerate', '30', '-i', `video=${device}`], description, bitrateKbps };
187
210
  default:
188
- this.log.warn(`Webcam capture via ffmpeg is not supported on platform "${process.platform}"; falling back to the synthetic test video`);
189
- return testPatternInput;
211
+ this.log.error(`Webcam capture via ffmpeg is not supported on platform "${process.platform}"; not injecting a video track`);
212
+ return undefined;
190
213
  }
191
214
  }
215
+ logFfmpegStderr(generator, kind) {
216
+ generator.stderr?.on('data', (chunk) => {
217
+ const message = chunk.toString().trim();
218
+ if (message)
219
+ this.log.debug(`Ffmpeg ${kind} generator: ${message}`);
220
+ });
221
+ }
192
222
  async generateVideoTrack(codec) {
193
223
  if (this.testVideoAttached)
194
224
  return;
@@ -198,6 +228,8 @@ export class WeriftWebRtcSession {
198
228
  return;
199
229
  }
200
230
  const videoInput = this.buildFfmpegVideoInputArgs(videoSource);
231
+ if (!videoInput)
232
+ return;
201
233
  this.log.debug(`Attempting to attach ${videoInput.description} video track at ${videoInput.bitrateKbps}kbps`);
202
234
  if (!hasFfmpeg()) {
203
235
  this.log.warn('Cannot inject video stream: missing dependency ffmpeg');
@@ -230,6 +262,7 @@ export class WeriftWebRtcSession {
230
262
  `rtp://127.0.0.1:${udpPort}`,
231
263
  ];
232
264
  const generator = runFfmpeg(ffmpegArgs);
265
+ this.logFfmpegStderr(generator, 'video');
233
266
  generator.once('error', (error) => {
234
267
  this.log.warn(`Video generator failed: ${getErrorMessage(error)}`);
235
268
  });
@@ -272,7 +305,7 @@ export class WeriftWebRtcSession {
272
305
  this.log.debug(`Preferred ${mimeType.toUpperCase()} codecs on ${adjustedTransceivers} audio transceiver(s)`);
273
306
  }
274
307
  }
275
- static TEST_VOICE_PATH = fileURLToPath(new URL('../../assets/test-voice.opus', import.meta.url));
308
+ static TEST_TONE_INPUT = 'sine=frequency=440:beep_factor=4:sample_rate=48000,volume=6dB';
276
309
  getConfiguredAudioSource() {
277
310
  const source = this.options.audioSource?.trim().toLowerCase() ?? 'none';
278
311
  switch (source) {
@@ -287,17 +320,13 @@ export class WeriftWebRtcSession {
287
320
  }
288
321
  }
289
322
  buildFfmpegAudioInputArgs(audioSource) {
290
- const testVoiceInput = {
291
- args: ['-re', '-stream_loop', '-1', '-i', WeriftWebRtcSession.TEST_VOICE_PATH],
292
- description: 'recorded test-voice clip',
293
- volumeFilter: 'volume=6dB',
294
- };
295
- if (audioSource === 'test')
296
- return testVoiceInput;
323
+ if (audioSource === 'test') {
324
+ return { args: ['-re', '-f', 'lavfi', '-i', WeriftWebRtcSession.TEST_TONE_INPUT], description: 'synthetic test tone' };
325
+ }
297
326
  const device = this.options.audioSourceDevice;
298
327
  if (!device) {
299
- this.log.warn(`options.audioSource=${audioSource} requires options.audioSourceDevice to be set; falling back to the recorded test-voice clip`);
300
- return testVoiceInput;
328
+ this.log.error(`options.audioSource=${audioSource} requires options.audioSourceDevice to be set; not injecting an audio track`);
329
+ return undefined;
301
330
  }
302
331
  if (audioSource === 'rtsp') {
303
332
  const description = `RTSP camera audio (${device})`;
@@ -314,8 +343,8 @@ export class WeriftWebRtcSession {
314
343
  case 'win32':
315
344
  return { args: ['-f', 'dshow', '-i', `audio=${device}`], description };
316
345
  default:
317
- this.log.warn(`Microphone capture via ffmpeg is not supported on platform "${process.platform}"; falling back to the recorded test-voice clip`);
318
- return testVoiceInput;
346
+ this.log.error(`Microphone capture via ffmpeg is not supported on platform "${process.platform}"; not injecting an audio track`);
347
+ return undefined;
319
348
  }
320
349
  }
321
350
  async generateAudioTrack(codec) {
@@ -327,6 +356,8 @@ export class WeriftWebRtcSession {
327
356
  return;
328
357
  }
329
358
  const audioInput = this.buildFfmpegAudioInputArgs(audioSource);
359
+ if (!audioInput)
360
+ return;
330
361
  this.log.debug(`Attempting to attach ${audioInput.description} audio track`);
331
362
  if (!hasFfmpeg()) {
332
363
  this.log.warn('Cannot inject audio stream: missing dependency ffmpeg');
@@ -349,13 +380,12 @@ export class WeriftWebRtcSession {
349
380
  'error',
350
381
  ...audioInput.args,
351
382
  '-vn',
352
- ...(audioInput.volumeFilter ? ['-af', audioInput.volumeFilter] : []),
353
383
  '-c:a',
354
384
  'libopus',
355
385
  '-b:a',
356
386
  '32k',
357
387
  '-ac',
358
- String(channels),
388
+ '1',
359
389
  '-ar',
360
390
  String(clockRate),
361
391
  '-f',
@@ -365,6 +395,7 @@ export class WeriftWebRtcSession {
365
395
  `rtp://127.0.0.1:${udpPort}`,
366
396
  ];
367
397
  const generator = runFfmpeg(ffmpegArgs);
398
+ this.logFfmpegStderr(generator, 'audio');
368
399
  generator.once('error', (error) => {
369
400
  this.log.warn(`Audio generator failed: ${getErrorMessage(error)}`);
370
401
  });
@@ -403,8 +434,8 @@ export class WeriftWebRtcSession {
403
434
  async createOffer(overrides) {
404
435
  this.applyOptionOverrides(overrides);
405
436
  const options = this.options;
406
- this.log.debug(`CreateOffer requested (video=${options.video}, audio=${options.audio}, videoResolution=${options.videoResolution ?? 'undefined'})`);
407
- if (options.video) {
437
+ this.log.debug(`CreateOffer requested (offerVideo=${options.offerVideo}, offerAudio=${options.offerAudio}, videoResolution=${options.videoResolution ?? 'undefined'})`);
438
+ if (options.offerVideo) {
408
439
  const preferredCodec = this.getPreferredInjectableVideoCodec();
409
440
  if (preferredCodec) {
410
441
  this.preferVideoCodecOnTransceivers(preferredCodec.mimeType.toLowerCase());
@@ -416,7 +447,7 @@ export class WeriftWebRtcSession {
416
447
  if (!this.testVideoAttached)
417
448
  this.peerConnection.addTransceiver('video', { direction: 'sendonly' });
418
449
  }
419
- if (options.audio)
450
+ if (options.offerAudio)
420
451
  this.peerConnection.addTransceiver('audio', { direction: 'sendonly' });
421
452
  const offer = await this.peerConnection.createOffer();
422
453
  await this.peerConnection.setLocalDescription(offer);
package/dist/chipTests.js CHANGED
@@ -21,11 +21,14 @@ import { RefrigeratorAlarm } from '@matter/types/clusters/refrigerator-alarm';
21
21
  import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
22
22
  import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
23
23
  import { SmokeCoAlarm } from '@matter/types/clusters/smoke-co-alarm';
24
+ import { Switch } from '@matter/types/clusters/switch';
24
25
  import { TariffPriceType, TariffUnit } from '@matter/types/globals';
26
+ import { wait } from '@matterbridge/utils/wait';
25
27
  import { MatterbridgeOccupancySensingServer } from './behaviors/occupancySensingServer.js';
26
28
  import { cliEmitter } from './cliEmitter.js';
27
29
  import { MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer } from './devices/roboticVacuumCleaner.js';
28
30
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
31
+ import { featuresFor } from './matterbridgeEndpointHelpers.js';
29
32
  const chipTestAppPipePath = '/tmp/matterbridge-chip-test-app-pipe';
30
33
  const chipTestValidEventTrigger = 0xfffffffffff10000n;
31
34
  const smokeCoAlarmWarningSmokeAlarmTrigger = 0x005c000000000090n;
@@ -1097,10 +1100,87 @@ async function handleChipTestAppPipeCommand(matterbridge, command) {
1097
1100
  }
1098
1101
  matterbridge.log.info(`CHIP test app pipe OperationalStateChange(${command.Operation}${command.Param === undefined ? '' : `, ${command.Param}`}) applied on endpoint ${endpointId}`);
1099
1102
  return;
1103
+ case 'SimulateSwitchIdle':
1104
+ await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
1105
+ matterbridge.log.info(`CHIP test app pipe set Switch.CurrentPosition to 0 (idle) on endpoint ${endpointId}`);
1106
+ return;
1107
+ case 'SimulateLongPress':
1108
+ await simulateChipTestSwitchLongPress(matterbridge, endpoint, endpointId, command);
1109
+ return;
1110
+ case 'SimulateLatchPosition': {
1111
+ const newPosition = command.PositionId ?? 0;
1112
+ if (endpoint.getAttribute(Switch.id, 'currentPosition', matterbridge.log) === newPosition) {
1113
+ matterbridge.log.info(`CHIP test app pipe left Switch latched at position ${newPosition} on endpoint ${endpointId}`);
1114
+ return;
1115
+ }
1116
+ await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
1117
+ await endpoint.triggerEvent(Switch.id, 'switchLatched', { newPosition }, matterbridge.log);
1118
+ matterbridge.log.info(`CHIP test app pipe latched Switch to position ${newPosition} on endpoint ${endpointId}`);
1119
+ return;
1120
+ }
1121
+ case 'SimulateMultiPress':
1122
+ await simulateChipTestSwitchMultiPress(matterbridge, endpoint, endpointId, command);
1123
+ return;
1100
1124
  default:
1101
1125
  matterbridge.log.warn(`Ignoring unsupported CHIP test app pipe command: ${JSON.stringify(command)}`);
1102
1126
  }
1103
1127
  }
1128
+ async function simulateChipTestSwitchLongPress(matterbridge, endpoint, endpointId, command) {
1129
+ if (!endpoint.hasClusterServer(Switch.id)) {
1130
+ matterbridge.log.warn(`Ignoring SimulateLongPress CHIP test app pipe command on endpoint ${endpointId} without a Switch cluster server`);
1131
+ return;
1132
+ }
1133
+ const features = featuresFor(endpoint, Switch.id);
1134
+ const newPosition = command.ButtonId ?? 1;
1135
+ const delayMs = command.LongPressDelayMillis ?? 0;
1136
+ const durationMs = command.LongPressDurationMillis ?? 0;
1137
+ await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
1138
+ await endpoint.triggerEvent(Switch.id, 'initialPress', { newPosition }, matterbridge.log);
1139
+ matterbridge.log.info(`CHIP test app pipe pressed Switch position ${newPosition} on endpoint ${endpointId} for ${durationMs}ms`);
1140
+ if (features.momentarySwitchLongPress) {
1141
+ await wait(Math.min(delayMs, durationMs));
1142
+ await endpoint.triggerEvent(Switch.id, 'longPress', { newPosition }, matterbridge.log);
1143
+ await wait(Math.max(durationMs - delayMs, 0));
1144
+ }
1145
+ else {
1146
+ await wait(durationMs);
1147
+ }
1148
+ await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
1149
+ if (features.momentarySwitchLongPress) {
1150
+ await endpoint.triggerEvent(Switch.id, 'longRelease', { previousPosition: newPosition }, matterbridge.log);
1151
+ }
1152
+ else if (features.momentarySwitchRelease) {
1153
+ await endpoint.triggerEvent(Switch.id, 'shortRelease', { previousPosition: newPosition }, matterbridge.log);
1154
+ }
1155
+ matterbridge.log.info(`CHIP test app pipe released Switch position ${newPosition} on endpoint ${endpointId}`);
1156
+ }
1157
+ async function simulateChipTestSwitchMultiPress(matterbridge, endpoint, endpointId, command) {
1158
+ if (!endpoint.hasClusterServer(Switch.id)) {
1159
+ matterbridge.log.warn(`Ignoring SimulateMultiPress CHIP test app pipe command on endpoint ${endpointId} without a Switch cluster server`);
1160
+ return;
1161
+ }
1162
+ const features = featuresFor(endpoint, Switch.id);
1163
+ const newPosition = command.ButtonId ?? 1;
1164
+ const presses = Math.max(command.MultiPressNumPresses ?? 1, 1);
1165
+ const pressedMs = command.MultiPressPressedTimeMillis ?? 0;
1166
+ const releasedMs = command.MultiPressReleasedTimeMillis ?? 0;
1167
+ const multiPressMax = endpoint.getAttribute(Switch.id, 'multiPressMax', matterbridge.log) ?? command.MultiPressMax ?? presses;
1168
+ for (let press = 1; press <= presses; press++) {
1169
+ await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
1170
+ await endpoint.triggerEvent(Switch.id, 'initialPress', { newPosition }, matterbridge.log);
1171
+ if (press > 1) {
1172
+ await endpoint.triggerEvent(Switch.id, 'multiPressOngoing', { newPosition, currentNumberOfPressesCounted: Math.min(press, multiPressMax) }, matterbridge.log);
1173
+ }
1174
+ await wait(pressedMs);
1175
+ await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
1176
+ if (features.momentarySwitchRelease) {
1177
+ await endpoint.triggerEvent(Switch.id, 'shortRelease', { previousPosition: newPosition }, matterbridge.log);
1178
+ }
1179
+ await wait(releasedMs);
1180
+ }
1181
+ await endpoint.triggerEvent(Switch.id, 'multiPressComplete', { previousPosition: newPosition, totalNumberOfPressesCounted: Math.min(presses, multiPressMax) }, matterbridge.log);
1182
+ matterbridge.log.info(`CHIP test app pipe completed ${presses} press(es) of Switch position ${newPosition} on endpoint ${endpointId}`);
1183
+ }
1104
1184
  function getChipTestEndpoint(matterbridge, endpointId) {
1105
1185
  const aggregatorEndpoint = matterbridge.aggregatorNode;
1106
1186
  if (!aggregatorEndpoint)
@@ -1127,5 +1207,14 @@ function isChipTestAppPipeCommand(value) {
1127
1207
  (!('SoilMoistureValue' in value) || typeof value.SoilMoistureValue === 'number') &&
1128
1208
  (!('Device' in value) || typeof value.Device === 'string') &&
1129
1209
  (!('Operation' in value) || typeof value.Operation === 'string') &&
1130
- (!('Param' in value) || typeof value.Param === 'number'));
1210
+ (!('Param' in value) || typeof value.Param === 'number') &&
1211
+ (!('ButtonId' in value) || typeof value.ButtonId === 'number') &&
1212
+ (!('LongPressDelayMillis' in value) || typeof value.LongPressDelayMillis === 'number') &&
1213
+ (!('LongPressDurationMillis' in value) || typeof value.LongPressDurationMillis === 'number') &&
1214
+ (!('FeatureMap' in value) || typeof value.FeatureMap === 'number') &&
1215
+ (!('PositionId' in value) || typeof value.PositionId === 'number') &&
1216
+ (!('MultiPressPressedTimeMillis' in value) || typeof value.MultiPressPressedTimeMillis === 'number') &&
1217
+ (!('MultiPressReleasedTimeMillis' in value) || typeof value.MultiPressReleasedTimeMillis === 'number') &&
1218
+ (!('MultiPressNumPresses' in value) || typeof value.MultiPressNumPresses === 'number') &&
1219
+ (!('MultiPressMax' in value) || typeof value.MultiPressMax === 'number'));
1131
1220
  }
@@ -5,6 +5,7 @@ import { AirQuality } from '@matter/types/clusters/air-quality';
5
5
  import { ClosureDimension } from '@matter/types/clusters/closure-dimension';
6
6
  import { DoorLock } from '@matter/types/clusters/door-lock';
7
7
  import { FanControl } from '@matter/types/clusters/fan-control';
8
+ import { Identify } from '@matter/types/clusters/identify';
8
9
  import { PowerSource } from '@matter/types/clusters/power-source';
9
10
  import { PowerTopology } from '@matter/types/clusters/power-topology';
10
11
  import { ResourceMonitoring } from '@matter/types/clusters/resource-monitoring';
@@ -14,18 +15,25 @@ import { Thermostat } from '@matter/types/clusters/thermostat';
14
15
  import { EndpointNumber } from '@matter/types/datatype';
15
16
  import { getErrorMessage } from '@matterbridge/utils/error';
16
17
  import { AirConditioner } from './devices/airConditioner.js';
18
+ import { AudioDoorbell } from './devices/audioDoorbell.js';
17
19
  import { BasicVideoPlayer } from './devices/basicVideoPlayer.js';
18
20
  import { BatteryStorage } from './devices/batteryStorage.js';
21
+ import { Camera } from './devices/camera.js';
22
+ import { CameraController } from './devices/cameraController.js';
19
23
  import { CastingVideoClient } from './devices/castingVideoClient.js';
20
24
  import { CastingVideoPlayer } from './devices/castingVideoPlayer.js';
25
+ import { Chime } from './devices/chime.js';
21
26
  import { Closure } from './devices/closure.js';
22
27
  import { ContentApp } from './devices/contentApp.js';
23
28
  import { Cooktop } from './devices/cooktop.js';
24
29
  import { Dishwasher } from './devices/dishwasher.js';
30
+ import { Doorbell } from './devices/doorbell.js';
25
31
  import { ElectricalUtilityMeter } from './devices/electricalUtilityMeter.js';
26
32
  import { Evse } from './devices/evse.js';
27
33
  import { ExtractorHood } from './devices/extractorHood.js';
34
+ import { FloodlightCamera } from './devices/floodlightCamera.js';
28
35
  import { HeatPump } from './devices/heatPump.js';
36
+ import { Intercom } from './devices/intercom.js';
29
37
  import { IrrigationSystem } from './devices/irrigationSystem.js';
30
38
  import { LaundryDryer } from './devices/laundryDryer.js';
31
39
  import { LaundryWasher } from './devices/laundryWasher.js';
@@ -33,8 +41,10 @@ import { MicrowaveOven } from './devices/microwaveOven.js';
33
41
  import { Oven } from './devices/oven.js';
34
42
  import { Refrigerator } from './devices/refrigerator.js';
35
43
  import { RoboticVacuumCleaner } from './devices/roboticVacuumCleaner.js';
44
+ import { SnapshotCamera } from './devices/snapshotCamera.js';
36
45
  import { SolarPower } from './devices/solarPower.js';
37
46
  import { Speaker } from './devices/speaker.js';
47
+ import { VideoDoorbell } from './devices/videoDoorbell.js';
38
48
  import { VideoRemoteControl } from './devices/videoRemoteControl.js';
39
49
  import { WaterHeater } from './devices/waterHeater.js';
40
50
  import { getSupportedDeviceType } from './matterbridgeDeviceTypes.js';
@@ -95,6 +105,15 @@ const demoPluginSchema = {
95
105
  },
96
106
  },
97
107
  };
108
+ let demoPluginConfig = {
109
+ name: demoPluginName,
110
+ type: demoPluginType,
111
+ version: demoPluginVersion,
112
+ debug: false,
113
+ unregisterOnShutdown: false,
114
+ whiteList: [],
115
+ blackList: [],
116
+ };
98
117
  export async function createDemoDevices(matterbridge) {
99
118
  if (matterbridge.bridgeMode !== 'bridge') {
100
119
  matterbridge.log.error('Demo devices can only be created in bridge mode');
@@ -108,20 +127,10 @@ export async function createDemoDevices(matterbridge) {
108
127
  }
109
128
  const configFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.config.json`);
110
129
  const schemaFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.schema.json`);
111
- const defaultConfig = {
112
- name: demoPluginName,
113
- type: demoPluginType,
114
- version: demoPluginVersion,
115
- debug: false,
116
- unregisterOnShutdown: false,
117
- whiteList: [],
118
- blackList: [],
119
- };
120
- let config = defaultConfig;
121
130
  try {
122
131
  const storedConfig = JSON.parse(await readFile(configFile, 'utf8'));
123
- config = {
124
- ...defaultConfig,
132
+ demoPluginConfig = {
133
+ ...demoPluginConfig,
125
134
  ...storedConfig,
126
135
  name: demoPluginName,
127
136
  type: demoPluginType,
@@ -135,7 +144,7 @@ export async function createDemoDevices(matterbridge) {
135
144
  matterbridge.log.error(`Failed to read demo devices config ${configFile}: ${getErrorMessage(error)}`);
136
145
  }
137
146
  try {
138
- await writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
147
+ await writeFile(configFile, JSON.stringify(demoPluginConfig, null, 2), 'utf8');
139
148
  await writeFile(schemaFile, JSON.stringify(demoPluginSchema, null, 2), 'utf8');
140
149
  }
141
150
  catch (error) {
@@ -152,14 +161,14 @@ export async function createDemoDevices(matterbridge) {
152
161
  enabled: false,
153
162
  private: true,
154
163
  registeredDevices: 0,
155
- configJson: config,
164
+ configJson: demoPluginConfig,
156
165
  schemaJson: demoPluginSchema,
157
166
  hasWhiteList: true,
158
167
  hasBlackList: true,
159
168
  });
160
169
  const registerDevice = async (device, deviceName, serialNumber) => {
161
- const whiteList = config.whiteList;
162
- const blackList = config.blackList;
170
+ const whiteList = demoPluginConfig.whiteList;
171
+ const blackList = demoPluginConfig.blackList;
163
172
  if (blackList.includes(deviceName)) {
164
173
  matterbridge.log.info(`Skipping demo device ${deviceName} because it is in the blacklist`);
165
174
  return;
@@ -267,34 +276,42 @@ export async function createDemoDevices(matterbridge) {
267
276
  ep = new MatterbridgeEndpoint([getSupportedDeviceType('PumpController'), bridgedNode, powerSource], { id: 'PumpController', number: EndpointNumber(6_05) });
268
277
  ep.createDefaultPowerSourceWiredClusterServer();
269
278
  await registerDevice(ep, 'Pump Controller', 'SWITCH-06-05');
270
- ep = new MatterbridgeEndpoint([getSupportedDeviceType('Aggregator'), bridgedNode, powerSource], { id: 'GenericSwitch', number: EndpointNumber(6_06) });
279
+ ep = new MatterbridgeEndpoint([getSupportedDeviceType('GenericSwitch'), bridgedNode, powerSource], { id: 'MomentarySwitch', number: EndpointNumber(6_06) });
280
+ ep.createDefaultPowerSourceBatteryClusterServer();
281
+ ep.createDefaultSwitchClusterServer();
282
+ await registerDevice(ep, 'Momentary Switch', 'SWITCH-06-06');
283
+ ep = new MatterbridgeEndpoint([getSupportedDeviceType('GenericSwitch'), bridgedNode, powerSource], { id: 'LatchingSwitch', number: EndpointNumber(6_06_1) });
284
+ ep.createDefaultPowerSourceBatteryClusterServer();
285
+ ep.createDefaultLatchingSwitchClusterServer();
286
+ await registerDevice(ep, 'Latching Switch', 'SWITCH-06-06-1');
287
+ ep = new MatterbridgeEndpoint([getSupportedDeviceType('Aggregator'), bridgedNode, powerSource], { id: 'GenericSwitch', number: EndpointNumber(6_06_2) });
271
288
  ep.createDefaultPowerSourceBatteryClusterServer();
272
289
  await ep.addFixedLabel('composed', 'GenericSwitch');
273
290
  ep.addChildDeviceType('Button1', getSupportedDeviceType('GenericSwitch'), {
274
- number: EndpointNumber(6_06_1),
291
+ number: EndpointNumber(6_06_3),
275
292
  tagList: [getSemtag(SwitchesTag.On), getSemtag(CommonNumberTag.One)],
276
293
  })
277
294
  .createDefaultMomentarySwitchClusterServer()
278
295
  .addRequiredClusters();
279
296
  ep.addChildDeviceType('Button2', getSupportedDeviceType('GenericSwitch'), {
280
- number: EndpointNumber(6_06_2),
297
+ number: EndpointNumber(6_06_4),
281
298
  tagList: [getSemtag(SwitchesTag.Off), getSemtag(CommonNumberTag.Two)],
282
299
  })
283
300
  .createDefaultMomentarySwitchClusterServer()
284
301
  .addRequiredClusters();
285
302
  ep.addChildDeviceType('Button3', getSupportedDeviceType('GenericSwitch'), {
286
- number: EndpointNumber(6_06_3),
303
+ number: EndpointNumber(6_06_5),
287
304
  tagList: [getSemtag(SwitchesTag.Up), getSemtag(CommonNumberTag.Three)],
288
305
  })
289
306
  .createDefaultMomentarySwitchClusterServer()
290
307
  .addRequiredClusters();
291
308
  ep.addChildDeviceType('Button4', getSupportedDeviceType('GenericSwitch'), {
292
- number: EndpointNumber(6_06_4),
309
+ number: EndpointNumber(6_06_6),
293
310
  tagList: [getSemtag(SwitchesTag.Down), getSemtag(CommonNumberTag.Four)],
294
311
  })
295
312
  .createDefaultMomentarySwitchClusterServer()
296
313
  .addRequiredClusters();
297
- await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06');
314
+ await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06-2');
298
315
  ep = new MatterbridgeEndpoint([getSupportedDeviceType('ContactSensor'), bridgedNode, powerSource], { id: 'ContactSensor', number: EndpointNumber(7_01) });
299
316
  ep.createDefaultPowerSourceBatteryClusterServer();
300
317
  ep.createDefaultBooleanStateClusterServer();
@@ -823,4 +840,83 @@ export async function createDemoDevices(matterbridge) {
823
840
  tagList: [getSemtag(ElectricalMeasurementTag.Ac), getSemtag(PowerSourceTag.Ev), getSemtag(CommodityTariffFlowTag.Import), getSemtag(CommodityTariffChronologyTag.Upcoming)],
824
841
  });
825
842
  await registerDevice(electricalUtilityMeterEvEndpoint, 'Electrical Utility Meter Ev', 'ENERGY-14-10');
843
+ ep = new Camera('Camera', 'CAMERA-16-01', {
844
+ id: 'Camera',
845
+ number: EndpointNumber(16_01),
846
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
847
+ });
848
+ await registerDevice(ep, 'Camera', 'CAMERA-16-01');
849
+ ep = new Camera('Camera Ptz', 'CAMERA-16-01-1', {
850
+ id: 'CameraPtz',
851
+ number: EndpointNumber(16_01_1),
852
+ ptz: true,
853
+ identifyType: Identify.IdentifyType.VisibleIndicator,
854
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
855
+ });
856
+ await registerDevice(ep, 'Camera Ptz', 'CAMERA-16-01-1');
857
+ ep = new FloodlightCamera('Floodlight Camera', 'CAMERA-16-02', {
858
+ id: 'FloodlightCamera',
859
+ number: EndpointNumber(16_02),
860
+ cameraOptions: {
861
+ number: EndpointNumber(16_02_1),
862
+ ptz: true,
863
+ identifyType: Identify.IdentifyType.VisibleIndicator,
864
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
865
+ },
866
+ lightOptions: { number: EndpointNumber(16_02_2) },
867
+ });
868
+ await registerDevice(ep, 'Floodlight Camera', 'CAMERA-16-02');
869
+ ep = new VideoDoorbell('Video Doorbell', 'CAMERA-16-03', {
870
+ id: 'VideoDoorbell',
871
+ number: EndpointNumber(16_03),
872
+ cameraOptions: {
873
+ number: EndpointNumber(16_03_1),
874
+ ptz: true,
875
+ identifyType: Identify.IdentifyType.VisibleIndicator,
876
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
877
+ },
878
+ doorbellOptions: { number: EndpointNumber(16_03_2) },
879
+ });
880
+ await registerDevice(ep, 'Video Doorbell', 'CAMERA-16-03');
881
+ ep = new Intercom('Intercom I', 'CAMERA-16-04', {
882
+ id: 'Intercom',
883
+ number: EndpointNumber(16_04),
884
+ identifyType: Identify.IdentifyType.VisibleIndicator,
885
+ weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
886
+ });
887
+ await registerDevice(ep, 'Intercom I', 'CAMERA-16-04');
888
+ ep = new Intercom('Intercom II', 'CAMERA-16-04-1', {
889
+ id: 'IntercomII',
890
+ number: EndpointNumber(16_04_1),
891
+ identifyType: Identify.IdentifyType.VisibleIndicator,
892
+ weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
893
+ });
894
+ await registerDevice(ep, 'Intercom II', 'CAMERA-16-04-1');
895
+ ep = new AudioDoorbell('Audio Doorbell', 'CAMERA-16-05', {
896
+ id: 'AudioDoorbell',
897
+ number: EndpointNumber(16_05),
898
+ weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
899
+ });
900
+ await registerDevice(ep, 'Audio Doorbell', 'CAMERA-16-05');
901
+ ep = new SnapshotCamera('Snapshot Camera', 'CAMERA-16-06', {
902
+ id: 'SnapshotCamera',
903
+ number: EndpointNumber(16_06),
904
+ identifyType: Identify.IdentifyType.VisibleIndicator,
905
+ snapshotSource: 'test',
906
+ });
907
+ await registerDevice(ep, 'Snapshot Camera', 'CAMERA-16-06');
908
+ ep = new Chime('Chime', 'CAMERA-16-07', {
909
+ id: 'Chime',
910
+ number: EndpointNumber(16_07),
911
+ identifyType: Identify.IdentifyType.AudibleBeep,
912
+ installedChimeSounds: [
913
+ { chimeId: 0, name: 'Default Chime' },
914
+ { chimeId: 1, name: 'Westminster' },
915
+ ],
916
+ });
917
+ await registerDevice(ep, 'Chime', 'CAMERA-16-07');
918
+ ep = new CameraController('Camera Controller', 'CAMERA-16-08', { id: 'CameraController', number: EndpointNumber(16_08) });
919
+ await registerDevice(ep, 'Camera Controller', 'CAMERA-16-08');
920
+ ep = new Doorbell('Doorbell', 'CAMERA-16-09', { id: 'Doorbell', number: EndpointNumber(16_09) });
921
+ await registerDevice(ep, 'Doorbell', 'CAMERA-16-09');
826
922
  }
@@ -0,0 +1,9 @@
1
+ export * from '../audioDoorbell.js';
2
+ export * from '../camera.js';
3
+ export * from '../cameraController.js';
4
+ export * from '../chime.js';
5
+ export * from '../doorbell.js';
6
+ export * from '../floodlightCamera.js';
7
+ export * from '../intercom.js';
8
+ export * from '../snapshotCamera.js';
9
+ export * from '../videoDoorbell.js';
@@ -0,0 +1,9 @@
1
+ export * from '../audioDoorbell.js';
2
+ export * from '../camera.js';
3
+ export * from '../cameraController.js';
4
+ export * from '../chime.js';
5
+ export * from '../doorbell.js';
6
+ export * from '../floodlightCamera.js';
7
+ export * from '../intercom.js';
8
+ export * from '../snapshotCamera.js';
9
+ export * from '../videoDoorbell.js';
@@ -0,0 +1,8 @@
1
+ import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
2
+ import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
3
+ export interface CameraControllerOptions extends MatterbridgeEndpointOptions {
4
+ powerSourceType?: 'Rechargeable' | 'Replaceable' | 'Battery' | 'Wired' | 'None';
5
+ }
6
+ export declare class CameraController extends MatterbridgeEndpoint {
7
+ constructor(name: string, serial: string, options?: CameraControllerOptions);
8
+ }
@@ -0,0 +1,35 @@
1
+ import { addWebRtcTransportProviderClient } from '../behaviors/clients.js';
2
+ import { createDefaultWebRtcTransportRequestorClusterServer } from '../behaviors/webRtcTransportRequestorServer.js';
3
+ import { cameraController, powerSource } from '../matterbridgeDeviceTypes.js';
4
+ import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
5
+ export class CameraController extends MatterbridgeEndpoint {
6
+ constructor(name, serial, options = {}) {
7
+ const { powerSourceType = 'Wired', id, number, tagList, mode } = options;
8
+ super(powerSourceType === 'None' ? [cameraController] : [cameraController, powerSource], {
9
+ id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
10
+ number,
11
+ tagList,
12
+ mode,
13
+ });
14
+ this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Camera Controller');
15
+ switch (powerSourceType) {
16
+ case 'Rechargeable':
17
+ this.createDefaultPowerSourceRechargeableBatteryClusterServer();
18
+ break;
19
+ case 'Replaceable':
20
+ this.createDefaultPowerSourceReplaceableBatteryClusterServer();
21
+ break;
22
+ case 'Battery':
23
+ this.createDefaultPowerSourceBatteryClusterServer();
24
+ break;
25
+ case 'Wired':
26
+ this.createDefaultPowerSourceWiredClusterServer();
27
+ break;
28
+ case 'None':
29
+ break;
30
+ }
31
+ createDefaultWebRtcTransportRequestorClusterServer(this);
32
+ addWebRtcTransportProviderClient(this);
33
+ this.addRequiredClusters();
34
+ }
35
+ }
@@ -1,11 +1,13 @@
1
1
  import type { Semtag } from '@matter/types';
2
+ import type { EndpointNumber } from '@matter/types/datatype';
2
3
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
3
4
  import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
4
5
  import type { CameraOptions } from './camera.js';
5
- export type FloodlightCameraCameraOptions = Omit<CameraOptions, 'id' | 'number' | 'tagList' | 'mode' | 'powerSourceType'>;
6
+ export type FloodlightCameraCameraOptions = Omit<CameraOptions, 'id' | 'mode' | 'powerSourceType'>;
6
7
  export interface FloodlightCameraLightOptions {
7
8
  name?: string;
8
9
  tagList?: Semtag[];
10
+ number?: EndpointNumber;
9
11
  onOff?: boolean;
10
12
  }
11
13
  export interface FloodlightCameraOptions extends MatterbridgeEndpointOptions {
@@ -15,5 +17,5 @@ export interface FloodlightCameraOptions extends MatterbridgeEndpointOptions {
15
17
  }
16
18
  export declare class FloodlightCamera extends MatterbridgeEndpoint {
17
19
  constructor(name: string, serial: string, options?: FloodlightCameraOptions);
18
- addLight(name: string, tagList?: Semtag[], onOff?: boolean): MatterbridgeEndpoint;
20
+ addLight(name: string, options?: FloodlightCameraLightOptions): MatterbridgeEndpoint;
19
21
  }
@@ -43,8 +43,8 @@ export class FloodlightCamera extends MatterbridgeEndpoint {
43
43
  { resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
44
44
  { resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
45
45
  { resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
46
- ], weriftOfferOptions, } = cameraOptions;
47
- const cameraChild = this.addChildDeviceType('Camera', camera, {});
46
+ ], weriftOfferOptions, number: cameraNumber, tagList: cameraTagList = [], } = cameraOptions;
47
+ const cameraChild = this.addChildDeviceType('Camera', camera, { number: cameraNumber, ...(cameraTagList.length > 0 ? { tagList: cameraTagList } : {}) });
48
48
  cameraChild.log.logName = 'Camera';
49
49
  if (identifyType !== Identify.IdentifyType.None) {
50
50
  cameraChild.createDefaultIdentifyClusterServer(identifyTime, identifyType);
@@ -67,11 +67,12 @@ export class FloodlightCamera extends MatterbridgeEndpoint {
67
67
  createDefaultWebRtcTransportProviderClusterServer(cameraChild, weriftOfferOptions);
68
68
  addWebRtcTransportRequestorClient(cameraChild);
69
69
  cameraChild.addRequiredClusters();
70
- const { name: lightName = 'Light', tagList: lightTagList = [], onOff: lightOnOff = false } = lightOptions;
71
- this.addLight(lightName, lightTagList, lightOnOff);
70
+ const { name: lightName = 'Light', tagList: lightTagList = [], number: lightNumber, onOff: lightOnOff = false } = lightOptions;
71
+ this.addLight(lightName, { tagList: lightTagList, number: lightNumber, onOff: lightOnOff });
72
72
  }
73
- addLight(name, tagList = [], onOff = false) {
74
- const light = this.addChildDeviceType(name, onOffLight, tagList.length > 0 ? { tagList } : {});
73
+ addLight(name, options = {}) {
74
+ const { tagList = [], number, onOff = false } = options;
75
+ const light = this.addChildDeviceType(name, onOffLight, { number, ...(tagList.length > 0 ? { tagList } : {}) });
75
76
  light.log.logName = name;
76
77
  light.createDefaultIdentifyClusterServer();
77
78
  light.createDefaultOnOffClusterServer(onOff);
@@ -1,6 +1,7 @@
1
1
  import { StreamUsage } from '@matter/types';
2
2
  import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
3
3
  import { Identify } from '@matter/types/clusters/identify';
4
+ import { type SnapshotSource } from '../behaviors/cameraAvStreamManagementServer.js';
4
5
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
5
6
  import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
6
7
  export interface SnapshotCameraOptions extends MatterbridgeEndpointOptions {
@@ -14,6 +15,8 @@ export interface SnapshotCameraOptions extends MatterbridgeEndpointOptions {
14
15
  maxNetworkBandwidth?: number;
15
16
  supportedStreamUsages?: StreamUsage[];
16
17
  streamUsagePriorities?: StreamUsage[];
18
+ snapshotSource?: SnapshotSource;
19
+ snapshotSourceDevice?: string;
17
20
  }
18
21
  export declare class SnapshotCamera extends MatterbridgeEndpoint {
19
22
  constructor(name: string, serial: string, options?: SnapshotCameraOptions);
@@ -10,7 +10,7 @@ export class SnapshotCamera extends MatterbridgeEndpoint {
10
10
  { resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
11
11
  { resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
12
12
  { resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
13
- ], maxNetworkBandwidth = 10000, supportedStreamUsages = [StreamUsage.Recording], streamUsagePriorities = [StreamUsage.Recording], id, number, tagList, mode, } = options;
13
+ ], maxNetworkBandwidth = 10000, supportedStreamUsages = [StreamUsage.Recording], streamUsagePriorities = [StreamUsage.Recording], snapshotSource, snapshotSourceDevice, id, number, tagList, mode, } = options;
14
14
  super(powerSourceType === 'None' ? [snapshotCamera] : [snapshotCamera, powerSource], {
15
15
  id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
16
16
  number,
@@ -45,6 +45,8 @@ export class SnapshotCamera extends MatterbridgeEndpoint {
45
45
  maxNetworkBandwidth,
46
46
  supportedStreamUsages,
47
47
  streamUsagePriorities,
48
+ snapshotSource,
49
+ snapshotSourceDevice,
48
50
  });
49
51
  this.addRequiredClusters();
50
52
  }
@@ -1,12 +1,14 @@
1
1
  import type { Semtag } from '@matter/types';
2
2
  import { Identify } from '@matter/types/clusters/identify';
3
+ import type { EndpointNumber } from '@matter/types/datatype';
3
4
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
4
5
  import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
5
6
  import type { CameraOptions } from './camera.js';
6
- export type VideoDoorbellCameraOptions = Omit<CameraOptions, 'id' | 'number' | 'tagList' | 'mode' | 'powerSourceType'>;
7
+ export type VideoDoorbellCameraOptions = Omit<CameraOptions, 'id' | 'mode' | 'powerSourceType'>;
7
8
  export interface VideoDoorbellDoorbellOptions {
8
9
  name?: string;
9
10
  tagList?: Semtag[];
11
+ number?: EndpointNumber;
10
12
  identifyTime?: number;
11
13
  identifyType?: Identify.IdentifyType;
12
14
  }
@@ -17,5 +19,5 @@ export interface VideoDoorbellOptions extends MatterbridgeEndpointOptions {
17
19
  }
18
20
  export declare class VideoDoorbell extends MatterbridgeEndpoint {
19
21
  constructor(name: string, serial: string, options?: VideoDoorbellOptions);
20
- addDoorbell(name: string, tagList?: Semtag[], identifyTime?: number, identifyType?: Identify.IdentifyType): MatterbridgeEndpoint;
22
+ addDoorbell(name: string, options?: VideoDoorbellDoorbellOptions): MatterbridgeEndpoint;
21
23
  }
@@ -43,8 +43,8 @@ export class VideoDoorbell extends MatterbridgeEndpoint {
43
43
  { resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
44
44
  { resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
45
45
  { resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
46
- ], weriftOfferOptions, } = cameraOptions;
47
- const cameraChild = this.addChildDeviceType('Camera', camera, {});
46
+ ], weriftOfferOptions, number: cameraNumber, tagList: cameraTagList = [], } = cameraOptions;
47
+ const cameraChild = this.addChildDeviceType('Camera', camera, { number: cameraNumber, ...(cameraTagList.length > 0 ? { tagList: cameraTagList } : {}) });
48
48
  cameraChild.log.logName = 'Camera';
49
49
  if (cameraIdentifyType !== Identify.IdentifyType.None) {
50
50
  cameraChild.createDefaultIdentifyClusterServer(cameraIdentifyTime, cameraIdentifyType);
@@ -67,11 +67,12 @@ export class VideoDoorbell extends MatterbridgeEndpoint {
67
67
  createDefaultWebRtcTransportProviderClusterServer(cameraChild, weriftOfferOptions);
68
68
  addWebRtcTransportRequestorClient(cameraChild);
69
69
  cameraChild.addRequiredClusters();
70
- const { name: doorbellName = 'Doorbell', tagList: doorbellTagList = [], identifyTime = 0, identifyType = Identify.IdentifyType.None } = doorbellOptions;
71
- this.addDoorbell(doorbellName, doorbellTagList, identifyTime, identifyType);
70
+ const { name: doorbellName = 'Doorbell', tagList: doorbellTagList = [], number: doorbellNumber, identifyTime = 0, identifyType = Identify.IdentifyType.None } = doorbellOptions;
71
+ this.addDoorbell(doorbellName, { tagList: doorbellTagList, number: doorbellNumber, identifyTime, identifyType });
72
72
  }
73
- addDoorbell(name, tagList = [], identifyTime = 0, identifyType = Identify.IdentifyType.None) {
74
- const doorbellChild = this.addChildDeviceType(name, doorbell, tagList.length > 0 ? { tagList } : {});
73
+ addDoorbell(name, options = {}) {
74
+ const { tagList = [], number, identifyTime = 0, identifyType = Identify.IdentifyType.None } = options;
75
+ const doorbellChild = this.addChildDeviceType(name, doorbell, { number, ...(tagList.length > 0 ? { tagList } : {}) });
75
76
  doorbellChild.log.logName = name;
76
77
  doorbellChild.createDefaultIdentifyClusterServer(identifyTime, identifyType);
77
78
  doorbellChild.createDefaultMomentarySwitchClusterServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.10.10-dev-20260912-8fddbe4",
3
+ "version": "3.10.10-dev-20260915-57739b4",
4
4
  "description": "Matterbridge core library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -54,6 +54,10 @@
54
54
  "types": "./dist/devices/export.d.ts",
55
55
  "import": "./dist/devices/export.js"
56
56
  },
57
+ "./devices/camera": {
58
+ "types": "./dist/devices/camera/export.d.ts",
59
+ "import": "./dist/devices/camera/export.js"
60
+ },
57
61
  "./clusters": {
58
62
  "types": "./dist/clusters/export.d.ts",
59
63
  "import": "./dist/clusters/export.js"
@@ -132,14 +136,14 @@
132
136
  },
133
137
  "dependencies": {
134
138
  "@matter/main": "0.17.9",
135
- "@matterbridge/dgram": "3.10.10-dev-20260912-8fddbe4",
136
- "@matterbridge/thread": "3.10.10-dev-20260912-8fddbe4",
137
- "@matterbridge/types": "3.10.10-dev-20260912-8fddbe4",
138
- "@matterbridge/utils": "3.10.10-dev-20260912-8fddbe4",
139
+ "@matterbridge/dgram": "3.10.10-dev-20260915-57739b4",
140
+ "@matterbridge/thread": "3.10.10-dev-20260915-57739b4",
141
+ "@matterbridge/types": "3.10.10-dev-20260915-57739b4",
142
+ "@matterbridge/utils": "3.10.10-dev-20260915-57739b4",
139
143
  "escape-html": "1.0.3",
140
144
  "express": "5.2.1",
141
145
  "express-rate-limit": "8.7.0",
142
- "multer": "2.3.0",
146
+ "multer": "2.4.0",
143
147
  "node-ansi-logger": "3.3.1",
144
148
  "node-persist-manager": "2.1.1",
145
149
  "werift": "0.24.4",