@matterbridge/core 3.10.10-dev-20260914-784e019 → 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;
@@ -42,10 +42,11 @@ export declare class WeriftWebRtcSession {
42
42
  private getConfiguredVideoResolution;
43
43
  private getConfiguredVideoSource;
44
44
  private buildFfmpegVideoInputArgs;
45
+ private logFfmpegStderr;
45
46
  private generateVideoTrack;
46
47
  private preferVideoCodecOnTransceivers;
47
48
  private preferAudioCodecOnTransceivers;
48
- private static readonly TEST_VOICE_PATH;
49
+ private static readonly TEST_TONE_INPUT;
49
50
  private getConfiguredAudioSource;
50
51
  private buildFfmpegAudioInputArgs;
51
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';
@@ -178,19 +177,19 @@ export class WeriftWebRtcSession {
178
177
  }
179
178
  }
180
179
  buildFfmpegVideoInputArgs(videoSource) {
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
- };
186
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
+ };
187
186
  this.log.debug(`Test pattern params: resolution=640x480, description="${testPatternInput.description}", bitrateKbps=${testPatternInput.bitrateKbps}`);
188
187
  return testPatternInput;
189
188
  }
190
189
  const device = this.options.videoSourceDevice;
191
190
  if (!device) {
192
- this.log.warn(`options.videoSource=${videoSource} requires options.videoSourceDevice to be set; falling back to the synthetic test video`);
193
- return testPatternInput;
191
+ this.log.error(`options.videoSource=${videoSource} requires options.videoSourceDevice to be set; not injecting a video track`);
192
+ return undefined;
194
193
  }
195
194
  const resolution = this.getConfiguredVideoResolution();
196
195
  const bitrateKbps = this.getConfiguredVideoBitrate();
@@ -209,10 +208,17 @@ export class WeriftWebRtcSession {
209
208
  case 'win32':
210
209
  return { args: ['-f', 'dshow', '-video_size', resolution, '-framerate', '30', '-i', `video=${device}`], description, bitrateKbps };
211
210
  default:
212
- this.log.warn(`Webcam capture via ffmpeg is not supported on platform "${process.platform}"; falling back to the synthetic test video`);
213
- 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;
214
213
  }
215
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
+ }
216
222
  async generateVideoTrack(codec) {
217
223
  if (this.testVideoAttached)
218
224
  return;
@@ -222,6 +228,8 @@ export class WeriftWebRtcSession {
222
228
  return;
223
229
  }
224
230
  const videoInput = this.buildFfmpegVideoInputArgs(videoSource);
231
+ if (!videoInput)
232
+ return;
225
233
  this.log.debug(`Attempting to attach ${videoInput.description} video track at ${videoInput.bitrateKbps}kbps`);
226
234
  if (!hasFfmpeg()) {
227
235
  this.log.warn('Cannot inject video stream: missing dependency ffmpeg');
@@ -254,6 +262,7 @@ export class WeriftWebRtcSession {
254
262
  `rtp://127.0.0.1:${udpPort}`,
255
263
  ];
256
264
  const generator = runFfmpeg(ffmpegArgs);
265
+ this.logFfmpegStderr(generator, 'video');
257
266
  generator.once('error', (error) => {
258
267
  this.log.warn(`Video generator failed: ${getErrorMessage(error)}`);
259
268
  });
@@ -296,7 +305,7 @@ export class WeriftWebRtcSession {
296
305
  this.log.debug(`Preferred ${mimeType.toUpperCase()} codecs on ${adjustedTransceivers} audio transceiver(s)`);
297
306
  }
298
307
  }
299
- 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';
300
309
  getConfiguredAudioSource() {
301
310
  const source = this.options.audioSource?.trim().toLowerCase() ?? 'none';
302
311
  switch (source) {
@@ -311,17 +320,13 @@ export class WeriftWebRtcSession {
311
320
  }
312
321
  }
313
322
  buildFfmpegAudioInputArgs(audioSource) {
314
- const testVoiceInput = {
315
- args: ['-re', '-stream_loop', '-1', '-i', WeriftWebRtcSession.TEST_VOICE_PATH],
316
- description: 'recorded test-voice clip',
317
- volumeFilter: 'volume=6dB',
318
- };
319
- if (audioSource === 'test')
320
- return testVoiceInput;
323
+ if (audioSource === 'test') {
324
+ return { args: ['-re', '-f', 'lavfi', '-i', WeriftWebRtcSession.TEST_TONE_INPUT], description: 'synthetic test tone' };
325
+ }
321
326
  const device = this.options.audioSourceDevice;
322
327
  if (!device) {
323
- this.log.warn(`options.audioSource=${audioSource} requires options.audioSourceDevice to be set; falling back to the recorded test-voice clip`);
324
- return testVoiceInput;
328
+ this.log.error(`options.audioSource=${audioSource} requires options.audioSourceDevice to be set; not injecting an audio track`);
329
+ return undefined;
325
330
  }
326
331
  if (audioSource === 'rtsp') {
327
332
  const description = `RTSP camera audio (${device})`;
@@ -338,8 +343,8 @@ export class WeriftWebRtcSession {
338
343
  case 'win32':
339
344
  return { args: ['-f', 'dshow', '-i', `audio=${device}`], description };
340
345
  default:
341
- this.log.warn(`Microphone capture via ffmpeg is not supported on platform "${process.platform}"; falling back to the recorded test-voice clip`);
342
- 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;
343
348
  }
344
349
  }
345
350
  async generateAudioTrack(codec) {
@@ -351,6 +356,8 @@ export class WeriftWebRtcSession {
351
356
  return;
352
357
  }
353
358
  const audioInput = this.buildFfmpegAudioInputArgs(audioSource);
359
+ if (!audioInput)
360
+ return;
354
361
  this.log.debug(`Attempting to attach ${audioInput.description} audio track`);
355
362
  if (!hasFfmpeg()) {
356
363
  this.log.warn('Cannot inject audio stream: missing dependency ffmpeg');
@@ -373,13 +380,12 @@ export class WeriftWebRtcSession {
373
380
  'error',
374
381
  ...audioInput.args,
375
382
  '-vn',
376
- ...(audioInput.volumeFilter ? ['-af', audioInput.volumeFilter] : []),
377
383
  '-c:a',
378
384
  'libopus',
379
385
  '-b:a',
380
386
  '32k',
381
387
  '-ac',
382
- String(channels),
388
+ '1',
383
389
  '-ar',
384
390
  String(clockRate),
385
391
  '-f',
@@ -389,6 +395,7 @@ export class WeriftWebRtcSession {
389
395
  `rtp://127.0.0.1:${udpPort}`,
390
396
  ];
391
397
  const generator = runFfmpeg(ffmpegArgs);
398
+ this.logFfmpegStderr(generator, 'audio');
392
399
  generator.once('error', (error) => {
393
400
  this.log.warn(`Audio generator failed: ${getErrorMessage(error)}`);
394
401
  });
@@ -427,8 +434,8 @@ export class WeriftWebRtcSession {
427
434
  async createOffer(overrides) {
428
435
  this.applyOptionOverrides(overrides);
429
436
  const options = this.options;
430
- this.log.debug(`CreateOffer requested (video=${options.video}, audio=${options.audio}, videoResolution=${options.videoResolution ?? 'undefined'})`);
431
- if (options.video) {
437
+ this.log.debug(`CreateOffer requested (offerVideo=${options.offerVideo}, offerAudio=${options.offerAudio}, videoResolution=${options.videoResolution ?? 'undefined'})`);
438
+ if (options.offerVideo) {
432
439
  const preferredCodec = this.getPreferredInjectableVideoCodec();
433
440
  if (preferredCodec) {
434
441
  this.preferVideoCodecOnTransceivers(preferredCodec.mimeType.toLowerCase());
@@ -440,7 +447,7 @@ export class WeriftWebRtcSession {
440
447
  if (!this.testVideoAttached)
441
448
  this.peerConnection.addTransceiver('video', { direction: 'sendonly' });
442
449
  }
443
- if (options.audio)
450
+ if (options.offerAudio)
444
451
  this.peerConnection.addTransceiver('audio', { direction: 'sendonly' });
445
452
  const offer = await this.peerConnection.createOffer();
446
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
  }
@@ -105,6 +105,15 @@ const demoPluginSchema = {
105
105
  },
106
106
  },
107
107
  };
108
+ let demoPluginConfig = {
109
+ name: demoPluginName,
110
+ type: demoPluginType,
111
+ version: demoPluginVersion,
112
+ debug: false,
113
+ unregisterOnShutdown: false,
114
+ whiteList: [],
115
+ blackList: [],
116
+ };
108
117
  export async function createDemoDevices(matterbridge) {
109
118
  if (matterbridge.bridgeMode !== 'bridge') {
110
119
  matterbridge.log.error('Demo devices can only be created in bridge mode');
@@ -118,20 +127,10 @@ export async function createDemoDevices(matterbridge) {
118
127
  }
119
128
  const configFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.config.json`);
120
129
  const schemaFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.schema.json`);
121
- const defaultConfig = {
122
- name: demoPluginName,
123
- type: demoPluginType,
124
- version: demoPluginVersion,
125
- debug: false,
126
- unregisterOnShutdown: false,
127
- whiteList: [],
128
- blackList: [],
129
- };
130
- let config = defaultConfig;
131
130
  try {
132
131
  const storedConfig = JSON.parse(await readFile(configFile, 'utf8'));
133
- config = {
134
- ...defaultConfig,
132
+ demoPluginConfig = {
133
+ ...demoPluginConfig,
135
134
  ...storedConfig,
136
135
  name: demoPluginName,
137
136
  type: demoPluginType,
@@ -145,7 +144,7 @@ export async function createDemoDevices(matterbridge) {
145
144
  matterbridge.log.error(`Failed to read demo devices config ${configFile}: ${getErrorMessage(error)}`);
146
145
  }
147
146
  try {
148
- await writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
147
+ await writeFile(configFile, JSON.stringify(demoPluginConfig, null, 2), 'utf8');
149
148
  await writeFile(schemaFile, JSON.stringify(demoPluginSchema, null, 2), 'utf8');
150
149
  }
151
150
  catch (error) {
@@ -162,14 +161,14 @@ export async function createDemoDevices(matterbridge) {
162
161
  enabled: false,
163
162
  private: true,
164
163
  registeredDevices: 0,
165
- configJson: config,
164
+ configJson: demoPluginConfig,
166
165
  schemaJson: demoPluginSchema,
167
166
  hasWhiteList: true,
168
167
  hasBlackList: true,
169
168
  });
170
169
  const registerDevice = async (device, deviceName, serialNumber) => {
171
- const whiteList = config.whiteList;
172
- const blackList = config.blackList;
170
+ const whiteList = demoPluginConfig.whiteList;
171
+ const blackList = demoPluginConfig.blackList;
173
172
  if (blackList.includes(deviceName)) {
174
173
  matterbridge.log.info(`Skipping demo device ${deviceName} because it is in the blacklist`);
175
174
  return;
@@ -277,34 +276,42 @@ export async function createDemoDevices(matterbridge) {
277
276
  ep = new MatterbridgeEndpoint([getSupportedDeviceType('PumpController'), bridgedNode, powerSource], { id: 'PumpController', number: EndpointNumber(6_05) });
278
277
  ep.createDefaultPowerSourceWiredClusterServer();
279
278
  await registerDevice(ep, 'Pump Controller', 'SWITCH-06-05');
280
- 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) });
281
288
  ep.createDefaultPowerSourceBatteryClusterServer();
282
289
  await ep.addFixedLabel('composed', 'GenericSwitch');
283
290
  ep.addChildDeviceType('Button1', getSupportedDeviceType('GenericSwitch'), {
284
- number: EndpointNumber(6_06_1),
291
+ number: EndpointNumber(6_06_3),
285
292
  tagList: [getSemtag(SwitchesTag.On), getSemtag(CommonNumberTag.One)],
286
293
  })
287
294
  .createDefaultMomentarySwitchClusterServer()
288
295
  .addRequiredClusters();
289
296
  ep.addChildDeviceType('Button2', getSupportedDeviceType('GenericSwitch'), {
290
- number: EndpointNumber(6_06_2),
297
+ number: EndpointNumber(6_06_4),
291
298
  tagList: [getSemtag(SwitchesTag.Off), getSemtag(CommonNumberTag.Two)],
292
299
  })
293
300
  .createDefaultMomentarySwitchClusterServer()
294
301
  .addRequiredClusters();
295
302
  ep.addChildDeviceType('Button3', getSupportedDeviceType('GenericSwitch'), {
296
- number: EndpointNumber(6_06_3),
303
+ number: EndpointNumber(6_06_5),
297
304
  tagList: [getSemtag(SwitchesTag.Up), getSemtag(CommonNumberTag.Three)],
298
305
  })
299
306
  .createDefaultMomentarySwitchClusterServer()
300
307
  .addRequiredClusters();
301
308
  ep.addChildDeviceType('Button4', getSupportedDeviceType('GenericSwitch'), {
302
- number: EndpointNumber(6_06_4),
309
+ number: EndpointNumber(6_06_6),
303
310
  tagList: [getSemtag(SwitchesTag.Down), getSemtag(CommonNumberTag.Four)],
304
311
  })
305
312
  .createDefaultMomentarySwitchClusterServer()
306
313
  .addRequiredClusters();
307
- await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06');
314
+ await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06-2');
308
315
  ep = new MatterbridgeEndpoint([getSupportedDeviceType('ContactSensor'), bridgedNode, powerSource], { id: 'ContactSensor', number: EndpointNumber(7_01) });
309
316
  ep.createDefaultPowerSourceBatteryClusterServer();
310
317
  ep.createDefaultBooleanStateClusterServer();
@@ -836,7 +843,7 @@ export async function createDemoDevices(matterbridge) {
836
843
  ep = new Camera('Camera', 'CAMERA-16-01', {
837
844
  id: 'Camera',
838
845
  number: EndpointNumber(16_01),
839
- weriftOfferOptions: { video: true, audio: true, videoSource: 'test', audioSource: 'test' },
846
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
840
847
  });
841
848
  await registerDevice(ep, 'Camera', 'CAMERA-16-01');
842
849
  ep = new Camera('Camera Ptz', 'CAMERA-16-01-1', {
@@ -844,33 +851,58 @@ export async function createDemoDevices(matterbridge) {
844
851
  number: EndpointNumber(16_01_1),
845
852
  ptz: true,
846
853
  identifyType: Identify.IdentifyType.VisibleIndicator,
847
- weriftOfferOptions: { video: true, audio: true, videoSource: 'test', audioSource: 'test' },
854
+ weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
848
855
  });
849
856
  await registerDevice(ep, 'Camera Ptz', 'CAMERA-16-01-1');
850
857
  ep = new FloodlightCamera('Floodlight Camera', 'CAMERA-16-02', {
851
858
  id: 'FloodlightCamera',
852
859
  number: EndpointNumber(16_02),
853
- cameraOptions: { number: EndpointNumber(16_02_1), ptz: true, identifyType: Identify.IdentifyType.VisibleIndicator },
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
+ },
854
866
  lightOptions: { number: EndpointNumber(16_02_2) },
855
867
  });
856
868
  await registerDevice(ep, 'Floodlight Camera', 'CAMERA-16-02');
857
869
  ep = new VideoDoorbell('Video Doorbell', 'CAMERA-16-03', {
858
870
  id: 'VideoDoorbell',
859
871
  number: EndpointNumber(16_03),
860
- cameraOptions: { number: EndpointNumber(16_03_1), ptz: true, identifyType: Identify.IdentifyType.VisibleIndicator },
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
+ },
861
878
  doorbellOptions: { number: EndpointNumber(16_03_2) },
862
879
  });
863
880
  await registerDevice(ep, 'Video Doorbell', 'CAMERA-16-03');
864
- ep = new Intercom('Intercom I', 'CAMERA-16-04', { id: 'Intercom', number: EndpointNumber(16_04), identifyType: Identify.IdentifyType.VisibleIndicator });
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
+ });
865
887
  await registerDevice(ep, 'Intercom I', 'CAMERA-16-04');
866
- ep = new Intercom('Intercom II', 'CAMERA-16-04-1', { id: 'IntercomII', number: EndpointNumber(16_04_1), identifyType: Identify.IdentifyType.VisibleIndicator });
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
+ });
867
894
  await registerDevice(ep, 'Intercom II', 'CAMERA-16-04-1');
868
- ep = new AudioDoorbell('Audio Doorbell', 'CAMERA-16-05', { id: 'AudioDoorbell', number: EndpointNumber(16_05) });
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
+ });
869
900
  await registerDevice(ep, 'Audio Doorbell', 'CAMERA-16-05');
870
901
  ep = new SnapshotCamera('Snapshot Camera', 'CAMERA-16-06', {
871
902
  id: 'SnapshotCamera',
872
903
  number: EndpointNumber(16_06),
873
904
  identifyType: Identify.IdentifyType.VisibleIndicator,
905
+ snapshotSource: 'test',
874
906
  });
875
907
  await registerDevice(ep, 'Snapshot Camera', 'CAMERA-16-06');
876
908
  ep = new Chime('Chime', 'CAMERA-16-07', {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.10.10-dev-20260914-784e019",
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/",
@@ -136,14 +136,14 @@
136
136
  },
137
137
  "dependencies": {
138
138
  "@matter/main": "0.17.9",
139
- "@matterbridge/dgram": "3.10.10-dev-20260914-784e019",
140
- "@matterbridge/thread": "3.10.10-dev-20260914-784e019",
141
- "@matterbridge/types": "3.10.10-dev-20260914-784e019",
142
- "@matterbridge/utils": "3.10.10-dev-20260914-784e019",
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",
143
143
  "escape-html": "1.0.3",
144
144
  "express": "5.2.1",
145
145
  "express-rate-limit": "8.7.0",
146
- "multer": "2.3.0",
146
+ "multer": "2.4.0",
147
147
  "node-ansi-logger": "3.3.1",
148
148
  "node-persist-manager": "2.1.1",
149
149
  "werift": "0.24.4",