homebridge-nest-accfactory 0.0.5 → 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 +42 -4
- package/README.md +37 -19
- package/dist/HomeKitDevice.js +132 -109
- package/dist/camera.js +344 -262
- package/dist/doorbell.js +5 -3
- package/dist/floodlight.js +4 -4
- package/dist/nexustalk.js +84 -52
- 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 +100 -71
- 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
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The following functions should be overriden in your class which extends this
|
|
10
10
|
//
|
|
11
|
-
// streamer.connect(
|
|
12
|
-
// streamer.close(
|
|
11
|
+
// streamer.connect()
|
|
12
|
+
// streamer.close()
|
|
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
|
|
|
@@ -38,15 +42,21 @@ export default class Streamer {
|
|
|
38
42
|
videoEnabled = undefined; // Video stream on camera enabled or not
|
|
39
43
|
audioEnabled = undefined; // Audio from camera enabled or not
|
|
40
44
|
online = undefined; // Camera online or not
|
|
41
|
-
host = ''; // Host to connect to or connected too
|
|
42
45
|
uuid = undefined; // UUID of the device connecting
|
|
43
|
-
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
|
+
};
|
|
44
53
|
|
|
45
54
|
// Internal data only for this class
|
|
46
55
|
#outputTimer = undefined; // Timer for non-blocking loop to stream output data
|
|
47
56
|
#outputs = {}; // Output streams ie: buffer, live, record
|
|
48
57
|
#cameraOfflineFrame = undefined; // Camera offline video frame
|
|
49
58
|
#cameraVideoOffFrame = undefined; // Video turned off on camera video frame
|
|
59
|
+
#cameraTransferringFrame = undefined; // Camera transferring between Nest/Google Home video frame
|
|
50
60
|
|
|
51
61
|
constructor(deviceData, options) {
|
|
52
62
|
// Setup logger object if passed as option
|
|
@@ -61,10 +71,11 @@ export default class Streamer {
|
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
// Store data we need from the device data passed it
|
|
74
|
+
this.migrating = deviceData?.migrating === true;
|
|
64
75
|
this.online = deviceData?.online === true;
|
|
65
76
|
this.videoEnabled = deviceData?.streaming_enabled === true;
|
|
66
77
|
this.audioEnabled = deviceData?.audio_enabled === true;
|
|
67
|
-
this.uuid = deviceData?.
|
|
78
|
+
this.uuid = deviceData?.nest_google_uuid;
|
|
68
79
|
|
|
69
80
|
// Setup location for *.h264 frame files. This can be overriden by a passed in option
|
|
70
81
|
let resourcePath = path.resolve(__dirname + '/res'); // Default location for *.h264 files
|
|
@@ -76,22 +87,19 @@ export default class Streamer {
|
|
|
76
87
|
resourcePath = path.resolve(options.resourcePath);
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
// load buffer for camera offline
|
|
90
|
+
// load buffer for camera offline in .h264 frame
|
|
80
91
|
if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE)) === true) {
|
|
81
92
|
this.#cameraOfflineFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFLINEH264FILE));
|
|
82
|
-
// remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
|
|
83
|
-
if (this.#cameraOfflineFrame.indexOf(H264NALStartcode) === 0) {
|
|
84
|
-
this.#cameraOfflineFrame = this.#cameraOfflineFrame.subarray(H264NALStartcode.length);
|
|
85
|
-
}
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
// load buffer for camera stream off
|
|
95
|
+
// load buffer for camera stream off in .h264 frame
|
|
89
96
|
if (fs.existsSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE)) === true) {
|
|
90
97
|
this.#cameraVideoOffFrame = fs.readFileSync(path.resolve(resourcePath + '/' + CAMERAOFFH264FILE));
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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));
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
// Start a non-blocking loop for output to the various streams which connect to our streamer object
|
|
@@ -101,20 +109,26 @@ export default class Streamer {
|
|
|
101
109
|
let lastTimeVideo = Date.now();
|
|
102
110
|
this.#outputTimer = setInterval(() => {
|
|
103
111
|
let dateNow = Date.now();
|
|
104
|
-
let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; //
|
|
112
|
+
let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; // 30fps
|
|
105
113
|
Object.values(this.#outputs).forEach((output) => {
|
|
106
|
-
// Monitor for camera going offline
|
|
114
|
+
// Monitor for camera going offline, video enabled/disabled or being transferred between Nest/Google Home
|
|
107
115
|
// We'll insert the appropriate video frame into the stream
|
|
108
116
|
if (this.online === false && this.#cameraOfflineFrame !== undefined && outputVideoFrame === true) {
|
|
109
117
|
// Camera is offline so feed in our custom h264 frame and AAC silence
|
|
110
|
-
output.buffer.push({ type: 'video',
|
|
111
|
-
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 });
|
|
112
120
|
lastTimeVideo = dateNow;
|
|
113
121
|
}
|
|
114
122
|
if (this.online === true && this.videoEnabled === false && this.#cameraVideoOffFrame !== undefined && outputVideoFrame === true) {
|
|
115
123
|
// Camera video is turned off so feed in our custom h264 frame and AAC silence
|
|
116
|
-
output.buffer.push({ type: 'video',
|
|
117
|
-
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 });
|
|
118
132
|
lastTimeVideo = dateNow;
|
|
119
133
|
}
|
|
120
134
|
|
|
@@ -125,14 +139,14 @@ export default class Streamer {
|
|
|
125
139
|
output.buffer.shift();
|
|
126
140
|
}
|
|
127
141
|
|
|
128
|
-
// Output the packet data to any
|
|
142
|
+
// Output the packet data to any 'live' or 'recording' streams
|
|
129
143
|
if (output.type === 'live' || output.type === 'record') {
|
|
130
144
|
let packet = output.buffer.shift();
|
|
131
145
|
if (packet?.type === 'video' && typeof output?.video?.write === 'function') {
|
|
132
146
|
// H264 NAL Units '0001' are required to be added to beginning of any video data we output
|
|
133
147
|
// If this is missing, add on beginning of data packet
|
|
134
|
-
if (packet.data.indexOf(
|
|
135
|
-
packet.data = Buffer.concat([
|
|
148
|
+
if (packet.data.indexOf(H264NALSTARTCODE) !== 0) {
|
|
149
|
+
packet.data = Buffer.concat([H264NALSTARTCODE, packet.data]);
|
|
136
150
|
}
|
|
137
151
|
output.video.write(packet.data);
|
|
138
152
|
}
|
|
@@ -152,11 +166,9 @@ export default class Streamer {
|
|
|
152
166
|
startBuffering() {
|
|
153
167
|
if (this.#outputs?.buffer === undefined) {
|
|
154
168
|
// No active buffer session, start connection to streamer
|
|
155
|
-
if (this.connected ===
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.connect(this.host);
|
|
159
|
-
}
|
|
169
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
170
|
+
this?.log?.debug && this.log.debug('Started buffering for uuid "%s"', this.uuid);
|
|
171
|
+
this.connect();
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
this.#outputs.buffer = {
|
|
@@ -192,23 +204,21 @@ export default class Streamer {
|
|
|
192
204
|
if (typeof this.talkingAudio === 'function') {
|
|
193
205
|
this.talkingAudio(data);
|
|
194
206
|
|
|
195
|
-
|
|
207
|
+
clearTimeout(talkbackTimeout);
|
|
196
208
|
talkbackTimeout = setTimeout(() => {
|
|
197
|
-
// no audio received in
|
|
209
|
+
// no audio received in 1000ms, so mark end of stream
|
|
198
210
|
this.talkingAudio(Buffer.alloc(0));
|
|
199
|
-
},
|
|
211
|
+
}, TALKBACKAUDIOTIMEOUT);
|
|
200
212
|
}
|
|
201
213
|
});
|
|
202
214
|
}
|
|
203
215
|
|
|
204
|
-
if (this.connected ===
|
|
205
|
-
// We do not have an active
|
|
206
|
-
|
|
207
|
-
this.connect(this.host);
|
|
208
|
-
}
|
|
216
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
217
|
+
// We do not have an active connection, so startup connection
|
|
218
|
+
this.connect();
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
// Add video/audio streams for our output loop to handle outputting
|
|
221
|
+
// Add video/audio streams for our output loop to handle outputting
|
|
212
222
|
this.#outputs[sessionID] = {
|
|
213
223
|
type: 'live',
|
|
214
224
|
video: videoStream,
|
|
@@ -220,8 +230,8 @@ export default class Streamer {
|
|
|
220
230
|
// finally, we've started live stream
|
|
221
231
|
this?.log?.debug &&
|
|
222
232
|
this.log.debug(
|
|
223
|
-
'Started live stream from "%s" %s "%s"',
|
|
224
|
-
this.
|
|
233
|
+
'Started live stream from uuid "%s" %s "%s"',
|
|
234
|
+
this.uuid,
|
|
225
235
|
talkbackStream !== null && typeof talkbackStream === 'object' ? 'with two-way audio and sesssion id of' : 'and sesssion id of',
|
|
226
236
|
sessionID,
|
|
227
237
|
);
|
|
@@ -241,14 +251,12 @@ export default class Streamer {
|
|
|
241
251
|
});
|
|
242
252
|
}
|
|
243
253
|
|
|
244
|
-
if (this.connected ===
|
|
245
|
-
// We do not have an active
|
|
246
|
-
|
|
247
|
-
this.connect(this.host);
|
|
248
|
-
}
|
|
254
|
+
if (this.connected === undefined && typeof this.connect === 'function') {
|
|
255
|
+
// We do not have an active connection, so startup connection
|
|
256
|
+
this.connect();
|
|
249
257
|
}
|
|
250
258
|
|
|
251
|
-
// Add video/audio streams for our output loop to handle outputting
|
|
259
|
+
// Add video/audio streams for our output loop to handle outputting
|
|
252
260
|
this.#outputs[sessionID] = {
|
|
253
261
|
type: 'record',
|
|
254
262
|
video: videoStream,
|
|
@@ -258,44 +266,54 @@ export default class Streamer {
|
|
|
258
266
|
};
|
|
259
267
|
|
|
260
268
|
// Finally we've started the recording stream
|
|
261
|
-
this?.log?.debug && this.log.debug('Started recording stream from "%s" with sesison id of "%s"', this.
|
|
269
|
+
this?.log?.debug && this.log.debug('Started recording stream from uuid "%s" with sesison id of "%s"', this.uuid, sessionID);
|
|
262
270
|
}
|
|
263
271
|
|
|
264
272
|
stopRecordStream(sessionID) {
|
|
265
273
|
// Request to stop a recording stream
|
|
266
|
-
if (
|
|
267
|
-
this?.log?.debug && this.log.debug('Stopped recording stream from "%s"', this.
|
|
274
|
+
if (this.#outputs?.[sessionID] !== undefined) {
|
|
275
|
+
this?.log?.debug && this.log.debug('Stopped recording stream from uuid "%s"', this.uuid);
|
|
268
276
|
delete this.#outputs[sessionID];
|
|
269
277
|
}
|
|
270
278
|
|
|
271
|
-
// If we have no more output streams active, we'll close the connection
|
|
279
|
+
// If we have no more output streams active, we'll close the connection
|
|
272
280
|
if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
|
|
273
|
-
this.close(
|
|
281
|
+
this.close();
|
|
274
282
|
}
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
stopLiveStream(sessionID) {
|
|
278
286
|
// Request to stop an active live stream
|
|
279
|
-
if (
|
|
280
|
-
this?.log?.debug && this.log.debug('Stopped live stream from "%s"', this.
|
|
287
|
+
if (this.#outputs?.[sessionID] !== undefined) {
|
|
288
|
+
this?.log?.debug && this.log.debug('Stopped live stream from uuid "%s"', this.uuid);
|
|
281
289
|
delete this.#outputs[sessionID];
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
// If we have no more output streams active, we'll close the connection
|
|
292
|
+
// If we have no more output streams active, we'll close the connection
|
|
285
293
|
if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
|
|
286
|
-
this.close(
|
|
294
|
+
this.close();
|
|
287
295
|
}
|
|
288
296
|
}
|
|
289
297
|
|
|
290
298
|
stopBuffering() {
|
|
291
299
|
if (this.#outputs?.buffer !== undefined) {
|
|
292
|
-
this?.log?.debug && this.log.debug('Stopped buffering from "%s"', this.
|
|
300
|
+
this?.log?.debug && this.log.debug('Stopped buffering from uuid "%s"', this.uuid);
|
|
293
301
|
delete this.#outputs.buffer;
|
|
294
302
|
}
|
|
295
303
|
|
|
296
|
-
// If we have no more output streams active, we'll close the connection
|
|
304
|
+
// If we have no more output streams active, we'll close the connection
|
|
297
305
|
if (Object.keys(this.#outputs).length === 0 && typeof this.close === 'function') {
|
|
298
|
-
this.close(
|
|
306
|
+
this.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
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
|
+
}
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
|
|
@@ -304,9 +322,20 @@ export default class Streamer {
|
|
|
304
322
|
return;
|
|
305
323
|
}
|
|
306
324
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
339
|
}
|
|
311
340
|
|
|
312
341
|
if (
|
|
@@ -319,23 +348,23 @@ export default class Streamer {
|
|
|
319
348
|
this.videoEnabled = deviceData?.streaming_enabled === true;
|
|
320
349
|
this.audioEnabled = deviceData?.audio_enabled === true;
|
|
321
350
|
if ((this.online === false || this.videoEnabled === false || this.audioEnabled === false) && typeof this.close === 'function') {
|
|
322
|
-
this.close(
|
|
351
|
+
this.close(); // as offline or streaming not enabled, close streamer
|
|
323
352
|
}
|
|
324
|
-
if (this.online === true && this.videoEnabled === true && typeof this.connect === 'function') {
|
|
325
|
-
this.connect(
|
|
353
|
+
if (this.online === true && this.videoEnabled === true && this.connected === undefined && typeof this.connect === 'function') {
|
|
354
|
+
this.connect(); // Connect for stream
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
357
|
}
|
|
329
358
|
|
|
330
|
-
addToOutput(type,
|
|
331
|
-
if (typeof type !== 'string' || type === '' ||
|
|
359
|
+
addToOutput(type, data) {
|
|
360
|
+
if (typeof type !== 'string' || type === '' || Buffer.isBuffer(data) === false) {
|
|
332
361
|
return;
|
|
333
362
|
}
|
|
334
363
|
|
|
335
364
|
Object.values(this.#outputs).forEach((output) => {
|
|
336
365
|
output.buffer.push({
|
|
366
|
+
time: Date.now(), // Timestamp of when tgis was added to buffer
|
|
337
367
|
type: type,
|
|
338
|
-
time: time,
|
|
339
368
|
data: data,
|
|
340
369
|
});
|
|
341
370
|
});
|