node-red-contrib-homekit-bridged 2.0.0-dev.5 → 2.0.0-dev.8
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/HAPHostNode.js +183 -141
- package/build/lib/HAPServiceNode.js +199 -172
- package/build/lib/HAPServiceNode2.js +207 -172
- package/build/lib/NRCHKBError.js +23 -2
- package/build/lib/PairingQRCode.js +62 -0
- package/build/lib/Storage.js +152 -90
- package/build/lib/api.js +654 -288
- package/build/lib/camera/CameraControl.js +119 -84
- package/build/lib/camera/CameraDelegate.js +481 -404
- package/build/lib/camera/MP4StreamingServer.js +148 -139
- package/build/lib/hap/HAPCharacteristic.js +25 -4
- package/build/lib/hap/HAPService.js +25 -4
- package/build/lib/hap/eve-app/EveCharacteristics.js +124 -81
- package/build/lib/hap/eve-app/EveServices.js +50 -17
- package/build/lib/hap/hap-nodejs.js +32 -0
- package/build/lib/migration/HomeKitService2Migration.js +34 -0
- package/build/lib/migration/NodeMigration.js +75 -0
- package/build/lib/types/AccessoryInformationType.js +15 -1
- package/build/lib/types/CameraConfigType.js +15 -1
- package/build/lib/types/CustomCharacteristicType.js +15 -1
- package/build/lib/types/HAPHostConfigType.js +15 -1
- package/build/lib/types/HAPHostNodeType.js +15 -1
- package/build/lib/types/HAPService2ConfigType.js +15 -1
- package/build/lib/types/HAPService2NodeType.js +15 -1
- package/build/lib/types/HAPServiceConfigType.js +15 -1
- package/build/lib/types/HAPServiceNodeType.js +15 -1
- package/build/lib/types/HAPStatusConfigType.js +15 -1
- package/build/lib/types/HAPStatusNodeType.js +15 -1
- package/build/lib/types/HostType.js +28 -7
- package/build/lib/types/NodeType.js +15 -1
- package/build/lib/types/PublishTimersType.js +15 -1
- package/build/lib/types/UniFiControllerConfigType.js +16 -0
- package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.js +28 -7
- package/build/lib/types/hap-nodejs/HapCategories.js +64 -43
- package/build/lib/types/storage/SerializedHostType.js +15 -1
- package/build/lib/types/storage/StorageType.js +34 -10
- package/build/lib/unifi/ProtectDiscovery.js +80 -0
- package/build/lib/utils/AccessoryUtils.js +152 -110
- package/build/lib/utils/BridgeUtils.js +82 -39
- package/build/lib/utils/CharacteristicUtils.js +5 -49
- package/build/lib/utils/CharacteristicUtils2.js +5 -49
- package/build/lib/utils/CharacteristicUtilsBase.js +81 -0
- package/build/lib/utils/NodeStatusUtils.js +89 -40
- package/build/lib/utils/ServiceUtils.js +433 -375
- package/build/lib/utils/ServiceUtils2.js +519 -309
- package/build/lib/utils/index.js +10 -11
- package/build/nodes/bridge.html +206 -168
- package/build/nodes/bridge.js +27 -9
- package/build/nodes/locales/en-US/node-red-contrib-homekit-bridged.json +22 -0
- package/build/nodes/nrchkb.html +1753 -117
- package/build/nodes/nrchkb.js +66 -88
- package/build/nodes/plugin-instance.html +509 -0
- package/build/nodes/plugin-instance.js +46 -0
- package/build/nodes/service.html +544 -307
- package/build/nodes/service.js +5 -6
- package/build/nodes/service2.html +1721 -455
- package/build/nodes/service2.js +5 -8
- package/build/nodes/standalone.html +208 -176
- package/build/nodes/standalone.js +27 -9
- package/build/nodes/status.html +51 -18
- package/build/nodes/status.js +47 -40
- package/build/nodes/unifi-controller.html +92 -0
- package/build/nodes/unifi-controller.js +20 -0
- package/build/plugins/embedded/homebridge-camera-ffmpeg/index.js +479 -0
- package/build/plugins/embedded/homebridge-unifi-protect/index.js +521 -0
- package/build/plugins/embedded/index.js +58 -0
- package/build/plugins/nrchkb-homekit-plugins.js +17 -0
- package/build/plugins/registry/index.js +203 -0
- package/build/plugins/registry/types.js +16 -0
- package/build/scripts/migrate-homekit-service-flows.js +47 -0
- package/examples/demo/01 - ALL Demos single import.json +1885 -1885
- package/examples/demo/02 - Air Purifier.json +279 -279
- package/examples/demo/03 - Air Quality sensor with Battery.json +254 -254
- package/examples/demo/04 - Dimmable Bulb.json +172 -172
- package/examples/demo/05 - Color Bulb (HSV).json +195 -195
- package/examples/demo/06 - Fan (simple, 3 speeds).json +240 -240
- package/examples/demo/07 - Fan (with speed, oscillate, rotation direction).json +175 -175
- package/examples/demo/08 - CO2 detector.json +224 -224
- package/examples/demo/09 - CO (carbon monoxide) example.json +255 -255
- package/examples/demo/10 - Door window contact sensor.json +234 -234
- package/examples/demos (advanced)/01 - Television with inputs and speaker.json +541 -541
- package/examples/switch/01 - Plain Switch.json +178 -178
- package/package.json +95 -84
- package/build/lib/HAPHostNode.d.ts +0 -1
- package/build/lib/HAPServiceNode.d.ts +0 -1
- package/build/lib/HAPServiceNode2.d.ts +0 -1
- package/build/lib/NRCHKBError.d.ts +0 -3
- package/build/lib/Storage.d.ts +0 -30
- package/build/lib/api.d.ts +0 -1
- package/build/lib/camera/CameraControl.d.ts +0 -3
- package/build/lib/camera/CameraDelegate.d.ts +0 -38
- package/build/lib/camera/MP4StreamingServer.d.ts +0 -26
- package/build/lib/hap/HAPCharacteristic.d.ts +0 -9
- package/build/lib/hap/HAPService.d.ts +0 -6
- package/build/lib/hap/eve-app/EveCharacteristics.d.ts +0 -20
- package/build/lib/hap/eve-app/EveServices.d.ts +0 -5
- package/build/lib/types/AccessoryInformationType.d.ts +0 -11
- package/build/lib/types/CameraConfigType.d.ts +0 -24
- package/build/lib/types/CustomCharacteristicType.d.ts +0 -6
- package/build/lib/types/HAPHostConfigType.d.ts +0 -22
- package/build/lib/types/HAPHostNodeType.d.ts +0 -14
- package/build/lib/types/HAPService2ConfigType.d.ts +0 -6
- package/build/lib/types/HAPService2NodeType.d.ts +0 -7
- package/build/lib/types/HAPServiceConfigType.d.ts +0 -26
- package/build/lib/types/HAPServiceNodeType.d.ts +0 -38
- package/build/lib/types/HAPStatusConfigType.d.ts +0 -5
- package/build/lib/types/HAPStatusNodeType.d.ts +0 -12
- package/build/lib/types/HostType.d.ts +0 -5
- package/build/lib/types/NodeType.d.ts +0 -3
- package/build/lib/types/PublishTimersType.d.ts +0 -4
- package/build/lib/types/hap-nodejs/HapAdaptiveLightingControllerMode.d.ts +0 -5
- package/build/lib/types/hap-nodejs/HapCategories.d.ts +0 -41
- package/build/lib/types/storage/SerializedHostType.d.ts +0 -5
- package/build/lib/types/storage/StorageType.d.ts +0 -8
- package/build/lib/utils/AccessoryUtils.d.ts +0 -1
- package/build/lib/utils/BridgeUtils.d.ts +0 -1
- package/build/lib/utils/CharacteristicUtils.d.ts +0 -1
- package/build/lib/utils/CharacteristicUtils2.d.ts +0 -1
- package/build/lib/utils/NodeStatusUtils.d.ts +0 -17
- package/build/lib/utils/ServiceUtils.d.ts +0 -1
- package/build/lib/utils/ServiceUtils2.d.ts +0 -1
- package/build/lib/utils/index.d.ts +0 -1
- package/build/nodes/bridge.d.ts +0 -1
- package/build/nodes/nrchkb.d.ts +0 -1
- package/build/nodes/service.d.ts +0 -1
- package/build/nodes/service2.d.ts +0 -1
- package/build/nodes/standalone.d.ts +0 -1
- package/build/nodes/status.d.ts +0 -1
|
@@ -1,430 +1,507 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 });
|
|
9
11
|
};
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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]); }
|
|
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;
|
|
21
19
|
};
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
exports
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
35
43
|
function getPort() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
41
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;
|
|
42
91
|
}
|
|
43
92
|
class CameraDelegate {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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;
|
|
55
199
|
}
|
|
56
|
-
|
|
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;
|
|
57
226
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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`;
|
|
67
252
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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} `;
|
|
70
262
|
}
|
|
71
|
-
|
|
72
|
-
|
|
263
|
+
if (this.ffmpegDebugOutput || this.config?.cameraConfigDebug) {
|
|
264
|
+
this.log.debug(
|
|
265
|
+
`FFMPEG command: ${ffmpegPath} ${videoffmpegCommand}${audioffmpegCommand}`
|
|
266
|
+
);
|
|
73
267
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
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
|
+
{
|
|
76
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
|
+
}
|
|
77
301
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
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!"));
|
|
85
308
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
);
|
|
98
321
|
}
|
|
322
|
+
}
|
|
323
|
+
cleanupOngoingSession();
|
|
99
324
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
}
|
|
325
|
+
this.ongoingSessions[sessionId] = {
|
|
326
|
+
localVideoPort,
|
|
327
|
+
process: ffmpegVideo
|
|
125
328
|
};
|
|
126
|
-
this.pendingSessions[sessionId]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
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];
|
|
283
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
|
+
}
|
|
284
364
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
);
|
|
291
433
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
});
|
|
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;
|
|
415
458
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
479
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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}`);
|
|
425
493
|
}
|
|
426
|
-
|
|
427
|
-
|
|
494
|
+
if (this.server) {
|
|
495
|
+
this.server.destroy();
|
|
496
|
+
this.server = void 0;
|
|
428
497
|
}
|
|
498
|
+
this.handlingStreamingRequest = false;
|
|
499
|
+
}
|
|
500
|
+
acknowledgeStream(streamId) {
|
|
501
|
+
this.closeRecordingStream(streamId);
|
|
502
|
+
}
|
|
429
503
|
}
|
|
430
|
-
|
|
504
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
505
|
+
0 && (module.exports = {
|
|
506
|
+
CameraDelegate
|
|
507
|
+
});
|