node-red-contrib-homekit-bridged 2.0.0-dev.4 → 2.0.0-dev.5

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/build/lib/api.js CHANGED
@@ -94,7 +94,7 @@ module.exports = (RED) => {
94
94
  }
95
95
  }
96
96
  catch (e) {
97
- console.error(e);
97
+ log.error(e);
98
98
  }
99
99
  }
100
100
  else if (releaseVersionFound) {
@@ -108,7 +108,7 @@ module.exports = (RED) => {
108
108
  }
109
109
  }
110
110
  catch (e) {
111
- console.error(e);
111
+ log.error(e);
112
112
  }
113
113
  }
114
114
  else {
@@ -0,0 +1,3 @@
1
+ import { type Accessory } from '@homebridge/hap-nodejs';
2
+ import type CameraConfigType from '../types/CameraConfigType';
3
+ export declare const configureCamera: (accessory: Accessory, config?: CameraConfigType) => void;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureCamera = void 0;
4
+ const hap_nodejs_1 = require("@homebridge/hap-nodejs");
5
+ const CameraDelegate_1 = require("./CameraDelegate");
6
+ const configureCamera = (accessory, config) => {
7
+ const streamDelegate = new CameraDelegate_1.CameraDelegate(accessory, config);
8
+ const cameraController = new hap_nodejs_1.CameraController({
9
+ cameraStreamCount: Math.max(1, (config === null || config === void 0 ? void 0 : config.cameraConfigMaxStreams) || 2),
10
+ delegate: streamDelegate,
11
+ streamingOptions: {
12
+ supportedCryptoSuites: [
13
+ 2,
14
+ 0
15
+ ],
16
+ video: {
17
+ codec: {
18
+ profiles: [0, 1, 2],
19
+ levels: [0, 1, 2]
20
+ },
21
+ resolutions: (() => {
22
+ const maxW = config === null || config === void 0 ? void 0 : config.cameraConfigMaxWidth;
23
+ const maxH = config === null || config === void 0 ? void 0 : config.cameraConfigMaxHeight;
24
+ const maxFps = config === null || config === void 0 ? void 0 : config.cameraConfigMaxFPS;
25
+ const defaults = [
26
+ [1920, 1080, 30],
27
+ [1280, 960, 30],
28
+ [1280, 720, 30],
29
+ [1024, 768, 30],
30
+ [640, 480, 30],
31
+ [640, 360, 30],
32
+ [480, 360, 30],
33
+ [480, 270, 30],
34
+ [320, 240, 30],
35
+ [320, 240, 15],
36
+ [320, 180, 30]
37
+ ];
38
+ return defaults
39
+ .filter(([w, h]) => (!maxW || w <= maxW) && (!maxH || h <= maxH))
40
+ .map(([w, h, f]) => [w, h, Math.min(f, maxFps || f)]);
41
+ })()
42
+ }
43
+ },
44
+ recording: {
45
+ options: {
46
+ prebufferLength: 4000,
47
+ mediaContainerConfiguration: {
48
+ type: 0,
49
+ fragmentLength: 4000
50
+ },
51
+ video: {
52
+ type: 0,
53
+ parameters: {
54
+ profiles: [2],
55
+ levels: [2]
56
+ },
57
+ resolutions: [
58
+ [320, 180, 30],
59
+ [320, 240, 15],
60
+ [320, 240, 30],
61
+ [480, 270, 30],
62
+ [480, 360, 30],
63
+ [640, 360, 30],
64
+ [640, 480, 30],
65
+ [1280, 720, 30],
66
+ [1280, 960, 30],
67
+ [1920, 1080, 30],
68
+ [1600, 1200, 30]
69
+ ]
70
+ },
71
+ audio: {
72
+ codecs: {
73
+ type: 1,
74
+ audioChannels: 1,
75
+ samplerate: 5,
76
+ bitrateMode: 0
77
+ }
78
+ }
79
+ },
80
+ delegate: streamDelegate
81
+ },
82
+ sensors: {
83
+ motion: true,
84
+ occupancy: true
85
+ }
86
+ });
87
+ streamDelegate.controller = cameraController;
88
+ accessory.configureController(cameraController);
89
+ };
90
+ exports.configureCamera = configureCamera;
@@ -0,0 +1,38 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { type Accessory, CameraController, type CameraRecordingConfiguration, type CameraRecordingDelegate, type CameraStreamingDelegate, type HDSProtocolSpecificErrorReason, type PrepareStreamCallback, type PrepareStreamRequest, type RecordingPacket, type SnapshotRequest, type SnapshotRequestCallback, SRTPCryptoSuites, type StreamingRequest, type StreamRequestCallback } from '@homebridge/hap-nodejs';
3
+ import type CameraConfigType from '../types/CameraConfigType';
4
+ import { MP4StreamingServer } from './MP4StreamingServer';
5
+ type SessionInfo = {
6
+ address: string;
7
+ videoPort: number;
8
+ localVideoPort: number;
9
+ videoCryptoSuite: SRTPCryptoSuites;
10
+ videoSRTP: Buffer;
11
+ videoSSRC: number;
12
+ };
13
+ type OngoingSession = {
14
+ localVideoPort: number;
15
+ process: ChildProcess;
16
+ };
17
+ export declare class CameraDelegate implements CameraStreamingDelegate, CameraRecordingDelegate {
18
+ private accessory;
19
+ private config?;
20
+ private ffmpegDebugOutput;
21
+ private log;
22
+ controller?: CameraController;
23
+ pendingSessions: Record<string, SessionInfo>;
24
+ ongoingSessions: Record<string, OngoingSession>;
25
+ configuration?: CameraRecordingConfiguration;
26
+ handlingStreamingRequest: boolean;
27
+ server?: MP4StreamingServer;
28
+ constructor(accessory: Accessory, config?: CameraConfigType | undefined);
29
+ handleSnapshotRequest(request: SnapshotRequest, callback: SnapshotRequestCallback): void;
30
+ prepareStream(request: PrepareStreamRequest, callback: PrepareStreamCallback): void;
31
+ handleStreamRequest(request: StreamingRequest, callback: StreamRequestCallback): void;
32
+ updateRecordingActive(active: boolean): void;
33
+ updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void;
34
+ handleRecordingStreamRequest(_streamId: number): AsyncGenerator<RecordingPacket>;
35
+ closeRecordingStream(streamId: number, reason?: HDSProtocolSpecificErrorReason): void;
36
+ acknowledgeStream(streamId: number): void;
37
+ }
38
+ export {};
@@ -0,0 +1,430 @@
1
+ "use strict";
2
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
3
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
4
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
5
+ var m = o[Symbol.asyncIterator], i;
6
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
7
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
8
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
9
+ };
10
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
11
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
12
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
13
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
14
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
15
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
16
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
17
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
18
+ function fulfill(value) { resume("next", value); }
19
+ function reject(value) { resume("throw", value); }
20
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
21
+ };
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.CameraDelegate = void 0;
27
+ const node_assert_1 = __importDefault(require("node:assert"));
28
+ const node_child_process_1 = require("node:child_process");
29
+ const hap_nodejs_1 = require("@homebridge/hap-nodejs");
30
+ const logger_1 = require("@nrchkb/logger");
31
+ const MP4StreamingServer_1 = require("./MP4StreamingServer");
32
+ const FFMPEGH264ProfileNames = ['baseline', 'main', 'high'];
33
+ const FFMPEGH264LevelNames = ['3.1', '3.2', '4.0'];
34
+ const ports = new Set();
35
+ function getPort() {
36
+ for (let i = 5011;; i++) {
37
+ if (!ports.has(i)) {
38
+ ports.add(i);
39
+ return i;
40
+ }
41
+ }
42
+ }
43
+ class CameraDelegate {
44
+ constructor(accessory, config) {
45
+ this.accessory = accessory;
46
+ this.config = config;
47
+ this.ffmpegDebugOutput = false;
48
+ this.log = (0, logger_1.logger)('NRCHKB', 'CameraDelegate');
49
+ this.pendingSessions = {};
50
+ this.ongoingSessions = {};
51
+ this.handlingStreamingRequest = false;
52
+ try {
53
+ const name = (accessory === null || accessory === void 0 ? void 0 : accessory.displayName) || (accessory === null || accessory === void 0 ? void 0 : accessory.UUID) || 'UnknownAccessory';
54
+ this.log = (0, logger_1.logger)('NRCHKB', 'CameraDelegate', name);
55
+ }
56
+ catch (_) {
57
+ }
58
+ }
59
+ handleSnapshotRequest(request, callback) {
60
+ var _a, _b, _c, _d, _e;
61
+ const ffmpegPath = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.cameraConfigVideoProcessor) || 'ffmpeg';
62
+ const stillSource = (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.cameraConfigStillImageSource) === null || _c === void 0 ? void 0 : _c.trim();
63
+ const mainSource = (_e = (_d = this.config) === null || _d === void 0 ? void 0 : _d.cameraConfigSource) === null || _e === void 0 ? void 0 : _e.trim();
64
+ let imageSource;
65
+ if (stillSource) {
66
+ imageSource = stillSource;
67
+ }
68
+ else if (mainSource) {
69
+ imageSource = mainSource;
70
+ }
71
+ else {
72
+ imageSource = `-f lavfi -i testsrc=s=${request.width}x${request.height}`;
73
+ }
74
+ const ffmpegCommand = `${imageSource} -t 1 -s ${request.width}x${request.height} -f image2 -`;
75
+ const ffmpeg = (0, node_child_process_1.spawn)(ffmpegPath, ffmpegCommand.split(' '), {
76
+ env: process.env
77
+ });
78
+ const snapshotBuffers = [];
79
+ ffmpeg.stdout.on('data', (data) => snapshotBuffers.push(data));
80
+ ffmpeg.stderr.on('data', (data) => {
81
+ var _a;
82
+ if (this.ffmpegDebugOutput || ((_a = this.config) === null || _a === void 0 ? void 0 : _a.cameraConfigDebug)) {
83
+ this.log.debug(`SNAPSHOT: ${String(data)}`);
84
+ }
85
+ });
86
+ ffmpeg.on('exit', (code, signal) => {
87
+ if (signal) {
88
+ this.log.error(`Snapshot process was killed with signal: ${signal}`);
89
+ callback(new Error(`killed with signal ${signal}`));
90
+ }
91
+ else if (code === 0) {
92
+ this.log.debug(`Successfully captured snapshot at ${request.width}x${request.height}`);
93
+ callback(undefined, Buffer.concat(snapshotBuffers));
94
+ }
95
+ else {
96
+ this.log.error(`Snapshot process exited with code ${code}`);
97
+ callback(new Error(`Snapshot process exited with code ${code}`));
98
+ }
99
+ });
100
+ }
101
+ prepareStream(request, callback) {
102
+ const sessionId = request.sessionID;
103
+ const targetAddress = request.targetAddress;
104
+ const video = request.video;
105
+ const videoCryptoSuite = video.srtpCryptoSuite;
106
+ const videoSrtpKey = video.srtp_key;
107
+ const videoSrtpSalt = video.srtp_salt;
108
+ const videoSSRC = hap_nodejs_1.CameraController.generateSynchronisationSource();
109
+ const localPort = getPort();
110
+ const sessionInfo = {
111
+ address: targetAddress,
112
+ videoPort: video.port,
113
+ localVideoPort: localPort,
114
+ videoCryptoSuite: videoCryptoSuite,
115
+ videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]),
116
+ videoSSRC: videoSSRC
117
+ };
118
+ const response = {
119
+ video: {
120
+ port: localPort,
121
+ ssrc: videoSSRC,
122
+ srtp_key: videoSrtpKey,
123
+ srtp_salt: videoSrtpSalt
124
+ }
125
+ };
126
+ this.pendingSessions[sessionId] = sessionInfo;
127
+ callback(undefined, response);
128
+ }
129
+ handleStreamRequest(request, callback) {
130
+ var _a, _b, _c, _d;
131
+ const sessionId = request.sessionID;
132
+ switch (request.type) {
133
+ case "start": {
134
+ const sessionInfo = this.pendingSessions[sessionId];
135
+ const video = request.video;
136
+ const profile = FFMPEGH264ProfileNames[video.profile];
137
+ const level = FFMPEGH264LevelNames[video.level];
138
+ const width = video.width;
139
+ const height = video.height;
140
+ let fps = video.fps;
141
+ const payloadType = video.pt;
142
+ let maxBitrate = video.max_bit_rate;
143
+ const mtu = video.mtu;
144
+ const address = sessionInfo.address;
145
+ const videoPort = sessionInfo.videoPort;
146
+ const localVideoPort = sessionInfo.localVideoPort;
147
+ const ssrc = sessionInfo.videoSSRC;
148
+ const cryptoSuite = sessionInfo.videoCryptoSuite;
149
+ const videoSRTP = sessionInfo.videoSRTP.toString('base64');
150
+ const cfg = this.config;
151
+ const ffmpegPath = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigVideoProcessor) || 'ffmpeg';
152
+ const source = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigSource) ||
153
+ `-f lavfi -i testsrc=s=${width}x${height}:r=${fps}`;
154
+ const vcodec = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigVideoCodec) || 'libx264';
155
+ const additional = (_a = cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigAdditionalCommandLine) === null || _a === void 0 ? void 0 : _a.trim();
156
+ const mapvideo = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigMapVideo) || '0:0';
157
+ const mapaudio = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigMapAudio) || '0:1';
158
+ const maxConfigBitrate = cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigMaxBitrate;
159
+ if (maxConfigBitrate &&
160
+ maxConfigBitrate > 0 &&
161
+ maxConfigBitrate < maxBitrate) {
162
+ maxBitrate = maxConfigBitrate;
163
+ }
164
+ const maxConfigFps = cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigMaxFPS;
165
+ if (maxConfigFps && maxConfigFps > 0 && maxConfigFps < fps) {
166
+ fps = maxConfigFps;
167
+ }
168
+ const vf = [];
169
+ if ((cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigVideoFilter) !== null &&
170
+ (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigVideoFilter) !== undefined &&
171
+ (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigVideoFilter) !== '') {
172
+ vf.push(cfg.cameraConfigVideoFilter);
173
+ if (cfg.cameraConfigHorizontalFlip)
174
+ vf.push('hflip');
175
+ if (cfg.cameraConfigVerticalFlip)
176
+ vf.push('vflip');
177
+ }
178
+ this.log.debug(`Starting video stream (${width}x${height}, ${fps} fps, ${maxBitrate} kbps, ${mtu} mtu)...`);
179
+ let videoffmpegCommand = `${source} -map ${mapvideo} -vcodec ${vcodec} -pix_fmt yuv420p -r ${fps} -f rawvideo ` +
180
+ `${additional ? `${additional} ` : ''}` +
181
+ `${vf.length > 0 ? `-vf ${vf.join(',')} ` : ''}` +
182
+ `-b:v ${maxBitrate}k -bufsize ${maxBitrate}k -maxrate ${maxBitrate}k ` +
183
+ `-profile:v ${profile} -level:v ${level} ` +
184
+ `-payload_type ${payloadType} -ssrc ${ssrc} -f rtp `;
185
+ if (cryptoSuite !== 2) {
186
+ let suite;
187
+ switch (cryptoSuite) {
188
+ case 0:
189
+ suite = 'AES_CM_128_HMAC_SHA1_80';
190
+ break;
191
+ case 1:
192
+ suite = 'AES_CM_256_HMAC_SHA1_80';
193
+ break;
194
+ }
195
+ videoffmpegCommand += `-srtp_out_suite ${suite} -srtp_out_params ${videoSRTP} s`;
196
+ }
197
+ videoffmpegCommand += `rtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${localVideoPort}&pkt_size=${(cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigPacketSize) || mtu}`;
198
+ const audioEnabled = ((_b = this.config) === null || _b === void 0 ? void 0 : _b.cameraConfigAudio) === true ||
199
+ ((_c = this.config) === null || _c === void 0 ? void 0 : _c.cameraConfigAudio) === 'true';
200
+ let audioffmpegCommand = '';
201
+ if (audioEnabled) {
202
+ const acodec = (cfg === null || cfg === void 0 ? void 0 : cfg.cameraConfigAudioCodec) || 'libfdk_aac';
203
+ const abitrate = 32;
204
+ const asamplerate = 16;
205
+ const apayloadType = 110;
206
+ audioffmpegCommand =
207
+ ` -map ${mapaudio} -acodec ${acodec} -profile:a aac_eld -flags +global_header -f null -ar ${asamplerate}k -b:a ${abitrate}k -bufsize ${abitrate}k -ac 1 ` +
208
+ `-payload_type ${apayloadType} `;
209
+ }
210
+ if (this.ffmpegDebugOutput || ((_d = this.config) === null || _d === void 0 ? void 0 : _d.cameraConfigDebug)) {
211
+ this.log.debug(`FFMPEG command: ${ffmpegPath} ${videoffmpegCommand}${audioffmpegCommand}`);
212
+ }
213
+ const ffmpegVideo = (0, node_child_process_1.spawn)(ffmpegPath, (videoffmpegCommand + audioffmpegCommand).split(' '), {
214
+ env: process.env
215
+ });
216
+ let started = false;
217
+ ffmpegVideo.stderr.on('data', (data) => {
218
+ var _a;
219
+ this.log.debug(data.toString('utf8'));
220
+ if (!started) {
221
+ started = true;
222
+ this.log.debug('FFMPEG: received first frame');
223
+ callback();
224
+ }
225
+ if (this.ffmpegDebugOutput || ((_a = this.config) === null || _a === void 0 ? void 0 : _a.cameraConfigDebug)) {
226
+ this.log.debug(`VIDEO: ${String(data)}`);
227
+ }
228
+ });
229
+ ffmpegVideo.on('error', (error) => {
230
+ this.log.error(`[Video] Failed to start video stream: ${error.message}`);
231
+ callback(new Error('ffmpeg process creation failed!'));
232
+ });
233
+ ffmpegVideo.on('exit', (code, signal) => {
234
+ var _a;
235
+ const message = '[Video] ffmpeg exited with code: ' +
236
+ code +
237
+ ' and signal: ' +
238
+ signal;
239
+ if (code == null || code === 255) {
240
+ this.log.debug(`${message} (Video stream stopped!)`);
241
+ }
242
+ else {
243
+ this.log.error(`${message} (error)`);
244
+ if (!started) {
245
+ callback(new Error(message));
246
+ }
247
+ else {
248
+ (_a = this.controller) === null || _a === void 0 ? void 0 : _a.forceStopStreamingSession(sessionId);
249
+ }
250
+ }
251
+ });
252
+ this.ongoingSessions[sessionId] = {
253
+ localVideoPort: localVideoPort,
254
+ process: ffmpegVideo
255
+ };
256
+ delete this.pendingSessions[sessionId];
257
+ break;
258
+ }
259
+ case "reconfigure":
260
+ this.log.debug('Received (unsupported) request to reconfigure to: ' +
261
+ JSON.stringify(request.video));
262
+ callback();
263
+ break;
264
+ case "stop": {
265
+ const ongoingSession = this.ongoingSessions[sessionId];
266
+ if (!ongoingSession) {
267
+ callback();
268
+ break;
269
+ }
270
+ ports.delete(ongoingSession.localVideoPort);
271
+ try {
272
+ ongoingSession.process.kill('SIGKILL');
273
+ }
274
+ catch (e) {
275
+ this.log.error('Error occurred terminating the video process!');
276
+ this.log.error(String(e));
277
+ }
278
+ delete this.ongoingSessions[sessionId];
279
+ this.log.debug('Stopped streaming session!');
280
+ callback();
281
+ break;
282
+ }
283
+ }
284
+ }
285
+ updateRecordingActive(active) {
286
+ this.log.debug(`Recording active set to ${active}`);
287
+ }
288
+ updateRecordingConfiguration(configuration) {
289
+ this.configuration = configuration;
290
+ this.log.debug(JSON.stringify(configuration));
291
+ }
292
+ handleRecordingStreamRequest(_streamId) {
293
+ return __asyncGenerator(this, arguments, function* handleRecordingStreamRequest_1() {
294
+ var _a, e_1, _b, _c;
295
+ var _d, _e, _f;
296
+ (0, node_assert_1.default)(!!this.configuration);
297
+ const STOP_AFTER_MOTION_STOP = false;
298
+ this.handlingStreamingRequest = true;
299
+ (0, node_assert_1.default)(this.configuration.videoCodec.type === 0);
300
+ const profile = this.configuration.videoCodec.parameters.profile === 2
301
+ ? 'high'
302
+ : this.configuration.videoCodec.parameters.profile === 1
303
+ ? 'main'
304
+ : 'baseline';
305
+ const level = this.configuration.videoCodec.parameters.level === 2
306
+ ? '4.0'
307
+ : this.configuration.videoCodec.parameters.level === 1
308
+ ? '3.2'
309
+ : '3.1';
310
+ const videoArgs = [
311
+ '-an',
312
+ '-sn',
313
+ '-dn',
314
+ '-codec:v',
315
+ 'libx264',
316
+ '-pix_fmt',
317
+ 'yuv420p',
318
+ '-profile:v',
319
+ profile,
320
+ '-level:v',
321
+ level,
322
+ '-b:v',
323
+ `${this.configuration.videoCodec.parameters.bitRate}k`,
324
+ '-force_key_frames',
325
+ `expr:eq(t,n_forced*${this.configuration.videoCodec.parameters.iFrameInterval / 1000})`,
326
+ '-r',
327
+ this.configuration.videoCodec.resolution[2].toString()
328
+ ];
329
+ let samplerate;
330
+ switch (this.configuration.audioCodec.samplerate) {
331
+ case 0:
332
+ samplerate = '8';
333
+ break;
334
+ case 1:
335
+ samplerate = '16';
336
+ break;
337
+ case 2:
338
+ samplerate = '24';
339
+ break;
340
+ case 3:
341
+ samplerate = '32';
342
+ break;
343
+ case 4:
344
+ samplerate = '44.1';
345
+ break;
346
+ case 5:
347
+ samplerate = '48';
348
+ break;
349
+ default:
350
+ throw new Error('Unsupported audio samplerate: ' +
351
+ this.configuration.audioCodec.samplerate);
352
+ }
353
+ const audioArgs = ((_e = (_d = this.controller) === null || _d === void 0 ? void 0 : _d.recordingManagement) === null || _e === void 0 ? void 0 : _e.recordingManagementService.getCharacteristic(hap_nodejs_1.Characteristic.RecordingAudioActive))
354
+ ? [
355
+ '-acodec',
356
+ 'libfdk_aac',
357
+ ...(this.configuration.audioCodec.type ===
358
+ 0
359
+ ? ['-profile:a', 'aac_low']
360
+ : ['-profile:a', 'aac_eld']),
361
+ '-ar',
362
+ `${samplerate}k`,
363
+ '-b:a',
364
+ `${this.configuration.audioCodec.bitrate}k`,
365
+ '-ac',
366
+ `${this.configuration.audioCodec.audioChannels}`
367
+ ]
368
+ : [];
369
+ this.server = new MP4StreamingServer_1.MP4StreamingServer('ffmpeg', `-f lavfi -i \
370
+ testsrc=s=${this.configuration.videoCodec.resolution[0]}x${this.configuration.videoCodec.resolution[1]}:r=${this.configuration.videoCodec.resolution[2]}`.split(/ /g), audioArgs, videoArgs);
371
+ yield __await(this.server.start());
372
+ if (!this.server || this.server.destroyed) {
373
+ return yield __await(void 0);
374
+ }
375
+ const pending = [];
376
+ try {
377
+ try {
378
+ for (var _g = true, _h = __asyncValues(this.server.generator()), _j; _j = yield __await(_h.next()), _a = _j.done, !_a; _g = true) {
379
+ _c = _j.value;
380
+ _g = false;
381
+ const box = _c;
382
+ pending.push(box.header, box.data);
383
+ const motionDetected = (_f = this.accessory
384
+ .getService(hap_nodejs_1.Service.MotionSensor)) === null || _f === void 0 ? void 0 : _f.getCharacteristic(hap_nodejs_1.Characteristic.MotionDetected).value;
385
+ this.log.debug(`mp4 box type ${box.type} and length ${box.length}`);
386
+ if (box.type === 'moov' || box.type === 'mdat') {
387
+ const fragment = Buffer.concat(pending);
388
+ pending.splice(0, pending.length);
389
+ const isLast = STOP_AFTER_MOTION_STOP && !motionDetected;
390
+ yield yield __await({
391
+ data: fragment,
392
+ isLast: isLast
393
+ });
394
+ if (isLast) {
395
+ this.log.debug('Ending session due to motion stopped!');
396
+ break;
397
+ }
398
+ }
399
+ }
400
+ }
401
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
402
+ finally {
403
+ try {
404
+ if (!_g && !_a && (_b = _h.return)) yield __await(_b.call(_h));
405
+ }
406
+ finally { if (e_1) throw e_1.error; }
407
+ }
408
+ }
409
+ catch (error) {
410
+ if (!error.message.startsWith('FFMPEG')) {
411
+ this.log.error(`Encountered unexpected error on generator ${error.stack}`);
412
+ }
413
+ }
414
+ });
415
+ }
416
+ closeRecordingStream(streamId, reason) {
417
+ if (reason) {
418
+ this.log.error(`Closing stream ${streamId} due to error: ${reason}`);
419
+ }
420
+ if (this.server) {
421
+ this.server.destroy();
422
+ this.server = undefined;
423
+ }
424
+ this.handlingStreamingRequest = false;
425
+ }
426
+ acknowledgeStream(streamId) {
427
+ this.closeRecordingStream(streamId);
428
+ }
429
+ }
430
+ exports.CameraDelegate = CameraDelegate;
@@ -0,0 +1,26 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ import { type Server, type Socket } from 'node:net';
3
+ interface MP4Atom {
4
+ header: Buffer;
5
+ length: number;
6
+ type: string;
7
+ data: Buffer;
8
+ }
9
+ export declare class MP4StreamingServer {
10
+ readonly server: Server;
11
+ readonly debugMode: boolean;
12
+ readonly ffmpegPath: string;
13
+ readonly args: string[];
14
+ socket?: Socket;
15
+ childProcess?: ChildProcess;
16
+ destroyed: boolean;
17
+ connectPromise: Promise<void>;
18
+ connectResolve?: () => void;
19
+ constructor(ffmpegPath: string, ffmpegInput: Array<string>, audioOutputArgs: Array<string>, videoOutputArgs: Array<string>);
20
+ start(): Promise<void>;
21
+ destroy(): void;
22
+ handleConnection(socket: Socket): void;
23
+ generator(): AsyncGenerator<MP4Atom>;
24
+ read(length: number): Promise<Buffer>;
25
+ }
26
+ export {};
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
12
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
13
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
14
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
15
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
16
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
17
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
18
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
19
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
20
+ function fulfill(value) { resume("next", value); }
21
+ function reject(value) { resume("throw", value); }
22
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.MP4StreamingServer = void 0;
26
+ const node_child_process_1 = require("node:child_process");
27
+ const node_events_1 = require("node:events");
28
+ const node_net_1 = require("node:net");
29
+ const logger_1 = require("@nrchkb/logger");
30
+ const log = (0, logger_1.logger)('NRCHKB', 'MP4StreamingServer');
31
+ class MP4StreamingServer {
32
+ constructor(ffmpegPath, ffmpegInput, audioOutputArgs, videoOutputArgs) {
33
+ this.debugMode = false;
34
+ this.destroyed = false;
35
+ this.connectPromise = new Promise((resolve) => {
36
+ this.connectResolve = resolve;
37
+ });
38
+ this.server = (0, node_net_1.createServer)(this.handleConnection.bind(this));
39
+ this.ffmpegPath = ffmpegPath;
40
+ this.args = [];
41
+ this.args.push(...ffmpegInput);
42
+ this.args.push(...audioOutputArgs);
43
+ this.args.push('-f', 'mp4');
44
+ this.args.push(...videoOutputArgs);
45
+ this.args.push('-fflags', '+genpts', '-reset_timestamps', '1');
46
+ this.args.push('-movflags', 'frag_keyframe+empty_moov+default_base_moof');
47
+ }
48
+ start() {
49
+ return __awaiter(this, void 0, void 0, function* () {
50
+ var _a, _b;
51
+ const promise = (0, node_events_1.once)(this.server, 'listening');
52
+ this.server.listen();
53
+ yield promise;
54
+ if (this.destroyed) {
55
+ return;
56
+ }
57
+ const port = this.server.address().port;
58
+ this.args.push(`tcp://127.0.0.1:${port}`);
59
+ log.debug(`${this.ffmpegPath} ${this.args.join(' ')}`);
60
+ this.childProcess = (0, node_child_process_1.spawn)(this.ffmpegPath, this.args, {
61
+ env: process.env,
62
+ stdio: this.debugMode ? 'pipe' : 'ignore'
63
+ });
64
+ if (!this.childProcess) {
65
+ log.error('ChildProcess is undefined directly after the init!');
66
+ }
67
+ if (this.debugMode) {
68
+ (_a = this.childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => log.debug(data.toString()));
69
+ (_b = this.childProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => log.debug(data.toString()));
70
+ }
71
+ });
72
+ }
73
+ destroy() {
74
+ var _a, _b;
75
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.destroy();
76
+ (_b = this.childProcess) === null || _b === void 0 ? void 0 : _b.kill();
77
+ this.socket = undefined;
78
+ this.childProcess = undefined;
79
+ this.destroyed = true;
80
+ }
81
+ handleConnection(socket) {
82
+ var _a;
83
+ this.server.close();
84
+ this.socket = socket;
85
+ (_a = this.connectResolve) === null || _a === void 0 ? void 0 : _a.call(this);
86
+ }
87
+ generator() {
88
+ return __asyncGenerator(this, arguments, function* generator_1() {
89
+ yield __await(this.connectPromise);
90
+ if (!this.socket || !this.childProcess) {
91
+ log.error('Socket undefined ' +
92
+ !!this.socket +
93
+ ' childProcess undefined ' +
94
+ !!this.childProcess);
95
+ throw new Error('Unexpected state!');
96
+ }
97
+ while (true) {
98
+ const header = yield __await(this.read(8));
99
+ const length = header.readInt32BE(0) - 8;
100
+ const type = header.subarray(4).toString();
101
+ const data = yield __await(this.read(length));
102
+ yield yield __await({
103
+ header: header,
104
+ length: length,
105
+ type: type,
106
+ data: data
107
+ });
108
+ }
109
+ });
110
+ }
111
+ read(length) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ if (!this.socket) {
114
+ throw Error('FFMPEG tried reading from closed socket!');
115
+ }
116
+ if (!length) {
117
+ return Buffer.alloc(0);
118
+ }
119
+ const value = this.socket.read(length);
120
+ if (value) {
121
+ return value;
122
+ }
123
+ return new Promise((resolve, reject) => {
124
+ const readHandler = () => {
125
+ var _a;
126
+ const value = (_a = this.socket) === null || _a === void 0 ? void 0 : _a.read(length);
127
+ if (value) {
128
+ cleanup();
129
+ resolve(value);
130
+ }
131
+ };
132
+ const endHandler = () => {
133
+ cleanup();
134
+ reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`));
135
+ };
136
+ const cleanup = () => {
137
+ var _a, _b;
138
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeListener('readable', readHandler);
139
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.removeListener('close', endHandler);
140
+ };
141
+ if (!this.socket) {
142
+ throw new Error('FFMPEG socket is closed now!');
143
+ }
144
+ this.socket.on('readable', readHandler);
145
+ this.socket.on('close', endHandler);
146
+ });
147
+ });
148
+ }
149
+ }
150
+ exports.MP4StreamingServer = MP4StreamingServer;
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const util = __importStar(require("node:util"));
40
40
  const hap_nodejs_1 = require("@homebridge/hap-nodejs");
41
41
  const logger_1 = require("@nrchkb/logger");
42
+ const CameraControl_1 = require("../camera/CameraControl");
42
43
  const NRCHKBError_1 = __importDefault(require("../NRCHKBError"));
43
44
  module.exports = (node) => {
44
45
  const log = (0, logger_1.logger)('NRCHKB', 'ServiceUtils', node.config.name, node);
@@ -267,13 +268,14 @@ module.exports = (node) => {
267
268
  }
268
269
  return service;
269
270
  };
270
- const configureCameraSource = (_accessory, _service, config) => {
271
+ const configureCameraSource = (accessory, _service, config) => {
271
272
  if (config.cameraConfigSource) {
272
273
  log.debug('Configuring Camera Source');
273
274
  if (!config.cameraConfigVideoProcessor) {
274
275
  log.error('Missing configuration for CameraControl: videoProcessor cannot be empty!');
275
276
  }
276
277
  else {
278
+ (0, CameraControl_1.configureCamera)(accessory, config);
277
279
  }
278
280
  }
279
281
  else {
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const hap_nodejs_1 = require("@homebridge/hap-nodejs");
7
7
  const logger_1 = require("@nrchkb/logger");
8
+ const CameraControl_1 = require("../camera/CameraControl");
8
9
  const NRCHKBError_1 = __importDefault(require("../NRCHKBError"));
9
10
  const Storage_1 = require("../Storage");
10
11
  module.exports = (node) => {
@@ -254,13 +255,14 @@ module.exports = (node) => {
254
255
  }
255
256
  return service;
256
257
  };
257
- const configureCameraSource = (_accessory, _service, config) => {
258
+ const configureCameraSource = (accessory, _service, config) => {
258
259
  if (config.cameraConfigSource) {
259
260
  log.debug('Configuring Camera Source');
260
261
  if (!config.cameraConfigVideoProcessor) {
261
262
  log.error('Missing configuration for CameraControl: videoProcessor cannot be empty!');
262
263
  }
263
264
  else {
265
+ (0, CameraControl_1.configureCamera)(accessory, config);
264
266
  }
265
267
  }
266
268
  else {
@@ -83,11 +83,11 @@ module.exports = (RED) => {
83
83
  log.error('node-red restart highly recommended');
84
84
  log.trace(error);
85
85
  }
86
- if (process.env.NRCHKB_EXPERIMENTAL === 'true') {
87
- log.debug('Registering nrchkb type');
88
- RED.nodes.registerType('nrchkb', function (config) {
89
- RED.nodes.createNode(this, config);
90
- });
91
- }
92
86
  });
87
+ if (process.env.NRCHKB_EXPERIMENTAL === 'true') {
88
+ log.debug('Registering nrchkb type');
89
+ RED.nodes.registerType('nrchkb', function (config) {
90
+ RED.nodes.createNode(this, config);
91
+ });
92
+ }
93
93
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-homekit-bridged",
3
- "version": "2.0.0-dev.4",
3
+ "version": "2.0.0-dev.5",
4
4
  "description": "Node-RED nodes to simulate Apple HomeKit devices.",
5
5
  "main": "build/nodes/nrchkb.js",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "test:watch": "vitest",
12
12
  "format": "npx @biomejs/biome check --write",
13
13
  "lint": "npx @biomejs/biome check",
14
- "prepare": "husky install"
14
+ "prepare": "husky || true"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",