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/CHANGELOG.md +53 -4
- package/README.md +37 -19
- package/dist/HomeKitDevice.js +132 -109
- package/dist/camera.js +344 -263
- package/dist/doorbell.js +5 -3
- package/dist/floodlight.js +3 -3
- package/dist/nexustalk.js +62 -36
- 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 +78 -37
- 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,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({
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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('
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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?.
|
|
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
|
|
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
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
'
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
345
|
-
'
|
|
346
|
-
'
|
|
347
|
-
'
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
'
|
|
361
|
-
'
|
|
362
|
-
'
|
|
363
|
-
this.#recordingConfig.videoCodec.
|
|
364
|
-
'
|
|
365
|
-
|
|
366
|
-
'
|
|
367
|
-
|
|
368
|
-
'
|
|
369
|
-
|
|
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
|
-
|
|
388
|
-
'
|
|
389
|
-
'
|
|
390
|
-
'
|
|
391
|
-
'
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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 (
|
|
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 ? '
|
|
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
|
|
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?.
|
|
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 (
|
|
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
|
|
706
|
-
'
|
|
707
|
-
'
|
|
708
|
-
'
|
|
709
|
-
'
|
|
710
|
-
'
|
|
711
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
commandLine
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
request.
|
|
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].
|
|
744
|
+
this.#hkSessions[request.sessionID].videoPort +
|
|
768
745
|
'?rtcpport=' +
|
|
769
|
-
this.#hkSessions[request.sessionID].
|
|
770
|
-
'&
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
-
'
|
|
845
|
-
'
|
|
846
|
-
'
|
|
847
|
-
'
|
|
848
|
-
'
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
' -ac 1'
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
887
|
-
'v=0
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
'
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
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
|
|
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: [
|