homebridge-nest-accfactory 0.0.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -4
- package/README.md +37 -19
- package/dist/HomeKitDevice.js +132 -109
- package/dist/camera.js +344 -262
- package/dist/doorbell.js +5 -3
- package/dist/floodlight.js +4 -4
- package/dist/nexustalk.js +84 -52
- package/dist/protect.js +2 -2
- package/dist/protobuf/googlehome/foyer.proto +216 -160
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_transfer.h264 +0 -0
- package/dist/streamer.js +100 -71
- package/dist/system.js +1321 -1263
- package/dist/thermostat.js +73 -27
- package/dist/webrtc.js +582 -0
- package/package.json +31 -29
package/dist/camera.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Nest Cameras
|
|
2
2
|
// Part of homebridge-nest-accfactory
|
|
3
3
|
//
|
|
4
|
-
// Code version
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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('
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
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?.
|
|
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
|
|
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
|
|
330
|
-
'
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
344
|
-
'
|
|
345
|
-
'
|
|
346
|
-
'
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
this.#recordingConfig.videoCodec.
|
|
363
|
-
'
|
|
364
|
-
|
|
365
|
-
'
|
|
366
|
-
|
|
367
|
-
'
|
|
368
|
-
|
|
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
|
-
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
-
'
|
|
390
|
-
'
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ? '
|
|
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
|
|
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?.
|
|
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 (
|
|
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
|
|
705
|
-
'
|
|
706
|
-
'
|
|
707
|
-
'
|
|
708
|
-
'
|
|
709
|
-
'
|
|
710
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
commandLine
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
request.
|
|
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].
|
|
744
|
+
this.#hkSessions[request.sessionID].videoPort +
|
|
767
745
|
'?rtcpport=' +
|
|
768
|
-
this.#hkSessions[request.sessionID].
|
|
769
|
-
'&
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
-
'
|
|
844
|
-
'
|
|
845
|
-
'
|
|
846
|
-
'
|
|
847
|
-
'
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
' -ac 1'
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
886
|
-
'v=0
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
'
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|