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/dist/streamer.js CHANGED
@@ -13,7 +13,11 @@
13
13
  // streamer.talkingAudio(talkingData)
14
14
  // streamer.update(deviceData) <- call super after
15
15
  //
16
- // Code version 13/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
 
@@ -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 = 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
+ };
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?.uuid;
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 image in .h264 frame
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 image in .h264 frame
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
- // remove any H264 NALU from beginning of any video data. We do this as they are added later when output by our ffmpeg router
91
- if (this.#cameraVideoOffFrame.indexOf(H264NALStartcode) === 0) {
92
- this.#cameraVideoOffFrame = this.#cameraVideoOffFrame.subarray(H264NALStartcode.length);
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; // 30 or 15 fps?
112
+ let outputVideoFrame = dateNow > lastTimeVideo + 90000 / 30; // 30fps
104
113
  Object.values(this.#outputs).forEach((output) => {
105
- // 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
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', time: dateNow, data: this.#cameraOfflineFrame });
110
- 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 });
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', time: dateNow, data: this.#cameraVideoOffFrame });
116
- 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 });
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 streams 'live' or 'recording' streams
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(H264NALStartcode) !== 0) {
134
- packet.data = Buffer.concat([H264NALStartcode, packet.data]);
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 === false && typeof this.connect === 'function') {
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
- talkbackTimeout = clearTimeout(talkbackTimeout);
207
+ clearTimeout(talkbackTimeout);
193
208
  talkbackTimeout = setTimeout(() => {
194
- // no audio received in 500ms, so mark end of stream
209
+ // no audio received in 1000ms, so mark end of stream
195
210
  this.talkingAudio(Buffer.alloc(0));
196
- }, 500);
211
+ }, TALKBACKAUDIOTIMEOUT);
197
212
  }
198
213
  });
199
214
  }
200
215
 
201
- if (this.connected === false && typeof this.connect === 'function') {
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 to
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 === false && typeof this.connect === 'function') {
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 to
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, time, data) {
319
- if (typeof type !== 'string' || type === '' || typeof time !== 'number' || time === 0) {
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
  });