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

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.
Files changed (125) hide show
  1. package/build/lib/HAPHostNode.js +183 -141
  2. package/build/lib/HAPServiceNode.js +199 -172
  3. package/build/lib/HAPServiceNode2.js +207 -172
  4. package/build/lib/NRCHKBError.js +23 -2
  5. package/build/lib/PairingQRCode.js +62 -0
  6. package/build/lib/Storage.js +157 -92
  7. package/build/lib/api.js +654 -288
  8. package/build/lib/camera/CameraControl.js +125 -0
  9. package/build/lib/camera/CameraDelegate.js +507 -0
  10. package/build/lib/camera/MP4StreamingServer.js +159 -0
  11. package/build/lib/hap/HAPCharacteristic.js +25 -4
  12. package/build/lib/hap/HAPService.js +25 -4
  13. package/build/lib/hap/eve-app/EveCharacteristics.js +124 -81
  14. package/build/lib/hap/eve-app/EveServices.js +50 -17
  15. package/build/lib/hap/hap-nodejs.js +32 -0
  16. package/build/lib/migration/HomeKitService2Migration.js +34 -0
  17. package/build/lib/migration/NodeMigration.js +75 -0
  18. package/build/lib/types/AccessoryInformationType.js +15 -1
  19. package/build/lib/types/CameraConfigType.js +15 -1
  20. package/build/lib/types/CustomCharacteristicType.js +15 -1
  21. package/build/lib/types/HAPHostConfigType.js +15 -1
  22. package/build/lib/types/HAPHostNodeType.js +15 -1
  23. package/build/lib/types/HAPService2ConfigType.js +15 -1
  24. package/build/lib/types/HAPService2NodeType.js +15 -1
  25. package/build/lib/types/HAPServiceConfigType.js +15 -1
  26. package/build/lib/types/HAPServiceNodeType.js +15 -1
  27. package/build/lib/types/HAPStatusConfigType.js +15 -1
  28. package/build/lib/types/HAPStatusNodeType.js +15 -1
  29. package/build/lib/types/HostType.js +28 -7
  30. package/build/lib/types/NodeType.js +15 -1
  31. package/build/lib/types/PublishTimersType.js +15 -1
  32. package/build/lib/types/UniFiControllerConfigType.js +16 -0
  33. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.js +28 -7
  34. package/build/lib/types/hap-nodejs/HapCategories.js +64 -43
  35. package/build/lib/types/storage/SerializedHostType.js +15 -1
  36. package/build/lib/types/storage/StorageType.js +34 -10
  37. package/build/lib/unifi/ProtectDiscovery.js +80 -0
  38. package/build/lib/utils/AccessoryUtils.js +152 -110
  39. package/build/lib/utils/BridgeUtils.js +82 -39
  40. package/build/lib/utils/CharacteristicUtils.js +5 -49
  41. package/build/lib/utils/CharacteristicUtils2.js +5 -49
  42. package/build/lib/utils/CharacteristicUtilsBase.js +81 -0
  43. package/build/lib/utils/NodeStatusUtils.js +89 -40
  44. package/build/lib/utils/ServiceUtils.js +434 -373
  45. package/build/lib/utils/ServiceUtils2.js +514 -307
  46. package/build/lib/utils/index.js +10 -11
  47. package/build/nodes/bridge.html +184 -166
  48. package/build/nodes/bridge.js +27 -9
  49. package/build/nodes/locales/en-US/node-red-contrib-homekit-bridged.json +22 -0
  50. package/build/nodes/nrchkb.html +1601 -88
  51. package/build/nodes/nrchkb.js +66 -88
  52. package/build/nodes/plugin-instance.html +499 -0
  53. package/build/nodes/plugin-instance.js +46 -0
  54. package/build/nodes/service.html +517 -299
  55. package/build/nodes/service.js +5 -6
  56. package/build/nodes/service2.html +1683 -460
  57. package/build/nodes/service2.js +5 -8
  58. package/build/nodes/standalone.html +187 -174
  59. package/build/nodes/standalone.js +27 -9
  60. package/build/nodes/status.html +51 -18
  61. package/build/nodes/status.js +47 -40
  62. package/build/nodes/unifi-controller.html +92 -0
  63. package/build/nodes/unifi-controller.js +20 -0
  64. package/build/plugins/embedded/homebridge-camera-ffmpeg/index.js +479 -0
  65. package/build/plugins/embedded/homebridge-unifi-protect/index.js +521 -0
  66. package/build/plugins/embedded/index.js +58 -0
  67. package/build/plugins/nrchkb-homekit-plugins.js +17 -0
  68. package/build/plugins/registry/index.js +203 -0
  69. package/build/plugins/registry/types.js +16 -0
  70. package/build/scripts/migrate-homekit-service-flows.js +47 -0
  71. package/examples/demo/01 - ALL Demos single import.json +1885 -1885
  72. package/examples/demo/02 - Air Purifier.json +279 -279
  73. package/examples/demo/03 - Air Quality sensor with Battery.json +254 -254
  74. package/examples/demo/04 - Dimmable Bulb.json +172 -172
  75. package/examples/demo/05 - Color Bulb (HSV).json +195 -195
  76. package/examples/demo/06 - Fan (simple, 3 speeds).json +240 -240
  77. package/examples/demo/07 - Fan (with speed, oscillate, rotation direction).json +175 -175
  78. package/examples/demo/08 - CO2 detector.json +224 -224
  79. package/examples/demo/09 - CO (carbon monoxide) example.json +255 -255
  80. package/examples/demo/10 - Door window contact sensor.json +234 -234
  81. package/examples/demos (advanced)/01 - Television with inputs and speaker.json +541 -541
  82. package/examples/switch/01 - Plain Switch.json +178 -178
  83. package/package.json +95 -84
  84. package/build/lib/HAPHostNode.d.ts +0 -1
  85. package/build/lib/HAPServiceNode.d.ts +0 -1
  86. package/build/lib/HAPServiceNode2.d.ts +0 -1
  87. package/build/lib/NRCHKBError.d.ts +0 -3
  88. package/build/lib/Storage.d.ts +0 -30
  89. package/build/lib/api.d.ts +0 -1
  90. package/build/lib/hap/HAPCharacteristic.d.ts +0 -9
  91. package/build/lib/hap/HAPService.d.ts +0 -6
  92. package/build/lib/hap/eve-app/EveCharacteristics.d.ts +0 -20
  93. package/build/lib/hap/eve-app/EveServices.d.ts +0 -5
  94. package/build/lib/types/AccessoryInformationType.d.ts +0 -11
  95. package/build/lib/types/CameraConfigType.d.ts +0 -24
  96. package/build/lib/types/CustomCharacteristicType.d.ts +0 -6
  97. package/build/lib/types/HAPHostConfigType.d.ts +0 -22
  98. package/build/lib/types/HAPHostNodeType.d.ts +0 -14
  99. package/build/lib/types/HAPService2ConfigType.d.ts +0 -6
  100. package/build/lib/types/HAPService2NodeType.d.ts +0 -7
  101. package/build/lib/types/HAPServiceConfigType.d.ts +0 -26
  102. package/build/lib/types/HAPServiceNodeType.d.ts +0 -38
  103. package/build/lib/types/HAPStatusConfigType.d.ts +0 -5
  104. package/build/lib/types/HAPStatusNodeType.d.ts +0 -12
  105. package/build/lib/types/HostType.d.ts +0 -5
  106. package/build/lib/types/NodeType.d.ts +0 -3
  107. package/build/lib/types/PublishTimersType.d.ts +0 -4
  108. package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.d.ts +0 -5
  109. package/build/lib/types/hap-nodejs/HapCategories.d.ts +0 -41
  110. package/build/lib/types/storage/SerializedHostType.d.ts +0 -5
  111. package/build/lib/types/storage/StorageType.d.ts +0 -8
  112. package/build/lib/utils/AccessoryUtils.d.ts +0 -1
  113. package/build/lib/utils/BridgeUtils.d.ts +0 -1
  114. package/build/lib/utils/CharacteristicUtils.d.ts +0 -1
  115. package/build/lib/utils/CharacteristicUtils2.d.ts +0 -1
  116. package/build/lib/utils/NodeStatusUtils.d.ts +0 -17
  117. package/build/lib/utils/ServiceUtils.d.ts +0 -1
  118. package/build/lib/utils/ServiceUtils2.d.ts +0 -1
  119. package/build/lib/utils/index.d.ts +0 -1
  120. package/build/nodes/bridge.d.ts +0 -1
  121. package/build/nodes/nrchkb.d.ts +0 -1
  122. package/build/nodes/service.d.ts +0 -1
  123. package/build/nodes/service2.d.ts +0 -1
  124. package/build/nodes/standalone.d.ts +0 -1
  125. package/build/nodes/status.d.ts +0 -1
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var CameraControl_exports = {};
20
+ __export(CameraControl_exports, {
21
+ configureCamera: () => configureCamera
22
+ });
23
+ module.exports = __toCommonJS(CameraControl_exports);
24
+ var import_hap_nodejs = require("@homebridge/hap-nodejs");
25
+ var import_CameraDelegate = require("./CameraDelegate");
26
+ const configureCamera = (accessory, config) => {
27
+ const streamDelegate = new import_CameraDelegate.CameraDelegate(accessory, config);
28
+ const cameraController = new import_hap_nodejs.CameraController({
29
+ cameraStreamCount: Math.max(1, config?.cameraConfigMaxStreams || 2),
30
+ // default 2
31
+ delegate: streamDelegate,
32
+ streamingOptions: {
33
+ // srtp: true, // legacy option which will just enable AES_CM_128_HMAC_SHA1_80 (can still be used though)
34
+ // iOS does not support NONE just there for testing with Wireshark, for example
35
+ supportedCryptoSuites: [
36
+ import_hap_nodejs.SRTPCryptoSuites.NONE,
37
+ import_hap_nodejs.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80
38
+ ],
39
+ video: {
40
+ codec: {
41
+ profiles: [
42
+ import_hap_nodejs.H264Profile.BASELINE,
43
+ import_hap_nodejs.H264Profile.MAIN,
44
+ import_hap_nodejs.H264Profile.HIGH
45
+ ],
46
+ levels: [
47
+ import_hap_nodejs.H264Level.LEVEL3_1,
48
+ import_hap_nodejs.H264Level.LEVEL3_2,
49
+ import_hap_nodejs.H264Level.LEVEL4_0
50
+ ]
51
+ },
52
+ resolutions: (() => {
53
+ const maxW = config?.cameraConfigMaxWidth;
54
+ const maxH = config?.cameraConfigMaxHeight;
55
+ const maxFps = config?.cameraConfigMaxFPS;
56
+ const defaults = [
57
+ [1920, 1080, 30],
58
+ [1280, 960, 30],
59
+ [1280, 720, 30],
60
+ [1024, 768, 30],
61
+ [640, 480, 30],
62
+ [640, 360, 30],
63
+ [480, 360, 30],
64
+ [480, 270, 30],
65
+ [320, 240, 30],
66
+ [320, 240, 15],
67
+ [320, 180, 30]
68
+ ];
69
+ return defaults.filter(
70
+ ([w, h]) => (!maxW || w <= maxW) && (!maxH || h <= maxH)
71
+ ).map(([w, h, f]) => [w, h, Math.min(f, maxFps || f)]);
72
+ })()
73
+ }
74
+ // audio options intentionally omitted here; delegate will honor config for RTP audio path
75
+ },
76
+ recording: {
77
+ options: {
78
+ prebufferLength: 4e3,
79
+ mediaContainerConfiguration: {
80
+ type: import_hap_nodejs.MediaContainerType.FRAGMENTED_MP4,
81
+ fragmentLength: 4e3
82
+ },
83
+ video: {
84
+ type: import_hap_nodejs.VideoCodecType.H264,
85
+ parameters: {
86
+ profiles: [import_hap_nodejs.H264Profile.HIGH],
87
+ levels: [import_hap_nodejs.H264Level.LEVEL4_0]
88
+ },
89
+ resolutions: [
90
+ [320, 180, 30],
91
+ [320, 240, 15],
92
+ [320, 240, 30],
93
+ [480, 270, 30],
94
+ [480, 360, 30],
95
+ [640, 360, 30],
96
+ [640, 480, 30],
97
+ [1280, 720, 30],
98
+ [1280, 960, 30],
99
+ [1920, 1080, 30],
100
+ [1600, 1200, 30]
101
+ ]
102
+ },
103
+ audio: {
104
+ codecs: {
105
+ type: import_hap_nodejs.AudioRecordingCodecType.AAC_ELD,
106
+ audioChannels: 1,
107
+ samplerate: import_hap_nodejs.AudioRecordingSamplerate.KHZ_48,
108
+ bitrateMode: import_hap_nodejs.AudioBitrate.VARIABLE
109
+ }
110
+ }
111
+ },
112
+ delegate: streamDelegate
113
+ },
114
+ sensors: {
115
+ motion: true,
116
+ occupancy: true
117
+ }
118
+ });
119
+ streamDelegate.controller = cameraController;
120
+ accessory.configureController(cameraController);
121
+ };
122
+ // Annotate the CommonJS export names for ESM import in node:
123
+ 0 && (module.exports = {
124
+ configureCamera
125
+ });
@@ -0,0 +1,507 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var CameraDelegate_exports = {};
30
+ __export(CameraDelegate_exports, {
31
+ CameraDelegate: () => CameraDelegate
32
+ });
33
+ module.exports = __toCommonJS(CameraDelegate_exports);
34
+ var import_node_assert = __toESM(require("node:assert"));
35
+ var import_node_child_process = require("node:child_process");
36
+ var import_hap_nodejs = require("@homebridge/hap-nodejs");
37
+ var import_logger = require("@nrchkb/logger");
38
+ var import_MP4StreamingServer = require("./MP4StreamingServer");
39
+ const FFMPEGH264ProfileNames = ["baseline", "main", "high"];
40
+ const FFMPEGH264LevelNames = ["3.1", "3.2", "4.0"];
41
+ const ports = /* @__PURE__ */ new Set();
42
+ const PENDING_SESSION_TIMEOUT_MS = 3e4;
43
+ function getPort() {
44
+ for (let i = 5011; ; i++) {
45
+ if (!ports.has(i)) {
46
+ ports.add(i);
47
+ return i;
48
+ }
49
+ }
50
+ }
51
+ function splitCommandLine(command) {
52
+ const args = [];
53
+ let current = "";
54
+ let quote = null;
55
+ let escaped = false;
56
+ for (const char of command.trim()) {
57
+ if (escaped) {
58
+ current += char;
59
+ escaped = false;
60
+ continue;
61
+ }
62
+ if (char === "\\") {
63
+ escaped = true;
64
+ continue;
65
+ }
66
+ if (quote) {
67
+ if (char === quote) {
68
+ quote = null;
69
+ } else {
70
+ current += char;
71
+ }
72
+ continue;
73
+ }
74
+ if (char === '"' || char === "'") {
75
+ quote = char;
76
+ continue;
77
+ }
78
+ if (char === " ") {
79
+ if (current) {
80
+ args.push(current);
81
+ current = "";
82
+ }
83
+ continue;
84
+ }
85
+ current += char;
86
+ }
87
+ if (current) {
88
+ args.push(current);
89
+ }
90
+ return args;
91
+ }
92
+ class CameraDelegate {
93
+ constructor(accessory, config) {
94
+ this.accessory = accessory;
95
+ this.config = config;
96
+ this.ffmpegDebugOutput = false;
97
+ this.log = (0, import_logger.logger)("NRCHKB", "CameraDelegate");
98
+ // keep track of sessions
99
+ this.pendingSessions = {};
100
+ this.ongoingSessions = {};
101
+ this.handlingStreamingRequest = false;
102
+ try {
103
+ const name = accessory?.displayName || accessory?.UUID || "UnknownAccessory";
104
+ this.log = (0, import_logger.logger)("NRCHKB", "CameraDelegate", name);
105
+ } catch (_) {
106
+ }
107
+ }
108
+ handleSnapshotRequest(request, callback) {
109
+ const ffmpegPath = this.config?.cameraConfigVideoProcessor || "ffmpeg";
110
+ const stillSource = this.config?.cameraConfigStillImageSource?.trim();
111
+ const mainSource = this.config?.cameraConfigSource?.trim();
112
+ let imageSource;
113
+ if (stillSource) {
114
+ imageSource = stillSource;
115
+ } else if (mainSource) {
116
+ imageSource = mainSource;
117
+ } else {
118
+ imageSource = `-f lavfi -i testsrc=s=${request.width}x${request.height}`;
119
+ }
120
+ const ffmpegCommand = `${imageSource} -t 1 -s ${request.width}x${request.height} -f image2 -`;
121
+ const ffmpeg = (0, import_node_child_process.spawn)(ffmpegPath, splitCommandLine(ffmpegCommand), {
122
+ env: process.env
123
+ });
124
+ const snapshotBuffers = [];
125
+ ffmpeg.stdout.on("data", (data) => snapshotBuffers.push(data));
126
+ ffmpeg.stderr.on("data", (data) => {
127
+ if (this.ffmpegDebugOutput || this.config?.cameraConfigDebug) {
128
+ this.log.debug(`SNAPSHOT: ${String(data)}`);
129
+ }
130
+ });
131
+ ffmpeg.on("exit", (code, signal) => {
132
+ if (signal) {
133
+ this.log.error(
134
+ `Snapshot process was killed with signal: ${signal}`
135
+ );
136
+ callback(new Error(`killed with signal ${signal}`));
137
+ } else if (code === 0) {
138
+ this.log.debug(
139
+ `Successfully captured snapshot at ${request.width}x${request.height}`
140
+ );
141
+ callback(void 0, Buffer.concat(snapshotBuffers));
142
+ } else {
143
+ this.log.error(`Snapshot process exited with code ${code}`);
144
+ callback(new Error(`Snapshot process exited with code ${code}`));
145
+ }
146
+ });
147
+ }
148
+ // called when iOS request rtp setup
149
+ prepareStream(request, callback) {
150
+ const sessionId = request.sessionID;
151
+ const targetAddress = request.targetAddress;
152
+ const video = request.video;
153
+ const videoCryptoSuite = video.srtpCryptoSuite;
154
+ const videoSrtpKey = video.srtp_key;
155
+ const videoSrtpSalt = video.srtp_salt;
156
+ const videoSSRC = import_hap_nodejs.CameraController.generateSynchronisationSource();
157
+ const localPort = getPort();
158
+ const timeout = setTimeout(() => {
159
+ const pendingSession = this.pendingSessions[sessionId];
160
+ if (!pendingSession) {
161
+ return;
162
+ }
163
+ ports.delete(pendingSession.localVideoPort);
164
+ delete this.pendingSessions[sessionId];
165
+ this.log.error(`Stream session ${sessionId} timed out before start`);
166
+ }, PENDING_SESSION_TIMEOUT_MS);
167
+ const sessionInfo = {
168
+ address: targetAddress,
169
+ videoPort: video.port,
170
+ localVideoPort: localPort,
171
+ videoCryptoSuite,
172
+ videoSRTP: Buffer.concat([videoSrtpKey, videoSrtpSalt]),
173
+ videoSSRC,
174
+ timeout
175
+ };
176
+ const response = {
177
+ video: {
178
+ port: localPort,
179
+ ssrc: videoSSRC,
180
+ srtp_key: videoSrtpKey,
181
+ srtp_salt: videoSrtpSalt
182
+ }
183
+ // audio is omitted as we do not support audio in this example
184
+ };
185
+ this.pendingSessions[sessionId] = sessionInfo;
186
+ callback(void 0, response);
187
+ }
188
+ // called when the iOS device asks stream to start/stop/reconfigure
189
+ handleStreamRequest(request, callback) {
190
+ const sessionId = request.sessionID;
191
+ switch (request.type) {
192
+ case import_hap_nodejs.StreamRequestTypes.START: {
193
+ const sessionInfo = this.pendingSessions[sessionId];
194
+ if (!sessionInfo) {
195
+ callback(
196
+ new Error(`No pending stream session ${sessionId}`)
197
+ );
198
+ break;
199
+ }
200
+ clearTimeout(sessionInfo.timeout);
201
+ const video = request.video;
202
+ const profile = FFMPEGH264ProfileNames[video.profile];
203
+ const level = FFMPEGH264LevelNames[video.level];
204
+ const width = video.width;
205
+ const height = video.height;
206
+ let fps = video.fps;
207
+ const payloadType = video.pt;
208
+ let maxBitrate = video.max_bit_rate;
209
+ const mtu = video.mtu;
210
+ const address = sessionInfo.address;
211
+ const videoPort = sessionInfo.videoPort;
212
+ const localVideoPort = sessionInfo.localVideoPort;
213
+ const ssrc = sessionInfo.videoSSRC;
214
+ const cryptoSuite = sessionInfo.videoCryptoSuite;
215
+ const videoSRTP = sessionInfo.videoSRTP.toString("base64");
216
+ const cfg = this.config;
217
+ const ffmpegPath = cfg?.cameraConfigVideoProcessor || "ffmpeg";
218
+ const source = cfg?.cameraConfigSource || `-f lavfi -i testsrc=s=${width}x${height}:r=${fps}`;
219
+ const vcodec = cfg?.cameraConfigVideoCodec || "libx264";
220
+ const additional = cfg?.cameraConfigAdditionalCommandLine?.trim();
221
+ const mapvideo = cfg?.cameraConfigMapVideo || "0:0";
222
+ const mapaudio = cfg?.cameraConfigMapAudio || "0:1";
223
+ const maxConfigBitrate = cfg?.cameraConfigMaxBitrate;
224
+ if (maxConfigBitrate && maxConfigBitrate > 0 && maxConfigBitrate < maxBitrate) {
225
+ maxBitrate = maxConfigBitrate;
226
+ }
227
+ const maxConfigFps = cfg?.cameraConfigMaxFPS;
228
+ if (maxConfigFps && maxConfigFps > 0 && maxConfigFps < fps) {
229
+ fps = maxConfigFps;
230
+ }
231
+ const vf = [];
232
+ if (cfg?.cameraConfigVideoFilter !== null && cfg?.cameraConfigVideoFilter !== void 0 && cfg?.cameraConfigVideoFilter !== "") {
233
+ vf.push(cfg.cameraConfigVideoFilter);
234
+ }
235
+ if (cfg?.cameraConfigHorizontalFlip) vf.push("hflip");
236
+ if (cfg?.cameraConfigVerticalFlip) vf.push("vflip");
237
+ this.log.debug(
238
+ `Starting video stream (${width}x${height}, ${fps} fps, ${maxBitrate} kbps, ${mtu} mtu)...`
239
+ );
240
+ let videoffmpegCommand = `${source} -map ${mapvideo} -vcodec ${vcodec} -pix_fmt yuv420p -r ${fps} -f rawvideo ${additional ? `${additional} ` : ""}${vf.length > 0 ? `-vf ${vf.join(",")} ` : ""}-b:v ${maxBitrate}k -bufsize ${maxBitrate}k -maxrate ${maxBitrate}k -profile:v ${profile} -level:v ${level} -payload_type ${payloadType} -ssrc ${ssrc} -f rtp `;
241
+ if (cryptoSuite !== import_hap_nodejs.SRTPCryptoSuites.NONE) {
242
+ let suite;
243
+ switch (cryptoSuite) {
244
+ case import_hap_nodejs.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80:
245
+ suite = "AES_CM_128_HMAC_SHA1_80";
246
+ break;
247
+ case import_hap_nodejs.SRTPCryptoSuites.AES_CM_256_HMAC_SHA1_80:
248
+ suite = "AES_CM_256_HMAC_SHA1_80";
249
+ break;
250
+ }
251
+ videoffmpegCommand += `-srtp_out_suite ${suite} -srtp_out_params ${videoSRTP} s`;
252
+ }
253
+ videoffmpegCommand += `rtp://${address}:${videoPort}?rtcpport=${videoPort}&localrtcpport=${localVideoPort}&pkt_size=${cfg?.cameraConfigPacketSize || mtu}`;
254
+ const audioEnabled = this.config?.cameraConfigAudio === true || this.config?.cameraConfigAudio === "true";
255
+ let audioffmpegCommand = "";
256
+ if (audioEnabled) {
257
+ const acodec = cfg?.cameraConfigAudioCodec || "libfdk_aac";
258
+ const abitrate = 32;
259
+ const asamplerate = 16;
260
+ const apayloadType = 110;
261
+ audioffmpegCommand = ` -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 -payload_type ${apayloadType} `;
262
+ }
263
+ if (this.ffmpegDebugOutput || this.config?.cameraConfigDebug) {
264
+ this.log.debug(
265
+ `FFMPEG command: ${ffmpegPath} ${videoffmpegCommand}${audioffmpegCommand}`
266
+ );
267
+ }
268
+ const cleanupOngoingSession = () => {
269
+ const ongoingSession = this.ongoingSessions[sessionId];
270
+ if (ongoingSession) {
271
+ ports.delete(ongoingSession.localVideoPort);
272
+ delete this.ongoingSessions[sessionId];
273
+ }
274
+ };
275
+ let callbackSettled = false;
276
+ const settleCallback = (error) => {
277
+ if (callbackSettled) {
278
+ return;
279
+ }
280
+ callbackSettled = true;
281
+ callback(error);
282
+ };
283
+ const ffmpegVideo = (0, import_node_child_process.spawn)(
284
+ ffmpegPath,
285
+ splitCommandLine(videoffmpegCommand + audioffmpegCommand),
286
+ {
287
+ env: process.env
288
+ }
289
+ );
290
+ let started = false;
291
+ ffmpegVideo.stderr.on("data", (data) => {
292
+ this.log.debug(data.toString("utf8"));
293
+ if (!started) {
294
+ started = true;
295
+ this.log.debug("FFMPEG: received first frame");
296
+ settleCallback();
297
+ }
298
+ if (this.ffmpegDebugOutput || this.config?.cameraConfigDebug) {
299
+ this.log.debug(`VIDEO: ${String(data)}`);
300
+ }
301
+ });
302
+ ffmpegVideo.on("error", (error) => {
303
+ this.log.error(
304
+ `[Video] Failed to start video stream: ${error.message}`
305
+ );
306
+ cleanupOngoingSession();
307
+ settleCallback(new Error("ffmpeg process creation failed!"));
308
+ });
309
+ ffmpegVideo.on("exit", (code, signal) => {
310
+ const message = "[Video] ffmpeg exited with code: " + code + " and signal: " + signal;
311
+ if (code == null || code === 255) {
312
+ this.log.debug(`${message} (Video stream stopped!)`);
313
+ } else {
314
+ this.log.error(`${message} (error)`);
315
+ if (!started) {
316
+ settleCallback(new Error(message));
317
+ } else {
318
+ this.controller?.forceStopStreamingSession(
319
+ sessionId
320
+ );
321
+ }
322
+ }
323
+ cleanupOngoingSession();
324
+ });
325
+ this.ongoingSessions[sessionId] = {
326
+ localVideoPort,
327
+ process: ffmpegVideo
328
+ };
329
+ delete this.pendingSessions[sessionId];
330
+ break;
331
+ }
332
+ case import_hap_nodejs.StreamRequestTypes.RECONFIGURE:
333
+ this.log.debug(
334
+ "Received (unsupported) request to reconfigure to: " + JSON.stringify(request.video)
335
+ );
336
+ callback();
337
+ break;
338
+ case import_hap_nodejs.StreamRequestTypes.STOP: {
339
+ const ongoingSession = this.ongoingSessions[sessionId];
340
+ const pendingSession = this.pendingSessions[sessionId];
341
+ if (pendingSession) {
342
+ clearTimeout(pendingSession.timeout);
343
+ ports.delete(pendingSession.localVideoPort);
344
+ delete this.pendingSessions[sessionId];
345
+ }
346
+ if (!ongoingSession) {
347
+ callback();
348
+ break;
349
+ }
350
+ ports.delete(ongoingSession.localVideoPort);
351
+ try {
352
+ ongoingSession.process.kill("SIGKILL");
353
+ } catch (e) {
354
+ this.log.error(
355
+ "Error occurred terminating the video process!"
356
+ );
357
+ this.log.error(String(e));
358
+ }
359
+ delete this.ongoingSessions[sessionId];
360
+ this.log.debug("Stopped streaming session!");
361
+ callback();
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ updateRecordingActive(active) {
367
+ this.log.debug(`Recording active set to ${active}`);
368
+ }
369
+ updateRecordingConfiguration(configuration) {
370
+ this.configuration = configuration;
371
+ this.log.debug(JSON.stringify(configuration));
372
+ }
373
+ /**
374
+ * This is a very minimal, very experimental example of how to implement fmp4 streaming with a
375
+ * CameraController supporting HomeKit Secure Video.
376
+ *
377
+ * An ideal implementation would diverge from this in the following ways:
378
+ * * It would implement a prebuffer and respect the recording `active` characteristic for that.
379
+ * * It would start to immediately record after a trigger event occurred and not just
380
+ * when the HomeKit Controller requests it (see the documentation of `CameraRecordingDelegate`).
381
+ */
382
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
383
+ async *handleRecordingStreamRequest(_streamId) {
384
+ (0, import_node_assert.default)(!!this.configuration);
385
+ const STOP_AFTER_MOTION_STOP = false;
386
+ this.handlingStreamingRequest = true;
387
+ (0, import_node_assert.default)(this.configuration.videoCodec.type === import_hap_nodejs.VideoCodecType.H264);
388
+ const profile = this.configuration.videoCodec.parameters.profile === import_hap_nodejs.H264Profile.HIGH ? "high" : this.configuration.videoCodec.parameters.profile === import_hap_nodejs.H264Profile.MAIN ? "main" : "baseline";
389
+ const level = this.configuration.videoCodec.parameters.level === import_hap_nodejs.H264Level.LEVEL4_0 ? "4.0" : this.configuration.videoCodec.parameters.level === import_hap_nodejs.H264Level.LEVEL3_2 ? "3.2" : "3.1";
390
+ const videoArgs = [
391
+ "-an",
392
+ "-sn",
393
+ "-dn",
394
+ "-codec:v",
395
+ "libx264",
396
+ "-pix_fmt",
397
+ "yuv420p",
398
+ "-profile:v",
399
+ profile,
400
+ "-level:v",
401
+ level,
402
+ "-b:v",
403
+ `${this.configuration.videoCodec.parameters.bitRate}k`,
404
+ "-force_key_frames",
405
+ `expr:eq(t,n_forced*${this.configuration.videoCodec.parameters.iFrameInterval / 1e3})`,
406
+ "-r",
407
+ this.configuration.videoCodec.resolution[2].toString()
408
+ ];
409
+ let samplerate;
410
+ switch (this.configuration.audioCodec.samplerate) {
411
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_8:
412
+ samplerate = "8";
413
+ break;
414
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_16:
415
+ samplerate = "16";
416
+ break;
417
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_24:
418
+ samplerate = "24";
419
+ break;
420
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_32:
421
+ samplerate = "32";
422
+ break;
423
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_44_1:
424
+ samplerate = "44.1";
425
+ break;
426
+ case import_hap_nodejs.AudioRecordingSamplerate.KHZ_48:
427
+ samplerate = "48";
428
+ break;
429
+ default:
430
+ throw new Error(
431
+ "Unsupported audio samplerate: " + this.configuration.audioCodec.samplerate
432
+ );
433
+ }
434
+ const audioArgs = this.controller?.recordingManagement?.recordingManagementService.getCharacteristic(
435
+ import_hap_nodejs.Characteristic.RecordingAudioActive
436
+ ) ? [
437
+ "-acodec",
438
+ "libfdk_aac",
439
+ ...this.configuration.audioCodec.type === import_hap_nodejs.AudioRecordingCodecType.AAC_LC ? ["-profile:a", "aac_low"] : ["-profile:a", "aac_eld"],
440
+ "-ar",
441
+ `${samplerate}k`,
442
+ "-b:a",
443
+ `${this.configuration.audioCodec.bitrate}k`,
444
+ "-ac",
445
+ `${this.configuration.audioCodec.audioChannels}`
446
+ ] : [];
447
+ this.server = new import_MP4StreamingServer.MP4StreamingServer(
448
+ "ffmpeg",
449
+ `-f lavfi -i testsrc=s=${this.configuration.videoCodec.resolution[0]}x${this.configuration.videoCodec.resolution[1]}:r=${this.configuration.videoCodec.resolution[2]}`.split(
450
+ / /g
451
+ ),
452
+ audioArgs,
453
+ videoArgs
454
+ );
455
+ await this.server.start();
456
+ if (!this.server || this.server.destroyed) {
457
+ return;
458
+ }
459
+ const pending = [];
460
+ try {
461
+ for await (const box of this.server.generator()) {
462
+ pending.push(box.header, box.data);
463
+ const motionDetected = this.accessory.getService(import_hap_nodejs.Service.MotionSensor)?.getCharacteristic(import_hap_nodejs.Characteristic.MotionDetected).value;
464
+ this.log.debug(
465
+ `mp4 box type ${box.type} and length ${box.length}`
466
+ );
467
+ if (box.type === "moov" || box.type === "mdat") {
468
+ const fragment = Buffer.concat(pending);
469
+ pending.splice(0, pending.length);
470
+ const isLast = STOP_AFTER_MOTION_STOP && !motionDetected;
471
+ yield {
472
+ data: fragment,
473
+ isLast
474
+ };
475
+ if (isLast) {
476
+ this.log.debug("Ending session due to motion stopped!");
477
+ break;
478
+ }
479
+ }
480
+ }
481
+ } catch (error) {
482
+ if (!error.message.startsWith("FFMPEG")) {
483
+ this.log.error(
484
+ `Encountered unexpected error on generator ${error.stack}`
485
+ );
486
+ }
487
+ }
488
+ }
489
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
490
+ closeRecordingStream(streamId, reason) {
491
+ if (reason) {
492
+ this.log.error(`Closing stream ${streamId} due to error: ${reason}`);
493
+ }
494
+ if (this.server) {
495
+ this.server.destroy();
496
+ this.server = void 0;
497
+ }
498
+ this.handlingStreamingRequest = false;
499
+ }
500
+ acknowledgeStream(streamId) {
501
+ this.closeRecordingStream(streamId);
502
+ }
503
+ }
504
+ // Annotate the CommonJS export names for ESM import in node:
505
+ 0 && (module.exports = {
506
+ CameraDelegate
507
+ });