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/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(host)
12
- // streamer.close(stopStreamFirst)
11
+ // streamer.connect()
12
+ // streamer.close()
13
13
  // streamer.talkingAudio(talkingData)
14
14
  // streamer.update(deviceData) <- call super after
15
15
  //
16
- // Code version 11/9/2024
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 H264NALStartcode = Buffer.from([0x00, 0x00, 0x00, 0x01]);
32
- const AACAudioSilence = Buffer.from([0x21, 0x10, 0x01, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
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 = false; // Streamer connected to endpoint
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?.uuid;
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 image in .h264 frame
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 image in .h264 frame
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
- // remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
92
- if (this.#cameraVideoOffFrame.indexOf(H264NALStartcode) === 0) {
93
- this.#cameraVideoOffFrame = this.#cameraVideoOffFrame.subarray(H264NALStartcode.length);
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; // 30 or 15 fps?
112
+ let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; // 30fps
105
113
  Object.values(this.#outputs).forEach((output) => {
106
- // Monitor for camera going offline and/or video enabled/disabled
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', time: dateNow, data: this.#cameraOfflineFrame });
111
- output.buffer.push({ type: 'audio', time: dateNow, data: AACAudioSilence });
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', time: dateNow, data: this.#cameraVideoOffFrame });
117
- output.buffer.push({ type: 'audio', time: dateNow, data: AACAudioSilence });
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 streams 'live' or 'recording' streams
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(H264NALStartcode) !== 0) {
135
- packet.data = Buffer.concat([H264NALStartcode, packet.data]);
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 === false && typeof this.host === 'string' && this.host !== '') {
156
- if (typeof this.connect === 'function') {
157
- this?.log?.debug && this.log.debug('Started buffering from "%s"', this.host);
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
- talkbackTimeout = clearTimeout(talkbackTimeout);
207
+ clearTimeout(talkbackTimeout);
196
208
  talkbackTimeout = setTimeout(() => {
197
- // no audio received in 500ms, so mark end of stream
209
+ // no audio received in 1000ms, so mark end of stream
198
210
  this.talkingAudio(Buffer.alloc(0));
199
- }, 500);
211
+ }, TALKBACKAUDIOTIMEOUT);
200
212
  }
201
213
  });
202
214
  }
203
215
 
204
- if (this.connected === false && typeof this.host === 'string' && this.host !== '') {
205
- // We do not have an active socket connection, so startup connection to host
206
- if (typeof this.connect === 'function') {
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 to
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.host,
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 === false && typeof this.host === 'string' && this.host !== '') {
245
- // We do not have an active socket connection, so startup connection to host
246
- if (typeof this.connect === 'function') {
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 to
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.host, sessionID);
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 (typeof this.#outputs[sessionID] === 'object') {
267
- this?.log?.debug && this.log.debug('Stopped recording stream from "%s"', this.host);
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 to host
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(true);
281
+ this.close();
274
282
  }
275
283
  }
276
284
 
277
285
  stopLiveStream(sessionID) {
278
286
  // Request to stop an active live stream
279
- if (typeof this.#outputs[sessionID] === 'object') {
280
- this?.log?.debug && this.log.debug('Stopped live stream from "%s"', this.host);
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 to host
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(true);
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.host);
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 to host
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(true);
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
- if (this.host !== deviceData.streaming_host) {
308
- this.host = deviceData.streaming_host;
309
- this?.log?.debug && this.log.debug('New streaming host has been requested for connection. Host requested is "%s"', this.host);
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(true); // as offline or streaming not enabled, close socket
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(this.host); // Connect to host for stream
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, time, data) {
331
- if (typeof type !== 'string' || type === '' || typeof time !== 'number' || time === 0) {
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
  });