homebridge-nest-accfactory 0.0.6 → 0.2.0
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/CHANGELOG.md +37 -3
- package/README.md +37 -19
- package/dist/HomeKitDevice.js +132 -109
- package/dist/camera.js +344 -263
- package/dist/doorbell.js +5 -3
- package/dist/floodlight.js +3 -3
- package/dist/nexustalk.js +62 -36
- package/dist/protect.js +2 -2
- package/dist/protobuf/googlehome/foyer.proto +216 -160
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_transfer.h264 +0 -0
- package/dist/streamer.js +78 -37
- package/dist/system.js +1321 -1263
- package/dist/thermostat.js +73 -27
- package/dist/webrtc.js +582 -0
- package/package.json +31 -29
package/dist/streamer.js
CHANGED
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
// streamer.talkingAudio(talkingData)
|
|
14
14
|
// streamer.update(deviceData) <- call super after
|
|
15
15
|
//
|
|
16
|
-
//
|
|
16
|
+
// The following defines should be overriden in your class which extends this
|
|
17
|
+
//
|
|
18
|
+
// blankAudio - Buffer containing a blank audio segment for the type of audio being used
|
|
19
|
+
//
|
|
20
|
+
// Code version 29/9/2024
|
|
17
21
|
// Mark Hulskamp
|
|
18
22
|
'use strict';
|
|
19
23
|
|
|
@@ -27,9 +31,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
27
31
|
// Define constants
|
|
28
32
|
const CAMERAOFFLINEH264FILE = 'Nest_camera_offline.h264'; // Camera offline H264 frame file
|
|
29
33
|
const CAMERAOFFH264FILE = 'Nest_camera_off.h264'; // Camera off H264 frame file
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
34
|
+
const CAMERATRANSFERJPGFILE = 'Nest_camera_transfer.jpg'; // Camera transferring H264 frame file
|
|
35
|
+
const TALKBACKAUDIOTIMEOUT = 1000;
|
|
36
|
+
const H264NALSTARTCODE = Buffer.from([0x00, 0x00, 0x00, 0x01]);
|
|
33
37
|
|
|
34
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
35
39
|
|
|
@@ -39,13 +43,20 @@ export default class Streamer {
|
|
|
39
43
|
audioEnabled = undefined; // Audio from camera enabled or not
|
|
40
44
|
online = undefined; // Camera online or not
|
|
41
45
|
uuid = undefined; // UUID of the device connecting
|
|
42
|
-
connected =
|
|
46
|
+
connected = undefined; // Stream endpoint connection: undefined = not connected , false = connecting , true = connected and streaming
|
|
47
|
+
blankAudio = undefined; // Blank audio 'frame'
|
|
48
|
+
codecs = {
|
|
49
|
+
video: undefined, // Video codec being used
|
|
50
|
+
audio: undefined, // Audio codec being used
|
|
51
|
+
talk: undefined, // Talking codec being used
|
|
52
|
+
};
|
|
43
53
|
|
|
44
54
|
// Internal data only for this class
|
|
45
55
|
#outputTimer = undefined; // Timer for non-blocking loop to stream output data
|
|
46
56
|
#outputs = {}; // Output streams ie: buffer, live, record
|
|
47
57
|
#cameraOfflineFrame = undefined; // Camera offline video frame
|
|
48
58
|
#cameraVideoOffFrame = undefined; // Video turned off on camera video frame
|
|
59
|
+
#cameraTransferringFrame = undefined; // Camera transferring between Nest/Google Home video frame
|
|
49
60
|
|
|
50
61
|
constructor(deviceData, options) {
|
|
51
62
|
// Setup logger object if passed as option
|
|
@@ -60,10 +71,11 @@ export default class Streamer {
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
// Store data we need from the device data passed it
|
|
74
|
+
this.migrating = deviceData?.migrating === true;
|
|
63
75
|
this.online = deviceData?.online === true;
|
|
64
76
|
this.videoEnabled = deviceData?.streaming_enabled === true;
|
|
65
77
|
this.audioEnabled = deviceData?.audio_enabled === true;
|
|
66
|
-
this.uuid = deviceData?.
|
|
78
|
+
this.uuid = deviceData?.nest_google_uuid;
|
|
67
79
|
|
|
68
80
|
// Setup location for *.h264 frame files. This can be overriden by a passed in option
|
|
69
81
|
let resourcePath = path.resolve(__dirname + '/res'); // Default location for *.h264 files
|
|
@@ -75,22 +87,19 @@ export default class Streamer {
|
|
|
75
87
|
resourcePath = path.resolve(options.resourcePath);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
// load buffer for camera offline
|
|
90
|
+
// load buffer for camera offline in .h264 frame
|
|
79
91
|
if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE)) === true) {
|
|
80
92
|
this.#cameraOfflineFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE));
|
|
81
|
-
// remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
|
|
82
|
-
if (this.#cameraOfflineFrame.indexOf(H264NALStartcode) === 0) {
|
|
83
|
-
this.#cameraOfflineFrame = this.#cameraOfflineFrame.subarray(H264NALStartcode.length);
|
|
84
|
-
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
// load buffer for camera stream off
|
|
95
|
+
// load buffer for camera stream off in .h264 frame
|
|
88
96
|
if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE)) === true) {
|
|
89
97
|
this.#cameraVideoOffFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE));
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// load buffer for camera transferring in .h264 frame
|
|
101
|
+
if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERATRANSFERJPGFILE)) === true) {
|
|
102
|
+
this.#cameraTransferringFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERATRANSFERJPGFILE));
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
// Start a non-blocking loop for output to the various streams which connect to our streamer object
|
|
@@ -100,20 +109,26 @@ export default class Streamer {
|
|
|
100
109
|
let lastTimeVideo = Date.now();
|
|
101
110
|
this.#outputTimer = setInterval(() => {
|
|
102
111
|
let dateNow = Date.now();
|
|
103
|
-
let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; //
|
|
112
|
+
let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; // 30fps
|
|
104
113
|
Object.values(this.#outputs).forEach((output) => {
|
|
105
|
-
// Monitor for camera going offline
|
|
114
|
+
// Monitor for camera going offline, video enabled/disabled or being transferred between Nest/Google Home
|
|
106
115
|
// We'll insert the appropriate video frame into the stream
|
|
107
116
|
if (this.online === false && this.#cameraOfflineFrame !== undefined && outputVideoFrame === true) {
|
|
108
117
|
// Camera is offline so feed in our custom h264 frame and AAC silence
|
|
109
|
-
output.buffer.push({ type: 'video',
|
|
110
|
-
output.buffer.push({ type: 'audio',
|
|
118
|
+
output.buffer.push({ time: dateNow, type: 'video', data: this.#cameraOfflineFrame });
|
|
119
|
+
output.buffer.push({ time: dateNow, type: 'audio', data: this.blankAudio });
|
|
111
120
|
lastTimeVideo = dateNow;
|
|
112
121
|
}
|
|
113
122
|
if (this.online === true && this.videoEnabled === false && this.#cameraVideoOffFrame !== undefined && outputVideoFrame === true) {
|
|
114
123
|
// Camera video is turned off so feed in our custom h264 frame and AAC silence
|
|
115
|
-
output.buffer.push({ type: 'video',
|
|
116
|
-
output.buffer.push({ type: 'audio',
|
|
124
|
+
output.buffer.push({ time: dateNow, type: 'video', data: this.#cameraVideoOffFrame });
|
|
125
|
+
output.buffer.push({ time: dateNow, type: 'audio', data: this.blankAudio });
|
|
126
|
+
lastTimeVideo = dateNow;
|
|
127
|
+
}
|
|
128
|
+
if (this.migrating === true && this.#cameraTransferringFrame !== undefined && outputVideoFrame === true) {
|
|
129
|
+
// Camera video is turned off so feed in our custom h264 frame and AAC silence
|
|
130
|
+
output.buffer.push({ time: dateNow, type: 'video', data: this.#cameraTransferringFrame });
|
|
131
|
+
output.buffer.push({ time: dateNow, type: 'audio', data: this.blankAudio });
|
|
117
132
|
lastTimeVideo = dateNow;
|
|
118
133
|
}
|
|
119
134
|
|
|
@@ -124,14 +139,14 @@ export default class Streamer {
|
|
|
124
139
|
output.buffer.shift();
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
// Output the packet data to any
|
|
142
|
+
// Output the packet data to any 'live' or 'recording' streams
|
|
128
143
|
if (output.type === 'live' || output.type === 'record') {
|
|
129
144
|
let packet = output.buffer.shift();
|
|
130
145
|
if (packet?.type === 'video' && typeof output?.video?.write === 'function') {
|
|
131
146
|
// H264 NAL Units '0001' are required to be added to beginning of any video data we output
|
|
132
147
|
// If this is missing, add on beginning of data packet
|
|
133
|
-
if (packet.data.indexOf(
|
|
134
|
-
packet.data = Buffer.concat([
|
|
148
|
+
if (packet.data.indexOf(H264NALSTARTCODE) !== 0) {
|
|
149
|
+
packet.data = Buffer.concat([H264NALSTARTCODE, packet.data]);
|
|
135
150
|
}
|
|
136
151
|
output.video.write(packet.data);
|
|
137
152
|
}
|
|
@@ -151,7 +166,7 @@ export default class Streamer {
|
|
|
151
166
|
startBuffering() {
|
|
152
167
|
if (this.#outputs?.buffer === undefined) {
|
|
153
168
|
// No active buffer session, start connection to streamer
|
|
154
|
-
if (this.connected ===
|
|
169
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
155
170
|
this?.log?.debug && this.log.debug('Started buffering for uuid "%s"', this.uuid);
|
|
156
171
|
this.connect();
|
|
157
172
|
}
|
|
@@ -189,21 +204,21 @@ export default class Streamer {
|
|
|
189
204
|
if (typeof this.talkingAudio === 'function') {
|
|
190
205
|
this.talkingAudio(data);
|
|
191
206
|
|
|
192
|
-
|
|
207
|
+
clearTimeout(talkbackTimeout);
|
|
193
208
|
talkbackTimeout = setTimeout(() => {
|
|
194
|
-
// no audio received in
|
|
209
|
+
// no audio received in 1000ms, so mark end of stream
|
|
195
210
|
this.talkingAudio(Buffer.alloc(0));
|
|
196
|
-
},
|
|
211
|
+
}, TALKBACKAUDIOTIMEOUT);
|
|
197
212
|
}
|
|
198
213
|
});
|
|
199
214
|
}
|
|
200
215
|
|
|
201
|
-
if (this.connected ===
|
|
216
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
202
217
|
// We do not have an active connection, so startup connection
|
|
203
218
|
this.connect();
|
|
204
219
|
}
|
|
205
220
|
|
|
206
|
-
// Add video/audio streams for our output loop to handle outputting
|
|
221
|
+
// Add video/audio streams for our output loop to handle outputting
|
|
207
222
|
this.#outputs[sessionID] = {
|
|
208
223
|
type: 'live',
|
|
209
224
|
video: videoStream,
|
|
@@ -236,12 +251,12 @@ export default class Streamer {
|
|
|
236
251
|
});
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
if (this.connected ===
|
|
254
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
240
255
|
// We do not have an active connection, so startup connection
|
|
241
256
|
this.connect();
|
|
242
257
|
}
|
|
243
258
|
|
|
244
|
-
// Add video/audio streams for our output loop to handle outputting
|
|
259
|
+
// Add video/audio streams for our output loop to handle outputting
|
|
245
260
|
this.#outputs[sessionID] = {
|
|
246
261
|
type: 'record',
|
|
247
262
|
video: videoStream,
|
|
@@ -292,11 +307,37 @@ export default class Streamer {
|
|
|
292
307
|
}
|
|
293
308
|
}
|
|
294
309
|
|
|
310
|
+
stopEverything() {
|
|
311
|
+
if (Object.keys(this.#outputs).length > 0) {
|
|
312
|
+
this?.log?.debug && this.log.debug('Stopped buffering, live and recording from uuid "%s"', this.uuid);
|
|
313
|
+
this.#outputs = {}; // No more outputs
|
|
314
|
+
if (typeof this.close === 'function') {
|
|
315
|
+
this.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
update(deviceData) {
|
|
296
321
|
if (typeof deviceData !== 'object') {
|
|
297
322
|
return;
|
|
298
323
|
}
|
|
299
324
|
|
|
325
|
+
this.migrating = deviceData.migrating;
|
|
326
|
+
|
|
327
|
+
if (this.uuid !== deviceData?.nest_google_uuid) {
|
|
328
|
+
this.uuid = deviceData?.nest_google_uuid;
|
|
329
|
+
|
|
330
|
+
if (Object.keys(this.#outputs).length > 0) {
|
|
331
|
+
// Since the uuid has change and a streamer may use this, if there any any active outpuyts, close and connect again
|
|
332
|
+
if (typeof this.close === 'function') {
|
|
333
|
+
this.close();
|
|
334
|
+
}
|
|
335
|
+
if (typeof this.connect === 'function') {
|
|
336
|
+
this.connect();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
300
341
|
if (
|
|
301
342
|
this.online !== deviceData.online ||
|
|
302
343
|
this.videoEnabled !== deviceData.streaming_enabled ||
|
|
@@ -309,21 +350,21 @@ export default class Streamer {
|
|
|
309
350
|
if ((this.online === false || this.videoEnabled === false || this.audioEnabled === false) && typeof this.close === 'function') {
|
|
310
351
|
this.close(); // as offline or streaming not enabled, close streamer
|
|
311
352
|
}
|
|
312
|
-
if (this.online === true && this.videoEnabled === true && typeof this.connect === 'function') {
|
|
353
|
+
if (this.online === true && this.videoEnabled === true && this.connected === undefined && typeof this.connect === 'function') {
|
|
313
354
|
this.connect(); // Connect for stream
|
|
314
355
|
}
|
|
315
356
|
}
|
|
316
357
|
}
|
|
317
358
|
|
|
318
|
-
addToOutput(type,
|
|
319
|
-
if (typeof type !== 'string' || type === '' ||
|
|
359
|
+
addToOutput(type, data) {
|
|
360
|
+
if (typeof type !== 'string' || type === '' || Buffer.isBuffer(data) === false) {
|
|
320
361
|
return;
|
|
321
362
|
}
|
|
322
363
|
|
|
323
364
|
Object.values(this.#outputs).forEach((output) => {
|
|
324
365
|
output.buffer.push({
|
|
366
|
+
time: Date.now(), // Timestamp of when tgis was added to buffer
|
|
325
367
|
type: type,
|
|
326
|
-
time: time,
|
|
327
368
|
data: data,
|
|
328
369
|
});
|
|
329
370
|
});
|