@matterbridge/core 3.10.13-dev-20261003-ba58ee6 → 3.10.13-dev-20261005-49e0dbd
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/export.d.ts +1 -0
- package/dist/behaviors/export.js +1 -0
- package/dist/behaviors/ffmpeg.d.ts +7 -0
- package/dist/behaviors/ffmpeg.js +106 -1
- package/dist/frontend.d.ts +1 -0
- package/dist/frontend.js +31 -9
- package/package.json +5 -5
|
@@ -6,6 +6,7 @@ export * from './deviceEnergyManagementModeServer.js';
|
|
|
6
6
|
export * from './deviceEnergyManagementServer.js';
|
|
7
7
|
export * from './doorLockServer.js';
|
|
8
8
|
export * from './fanControlServer.js';
|
|
9
|
+
export * from './ffmpeg.js';
|
|
9
10
|
export * from './hepaFilterMonitoringServer.js';
|
|
10
11
|
export * from './identifyServer.js';
|
|
11
12
|
export * from './levelControlServer.js';
|
package/dist/behaviors/export.js
CHANGED
|
@@ -6,6 +6,7 @@ export * from './deviceEnergyManagementModeServer.js';
|
|
|
6
6
|
export * from './deviceEnergyManagementServer.js';
|
|
7
7
|
export * from './doorLockServer.js';
|
|
8
8
|
export * from './fanControlServer.js';
|
|
9
|
+
export * from './ffmpeg.js';
|
|
9
10
|
export * from './hepaFilterMonitoringServer.js';
|
|
10
11
|
export * from './identifyServer.js';
|
|
11
12
|
export * from './levelControlServer.js';
|
|
@@ -2,3 +2,10 @@ import { type ChildProcess } from 'node:child_process';
|
|
|
2
2
|
export declare function hasFfmpeg(): boolean;
|
|
3
3
|
export declare function redactSource(source: string): string;
|
|
4
4
|
export declare function runFfmpeg(args: string[]): ChildProcess;
|
|
5
|
+
export declare function parseAvfoundationWebcams(output: string): string[];
|
|
6
|
+
export declare function parseDshowWebcams(output: string): string[];
|
|
7
|
+
export declare function parseV4l2Webcams(output: string): string[];
|
|
8
|
+
export declare function listWebcams(): Promise<string[]>;
|
|
9
|
+
export declare function getPlayWebcamArgs(videoSource: string, audioSource?: string): string[];
|
|
10
|
+
export declare function playWebcam(videoSource: string, audioSource?: string): ChildProcess;
|
|
11
|
+
export declare function installFfmpeg(): Promise<boolean>;
|
package/dist/behaviors/ffmpeg.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
3
|
import { access, readdir } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { getErrorMessage } from '@matterbridge/utils';
|
|
5
6
|
import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
|
|
6
7
|
const log = new AnsiLogger({ logName: 'Ffmpeg', logLevel: "debug", logNameColor: MAGENTA, logTimestampFormat: 4 });
|
|
7
8
|
async function runProbe(command, args) {
|
|
@@ -83,7 +84,7 @@ async function resolveFfmpeg() {
|
|
|
83
84
|
log.debug('Could not resolve ffmpeg');
|
|
84
85
|
return undefined;
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
let ffmpegCommand = await resolveFfmpeg();
|
|
87
88
|
if (ffmpegCommand)
|
|
88
89
|
log.debug(`Using ffmpeg: ${ffmpegCommand}`);
|
|
89
90
|
else
|
|
@@ -101,3 +102,107 @@ export function runFfmpeg(args) {
|
|
|
101
102
|
log.debug(`Spawning ffmpeg: ${ffmpegCommand} ${args.map(redactSource).join(' ')}`);
|
|
102
103
|
return spawn(ffmpegCommand, args);
|
|
103
104
|
}
|
|
105
|
+
async function ffmpegOutput(args) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const child = runFfmpeg(args);
|
|
108
|
+
let output = '';
|
|
109
|
+
child.stdout?.on('data', (data) => (output += data.toString()));
|
|
110
|
+
child.stderr?.on('data', (data) => (output += data.toString()));
|
|
111
|
+
child.on('error', reject);
|
|
112
|
+
child.on('close', () => resolve(output));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function parseAvfoundationWebcams(output) {
|
|
116
|
+
const webcams = [];
|
|
117
|
+
let video = false;
|
|
118
|
+
for (const line of output.split(/\r?\n/)) {
|
|
119
|
+
if (line.includes('AVFoundation video devices'))
|
|
120
|
+
video = true;
|
|
121
|
+
else if (line.includes('AVFoundation audio devices'))
|
|
122
|
+
video = false;
|
|
123
|
+
else if (video) {
|
|
124
|
+
const name = /\]\s+\[\d+\]\s+(.+?)\s*$/.exec(line)?.[1];
|
|
125
|
+
if (name && !name.startsWith('Capture screen'))
|
|
126
|
+
webcams.push(name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return webcams;
|
|
130
|
+
}
|
|
131
|
+
export function parseDshowWebcams(output) {
|
|
132
|
+
const webcams = [];
|
|
133
|
+
let video = false;
|
|
134
|
+
for (const line of output.split(/\r?\n/)) {
|
|
135
|
+
if (line.includes('DirectShow video devices'))
|
|
136
|
+
video = true;
|
|
137
|
+
else if (line.includes('DirectShow audio devices'))
|
|
138
|
+
video = false;
|
|
139
|
+
const match = /"(.+?)"(?:\s+\((video|audio|none)(?:,\s*(?:video|audio|none))*\))?\s*$/.exec(line);
|
|
140
|
+
if (match && (match[2] === 'video' || (match[2] === undefined && video)))
|
|
141
|
+
webcams.push(match[1]);
|
|
142
|
+
}
|
|
143
|
+
return webcams;
|
|
144
|
+
}
|
|
145
|
+
export function parseV4l2Webcams(output) {
|
|
146
|
+
const cameras = new Map();
|
|
147
|
+
for (const line of output.split(/\r?\n/)) {
|
|
148
|
+
const match = /^\s*\/dev\/video(\d+)\s+\[(.+)\]/.exec(line);
|
|
149
|
+
if (!match)
|
|
150
|
+
continue;
|
|
151
|
+
const node = Number(match[1]);
|
|
152
|
+
cameras.set(match[2], Math.min(node, cameras.get(match[2]) ?? node));
|
|
153
|
+
}
|
|
154
|
+
return [...cameras.values()].toSorted((a, b) => a - b).map((node) => `/dev/video${node}`);
|
|
155
|
+
}
|
|
156
|
+
export async function listWebcams() {
|
|
157
|
+
if (process.platform === 'darwin')
|
|
158
|
+
return parseAvfoundationWebcams(await ffmpegOutput(['-hide_banner', '-f', 'avfoundation', '-list_devices', 'true', '-i', '']));
|
|
159
|
+
if (process.platform === 'win32')
|
|
160
|
+
return parseDshowWebcams(await ffmpegOutput(['-hide_banner', '-list_devices', 'true', '-f', 'dshow', '-i', 'dummy']));
|
|
161
|
+
return parseV4l2Webcams(await ffmpegOutput(['-hide_banner', '-sources', 'v4l2']));
|
|
162
|
+
}
|
|
163
|
+
export function getPlayWebcamArgs(videoSource, audioSource) {
|
|
164
|
+
if (process.platform === 'darwin')
|
|
165
|
+
return ['-hide_banner', '-window_title', videoSource, '-f', 'avfoundation', '-framerate', '30', '-i', audioSource ? `${videoSource}:${audioSource}` : videoSource];
|
|
166
|
+
if (process.platform === 'win32')
|
|
167
|
+
return ['-hide_banner', '-window_title', videoSource, '-f', 'dshow', '-i', audioSource ? `video=${videoSource}:audio=${audioSource}` : `video=${videoSource}`];
|
|
168
|
+
return ['-hide_banner', '-window_title', videoSource, '-f', 'v4l2', '-i', videoSource];
|
|
169
|
+
}
|
|
170
|
+
export function playWebcam(videoSource, audioSource) {
|
|
171
|
+
if (!ffmpegCommand) {
|
|
172
|
+
throw new Error('Cannot play the webcam: ffmpeg not found on this host');
|
|
173
|
+
}
|
|
174
|
+
const ffplayCommand = path.isAbsolute(ffmpegCommand) ? path.join(path.dirname(ffmpegCommand), path.basename(ffmpegCommand).replace(/^ffmpeg/i, 'ffplay')) : 'ffplay';
|
|
175
|
+
const args = getPlayWebcamArgs(videoSource, audioSource);
|
|
176
|
+
log.debug(`Spawning ffplay: ${ffplayCommand} ${args.map(redactSource).join(' ')}`);
|
|
177
|
+
return spawn(ffplayCommand, args);
|
|
178
|
+
}
|
|
179
|
+
export async function installFfmpeg() {
|
|
180
|
+
if (process.platform !== 'linux') {
|
|
181
|
+
log.warn(`Cannot install ffmpeg: unsupported platform ${process.platform}`);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (process.getuid?.() !== 0) {
|
|
185
|
+
log.warn('Cannot install ffmpeg: root privileges are required');
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
if (await isRunnable('apk')) {
|
|
190
|
+
await runProbe('apk', ['add', '--no-cache', 'ffmpeg']);
|
|
191
|
+
}
|
|
192
|
+
else if (await isRunnable('apt-get')) {
|
|
193
|
+
await runProbe('apt-get', ['update']);
|
|
194
|
+
await runProbe('env', ['DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '--no-install-recommends', 'ffmpeg']);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
log.warn('Cannot install ffmpeg: no supported package manager found (apk, apt-get)');
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
log.error(`Failed to install ffmpeg: ${getErrorMessage(error)}`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
ffmpegCommand = await resolveFfmpeg();
|
|
206
|
+
log.info(ffmpegCommand ? `Installed ffmpeg: ${ffmpegCommand}` : 'ffmpeg was installed but could not be resolved');
|
|
207
|
+
return ffmpegCommand !== undefined;
|
|
208
|
+
}
|
package/dist/frontend.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class Frontend extends EventEmitter<FrontendEvents> {
|
|
|
27
27
|
private serverFetchTimeout;
|
|
28
28
|
private readonly debug;
|
|
29
29
|
private readonly verbose;
|
|
30
|
+
private readonly diagnostic;
|
|
30
31
|
readonly readOnly: boolean;
|
|
31
32
|
readonly shellyBoard: boolean;
|
|
32
33
|
shellySysUpdate: boolean;
|
package/dist/frontend.js
CHANGED
|
@@ -22,6 +22,7 @@ import { BroadcastServer } from '@matterbridge/thread/server';
|
|
|
22
22
|
import { MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_DIAGNOSTIC_FILE, MATTERBRIDGE_HISTORY_FILE, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, } from '@matterbridge/types';
|
|
23
23
|
import { isBun } from '@matterbridge/utils/bun';
|
|
24
24
|
import { getParameter, hasParameter } from '@matterbridge/utils/cli';
|
|
25
|
+
import { writeDiagnostic } from '@matterbridge/utils/diagnostic';
|
|
25
26
|
import { getEnumDescription } from '@matterbridge/utils/enum';
|
|
26
27
|
import { getErrorMessage, inspectError, logError } from '@matterbridge/utils/error';
|
|
27
28
|
import { formatBytes, formatPercent, formatUptime } from '@matterbridge/utils/format';
|
|
@@ -50,6 +51,7 @@ export class Frontend extends EventEmitter {
|
|
|
50
51
|
serverFetchTimeout = 2000;
|
|
51
52
|
debug = hasParameter('debug') || hasParameter('verbose');
|
|
52
53
|
verbose = hasParameter('verbose');
|
|
54
|
+
diagnostic = hasParameter('diagnostic');
|
|
53
55
|
readOnly = hasParameter('readonly') || hasParameter('shelly');
|
|
54
56
|
shellyBoard = hasParameter('shelly');
|
|
55
57
|
shellySysUpdate = false;
|
|
@@ -1780,13 +1782,24 @@ export class Frontend extends EventEmitter {
|
|
|
1780
1782
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: matter });
|
|
1781
1783
|
}
|
|
1782
1784
|
else if (data.method === '/api/settings') {
|
|
1783
|
-
|
|
1785
|
+
const start = performance.now();
|
|
1786
|
+
const settings = this.getApiSettings();
|
|
1787
|
+
if (this.diagnostic)
|
|
1788
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiSettings() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1789
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: settings });
|
|
1784
1790
|
}
|
|
1785
1791
|
else if (data.method === '/api/plugins') {
|
|
1786
|
-
|
|
1792
|
+
const start = performance.now();
|
|
1793
|
+
const plugins = this.getApiPlugins();
|
|
1794
|
+
if (this.diagnostic)
|
|
1795
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1796
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: plugins });
|
|
1787
1797
|
}
|
|
1788
1798
|
else if (data.method === '/api/devices') {
|
|
1799
|
+
const start = performance.now();
|
|
1789
1800
|
const devices = this.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
|
|
1801
|
+
if (this.diagnostic)
|
|
1802
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1790
1803
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
|
|
1791
1804
|
}
|
|
1792
1805
|
else if (data.method === '/api/clusters') {
|
|
@@ -1798,15 +1811,18 @@ export class Frontend extends EventEmitter {
|
|
|
1798
1811
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
|
|
1799
1812
|
return;
|
|
1800
1813
|
}
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1814
|
+
const start = performance.now();
|
|
1815
|
+
const clusters = this.getClusters(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
|
|
1816
|
+
if (this.diagnostic)
|
|
1817
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getClusters() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1818
|
+
if (clusters) {
|
|
1803
1819
|
sendResponse({
|
|
1804
1820
|
id: data.id,
|
|
1805
1821
|
method: data.method,
|
|
1806
1822
|
src: 'Matterbridge',
|
|
1807
1823
|
dst: data.src,
|
|
1808
1824
|
success: true,
|
|
1809
|
-
response,
|
|
1825
|
+
response: clusters,
|
|
1810
1826
|
});
|
|
1811
1827
|
}
|
|
1812
1828
|
else {
|
|
@@ -1823,8 +1839,11 @@ export class Frontend extends EventEmitter {
|
|
|
1823
1839
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/devices' });
|
|
1824
1840
|
return;
|
|
1825
1841
|
}
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1842
|
+
const start = performance.now();
|
|
1843
|
+
const selectDevices = !plugin.platform ? [] : plugin.platform.getSelectDevices().toSorted((keyA, keyB) => keyA.name.localeCompare(keyB.name));
|
|
1844
|
+
if (this.diagnostic)
|
|
1845
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getSelectDevices() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1846
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectDevices });
|
|
1828
1847
|
}
|
|
1829
1848
|
else if (data.method === '/api/select/entities') {
|
|
1830
1849
|
if (!isValidString(data.params.plugin, 10)) {
|
|
@@ -1836,8 +1855,11 @@ export class Frontend extends EventEmitter {
|
|
|
1836
1855
|
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Plugin not found in /api/select/entities' });
|
|
1837
1856
|
return;
|
|
1838
1857
|
}
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1858
|
+
const start = performance.now();
|
|
1859
|
+
const selectEntities = !plugin.platform ? [] : plugin.platform.getSelectEntities().toSorted((keyA, keyB) => keyA.name.localeCompare(keyB.name));
|
|
1860
|
+
if (this.diagnostic)
|
|
1861
|
+
writeDiagnostic(`Frontend:${data.sender}`, `getSelectEntities() took ${(performance.now() - start).toFixed(2)} ms`);
|
|
1862
|
+
sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: selectEntities });
|
|
1841
1863
|
}
|
|
1842
1864
|
else if (data.method === '/api/action') {
|
|
1843
1865
|
const localData = data;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matterbridge/core",
|
|
3
|
-
"version": "3.10.13-dev-
|
|
3
|
+
"version": "3.10.13-dev-20261005-49e0dbd",
|
|
4
4
|
"description": "Matterbridge core library",
|
|
5
5
|
"author": "https://github.com/Luligu",
|
|
6
6
|
"homepage": "https://matterbridge.io/",
|
|
@@ -136,10 +136,10 @@
|
|
|
136
136
|
},
|
|
137
137
|
"dependencies": {
|
|
138
138
|
"@matter/main": "0.17.9",
|
|
139
|
-
"@matterbridge/dgram": "3.10.13-dev-
|
|
140
|
-
"@matterbridge/thread": "3.10.13-dev-
|
|
141
|
-
"@matterbridge/types": "3.10.13-dev-
|
|
142
|
-
"@matterbridge/utils": "3.10.13-dev-
|
|
139
|
+
"@matterbridge/dgram": "3.10.13-dev-20261005-49e0dbd",
|
|
140
|
+
"@matterbridge/thread": "3.10.13-dev-20261005-49e0dbd",
|
|
141
|
+
"@matterbridge/types": "3.10.13-dev-20261005-49e0dbd",
|
|
142
|
+
"@matterbridge/utils": "3.10.13-dev-20261005-49e0dbd",
|
|
143
143
|
"escape-html": "1.0.3",
|
|
144
144
|
"express": "5.2.1",
|
|
145
145
|
"express-rate-limit": "8.7.0",
|