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/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.06.05
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.HISTORY = HomeKitHistory;
31
+ HomeKitDevice.EVEHOME = HomeKitHistory;
32
32
 
33
33
  export default (api) => {
34
- // Register our platform with HomeBridge
34
+ // Register our platform with Homebridge
35
35
  api.registerPlatform(HomeKitDevice.PLATFORM_NAME, NestAccfactory);
36
36
  };