homebridge-nest-accfactory 0.3.0 → 0.3.2
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/CHANGELOG.md +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- package/package.json +19 -16
package/dist/ffmpeg.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// FFmpeg manager for binary probing + session tracking
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 2025.07.07
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define nodejs module requirements
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
import child_process from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
// FFmpeg object
|
|
16
|
+
export default class FFmpeg {
|
|
17
|
+
#binary = undefined;
|
|
18
|
+
#version = undefined;
|
|
19
|
+
#features = {};
|
|
20
|
+
#sessions = new Map(); // Map of "uuid:sessionID:sessionType" => ChildProcess
|
|
21
|
+
#log = undefined; // Logging object
|
|
22
|
+
|
|
23
|
+
constructor(binaryPath = undefined, log = undefined) {
|
|
24
|
+
this.#log = log;
|
|
25
|
+
|
|
26
|
+
if (typeof binaryPath === 'string' && binaryPath !== '') {
|
|
27
|
+
let resolved = path.resolve(binaryPath);
|
|
28
|
+
if (resolved.endsWith('/ffmpeg') === false) {
|
|
29
|
+
resolved += '/ffmpeg';
|
|
30
|
+
}
|
|
31
|
+
this.#binary = resolved;
|
|
32
|
+
} else {
|
|
33
|
+
this.#binary = 'ffmpeg'; // Fallback to system PATH
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.#probeBinary();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate binary, extract version + feature flags
|
|
40
|
+
#probeBinary() {
|
|
41
|
+
if (fs.existsSync(this.#binary) === false) {
|
|
42
|
+
// Specified binary does not exist
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let versionOutput = child_process.spawnSync(this.#binary, ['-version'], { env: process.env });
|
|
47
|
+
if (versionOutput?.stdout === null || versionOutput.status !== 0) {
|
|
48
|
+
// Failed to execute specified binary with -version command
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let stdout = String(versionOutput.stdout);
|
|
53
|
+
let match = stdout.match(/^ffmpeg version ([^\s]+)/);
|
|
54
|
+
if (match !== null) {
|
|
55
|
+
this.#version = match[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse --enable-xxx flags from build config
|
|
59
|
+
let enabledLibs = stdout.match(/--enable-[^\s]+/g) || [];
|
|
60
|
+
this.#features.enabled = enabledLibs.map((f) => f.replace('--enable-', ''));
|
|
61
|
+
|
|
62
|
+
// Parse encoders (for HW accel + audio)
|
|
63
|
+
let encodersOutput = child_process.spawnSync(this.#binary, ['-encoders'], { env: process.env });
|
|
64
|
+
if (encodersOutput?.stdout !== null && encodersOutput.status === 0) {
|
|
65
|
+
let encoders = String(encodersOutput.stdout);
|
|
66
|
+
this.#features.encoders = [];
|
|
67
|
+
for (let line of encoders.split('\n')) {
|
|
68
|
+
let match = line.match(/^\s*[A-Z.]+\s+([^\s]+)/);
|
|
69
|
+
if (match !== null) {
|
|
70
|
+
this.#features.encoders.push(match[1]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.#features.h264_nvenc = encoders.includes('h264_nvenc') === true;
|
|
75
|
+
this.#features.h264_vaapi = encoders.includes('h264_vaapi') === true;
|
|
76
|
+
this.#features.h264_v4l2m2m = encoders.includes('h264_v4l2m2m') === true;
|
|
77
|
+
this.#features.h264_qsv = encoders.includes('h264_qsv') === true;
|
|
78
|
+
this.#features.h264_videotoolbox = encoders.includes('h264_videotoolbox') === true;
|
|
79
|
+
|
|
80
|
+
// Platform-aware preferred hardware encoder
|
|
81
|
+
this.#features.hardwareH264Codec = undefined;
|
|
82
|
+
let platform = os.platform();
|
|
83
|
+
let hasDri = fs.existsSync('/dev/dri/renderD128') === true || fs.existsSync('/dev/dri/card0') === true;
|
|
84
|
+
let hasVideo = fs.existsSync('/dev/video0') === true;
|
|
85
|
+
let hasIntelQSV = fs.existsSync('/dev/dri') === true && fs.readdirSync('/dev/dri').some((f) => f.startsWith('render')) === true;
|
|
86
|
+
|
|
87
|
+
// macOS: prefer videotoolbox
|
|
88
|
+
if (platform === 'darwin' && this.#features.h264_videotoolbox === true) {
|
|
89
|
+
this.#features.hardwareH264Codec = 'h264_videotoolbox';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Linux: prioritise nvenc > qsv > vaapi > v4l2m2m, only if required devices exist
|
|
93
|
+
else if (platform === 'linux') {
|
|
94
|
+
let linuxEncoders = [
|
|
95
|
+
{ key: 'h264_nvenc', device: hasDri },
|
|
96
|
+
{ key: 'h264_qsv', device: hasIntelQSV },
|
|
97
|
+
{ key: 'h264_vaapi', device: hasDri },
|
|
98
|
+
{ key: 'h264_v4l2m2m', device: hasVideo },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (let encoder of linuxEncoders) {
|
|
102
|
+
if (this.#features[encoder.key] === true) {
|
|
103
|
+
if (encoder.device !== true) {
|
|
104
|
+
this.#features[encoder.key] = false; // Disable if device not available
|
|
105
|
+
} else if (this.#features.hardwareH264Codec === undefined) {
|
|
106
|
+
this.#features.hardwareH264Codec = encoder.key; // First match becomes selected codec
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Windows: qsv preferred
|
|
113
|
+
else if (platform === 'win32' && this.#features.h264_qsv === true) {
|
|
114
|
+
this.#features.hardwareH264Codec = 'h264_qsv';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Parse decoders
|
|
119
|
+
let decoderOutput = child_process.spawnSync(this.#binary, ['-decoders'], { env: process.env });
|
|
120
|
+
if (decoderOutput?.stdout !== null && decoderOutput.status === 0) {
|
|
121
|
+
this.#features.decoders = [];
|
|
122
|
+
let lines = String(decoderOutput.stdout).split('\n');
|
|
123
|
+
for (let line of lines) {
|
|
124
|
+
let match = line.match(/^\s*[A-Z.]+\s+([^\s]+)/);
|
|
125
|
+
if (match !== null) {
|
|
126
|
+
this.#features.decoders.push(match[1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse muxers
|
|
132
|
+
let muxerOutput = child_process.spawnSync(this.#binary, ['-muxers'], { env: process.env });
|
|
133
|
+
if (muxerOutput?.stdout !== null && muxerOutput.status === 0) {
|
|
134
|
+
this.#features.muxers = [];
|
|
135
|
+
let lines = String(muxerOutput.stdout).split('\n');
|
|
136
|
+
for (let line of lines) {
|
|
137
|
+
let match = line.match(/^\s*[E][A-Z.]*\s+([^\s]+)/);
|
|
138
|
+
if (match !== null) {
|
|
139
|
+
this.#features.muxers.push(match[1]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Parse demuxers
|
|
145
|
+
let demuxerOutput = child_process.spawnSync(this.#binary, ['-demuxers'], { env: process.env });
|
|
146
|
+
if (demuxerOutput?.stdout !== null && demuxerOutput.status === 0) {
|
|
147
|
+
this.#features.demuxers = [];
|
|
148
|
+
let lines = String(demuxerOutput.stdout).split('\n');
|
|
149
|
+
for (let line of lines) {
|
|
150
|
+
let match = line.match(/^\s*[D][A-Z.]*\s+([^\s]+)/);
|
|
151
|
+
if (match !== null) {
|
|
152
|
+
this.#features.demuxers.push(match[1]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
hasMinimumSupport(min = {}) {
|
|
159
|
+
if (typeof this.#version !== 'string') {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
typeof min?.version === 'string' &&
|
|
165
|
+
this.#version.localeCompare(min.version, undefined, {
|
|
166
|
+
numeric: true,
|
|
167
|
+
sensitivity: 'case',
|
|
168
|
+
caseFirst: 'upper',
|
|
169
|
+
}) === -1
|
|
170
|
+
) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let enc = this.#features.encoders || [];
|
|
175
|
+
let dec = this.#features.decoders || [];
|
|
176
|
+
let mux = this.#features.muxers || [];
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(min?.encoders) === true) {
|
|
179
|
+
for (let e of min.encoders) {
|
|
180
|
+
if (enc.includes(e) === false) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(min?.decoders) === true) {
|
|
187
|
+
for (let d of min.decoders) {
|
|
188
|
+
if (dec.includes(d) === false) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(min?.muxers) === true) {
|
|
195
|
+
for (let m of min.muxers) {
|
|
196
|
+
if (mux.includes(m) === false) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get binary() {
|
|
206
|
+
return this.#binary;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
get version() {
|
|
210
|
+
return this.#version;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get features() {
|
|
214
|
+
return this.#features;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get supportsHardwareH264() {
|
|
218
|
+
return (
|
|
219
|
+
this.#features?.h264_nvenc === true ||
|
|
220
|
+
this.#features?.h264_vaapi === true ||
|
|
221
|
+
this.#features?.h264_v4l2m2m === true ||
|
|
222
|
+
this.#features?.h264_qsv === true ||
|
|
223
|
+
this.#features?.h264_videotoolbox === true
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
get hardwareH264Codec() {
|
|
228
|
+
return this.#features?.hardwareH264Codec;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
createSession(uuid, sessionID, args, sessionType = 'default', errorCallback, pipeCount = 3) {
|
|
232
|
+
let key = String(uuid) + ':' + String(sessionID) + ':' + String(sessionType);
|
|
233
|
+
if (this.#sessions.has(key) === true) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Ensure at least 3 pipes (stdin, stdout, stderr)
|
|
238
|
+
if (pipeCount < 3) {
|
|
239
|
+
pipeCount = 3;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let stdio = Array.from({ length: pipeCount }, () => 'pipe');
|
|
243
|
+
let child = child_process.spawn(this.#binary, args, { stdio, env: process.env });
|
|
244
|
+
this.#sessions.set(key, child);
|
|
245
|
+
|
|
246
|
+
child?.stderr?.on?.('data', (data) => {
|
|
247
|
+
errorCallback?.(data);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
child?.on?.('exit', () => {
|
|
251
|
+
this.#sessions.delete(key);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Safely attach no-op .on('error') to prevent EPIPE crash
|
|
255
|
+
for (let i = 0; i < pipeCount; i++) {
|
|
256
|
+
child?.stdio?.[i]?.on?.('error', (error) => {
|
|
257
|
+
if (error?.code === 'EPIPE') {
|
|
258
|
+
// Empty
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Return stdin, stdout, stderr as named aliases, plus stdio array
|
|
264
|
+
return {
|
|
265
|
+
process: child,
|
|
266
|
+
stdin: stdio[0] === 'pipe' ? child.stdio[0] : undefined,
|
|
267
|
+
stdout: stdio[1] === 'pipe' ? child.stdio[1] : undefined,
|
|
268
|
+
stderr: stdio[2] === 'pipe' ? child.stdio[2] : undefined,
|
|
269
|
+
stdio: child.stdio, // gives access to [3], [4], etc.
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
killSession(uuid, sessionID, sessionType = 'default', signal = 'SIGTERM') {
|
|
274
|
+
let key = String(uuid) + ':' + String(sessionID) + ':' + String(sessionType);
|
|
275
|
+
let child = this.#sessions.get(key);
|
|
276
|
+
child?.kill?.(signal);
|
|
277
|
+
this.#sessions.delete(key);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
hasSession(uuid, sessionID, sessionType = 'default') {
|
|
281
|
+
let key = String(uuid) + ':' + String(sessionID) + ':' + String(sessionType);
|
|
282
|
+
return this.#sessions.has(key);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
listSessions() {
|
|
286
|
+
return Array.from(this.#sessions.keys());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
killAllSessions(uuid, signal = 'SIGKILL') {
|
|
290
|
+
for (let [key, child] of this.#sessions.entries()) {
|
|
291
|
+
if (key.startsWith(String(uuid) + ':') === true) {
|
|
292
|
+
child?.kill?.(signal);
|
|
293
|
+
this.#sessions.delete(key);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// Supports both Nest REST and Protobuf APIs for communication
|
|
19
19
|
//
|
|
20
|
-
// Code version 2025.
|
|
20
|
+
// Code version 2025.07.29
|
|
21
21
|
// Mark Hulskamp
|
|
22
22
|
'use strict';
|
|
23
23
|
|
|
@@ -28,9 +28,9 @@ HomeKitDevice.PLUGIN_NAME = 'homebridge-nest-accfactory';
|
|
|
28
28
|
HomeKitDevice.PLATFORM_NAME = 'NestAccfactory';
|
|
29
29
|
|
|
30
30
|
import HomeKitHistory from './HomeKitHistory.js';
|
|
31
|
-
HomeKitDevice.
|
|
31
|
+
HomeKitDevice.EVEHOME = HomeKitHistory;
|
|
32
32
|
|
|
33
33
|
export default (api) => {
|
|
34
|
-
// Register our platform with
|
|
34
|
+
// Register our platform with Homebridge
|
|
35
35
|
api.registerPlatform(HomeKitDevice.PLATFORM_NAME, NestAccfactory);
|
|
36
36
|
};
|