homebridge-nest-accfactory 0.0.6 → 0.2.1

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