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/camera.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Nest Cameras
2
2
  // Part of homebridge-nest-accfactory
3
3
  //
4
- // Code version 12/9/2024
4
+ // Code version 30/9/2024
5
5
  // Mark Hulskamp
6
6
  'use strict';
7
7
 
@@ -20,11 +20,11 @@ import { fileURLToPath } from 'node:url';
20
20
  // Define our modules
21
21
  import HomeKitDevice from './HomeKitDevice.js';
22
22
  import NexusTalk from './nexustalk.js';
23
- //import WebRTC from './webrtc.js';
24
- let WebRTC = undefined;
23
+ import WebRTC from './webrtc.js';
25
24
 
26
25
  const CAMERAOFFLINEJPGFILE = 'Nest_camera_offline.jpg'; // Camera offline jpg image file
27
26
  const CAMERAOFFJPGFILE = 'Nest_camera_off.jpg'; // Camera video off jpg image file
27
+ const CAMERATRANSFERJPGFILE = 'Nest_camera_transfer.jpg'; // Camera transferring jpg image file
28
28
  const MP4BOX = 'mp4box'; // MP4 box fragement event for HKSV recording
29
29
  const SNAPSHOTCACHETIMEOUT = 30000; // Timeout for retaining snapshot image (in milliseconds)
30
30
  const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
@@ -33,6 +33,7 @@ export default class NestCamera extends HomeKitDevice {
33
33
  controller = undefined; // HomeKit Camera/Doorbell controller service
34
34
  streamer = undefined; // Streamer object for live/recording stream
35
35
  motionServices = undefined; // Object of Camera/Doorbell motion sensor(s)
36
+ batteryService = undefined; // If a camera has a battery <-- todo
36
37
  operatingModeService = undefined; // Link to camera/doorbell operating mode service
37
38
  personTimer = undefined; // Cooldown timer for person/face events
38
39
  motionTimer = undefined; // Cooldown timer for motion events
@@ -45,6 +46,7 @@ export default class NestCamera extends HomeKitDevice {
45
46
  #recordingConfig = {}; // HomeKit Secure Video recording configuration
46
47
  #cameraOfflineImage = undefined; // JPG image buffer for camera offline
47
48
  #cameraVideoOffImage = undefined; // JPG image buffer for camera video off
49
+ #cameraTransferringImage = undefined; // JPG image buffer for camera transferring between Nest/Google Home
48
50
 
49
51
  constructor(accessory, api, log, eventEmitter, deviceData) {
50
52
  super(accessory, api, log, eventEmitter, deviceData);
@@ -60,6 +62,12 @@ export default class NestCamera extends HomeKitDevice {
60
62
  if (fs.existsSync(imageFile) === true) {
61
63
  this.#cameraVideoOffImage = fs.readFileSync(imageFile);
62
64
  }
65
+
66
+ // buffer for camera transferring jpg image
67
+ imageFile = path.resolve(__dirname + '/res/' + CAMERATRANSFERJPGFILE);
68
+ if (fs.existsSync(imageFile) === true) {
69
+ this.#cameraTransferringImage = fs.readFileSync(imageFile);
70
+ }
63
71
  }
64
72
 
65
73
  // Class functions
@@ -99,7 +107,7 @@ export default class NestCamera extends HomeKitDevice {
99
107
  (value === true && this.deviceData.statusled_brightness !== 0) ||
100
108
  (value === false && this.deviceData.statusled_brightness !== 1)
101
109
  ) {
102
- this.set({ statusled_brightness: value === true ? 0 : 1 });
110
+ this.set({ uuid: this.deviceData.nest_google_uuid, statusled_brightness: value === true ? 0 : 1 });
103
111
  if (this?.log?.info) {
104
112
  this.log.info('Recording status LED on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
105
113
  }
@@ -119,7 +127,7 @@ export default class NestCamera extends HomeKitDevice {
119
127
  this.operatingModeService.getCharacteristic(this.hap.Characteristic.NightVision).onSet((value) => {
120
128
  // only change IRLed status value if different than on-device
121
129
  if ((value === false && this.deviceData.irled_enabled === true) || (value === true && this.deviceData.irled_enabled === false)) {
122
- this.set({ irled_enabled: value === true ? 'auto_on' : 'always_off' });
130
+ this.set({ uuid: this.deviceData.nest_google_uuid, irled_enabled: value === true ? 'auto_on' : 'always_off' });
123
131
 
124
132
  if (this?.log?.info) {
125
133
  this.log.info('Night vision on "%s" was turned', this.deviceData.description, value === true ? 'on' : 'off');
@@ -144,7 +152,7 @@ export default class NestCamera extends HomeKitDevice {
144
152
  (this.deviceData.streaming_enabled === true && value === true)
145
153
  ) {
146
154
  // Camera state does not reflect requested state, so fix
147
- this.set({ streaming_enabled: value === false ? true : false });
155
+ this.set({ uuid: this.deviceData.nest_google_uuid, streaming_enabled: value === false ? true : false });
148
156
  if (this?.log?.info) {
149
157
  this.log.info('Camera on "%s" was turned', this.deviceData.description, value === false ? 'on' : 'off');
150
158
  }
@@ -178,7 +186,10 @@ export default class NestCamera extends HomeKitDevice {
178
186
  (this.deviceData.audio_enabled === true && value === this.hap.Characteristic.RecordingAudioActive.DISABLE) ||
179
187
  (this.deviceData.audio_enabled === false && value === this.hap.Characteristic.RecordingAudioActive.ENABLE)
180
188
  ) {
181
- this.set({ audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false });
189
+ this.set({
190
+ uuid: this.deviceData.nest_google_uuid,
191
+ audio_enabled: value === this.hap.Characteristic.RecordingAudioActive.ENABLE ? true : false,
192
+ });
182
193
  if (this?.log?.info) {
183
194
  this.log.info(
184
195
  'Audio recording on "%s" was turned',
@@ -199,35 +210,17 @@ export default class NestCamera extends HomeKitDevice {
199
210
  }
200
211
  }
201
212
 
202
- // Depending on the streaming profiles that the camera supports, this will be either nexustalk or webrtc
203
- // We'll also start pre-buffering if required for HKSV
204
- if (this.deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === true && this.streamer === undefined && WebRTC !== undefined) {
205
- this.streamer = new WebRTC(this.deviceData, {
206
- log: this.log,
207
- buffer:
208
- this.deviceData.hksv === true &&
209
- this?.controller?.recordingManagement?.recordingManagementService !== undefined &&
210
- this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.Active).value ===
211
- this.hap.Characteristic.Active.ACTIVE,
212
- });
213
+ if (this.deviceData.migrating === true) {
214
+ // Migration happening between Nest <-> Google Home apps
215
+ this?.log?.warn && this.log.warn('Migration between Nest <-> Google Home apps is underway for "%s"', this.deviceData.description);
213
216
  }
214
217
 
215
218
  if (
216
- this.deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === true &&
217
- this.streamer === undefined &&
218
- NexusTalk !== undefined
219
+ (this.deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === false &&
220
+ this.deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === false) ||
221
+ (this.deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === true && WebRTC === undefined) ||
222
+ (this.deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === true && NexusTalk === undefined)
219
223
  ) {
220
- this.streamer = new NexusTalk(this.deviceData, {
221
- log: this.log,
222
- buffer:
223
- this.deviceData.hksv === true &&
224
- this?.controller?.recordingManagement?.recordingManagementService !== undefined &&
225
- this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.Active).value ===
226
- this.hap.Characteristic.Active.ACTIVE,
227
- });
228
- }
229
-
230
- if (this.streamer === undefined) {
231
224
  this?.log?.error &&
232
225
  this.log.error(
233
226
  'No suitable streaming protocol is present for "%s". Streaming and recording will be unavailable',
@@ -252,16 +245,20 @@ export default class NestCamera extends HomeKitDevice {
252
245
  postSetupDetails.push(
253
246
  'HomeKit Secure Video support' + (this.streamer?.isBuffering() === true ? ' and recording buffer started' : ''),
254
247
  );
248
+ this.deviceData.localAccess === true && postSetupDetails.push('Local access');
255
249
  return postSetupDetails;
256
250
  }
257
251
 
258
252
  removeServices() {
259
253
  // Clean up our camera object since this device is being removed
260
- this.motionTimer = clearTimeout(this.motionTimer);
261
- this.personTimer = clearTimeout(this.personTimer);
262
- this.snapshotTimer = clearTimeout(this.snapshotTimer);
254
+ clearTimeout(this.motionTimer);
255
+ clearTimeout(this.personTimer);
256
+ clearTimeout(this.snapshotTimer);
257
+ this.motionTimer = undefined;
258
+ this.personTimer = undefined;
259
+ this.snapshotTimer = undefined;
263
260
 
264
- this.streamer?.isBuffering() === true && this.streamer.stopBuffering();
261
+ this.streamer !== undefined && this.streamer.stopEverything();
265
262
 
266
263
  // Stop any on-going HomeKit sessions, either live or recording
267
264
  // We'll terminate any ffmpeg, rtpSpliter etc processes
@@ -296,7 +293,7 @@ export default class NestCamera extends HomeKitDevice {
296
293
  // Taken and adapted from:
297
294
  // https://github.com/hjdhjd/homebridge-unifi-protect/blob/eee6a4e379272b659baa6c19986d51f5bf2cbbbc/src/protect-ffmpeg-record.ts
298
295
  async *handleRecordingStreamRequest(sessionID) {
299
- if (this.deviceData?.ffmpeg?.path === undefined) {
296
+ if (this.deviceData?.ffmpeg?.binary === undefined) {
300
297
  this?.log?.warn &&
301
298
  this.log.warn(
302
299
  'Received request to start recording for "%s" however we do not have an ffmpeg binary present',
@@ -318,96 +315,86 @@ export default class NestCamera extends HomeKitDevice {
318
315
  if (this.streamer === undefined) {
319
316
  this?.log?.error &&
320
317
  this.log.error(
321
- 'Received request to start recording for "%s" however we do not any associated streaming protocol supported',
318
+ 'Received request to start recording for "%s" however we do not any associated streaming protocol support',
322
319
  this.deviceData.description,
323
320
  );
324
321
  return;
325
322
  }
326
323
 
327
324
  // Build our ffmpeg command string for recording the video/audio stream
328
- let commandLine =
329
- '-hide_banner -nostats' +
330
- ' -fflags +discardcorrupt' +
331
- ' -max_delay 500000' +
332
- ' -flags low_delay' +
333
- ' -f h264 -i pipe:0' + // Video data only on stdin
334
- (this.deviceData.audio_enabled === true &&
335
- this.deviceData?.ffmpeg?.libfdk_aac === true &&
325
+ let commandLine = [
326
+ '-hide_banner',
327
+ '-nostats',
328
+ '-fflags +discardcorrupt',
329
+ '-max_delay 500000',
330
+ '-flags low_delay',
331
+ '-f h264',
332
+ '-i pipe:0',
333
+ ];
334
+
335
+ let includeAudio = false;
336
+ if (
337
+ this.deviceData.audio_enabled === true &&
336
338
  this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
337
- .value === this.hap.Characteristic.RecordingAudioActive.ENABLE
338
- ? ' -f aac -i pipe:3'
339
- : ''); // Audio data only on extra pipe created in spawn command
339
+ .value === this.hap.Characteristic.RecordingAudioActive.ENABLE &&
340
+ ((this.streamer?.codecs?.audio === 'aac' && this.deviceData?.ffmpeg?.libfdk_aac === true) ||
341
+ (this.streamer?.codecs?.audio === 'opus' && this.deviceData?.ffmpeg?.libopus === true))
342
+ ) {
343
+ // audio data only on extra pipe created in spawn command
344
+ commandLine.push('-i pipe:3');
345
+ includeAudio = true;
346
+ }
340
347
 
341
348
  // Build our video command for ffmpeg
342
- commandLine =
343
- commandLine +
344
- ' -map 0:v' + // stdin, the first input is video data
345
- ' -codec:v libx264' +
346
- ' -preset veryfast' +
347
- ' -profile:v ' +
348
- (this.#recordingConfig.videoCodec.parameters.profile === this.hap.H264Profile.HIGH
349
- ? 'high'
350
- : this.#recordingConfig.videoCodec.parameters.profile === this.hap.H264Profile.MAIN
351
- ? 'main'
352
- : 'baseline') +
353
- ' -level:v ' +
354
- (this.#recordingConfig.videoCodec.parameters.level === this.hap.H264Level.LEVEL4_0
355
- ? '4.0'
356
- : this.#recordingConfig.videoCodec.parameters.level === this.hap.H264Level.LEVEL3_2
357
- ? '3.2'
358
- : '3.1') +
359
- ' -noautoscale' +
360
- ' -bf 0' +
361
- ' -filter:v fps=fps=' +
362
- this.#recordingConfig.videoCodec.resolution[2] + // convert to framerate HomeKit has requested
363
- ' -g:v ' +
364
- (this.#recordingConfig.videoCodec.resolution[2] * this.#recordingConfig.videoCodec.parameters.iFrameInterval) / 1000 +
365
- ' -b:v ' +
366
- this.#recordingConfig.videoCodec.parameters.bitRate +
367
- 'k' +
368
- ' -fps_mode passthrough' +
369
- ' -movflags frag_keyframe+empty_moov+default_base_moof' +
370
- ' -reset_timestamps 1' +
371
- ' -video_track_timescale 90000' +
372
- ' -bufsize ' +
373
- 2 * this.#recordingConfig.videoCodec.parameters.bitRate +
374
- 'k';
349
+ commandLine.push(
350
+ '-map 0:v:0',
351
+ '-codec:v libx264',
352
+ '-preset veryfast',
353
+ '-profile:v ' +
354
+ (this.#recordingConfig.videoCodec.parameters.profile === this.hap.H264Profile.HIGH
355
+ ? 'high'
356
+ : this.#recordingConfig.videoCodec.parameters.profile === this.hap.H264Profile.MAIN
357
+ ? 'main'
358
+ : 'baseline'),
359
+ '-level:v ' +
360
+ (this.#recordingConfig.videoCodec.parameters.level === this.hap.H264Level.LEVEL4_0
361
+ ? '4.0'
362
+ : this.#recordingConfig.videoCodec.parameters.level === this.hap.H264Level.LEVEL3_2
363
+ ? '3.2'
364
+ : '3.1'),
365
+ '-noautoscale',
366
+ '-bf 0',
367
+ '-filter:v fps=fps=' + this.#recordingConfig.videoCodec.resolution[2],
368
+ '-g:v ' + (this.#recordingConfig.videoCodec.resolution[2] * this.#recordingConfig.videoCodec.parameters.iFrameInterval) / 1000,
369
+ '-b:v ' + this.#recordingConfig.videoCodec.parameters.bitRate + 'k',
370
+ '-fps_mode passthrough',
371
+ '-movflags frag_keyframe+empty_moov+default_base_moof',
372
+ '-reset_timestamps 1',
373
+ '-video_track_timescale 90000',
374
+ '-bufsize ' + 2 * this.#recordingConfig.videoCodec.parameters.bitRate + 'k',
375
+ );
375
376
 
376
377
  // We have seperate video and audio streams that need to be muxed together if audio enabled
377
- if (
378
- this.deviceData.audio_enabled === true &&
379
- this.deviceData?.ffmpeg?.libfdk_aac === true &&
380
- this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.RecordingAudioActive)
381
- .value === this.hap.Characteristic.RecordingAudioActive.ENABLE
382
- ) {
378
+ if (includeAudio === true) {
383
379
  let audioSampleRates = ['8', '16', '24', '32', '44.1', '48'];
384
380
 
385
- commandLine =
386
- commandLine +
387
- ' -map 1:a' + // pipe:3, the second input is audio data
388
- ' -codec:a libfdk_aac' +
389
- ' -profile:a aac_low' + // HAP.AudioRecordingCodecType.AAC_LC
390
- ' -ar ' +
391
- audioSampleRates[this.#recordingConfig.audioCodec.samplerate] +
392
- 'k' +
393
- ' -b:a ' +
394
- this.#recordingConfig.audioCodec.bitrate +
395
- 'k' +
396
- ' -ac ' +
397
- this.#recordingConfig.audioCodec.audioChannels;
381
+ commandLine.push(
382
+ '-map 1:a:0',
383
+ '-codec:a libfdk_aac',
384
+ '-profile:a aac_low',
385
+ '-ar ' + audioSampleRates[this.#recordingConfig.audioCodec.samplerate] + 'k',
386
+ '-b:a ' + this.#recordingConfig.audioCodec.bitrate + 'k',
387
+ '-ac ' + this.#recordingConfig.audioCodec.audioChannels,
388
+ );
398
389
  }
399
390
 
400
- commandLine = commandLine + ' -f mp4 pipe:1'; // output to stdout in mp4
391
+ commandLine.push('-f mp4 pipe:1'); // output to stdout in mp4
401
392
 
402
393
  this.#hkSessions[sessionID] = {};
403
- this.#hkSessions[sessionID].ffmpeg = child_process.spawn(
404
- path.resolve(this.deviceData.ffmpeg.path + '/ffmpeg'),
405
- commandLine.split(' '),
406
- {
407
- env: process.env,
408
- stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
409
- },
410
- ); // Extra pipe, #3 for audio data
394
+ this.#hkSessions[sessionID].ffmpeg = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
395
+ env: process.env,
396
+ stdio: ['pipe', 'pipe', 'pipe', includeAudio === true ? 'pipe' : ''],
397
+ }); // Extra pipe, #3 for audio data
411
398
 
412
399
  this.#hkSessions[sessionID].video = this.#hkSessions[sessionID].ffmpeg.stdin; // Video data on stdio pipe for ffmpeg
413
400
  this.#hkSessions[sessionID].audio = this.#hkSessions[sessionID]?.ffmpeg?.stdio?.[3]
@@ -476,15 +463,11 @@ export default class NestCamera extends HomeKitDevice {
476
463
  this.streamer.startRecordStream(
477
464
  sessionID,
478
465
  this.#hkSessions[sessionID].ffmpeg.stdin,
479
- this.#hkSessions[sessionID]?.ffmpeg?.stdio?.[3] ? this.#hkSessions[sessionID].ffmpeg.stdio[3] : null,
466
+ this.#hkSessions[sessionID]?.ffmpeg?.stdio?.[3] && includeAudio === true ? this.#hkSessions[sessionID].ffmpeg.stdio[3] : null,
480
467
  );
481
468
 
482
469
  this?.log?.info &&
483
- this.log.info(
484
- 'Started recording from "%s" %s',
485
- this.deviceData.description,
486
- this.#hkSessions[sessionID]?.ffmpeg?.stdio?.[3] ? '' : 'without audio',
487
- );
470
+ this.log.info('Started recording from "%s" %s', this.deviceData.description, includeAudio === false ? 'without audio' : '');
488
471
 
489
472
  // Loop generating MOOF/MDAT box pairs for HomeKit Secure Video.
490
473
  // HAP-NodeJS cancels this async generator function when recording completes also
@@ -580,37 +563,47 @@ export default class NestCamera extends HomeKitDevice {
580
563
  // Get current image from camera/doorbell
581
564
  let imageBuffer = undefined;
582
565
 
583
- if (this.deviceData.streaming_enabled === true && this.deviceData.online === true) {
584
- let response = await this.get({ camera_snapshot: '' });
566
+ if (this.deviceData.migrating === false && this.deviceData.streaming_enabled === true && this.deviceData.online === true) {
567
+ let response = await this.get({ uuid: this.deviceData.nest_google_uuid, camera_snapshot: '' });
585
568
  if (Buffer.isBuffer(response?.camera_snapshot) === true) {
586
569
  imageBuffer = response.camera_snapshot;
587
570
  this.lastSnapshotImage = response.camera_snapshot;
588
571
 
589
572
  // Keep this snapshot image cached for a certain period
590
- this.snapshotTimer = clearTimeout(this.snapshotTimer);
573
+ clearTimeout(this.snapshotTimer);
591
574
  this.snapshotTimer = setTimeout(() => {
592
575
  this.lastSnapshotImage = undefined;
593
576
  }, SNAPSHOTCACHETIMEOUT);
594
577
  }
595
578
  }
596
579
 
597
- if (this.deviceData.streaming_enabled === false && this.deviceData.online === true && this.#cameraVideoOffImage !== undefined) {
580
+ if (
581
+ this.deviceData.migrating === false &&
582
+ this.deviceData.streaming_enabled === false &&
583
+ this.deviceData.online === true &&
584
+ this.#cameraVideoOffImage !== undefined
585
+ ) {
598
586
  // Return 'camera switched off' jpg to image buffer
599
587
  imageBuffer = this.#cameraVideoOffImage;
600
588
  }
601
589
 
602
- if (this.deviceData.online === false && this.#cameraOfflineImage !== undefined) {
590
+ if (this.deviceData.migrating === false && this.deviceData.online === false && this.#cameraOfflineImage !== undefined) {
603
591
  // Return 'camera offline' jpg to image buffer
604
592
  imageBuffer = this.#cameraOfflineImage;
605
593
  }
606
594
 
595
+ if (this.deviceData.migrating === true && this.#cameraTransferringImage !== undefined) {
596
+ // Return 'camera transferring' jpg to image buffer
597
+ imageBuffer = this.#cameraTransferringImage;
598
+ }
599
+
607
600
  if (imageBuffer === undefined) {
608
601
  // If we get here, we have no snapshot image
609
602
  // We'll use the last success snapshop as long as its within a certain time period
610
603
  imageBuffer = this.lastSnapshotImage;
611
604
  }
612
605
 
613
- callback(imageBuffer?.length === 0 ? 'Unabled to obtain Camera/Doorbell snapshot' : null, imageBuffer);
606
+ callback(imageBuffer?.length === 0 ? 'Unable to obtain Camera/Doorbell snapshot' : null, imageBuffer);
614
607
  }
615
608
 
616
609
  async prepareStream(request, callback) {
@@ -684,12 +677,12 @@ export default class NestCamera extends HomeKitDevice {
684
677
  // We have no streamer object configured, so cannot do live streams!!
685
678
  this?.log?.error &&
686
679
  this.log.error(
687
- 'Received request to start live video for "%s" however we do not any associated streaming protocol supported',
680
+ 'Received request to start live video for "%s" however we do not any associated streaming protocol support',
688
681
  this.deviceData.description,
689
682
  );
690
683
  }
691
684
 
692
- if (request.type === this.hap.StreamRequestTypes.START && this.deviceData?.ffmpeg?.path === undefined) {
685
+ if (request.type === this.hap.StreamRequestTypes.START && this.deviceData?.ffmpeg?.binary === undefined) {
693
686
  // No ffmpeg binary present, so cannot do live streams!!
694
687
  this?.log?.warn &&
695
688
  this.log.warn(
@@ -698,94 +691,116 @@ export default class NestCamera extends HomeKitDevice {
698
691
  );
699
692
  }
700
693
 
701
- if (request.type === this.hap.StreamRequestTypes.START && this.streamer !== undefined && this.deviceData?.ffmpeg?.path !== undefined) {
694
+ if (
695
+ request.type === this.hap.StreamRequestTypes.START &&
696
+ this.streamer !== undefined &&
697
+ this.deviceData?.ffmpeg?.binary !== undefined
698
+ ) {
702
699
  // Build our ffmpeg command string for the liveview video/audio stream
703
- let commandLine =
704
- '-hide_banner -nostats' +
705
- ' -use_wallclock_as_timestamps 1' +
706
- ' -fflags +discardcorrupt' +
707
- ' -max_delay 500000' +
708
- ' -flags low_delay' +
709
- ' -f h264 -i pipe:0' + // Video data only on stdin
710
- (this.deviceData.audio_enabled === true && this.deviceData?.ffmpeg?.libfdk_aac === true ? ' -f aac -i pipe:3' : ''); // Audio data only on extra pipe created in spawn command
700
+ let commandLine = [
701
+ '-hide_banner',
702
+ '-nostats',
703
+ '-use_wallclock_as_timestamps 1',
704
+ '-fflags +discardcorrupt',
705
+ '-max_delay 500000',
706
+ '-flags low_delay',
707
+ '-f h264',
708
+ '-i pipe:0',
709
+ ];
710
+
711
+ let includeAudio = false;
712
+ if (
713
+ this.deviceData.audio_enabled === true &&
714
+ this.streamer?.codecs?.audio === 'aac' &&
715
+ this.deviceData?.ffmpeg?.libfdk_aac === true
716
+ ) {
717
+ // Audio data only on extra pipe created in spawn command
718
+ commandLine.push('-f aac -i pipe:3');
719
+ includeAudio = true;
720
+ }
711
721
 
712
- // Build our video command for ffmpeg
713
- commandLine =
714
- commandLine +
715
- ' -map 0:v' + // stdin, the first input is video data
716
- ' -codec:v copy' +
717
- ' -fps_mode passthrough' +
718
- ' -reset_timestamps 1' +
719
- ' -video_track_timescale 90000' +
720
- ' -payload_type ' +
721
- request.video.pt +
722
- ' -ssrc ' +
723
- this.#hkSessions[request.sessionID].videoSSRC +
724
- ' -f rtp' +
725
- ' -srtp_out_suite ' +
726
- this.hap.SRTPCryptoSuites[this.#hkSessions[request.sessionID].videoCryptoSuite] +
727
- ' -srtp_out_params ' +
728
- this.#hkSessions[request.sessionID].videoSRTP.toString('base64') +
729
- ' srtp://' +
730
- this.#hkSessions[request.sessionID].address +
731
- ':' +
732
- this.#hkSessions[request.sessionID].videoPort +
733
- '?rtcpport=' +
734
- this.#hkSessions[request.sessionID].videoPort +
735
- '&pkt_size=' +
736
- request.video.mtu;
722
+ if (this.deviceData.audio_enabled === true && this.streamer?.codecs?.audio === 'opus' && this.deviceData?.ffmpeg?.libopus === true) {
723
+ // Audio data only on extra pipe created in spawn command
724
+ commandLine.push('-i pipe:3');
725
+ includeAudio = true;
726
+ }
737
727
 
738
- // We have seperate video and audio streams that need to be muxed together if audio enabled
739
- if (this.deviceData.audio_enabled === true && this.deviceData?.ffmpeg?.libfdk_aac === true) {
740
- commandLine =
741
- commandLine +
742
- ' -map 1:a' + // pipe:3, the second input is audio data
743
- ' -codec:a libfdk_aac' +
744
- ' -profile:a aac_eld' + //+ this.hap.AudioStreamingCodecType.AAC_ELD
745
- ' -flags +global_header' +
746
- ' -ar ' +
747
- request.audio.sample_rate +
748
- 'k' +
749
- ' -b:a ' +
750
- request.audio.max_bit_rate +
751
- 'k' +
752
- ' -ac ' +
753
- request.audio.channel +
754
- ' -payload_type ' +
755
- request.audio.pt +
756
- ' -ssrc ' +
757
- this.#hkSessions[request.sessionID].audioSSRC +
758
- ' -f rtp' +
759
- ' -srtp_out_suite ' +
760
- this.hap.SRTPCryptoSuites[this.#hkSessions[request.sessionID].audioCryptoSuite] +
761
- ' -srtp_out_params ' +
762
- this.#hkSessions[request.sessionID].audioSRTP.toString('base64') +
728
+ // Build our video command for ffmpeg
729
+ commandLine.push(
730
+ '-map 0:v:0',
731
+ '-codec:v copy',
732
+ '-fps_mode passthrough',
733
+ '-reset_timestamps 1',
734
+ '-video_track_timescale 90000',
735
+ '-payload_type ' + request.video.pt,
736
+ '-ssrc ' + this.#hkSessions[request.sessionID].videoSSRC,
737
+ '-f rtp',
738
+ '-srtp_out_suite ' + this.hap.SRTPCryptoSuites[this.#hkSessions[request.sessionID].videoCryptoSuite],
739
+ '-srtp_out_params ' +
740
+ this.#hkSessions[request.sessionID].videoSRTP.toString('base64') +
763
741
  ' srtp://' +
764
742
  this.#hkSessions[request.sessionID].address +
765
743
  ':' +
766
- this.#hkSessions[request.sessionID].audioPort +
744
+ this.#hkSessions[request.sessionID].videoPort +
767
745
  '?rtcpport=' +
768
- this.#hkSessions[request.sessionID].audioPort +
769
- '&localrtcpport=' +
770
- this.#hkSessions[request.sessionID].localAudioPort +
771
- '&pkt_size=188';
746
+ this.#hkSessions[request.sessionID].videoPort +
747
+ '&pkt_size=' +
748
+ request.video.mtu,
749
+ );
750
+
751
+ // We have seperate video and audio streams that need to be muxed together if audio enabled
752
+ if (includeAudio === true) {
753
+ if (request.audio.codec === this.hap.AudioStreamingCodecType.AAC_ELD) {
754
+ commandLine.push('-map 1:a:0', '-codec:a libfdk_aac', '-profile:a aac_eld');
755
+ }
756
+
757
+ if (request.audio.codec === this.hap.AudioStreamingCodecType.OPUS) {
758
+ commandLine.push(
759
+ '-map 1:a:0',
760
+ '-codec:a libopus',
761
+ '-application lowdelay',
762
+ '-frame_duration ' + request.audio.packet_time.toString(),
763
+ );
764
+ }
765
+
766
+ commandLine.push(
767
+ '-flags +global_header',
768
+ '-ar ' + request.audio.sample_rate + 'k',
769
+ '-b:a ' + request.audio.max_bit_rate + 'k',
770
+ '-ac ' + request.audio.channel,
771
+ '-payload_type ' + request.audio.pt,
772
+ '-ssrc ' + this.#hkSessions[request.sessionID].audioSSRC,
773
+ '-f rtp',
774
+ '-srtp_out_suite ' + this.hap.SRTPCryptoSuites[this.#hkSessions[request.sessionID].audioCryptoSuite],
775
+ '-srtp_out_params ' +
776
+ this.#hkSessions[request.sessionID].audioSRTP.toString('base64') +
777
+ ' srtp://' +
778
+ this.#hkSessions[request.sessionID].address +
779
+ ':' +
780
+ this.#hkSessions[request.sessionID].audioPort +
781
+ '?rtcpport=' +
782
+ this.#hkSessions[request.sessionID].audioPort +
783
+ '&localrtcpport=' +
784
+ this.#hkSessions[request.sessionID].localAudioPort +
785
+ '&pkt_size=188',
786
+ );
772
787
  }
773
788
 
774
789
  // Start our ffmpeg streaming process and stream from our streamer
775
- let ffmpegStreaming = child_process.spawn(path.resolve(this.deviceData.ffmpeg.path + '/ffmpeg'), commandLine.split(' '), {
790
+ // video is pipe #1
791
+ // audio is pipe #3 if including audio
792
+ let ffmpegStreaming = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
776
793
  env: process.env,
777
- stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
778
- }); // Extra pipe, #3 for audio data
794
+ stdio: ['pipe', 'pipe', 'pipe', includeAudio === true ? 'pipe' : ''],
795
+ });
779
796
 
780
797
  // ffmpeg console output is via stderr
781
- /*
782
798
  ffmpegStreaming.stderr.on('data', (data) => {
783
799
  if (data.toString().includes('frame=') === false) {
784
800
  // Monitor ffmpeg output while testing. Use 'ffmpeg as a debug option'
785
801
  this?.log?.debug && this.log.debug(data.toString());
786
802
  }
787
803
  });
788
- */
789
804
 
790
805
  ffmpegStreaming.on('exit', (code, signal) => {
791
806
  if (signal !== 'SIGKILL' || signal === null) {
@@ -795,7 +810,9 @@ export default class NestCamera extends HomeKitDevice {
795
810
  this.deviceData.description,
796
811
  code,
797
812
  );
798
- this.controller.forceStopStreamingSession(request.sessionID);
813
+
814
+ // Clean up or streaming request, but calling it again with a 'STOP' reques
815
+ this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
799
816
  }
800
817
  });
801
818
 
@@ -807,7 +824,8 @@ export default class NestCamera extends HomeKitDevice {
807
824
  // We only enable two/way audio on camera/doorbell if we have the required libraries in ffmpeg AND two-way/audio is enabled
808
825
  let ffmpegAudioTalkback = null; // No ffmpeg process for return audio yet
809
826
  if (
810
- this.deviceData?.ffmpeg?.libspeex === true &&
827
+ ((this.streamer.codecs.talk === 'speex' && this.deviceData?.ffmpeg?.libspeex === true) ||
828
+ (this.streamer.codecs.talk === 'opus' && this.deviceData?.ffmpeg?.libopus === true)) &&
811
829
  this.deviceData?.ffmpeg?.libfdk_aac === true &&
812
830
  this.deviceData.audio_enabled === true &&
813
831
  this.deviceData.has_speaker === true &&
@@ -838,21 +856,26 @@ export default class NestCamera extends HomeKitDevice {
838
856
  });
839
857
 
840
858
  // Build ffmpeg command
841
- let commandLine =
842
- '-hide_banner -nostats' +
843
- ' -protocol_whitelist pipe,udp,rtp' +
844
- ' -f sdp' +
845
- ' -codec:a libfdk_aac' +
846
- ' -i pipe:0' +
847
- ' -map 0:a' +
848
- ' -codec:a libspeex' +
849
- ' -frames_per_packet 4' +
850
- ' -vad 1' + // testing to filter background noise?
851
- ' -ac 1' +
852
- ' -ar 16k' +
853
- ' -f data pipe:1';
854
-
855
- ffmpegAudioTalkback = child_process.spawn(path.resolve(this.deviceData.ffmpeg.path + '/ffmpeg'), commandLine.split(' '), {
859
+ let commandLine = [
860
+ '-hide_banner -nostats',
861
+ '-protocol_whitelist pipe,udp,rtp',
862
+ '-f sdp',
863
+ '-codec:a libfdk_aac',
864
+ '-i pipe:0',
865
+ '-map 0:a',
866
+ ];
867
+
868
+ if (this.streamer.codecs.talk === 'speex') {
869
+ commandLine.push('-codec:a libspeex', '-frames_per_packet 4', '-vad 1', '-ac 1', '-ar 16k');
870
+ }
871
+
872
+ if (this.streamer.codecs.talk === 'opus') {
873
+ commandLine.push('-codec:a libopus', '-application lowdelay', '-ac 2', '-ar 48k');
874
+ }
875
+
876
+ commandLine.push('-f data pipe:1');
877
+
878
+ ffmpegAudioTalkback = child_process.spawn(this.deviceData.ffmpeg.binary, commandLine.join(' ').split(' '), {
856
879
  env: process.env,
857
880
  });
858
881
 
@@ -864,7 +887,9 @@ export default class NestCamera extends HomeKitDevice {
864
887
  this.deviceData.description,
865
888
  code,
866
889
  );
867
- this.controller.forceStopStreamingSession(request.sessionID);
890
+
891
+ // Clean up or streaming request, but calling it again with a 'STOP' request
892
+ this.handleStreamRequest({ type: this.hap.StreamRequestTypes.STOP, sessionID: request.sessionID }, null);
868
893
  }
869
894
  });
870
895
 
@@ -874,52 +899,47 @@ export default class NestCamera extends HomeKitDevice {
874
899
  });
875
900
 
876
901
  // ffmpeg console output is via stderr
877
- /*
878
902
  ffmpegAudioTalkback.stderr.on('data', (data) => {
879
903
  this?.log?.debug && this.log.debug(data.toString());
880
904
  });
881
- */
882
905
 
883
906
  // Write out SDP configuration
884
907
  // Tried to align the SDP configuration to what HomeKit has sent us in its audio request details
885
- ffmpegAudioTalkback.stdin.write(
886
- 'v=0\n' +
887
- 'o=- 0 0 IN ' +
888
- (this.#hkSessions[request.sessionID].ipv6 ? 'IP6' : 'IP4') +
889
- ' ' +
890
- this.#hkSessions[request.sessionID].address +
891
- '\n' +
892
- 's=Nest Audio Talkback\n' +
893
- 'c=IN ' +
894
- (this.#hkSessions[request.sessionID].ipv6 ? 'IP6' : 'IP4') +
895
- ' ' +
896
- this.#hkSessions[request.sessionID].address +
897
- '\n' +
898
- 't=0 0\n' +
899
- 'm=audio ' +
900
- this.#hkSessions[request.sessionID].audioTalkbackPort +
901
- ' RTP/AVP ' +
902
- request.audio.pt +
903
- '\n' +
904
- 'b=AS:' +
905
- request.audio.max_bit_rate +
906
- '\n' +
907
- 'a=ptime:' +
908
- request.audio.packet_time +
909
- '\n' +
910
- 'a=rtpmap:' +
911
- request.audio.pt +
912
- ' MPEG4-GENERIC/' +
913
- request.audio.sample_rate * 1000 +
914
- '/1\n' +
908
+ let sdpResponse = [
909
+ 'v=0',
910
+ 'o=- 0 0 IN ' + (this.#hkSessions[request.sessionID].ipv6 ? 'IP6' : 'IP4') + ' ' + this.#hkSessions[request.sessionID].address,
911
+ 's=HomeKit Audio Talkback',
912
+ 'c=IN ' + (this.#hkSessions[request.sessionID].ipv6 ? 'IP6' : 'IP4') + ' ' + this.#hkSessions[request.sessionID].address,
913
+ 't=0 0',
914
+ 'm=audio ' + this.#hkSessions[request.sessionID].audioTalkbackPort + ' RTP/AVP ' + request.audio.pt,
915
+ 'b=AS:' + request.audio.max_bit_rate,
916
+ 'a=ptime:' + request.audio.packet_time,
917
+ ];
918
+
919
+ if (request.audio.codec === this.hap.AudioStreamingCodecType.AAC_ELD) {
920
+ sdpResponse.push(
921
+ 'a=rtpmap:' + request.audio.pt + ' MPEG4-GENERIC/' + request.audio.sample_rate * 1000 + '/' + request.audio.channel,
915
922
  'a=fmtp:' +
916
- request.audio.pt +
917
- ' profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8F0212C00BC00\n' +
918
- 'a=crypto:1 ' +
923
+ request.audio.pt +
924
+ ' profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8F0212C00BC00',
925
+ );
926
+ }
927
+
928
+ if (request.audio.codec === this.hap.AudioStreamingCodecType.OPUS) {
929
+ sdpResponse.push(
930
+ 'a=rtpmap:' + request.audio.pt + ' opus/' + request.audio.sample_rate * 1000 + '/' + request.audio.channel,
931
+ 'a=fmtp:' + request.audio.pt + ' minptime=10;useinbandfec=1"',
932
+ );
933
+ }
934
+
935
+ sdpResponse.push(
936
+ 'a=crypto:1 ' +
919
937
  this.hap.SRTPCryptoSuites[this.#hkSessions[request.sessionID].audioCryptoSuite] +
920
938
  ' inline:' +
921
939
  this.#hkSessions[request.sessionID].audioSRTP.toString('base64'),
922
940
  );
941
+
942
+ ffmpegAudioTalkback.stdin.write(sdpResponse.join('\r\n'));
923
943
  ffmpegAudioTalkback.stdin.end();
924
944
  }
925
945
 
@@ -949,6 +969,9 @@ export default class NestCamera extends HomeKitDevice {
949
969
  if (request.type === this.hap.StreamRequestTypes.STOP && typeof this.#hkSessions[request.sessionID] === 'object') {
950
970
  this.streamer !== undefined && this.streamer.stopLiveStream(request.sessionID);
951
971
 
972
+ // Close HomeKit session
973
+ this.controller.forceStopStreamingSession(request.sessionID);
974
+
952
975
  // Close off any running ffmpeg and/or splitter processes we created
953
976
  if (typeof this.#hkSessions[request.sessionID]?.rtpSplitter?.close === 'function') {
954
977
  this.#hkSessions[request.sessionID].rtpSplitter.close();
@@ -976,27 +999,85 @@ export default class NestCamera extends HomeKitDevice {
976
999
  return;
977
1000
  }
978
1001
 
979
- // For non-HKSV enabled devices, we will process any activity zone changes to add or remove any motion services
980
- if (deviceData.hksv === false && JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones)) {
981
- // Check to see if any activity zones were added
982
- deviceData.activity_zones.forEach((zone) => {
983
- if (typeof this.motionServices[zone.id]?.service === 'undefined') {
1002
+ if (this.deviceData.migrating === false && deviceData.migrating === true) {
1003
+ // Migration happening between Nest <-> Google Home apps. We'll stop any active streams, close the current streaming object
1004
+ this?.log?.warn && this.log.warn('Migration between Nest <-> Google Home apps has started for "%s"', deviceData.description);
1005
+ this.streamer !== undefined && this.streamer.stopEverything();
1006
+ this.streamer = undefined;
1007
+ }
1008
+
1009
+ if (this.deviceData.migrating === true && deviceData.migrating === false) {
1010
+ // Migration has completed between Nest <-> Google Home apps
1011
+ this?.log?.success && this.log.success('Migration between Nest <-> Google Home apps has completed for "%s"', deviceData.description);
1012
+ }
1013
+
1014
+ // Handle case of changes in streaming protocols OR just finished migration between Nest <-> Google Home apps
1015
+ if (this.streamer === undefined && deviceData.migrating === false) {
1016
+ if (JSON.stringify(deviceData.streaming_protocols) !== JSON.stringify(this.deviceData.streaming_protocols)) {
1017
+ this?.log?.warn && this.log.warn('Available streaming protocols have changed for "%s"', deviceData.description);
1018
+ this.streamer !== undefined && this.streamer.stopEverything();
1019
+ this.streamer = undefined;
1020
+ }
1021
+ if (deviceData.streaming_protocols.includes('PROTOCOL_WEBRTC') === true && WebRTC !== undefined) {
1022
+ this?.log?.debug && this.log.debug('Using WebRTC streamer for "%s"', deviceData.description);
1023
+ this.streamer = new WebRTC(deviceData, {
1024
+ log: this.log,
1025
+ buffer:
1026
+ deviceData.hksv === true &&
1027
+ this?.controller?.recordingManagement?.recordingManagementService !== undefined &&
1028
+ this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.Active).value ===
1029
+ this.hap.Characteristic.Active.ACTIVE,
1030
+ });
1031
+ }
1032
+
1033
+ if (deviceData.streaming_protocols.includes('PROTOCOL_NEXUSTALK') === true && NexusTalk !== undefined) {
1034
+ this?.log?.debug && this.log.debug('Using NexusTalk streamer for "%s"', deviceData.description);
1035
+ this.streamer = new NexusTalk(deviceData, {
1036
+ log: this.log,
1037
+ buffer:
1038
+ deviceData.hksv === true &&
1039
+ this?.controller?.recordingManagement?.recordingManagementService !== undefined &&
1040
+ this.controller.recordingManagement.recordingManagementService.getCharacteristic(this.hap.Characteristic.Active).value ===
1041
+ this.hap.Characteristic.Active.ACTIVE,
1042
+ });
1043
+ }
1044
+ }
1045
+
1046
+ // Check to see if any activity zones were added for both non-HKSV and HKSV enabled devices
1047
+ deviceData.activity_zones.forEach((zone) => {
1048
+ if (
1049
+ JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones) &&
1050
+ (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1))
1051
+ ) {
1052
+ if (this.motionServices?.[zone.id]?.service === undefined) {
984
1053
  // Zone doesn't have an associated motion sensor, so add one
985
1054
  let tempService = this.accessory.addService(this.hap.Service.MotionSensor, zone.id === 1 ? '' : zone.name, zone.id);
1055
+ if (tempService.testCharacteristic(this.hap.Characteristic.Active) === false) {
1056
+ tempService.addCharacteristic(this.hap.Characteristic.Active);
1057
+ }
986
1058
  tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
987
- this.motionServices[zone.id] = { service: tempService };
1059
+ this.motionServices[zone.id] = { service: tempService, timer: undefined };
988
1060
  }
989
- });
1061
+ }
1062
+ });
1063
+
1064
+ // Check to see if any activity zones were removed for both non-HKSV and HKSV enabled devices
1065
+ // We'll also update the online status of the camera in the motion service here
1066
+ Object.entries(this.motionServices).forEach(([zoneID, service]) => {
1067
+ // Set online status
1068
+ service.service.updateCharacteristic(
1069
+ this.hap.Characteristic.Active,
1070
+ deviceData.online === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE,
1071
+ );
990
1072
 
991
- // Check to see if any activity zones were removed
992
- Object.entries(this.motionServices).forEach(([zoneID, service]) => {
1073
+ if (JSON.stringify(deviceData.activity_zones) !== JSON.stringify(this.deviceData.activity_zones) && zoneID !== 1) {
993
1074
  if (deviceData.activity_zones.findIndex(({ id }) => id === zoneID) === -1) {
994
1075
  // Motion service we created doesn't appear in zone list anymore, so assume deleted
995
1076
  this.accessory.removeService(service.service);
996
1077
  delete this.motionServices[zoneID];
997
1078
  }
998
- });
999
- }
1079
+ }
1080
+ });
1000
1081
 
1001
1082
  if (this.operatingModeService !== undefined) {
1002
1083
  // Update camera off/on status
@@ -1007,7 +1088,7 @@ export default class NestCamera extends HomeKitDevice {
1007
1088
  : this.hap.Characteristic.ManuallyDisabled.ENABLED,
1008
1089
  );
1009
1090
 
1010
- if (deviceData.has_statusled === true && typeof deviceData.statusled_brightness === 'number') {
1091
+ if (deviceData.has_statusled === true) {
1011
1092
  // Set camera recording indicator. This cannot be turned off on Nest Cameras/Doorbells
1012
1093
  // 0 = auto
1013
1094
  // 1 = low
@@ -1162,10 +1243,11 @@ export default class NestCamera extends HomeKitDevice {
1162
1243
  this.deviceData.activity_zones.forEach((zone) => {
1163
1244
  if (this.deviceData.hksv === false || (this.deviceData.hksv === true && zone.id === 1)) {
1164
1245
  let tempService = this.accessory.addService(this.hap.Service.MotionSensor, zone.id === 1 ? '' : zone.name, zone.id);
1246
+ if (tempService.testCharacteristic(this.hap.Characteristic.Active) === false) {
1247
+ tempService.addCharacteristic(this.hap.Characteristic.Active);
1248
+ }
1165
1249
  tempService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false); // No motion initially
1166
- this.motionServices[zone.id] = {
1167
- service: tempService,
1168
- };
1250
+ this.motionServices[zone.id] = { service: tempService, timer: undefined };
1169
1251
  }
1170
1252
  });
1171
1253
  }
@@ -1206,7 +1288,7 @@ export default class NestCamera extends HomeKitDevice {
1206
1288
  audio: {
1207
1289
  twoWayAudio:
1208
1290
  this.deviceData?.ffmpeg?.libfdk_aac === true &&
1209
- this.deviceData?.ffmpeg?.libspeex === true &&
1291
+ (this.deviceData?.ffmpeg?.libspeex === true || this.deviceData?.ffmpeg?.libopus === true) &&
1210
1292
  this.deviceData.has_speaker === true &&
1211
1293
  this.deviceData.has_microphone === true,
1212
1294
  codecs: [