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.
- 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 +157 -92
- package/build/lib/api.js +654 -288
- package/build/lib/camera/CameraControl.js +125 -0
- package/build/lib/camera/CameraDelegate.js +507 -0
- package/build/lib/camera/MP4StreamingServer.js +159 -0
- 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 +434 -373
- package/build/lib/utils/ServiceUtils2.js +514 -307
- package/build/lib/utils/index.js +10 -11
- package/build/nodes/bridge.html +184 -166
- 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 +1601 -88
- package/build/nodes/nrchkb.js +66 -88
- package/build/nodes/plugin-instance.html +499 -0
- package/build/nodes/plugin-instance.js +46 -0
- package/build/nodes/service.html +517 -299
- package/build/nodes/service.js +5 -6
- package/build/nodes/service2.html +1683 -460
- package/build/nodes/service2.js +5 -8
- package/build/nodes/standalone.html +187 -174
- 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/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
|
@@ -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
|
+
});
|