@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.
- package/dist/behaviors/cameraAvStreamManagementServer.d.ts +13 -8
- package/dist/behaviors/cameraAvStreamManagementServer.js +57 -13
- package/dist/behaviors/snapshot.d.ts +1 -0
- package/dist/behaviors/snapshot.js +5 -0
- package/dist/behaviors/webRtcTransportProviderServer.js +6 -6
- package/dist/behaviors/weriftSession.d.ts +4 -3
- package/dist/behaviors/weriftSession.js +34 -27
- package/dist/chipTests.js +90 -1
- package/dist/demoDevices.js +61 -29
- package/dist/devices/snapshotCamera.d.ts +3 -0
- package/dist/devices/snapshotCamera.js +3 -1
- package/package.json +6 -6
|
@@ -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
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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.
|
|
193
|
-
return
|
|
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.
|
|
213
|
-
return
|
|
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
|
|
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
|
-
|
|
315
|
-
args: ['-re', '-
|
|
316
|
-
|
|
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.
|
|
324
|
-
return
|
|
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.
|
|
342
|
-
return
|
|
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
|
-
|
|
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 (
|
|
431
|
-
if (options.
|
|
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.
|
|
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
|
}
|
package/dist/demoDevices.js
CHANGED
|
@@ -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
|
-
|
|
134
|
-
...
|
|
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(
|
|
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:
|
|
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 =
|
|
172
|
-
const 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('
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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', {
|
|
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', {
|
|
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', {
|
|
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-
|
|
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-
|
|
140
|
-
"@matterbridge/thread": "3.10.10-dev-
|
|
141
|
-
"@matterbridge/types": "3.10.10-dev-
|
|
142
|
-
"@matterbridge/utils": "3.10.10-dev-
|
|
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.
|
|
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",
|