homebridge-nest-accfactory 0.3.1 → 0.3.2
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 +27 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +495 -262
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +67 -85
- package/dist/consts.js +160 -0
- package/dist/devices.js +35 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +157 -124
- package/dist/plugins/camera.js +1153 -924
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +239 -70
- package/dist/plugins/tempsensor.js +158 -34
- package/dist/plugins/thermostat.js +890 -454
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +486 -244
- package/dist/system.js +1739 -2852
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +354 -225
- package/package.json +19 -16
package/dist/webrtc.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
// Handles connection and data from Google WebRTC systems
|
|
5
5
|
// Currently a "work in progress"
|
|
6
6
|
//
|
|
7
|
-
// Code version 2025.
|
|
7
|
+
// Code version 2025.07.23
|
|
8
8
|
// Mark Hulskamp
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
// Define external library requirements
|
|
12
12
|
import protobuf from 'protobufjs';
|
|
13
13
|
import * as werift from 'werift';
|
|
14
|
+
import { Decoder } from '@evan/opus';
|
|
14
15
|
|
|
15
16
|
// Define nodejs module requirements
|
|
16
17
|
import EventEmitter from 'node:events';
|
|
@@ -20,38 +21,28 @@ import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timer
|
|
|
20
21
|
import fs from 'node:fs';
|
|
21
22
|
import path from 'node:path';
|
|
22
23
|
import crypto from 'node:crypto';
|
|
23
|
-
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
25
|
// Define our modules
|
|
26
26
|
import Streamer from './streamer.js';
|
|
27
27
|
|
|
28
28
|
// Define constants
|
|
29
|
-
|
|
29
|
+
import { USER_AGENT, __dirname } from './consts.js';
|
|
30
|
+
|
|
31
|
+
const EXTEND_INTERVAL = 30000; // Send extend command to Google Home Foyer every this period for active streams
|
|
30
32
|
const RTP_PACKET_HEADER_SIZE = 12;
|
|
31
33
|
const RTP_VIDEO_PAYLOAD_TYPE = 102;
|
|
32
34
|
const RTP_AUDIO_PAYLOAD_TYPE = 111;
|
|
33
35
|
//const RTP_TALKBACK_PAYLOAD_TYPE = 110;
|
|
34
|
-
const USER_AGENT = 'Nest/5.78.0 (iOScom.nestlabs.jasper.release) os=18.0'; // User Agent string
|
|
35
36
|
const GOOGLE_HOME_FOYER_PREFIX = 'google.internal.home.foyer.v1.';
|
|
36
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
37
|
-
|
|
38
|
-
// Blank audio in AAC format, mono channel @48000
|
|
39
|
-
const AAC_MONO_48000_BLANK = Buffer.from([
|
|
40
|
-
0xff, 0xf1, 0x4c, 0x40, 0x03, 0x9f, 0xfc, 0xde, 0x02, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x39, 0x2e, 0x31, 0x38, 0x2e, 0x31, 0x30, 0x30,
|
|
41
|
-
0x00, 0x02, 0x30, 0x40, 0x0e,
|
|
42
|
-
]);
|
|
43
37
|
|
|
44
|
-
// Blank audio in
|
|
45
|
-
|
|
38
|
+
// Blank audio in Opus format, mono channel @48000
|
|
39
|
+
const PCM_S16LE_48000_STEREO_BLANK = Buffer.alloc(1920 * 2 * 2); // 20ms stereo silence at 48kHz
|
|
46
40
|
|
|
47
41
|
// WebRTC object
|
|
48
42
|
export default class WebRTC extends Streamer {
|
|
49
43
|
token = undefined; // oauth2 token
|
|
50
44
|
localAccess = false; // Do we try direct local access to the camera or via Google Home first
|
|
51
|
-
|
|
52
|
-
stalledTimer = undefined; // Timer object for no received data
|
|
53
|
-
pingTimer = undefined; // Google Hopme Foyer periodic ping
|
|
54
|
-
blankAudio = AAC_MONO_48000_BLANK;
|
|
45
|
+
blankAudio = PCM_S16LE_48000_STEREO_BLANK;
|
|
55
46
|
video = {}; // Video stream details once connected
|
|
56
47
|
audio = {}; // Audio stream details once connected
|
|
57
48
|
|
|
@@ -64,9 +55,22 @@ export default class WebRTC extends Streamer {
|
|
|
64
55
|
#peerConnection = undefined;
|
|
65
56
|
#videoTransceiver = undefined;
|
|
66
57
|
#audioTransceiver = undefined;
|
|
58
|
+
#opusDecoder = new Decoder({ channels: 2, sample_rate: 48000 });
|
|
59
|
+
#extendTimer = undefined; // Stream extend timer
|
|
60
|
+
#stalledTimer = undefined; // Timer object for no received data
|
|
61
|
+
#pingTimer = undefined; // Google Hopme Foyer periodic ping
|
|
62
|
+
|
|
63
|
+
// Codecs being used for video, audio and talking
|
|
64
|
+
get codecs() {
|
|
65
|
+
return {
|
|
66
|
+
video: Streamer.CODEC_TYPE.H264, // Video is H264
|
|
67
|
+
audio: Streamer.CODEC_TYPE.PCM, // Audio is PCM (we decode Opus to PCM output)
|
|
68
|
+
talkback: Streamer.CODEC_TYPE.OPUS, // Talking is also Opus
|
|
69
|
+
};
|
|
70
|
+
}
|
|
67
71
|
|
|
68
|
-
constructor(deviceData, options) {
|
|
69
|
-
super(deviceData, options);
|
|
72
|
+
constructor(uuid, deviceData, options) {
|
|
73
|
+
super(uuid, deviceData, options);
|
|
70
74
|
|
|
71
75
|
// Load the protobuf for Google Home Foyer. Needed to communicate with camera devices using webrtc
|
|
72
76
|
if (fs.existsSync(path.resolve(__dirname + '/protobuf/googlehome/foyer.proto')) === true) {
|
|
@@ -83,26 +87,14 @@ export default class WebRTC extends Streamer {
|
|
|
83
87
|
if (deviceData?.apiAccess?.fieldTest === true) {
|
|
84
88
|
this.#googleHomeFoyerAPIHost = 'https://preprod-googlehomefoyer-pa.sandbox.googleapis.com';
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
// Set our streamer codec types
|
|
88
|
-
this.codecs = {
|
|
89
|
-
video: 'h264',
|
|
90
|
-
audio: 'opus',
|
|
91
|
-
talk: 'opus',
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
// If specified option to start buffering, kick off
|
|
95
|
-
if (options?.buffer === true) {
|
|
96
|
-
this.startBuffering();
|
|
97
|
-
}
|
|
98
90
|
}
|
|
99
91
|
|
|
100
92
|
// Class functions
|
|
101
93
|
async connect() {
|
|
102
|
-
clearInterval(this
|
|
103
|
-
clearTimeout(this
|
|
104
|
-
this
|
|
105
|
-
this
|
|
94
|
+
clearInterval(this.#extendTimer);
|
|
95
|
+
clearTimeout(this.#stalledTimer);
|
|
96
|
+
this.#extendTimer = undefined;
|
|
97
|
+
this.#stalledTimer = undefined;
|
|
106
98
|
this.#id = undefined;
|
|
107
99
|
|
|
108
100
|
if (this.online === true && this.videoEnabled === true) {
|
|
@@ -121,7 +113,7 @@ export default class WebRTC extends Streamer {
|
|
|
121
113
|
// Test to see if our uuid matches here
|
|
122
114
|
let currentGoogleUuid = device?.id?.googleUuid;
|
|
123
115
|
Object.values(device.otherIds.otherThirdPartyId).forEach((other) => {
|
|
124
|
-
if (other?.id === this.
|
|
116
|
+
if (other?.id === this.nest_google_uuid) {
|
|
125
117
|
this.#googleHomeDeviceUUID = currentGoogleUuid;
|
|
126
118
|
}
|
|
127
119
|
});
|
|
@@ -145,7 +137,7 @@ export default class WebRTC extends Streamer {
|
|
|
145
137
|
|
|
146
138
|
if (homeFoyerResponse.status !== 0) {
|
|
147
139
|
this.connected = undefined;
|
|
148
|
-
this?.log?.debug?.('Request to start camera viewing was not accepted for uuid "%s"', this.
|
|
140
|
+
this?.log?.debug?.('Request to start camera viewing was not accepted for uuid "%s"', this.nest_google_uuid);
|
|
149
141
|
}
|
|
150
142
|
|
|
151
143
|
if (homeFoyerResponse.status === 0) {
|
|
@@ -200,11 +192,11 @@ export default class WebRTC extends Streamer {
|
|
|
200
192
|
let webRTCOffer = await this.#peerConnection.createOffer();
|
|
201
193
|
await this.#peerConnection.setLocalDescription(webRTCOffer);
|
|
202
194
|
|
|
203
|
-
this?.log?.debug?.('Sending WebRTC offer for uuid "%s"', this.
|
|
195
|
+
this?.log?.debug?.('Sending WebRTC offer for uuid "%s"', this.nest_google_uuid);
|
|
204
196
|
|
|
205
197
|
homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
|
|
206
198
|
command: 'offer',
|
|
207
|
-
deviceId: this.
|
|
199
|
+
deviceId: this.nest_google_uuid,
|
|
208
200
|
local: this.localAccess,
|
|
209
201
|
streamContext: 'STREAM_CONTEXT_DEFAULT',
|
|
210
202
|
requestedVideoResolution: 'VIDEO_RESOLUTION_FULL_HIGH',
|
|
@@ -213,7 +205,7 @@ export default class WebRTC extends Streamer {
|
|
|
213
205
|
|
|
214
206
|
if (homeFoyerResponse.status !== 0) {
|
|
215
207
|
this.connected = undefined;
|
|
216
|
-
this?.log?.debug?.('WebRTC offer was not agreed with remote for uuid "%s"', this.
|
|
208
|
+
this?.log?.debug?.('WebRTC offer was not agreed with remote for uuid "%s"', this.nest_google_uuid);
|
|
217
209
|
}
|
|
218
210
|
|
|
219
211
|
if (
|
|
@@ -221,58 +213,53 @@ export default class WebRTC extends Streamer {
|
|
|
221
213
|
homeFoyerResponse.data?.[0]?.responseType === 'answer' &&
|
|
222
214
|
homeFoyerResponse.data?.[0]?.streamId !== undefined
|
|
223
215
|
) {
|
|
224
|
-
this?.log?.debug?.('WebRTC offer agreed with remote for uuid "%s"', this.
|
|
216
|
+
this?.log?.debug?.('WebRTC offer agreed with remote for uuid "%s"', this.nest_google_uuid);
|
|
225
217
|
|
|
226
|
-
this.#audioTransceiver?.onTrack
|
|
227
|
-
this.#
|
|
228
|
-
this.#handlePlaybackBegin(track);
|
|
218
|
+
this.#audioTransceiver?.onTrack?.subscribe?.((track) => {
|
|
219
|
+
this.#handlePlaybackBegin(track);
|
|
229
220
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
});
|
|
221
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
222
|
+
this.#handlePlaybackPacket(rtp);
|
|
233
223
|
});
|
|
224
|
+
});
|
|
234
225
|
|
|
235
|
-
this.#videoTransceiver?.onTrack
|
|
236
|
-
this.#
|
|
237
|
-
this.#handlePlaybackBegin(track);
|
|
226
|
+
this.#videoTransceiver?.onTrack?.subscribe?.((track) => {
|
|
227
|
+
this.#handlePlaybackBegin(track);
|
|
238
228
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
}, 2000);
|
|
248
|
-
});
|
|
229
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
230
|
+
this.#handlePlaybackPacket(rtp);
|
|
231
|
+
});
|
|
232
|
+
track.onReceiveRtcp.once(() => {
|
|
233
|
+
setInterval(() => {
|
|
234
|
+
this.#videoTransceiver?.receiver?.sendRtcpPLI?.(track.ssrc);
|
|
235
|
+
}, 2000);
|
|
249
236
|
});
|
|
237
|
+
});
|
|
250
238
|
|
|
251
239
|
this.#id = homeFoyerResponse.data[0].streamId;
|
|
252
|
-
this.#peerConnection &&
|
|
253
|
-
(await this.#peerConnection.setRemoteDescription({
|
|
254
|
-
type: 'answer',
|
|
255
|
-
sdp: homeFoyerResponse.data[0].sdp,
|
|
256
|
-
}));
|
|
257
240
|
|
|
258
|
-
this?.
|
|
241
|
+
await this.#peerConnection?.setRemoteDescription?.({
|
|
242
|
+
type: 'answer',
|
|
243
|
+
sdp: homeFoyerResponse.data[0].sdp,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this?.log?.debug?.('Playback started from WebRTC for uuid "%s" with session ID "%s"', this.nest_google_uuid, this.#id);
|
|
259
247
|
this.connected = true;
|
|
260
248
|
|
|
261
249
|
// Monitor connection status. If closed and there are still output streams, re-connect
|
|
262
250
|
// Never seem to get a 'connected' status. Could use that for something?
|
|
263
|
-
this.#peerConnection
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
this.connect();
|
|
270
|
-
}
|
|
251
|
+
this.#peerConnection?.iceConnectionStateChange?.subscribe?.((state) => {
|
|
252
|
+
if (state !== 'connected' && state !== 'connecting') {
|
|
253
|
+
this?.log?.debug?.('Connection closed to WebRTC for uuid "%s"', this.nest_google_uuid);
|
|
254
|
+
this.connected = undefined;
|
|
255
|
+
if (this.isStreaming() === true || this.isBuffering() === true) {
|
|
256
|
+
this.connect();
|
|
271
257
|
}
|
|
272
|
-
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
273
260
|
|
|
274
261
|
// Create a timer to extend the active stream every period as defined
|
|
275
|
-
this
|
|
262
|
+
this.#extendTimer = setInterval(async () => {
|
|
276
263
|
if (
|
|
277
264
|
this.#googleHomeFoyer !== undefined &&
|
|
278
265
|
this.connected === true &&
|
|
@@ -281,16 +268,14 @@ export default class WebRTC extends Streamer {
|
|
|
281
268
|
) {
|
|
282
269
|
let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
|
|
283
270
|
command: 'extend',
|
|
284
|
-
deviceId: this.
|
|
271
|
+
deviceId: this.nest_google_uuid,
|
|
285
272
|
streamId: this.#id,
|
|
286
273
|
});
|
|
287
274
|
|
|
288
275
|
if (homeFoyerResponse?.data?.[0]?.streamExtensionStatus !== 'STATUS_STREAM_EXTENDED') {
|
|
289
|
-
this?.log?.debug?.('Error occurred while requested stream extension for uuid "%s"', this.
|
|
276
|
+
this?.log?.debug?.('Error occurred while requested stream extension for uuid "%s"', this.nest_google_uuid);
|
|
290
277
|
|
|
291
|
-
|
|
292
|
-
await this.#peerConnection.close();
|
|
293
|
-
}
|
|
278
|
+
await this.#peerConnection?.close?.();
|
|
294
279
|
}
|
|
295
280
|
}
|
|
296
281
|
}, EXTEND_INTERVAL);
|
|
@@ -313,27 +298,23 @@ export default class WebRTC extends Streamer {
|
|
|
313
298
|
});
|
|
314
299
|
}
|
|
315
300
|
|
|
316
|
-
this?.log?.debug?.('Notifying remote about closing connection for uuid "%s"', this.
|
|
301
|
+
this?.log?.debug?.('Notifying remote about closing connection for uuid "%s"', this.nest_google_uuid);
|
|
317
302
|
await this.#googleHomeFoyerCommand('CameraService', 'JoinStream', {
|
|
318
303
|
command: 'end',
|
|
319
|
-
deviceId: this.
|
|
304
|
+
deviceId: this.nest_google_uuid,
|
|
320
305
|
streamId: this.#id,
|
|
321
306
|
endStreamReason: 'REASON_USER_EXITED_SESSION',
|
|
322
307
|
});
|
|
323
308
|
}
|
|
324
309
|
|
|
325
|
-
|
|
326
|
-
this.#googleHomeFoyer.destroy();
|
|
327
|
-
}
|
|
310
|
+
this.#googleHomeFoyer?.destroy?.();
|
|
328
311
|
|
|
329
|
-
|
|
330
|
-
await this.#peerConnection.close();
|
|
331
|
-
}
|
|
312
|
+
await this.#peerConnection?.close?.();
|
|
332
313
|
|
|
333
|
-
clearInterval(this
|
|
334
|
-
clearInterval(this
|
|
335
|
-
this
|
|
336
|
-
this
|
|
314
|
+
clearInterval(this.#extendTimer);
|
|
315
|
+
clearInterval(this.#stalledTimer);
|
|
316
|
+
this.#extendTimer = undefined;
|
|
317
|
+
this.#stalledTimer = undefined;
|
|
337
318
|
this.#id = undefined;
|
|
338
319
|
this.#googleHomeFoyer = undefined;
|
|
339
320
|
this.#peerConnection = undefined;
|
|
@@ -344,23 +325,28 @@ export default class WebRTC extends Streamer {
|
|
|
344
325
|
this.audio = {};
|
|
345
326
|
}
|
|
346
327
|
|
|
347
|
-
|
|
328
|
+
async onUpdate(deviceData) {
|
|
348
329
|
if (typeof deviceData !== 'object') {
|
|
349
330
|
return;
|
|
350
331
|
}
|
|
351
332
|
|
|
352
|
-
if (
|
|
333
|
+
if (
|
|
334
|
+
typeof deviceData?.apiAccess?.oauth2 === 'string' &&
|
|
335
|
+
deviceData.apiAccess.oauth2 !== '' &&
|
|
336
|
+
deviceData.apiAccess.oauth2 !== this.token
|
|
337
|
+
) {
|
|
353
338
|
// OAuth2 token has changed
|
|
354
339
|
this.token = deviceData.apiAccess.oauth2;
|
|
340
|
+
if (this.isStreaming() === true || this.isBuffering() === true) {
|
|
341
|
+
this?.log?.debug?.('OAuth2 token has been updated for WebRTC on uuid "%s". Restarting connection.', this.nest_google_uuid);
|
|
342
|
+
await this.#peerConnection?.close?.();
|
|
343
|
+
}
|
|
355
344
|
}
|
|
356
|
-
|
|
357
|
-
// Let our parent handle the remaining updates
|
|
358
|
-
super.update(deviceData);
|
|
359
345
|
}
|
|
360
346
|
|
|
361
|
-
async
|
|
347
|
+
async sendTalkback(talkingBuffer) {
|
|
362
348
|
if (
|
|
363
|
-
Buffer.isBuffer(
|
|
349
|
+
Buffer.isBuffer(talkingBuffer) === false ||
|
|
364
350
|
this.#googleHomeDeviceUUID === undefined ||
|
|
365
351
|
this.#id === undefined ||
|
|
366
352
|
typeof this.#audioTransceiver?.sender?.sendRtp !== 'function'
|
|
@@ -368,7 +354,7 @@ export default class WebRTC extends Streamer {
|
|
|
368
354
|
return;
|
|
369
355
|
}
|
|
370
356
|
|
|
371
|
-
if (
|
|
357
|
+
if (talkingBuffer.length !== 0) {
|
|
372
358
|
if (this.audio?.talking === undefined) {
|
|
373
359
|
this.audio.talking = false;
|
|
374
360
|
let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'SendTalkback', {
|
|
@@ -381,11 +367,11 @@ export default class WebRTC extends Streamer {
|
|
|
381
367
|
|
|
382
368
|
if (homeFoyerResponse?.status !== 0) {
|
|
383
369
|
this.audio.talking = undefined;
|
|
384
|
-
this?.log?.debug?.('Error occurred while requesting talkback to start for uuid "%s"', this.
|
|
370
|
+
this?.log?.debug?.('Error occurred while requesting talkback to start for uuid "%s"', this.nest_google_uuid);
|
|
385
371
|
}
|
|
386
372
|
if (homeFoyerResponse?.status === 0) {
|
|
387
373
|
this.audio.talking = true;
|
|
388
|
-
this?.log?.debug?.('Talking start on uuid "%s"', this.
|
|
374
|
+
this?.log?.debug?.('Talking start on uuid "%s"', this.nest_google_uuid);
|
|
389
375
|
}
|
|
390
376
|
}
|
|
391
377
|
|
|
@@ -396,14 +382,14 @@ export default class WebRTC extends Streamer {
|
|
|
396
382
|
rtpHeader.marker = true;
|
|
397
383
|
rtpHeader.payloadOffset = RTP_PACKET_HEADER_SIZE;
|
|
398
384
|
rtpHeader.payloadType = this.audio.id; // As the camera is send/recv, we use the same payload type id as the incoming audio
|
|
399
|
-
rtpHeader.timestamp = Date.now()
|
|
385
|
+
rtpHeader.timestamp = Date.now() >>> 0; // Think the time stamp difference should be 960ms per audio packet?
|
|
400
386
|
rtpHeader.sequenceNumber = this.audio.talkSquenceNumber++ & 0xffff;
|
|
401
|
-
let rtpPacket = new werift.RtpPacket(rtpHeader,
|
|
387
|
+
let rtpPacket = new werift.RtpPacket(rtpHeader, talkingBuffer);
|
|
402
388
|
this.#audioTransceiver.sender.sendRtp(rtpPacket.serialize());
|
|
403
389
|
}
|
|
404
390
|
}
|
|
405
391
|
|
|
406
|
-
if (
|
|
392
|
+
if (talkingBuffer.length === 0 && this.audio?.talking === true) {
|
|
407
393
|
// Buffer length of zero, ised to signal no more talking data for the moment
|
|
408
394
|
let homeFoyerResponse = await this.#googleHomeFoyerCommand('CameraService', 'SendTalkback', {
|
|
409
395
|
googleDeviceId: {
|
|
@@ -413,90 +399,206 @@ export default class WebRTC extends Streamer {
|
|
|
413
399
|
command: 'COMMAND_STOP',
|
|
414
400
|
});
|
|
415
401
|
if (homeFoyerResponse?.status !== 0) {
|
|
416
|
-
this?.log?.debug?.('Error occurred while requesting talkback to stop for uuid "%s"', this.
|
|
402
|
+
this?.log?.debug?.('Error occurred while requesting talkback to stop for uuid "%s"', this.nest_google_uuid);
|
|
417
403
|
}
|
|
418
404
|
if (homeFoyerResponse?.status === 0) {
|
|
419
|
-
this?.log?.debug?.('Talking ended on uuid "%s"', this.
|
|
405
|
+
this?.log?.debug?.('Talking ended on uuid "%s"', this.nest_google_uuid);
|
|
420
406
|
}
|
|
421
407
|
this.audio.talking = undefined;
|
|
422
408
|
}
|
|
423
409
|
}
|
|
424
410
|
|
|
425
411
|
#handlePlaybackBegin(weriftTrack) {
|
|
412
|
+
// Handle the beginning of RTP playback for an audio or video track
|
|
426
413
|
if (weriftTrack === undefined || typeof weriftTrack !== 'object') {
|
|
427
414
|
return;
|
|
428
415
|
}
|
|
429
416
|
|
|
430
417
|
if (weriftTrack?.kind === 'audio') {
|
|
431
|
-
// Store details about the audio track
|
|
432
418
|
this.audio = {
|
|
433
|
-
id: weriftTrack.codec.payloadType, //
|
|
434
|
-
|
|
419
|
+
id: weriftTrack.codec.payloadType, // RTP payload type for audio
|
|
420
|
+
baseTimestamp: undefined,
|
|
421
|
+
baseTime: undefined,
|
|
435
422
|
sampleRate: 48000,
|
|
436
|
-
opus: undefined,
|
|
423
|
+
opus: undefined,
|
|
437
424
|
talkSquenceNumber: weriftTrack?.sender?.sequenceNumber === undefined ? 0 : weriftTrack.sender.sequenceNumber,
|
|
438
|
-
talking: undefined,
|
|
425
|
+
talking: undefined,
|
|
439
426
|
};
|
|
440
427
|
}
|
|
441
428
|
|
|
442
429
|
if (weriftTrack?.kind === 'video') {
|
|
443
|
-
// Store details about the video track
|
|
444
430
|
this.video = {
|
|
445
|
-
id: weriftTrack.codec.payloadType, //
|
|
446
|
-
|
|
431
|
+
id: weriftTrack.codec.payloadType, // RTP payload type for video
|
|
432
|
+
baseTimestamp: undefined,
|
|
433
|
+
baseTime: undefined,
|
|
447
434
|
sampleRate: 90000,
|
|
448
|
-
h264:
|
|
435
|
+
h264: {
|
|
436
|
+
fuBuffer: undefined,
|
|
437
|
+
fuTimer: undefined,
|
|
438
|
+
fuSeqStart: undefined,
|
|
439
|
+
fuType: undefined,
|
|
440
|
+
fuTimestamp: undefined,
|
|
441
|
+
lastSPS: undefined,
|
|
442
|
+
lastPPS: undefined,
|
|
443
|
+
},
|
|
449
444
|
};
|
|
450
445
|
}
|
|
451
446
|
}
|
|
452
447
|
|
|
453
|
-
|
|
454
|
-
if (
|
|
448
|
+
#handlePlaybackPacket(rtpPacket) {
|
|
449
|
+
if (typeof rtpPacket !== 'object' || rtpPacket === undefined) {
|
|
455
450
|
return;
|
|
456
451
|
}
|
|
457
452
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
this?.log?.debug?.(
|
|
463
|
-
'We have not received any data from webrtc in the past "%s" seconds for uuid "%s". Attempting restart',
|
|
464
|
-
10,
|
|
465
|
-
this.uuid,
|
|
466
|
-
);
|
|
467
|
-
|
|
468
|
-
if (typeof this.#peerConnection?.close === 'function') {
|
|
469
|
-
await this.#peerConnection.close();
|
|
470
|
-
}
|
|
453
|
+
// Create timer for stalled rtp output. Restart strem if so
|
|
454
|
+
clearTimeout(this.#stalledTimer);
|
|
455
|
+
this.#stalledTimer = setTimeout(async () => {
|
|
456
|
+
await this.#peerConnection?.close?.();
|
|
471
457
|
}, 10000);
|
|
472
458
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
459
|
+
const calculateTimestamp = (packet, stream) => {
|
|
460
|
+
if (typeof packet?.header?.timestamp !== 'number' || typeof stream?.sampleRate !== 'number') {
|
|
461
|
+
return Date.now();
|
|
462
|
+
}
|
|
463
|
+
if (stream.baseTimestamp === undefined) {
|
|
464
|
+
stream.baseTimestamp = packet.header.timestamp;
|
|
465
|
+
stream.baseTime = Date.now();
|
|
466
|
+
}
|
|
467
|
+
let deltaTicks = (packet.header.timestamp - stream.baseTimestamp + 0x100000000) % 0x100000000;
|
|
468
|
+
let deltaMs = (deltaTicks / stream.sampleRate) * 1000;
|
|
469
|
+
return stream.baseTime + deltaMs;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Video packet processing
|
|
473
|
+
if (rtpPacket.header.payloadType === this.video?.id) {
|
|
474
|
+
let seq = rtpPacket.header.sequenceNumber;
|
|
475
|
+
if (this.video.h264.lastSeq !== undefined) {
|
|
476
|
+
let gap = (seq - this.video.h264.lastSeq + 0x10000) % 0x10000;
|
|
477
|
+
if (gap !== 1) {
|
|
478
|
+
// sequence gap (ignored silently)
|
|
480
479
|
}
|
|
481
480
|
}
|
|
481
|
+
this.video.h264.lastSeq = seq;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
484
|
+
if (rtpPacket.header.payloadType === this.video?.id) {
|
|
485
|
+
let payload = rtpPacket.payload;
|
|
486
|
+
if (!Buffer.isBuffer(payload) || payload.length < 1) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let nalType = payload[0] & 0x1f;
|
|
490
491
|
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
// STAP-A
|
|
493
|
+
if (nalType === Streamer.H264NALUS.TYPES.STAP_A) {
|
|
494
|
+
let offset = 1;
|
|
495
|
+
while (offset + 2 <= payload.length) {
|
|
496
|
+
let size = payload.readUInt16BE(offset);
|
|
497
|
+
offset += 2;
|
|
498
|
+
if (size < 1 || offset + size > payload.length) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
let nalu = payload.subarray(offset, offset + size);
|
|
502
|
+
let type = nalu[0] & 0x1f;
|
|
503
|
+
if (type === Streamer.H264NALUS.TYPES.SPS) {
|
|
504
|
+
this.video.h264.sps = nalu;
|
|
505
|
+
} else if (type === Streamer.H264NALUS.TYPES.PPS) {
|
|
506
|
+
this.video.h264.pps = nalu;
|
|
507
|
+
}
|
|
508
|
+
offset += size;
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// FU-A
|
|
514
|
+
if (nalType === Streamer.H264NALUS.TYPES.FU_A) {
|
|
515
|
+
let indicator = payload[0];
|
|
516
|
+
let header = payload[1];
|
|
517
|
+
let start = (header & 0x80) !== 0;
|
|
518
|
+
let end = (header & 0x40) !== 0;
|
|
519
|
+
let type = header & 0x1f;
|
|
520
|
+
let nalHeader = (indicator & 0xe0) | type;
|
|
521
|
+
|
|
522
|
+
if (this.video.h264.fragmentTime && Date.now() - this.video.h264.fragmentTime > 2000) {
|
|
523
|
+
delete this.video.h264.fragment;
|
|
524
|
+
delete this.video.h264.fragmentTime;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (start) {
|
|
528
|
+
if (Array.isArray(this.video.h264.fragment)) {
|
|
529
|
+
delete this.video.h264.fragment;
|
|
530
|
+
delete this.video.h264.fragmentTime;
|
|
531
|
+
}
|
|
532
|
+
this.video.h264.fragment = [Buffer.from([nalHeader]), payload.subarray(2)];
|
|
533
|
+
this.video.h264.fragmentTime = Date.now();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (Array.isArray(this.video.h264.fragment)) {
|
|
538
|
+
this.video.h264.fragment.push(payload.subarray(2));
|
|
539
|
+
if (end) {
|
|
540
|
+
let buffer = Buffer.concat(this.video.h264.fragment);
|
|
541
|
+
delete this.video.h264.fragment;
|
|
542
|
+
delete this.video.h264.fragmentTime;
|
|
543
|
+
|
|
544
|
+
let ts = calculateTimestamp(rtpPacket, this.video);
|
|
545
|
+
if (type === Streamer.H264NALUS.TYPES.IDR) {
|
|
546
|
+
if (this.video.h264.sps) {
|
|
547
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, this.video.h264.sps, ts);
|
|
548
|
+
}
|
|
549
|
+
if (this.video.h264.pps) {
|
|
550
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, this.video.h264.pps, ts);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, buffer, ts);
|
|
555
|
+
}
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Raw NAL
|
|
563
|
+
let type = nalType;
|
|
564
|
+
if (type === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let ts = calculateTimestamp(rtpPacket, this.video);
|
|
569
|
+
if (type === Streamer.H264NALUS.TYPES.IDR) {
|
|
570
|
+
if (this.video.h264.sps) {
|
|
571
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, this.video.h264.sps, ts);
|
|
572
|
+
}
|
|
573
|
+
if (this.video.h264.pps) {
|
|
574
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, this.video.h264.pps, ts);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, payload, ts);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Audio packet processing
|
|
583
|
+
if (rtpPacket.header.payloadType === this.audio?.id) {
|
|
584
|
+
let opus = werift.OpusRtpPayload.deSerialize(rtpPacket.payload);
|
|
585
|
+
if (opus?.payload?.[0] === 0xfc) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let ts = calculateTimestamp(rtpPacket, this.audio);
|
|
590
|
+
try {
|
|
591
|
+
let pcm = this.#opusDecoder.decode(opus.payload);
|
|
592
|
+
this.add(Streamer.PACKET_TYPE.AUDIO, Buffer.from(pcm), ts);
|
|
593
|
+
} catch {
|
|
594
|
+
this.add(Streamer.PACKET_TYPE.AUDIO, this.blankAudio, ts);
|
|
493
595
|
}
|
|
494
596
|
}
|
|
495
597
|
}
|
|
496
598
|
|
|
497
599
|
// Need more work in here*
|
|
498
|
-
//
|
|
499
|
-
//
|
|
600
|
+
// < error handling
|
|
601
|
+
// < timeout?
|
|
500
602
|
async #googleHomeFoyerCommand(service, command, values) {
|
|
501
603
|
if (typeof service !== 'string' || service === '' || typeof command !== 'string' || command === '' || typeof values !== 'object') {
|
|
502
604
|
return;
|
|
@@ -513,99 +615,126 @@ export default class WebRTC extends Streamer {
|
|
|
513
615
|
};
|
|
514
616
|
|
|
515
617
|
if (TraitMapRequest !== null && TraitMapResponse !== null && this.token !== undefined) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
this
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
618
|
+
try {
|
|
619
|
+
if (this.#googleHomeFoyer === undefined || (this.#googleHomeFoyer?.connected === false && this.#googleHomeFoyer?.closed === true)) {
|
|
620
|
+
// No current HTTP/2 connection or current session is closed
|
|
621
|
+
this?.log?.debug?.('Connection started to Google Home Foyer "%s"', this.#googleHomeFoyerAPIHost);
|
|
622
|
+
this.#googleHomeFoyer = http2.connect(this.#googleHomeFoyerAPIHost);
|
|
623
|
+
|
|
624
|
+
this.#googleHomeFoyer.on('connect', () => {
|
|
625
|
+
this?.log?.debug?.('Connection established to Google Home Foyer "%s"', this.#googleHomeFoyerAPIHost);
|
|
626
|
+
|
|
627
|
+
clearInterval(this.#pingTimer);
|
|
628
|
+
this.#pingTimer = setInterval(() => {
|
|
629
|
+
if (this.#googleHomeFoyer !== undefined) {
|
|
630
|
+
// eslint-disable-next-line no-unused-vars
|
|
631
|
+
this.#googleHomeFoyer.ping((error, duration, payload) => {
|
|
632
|
+
// Do we log error to debug?
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}, 60000); // Every minute?
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// eslint-disable-next-line no-unused-vars
|
|
639
|
+
this.#googleHomeFoyer.on('goaway', (errorCode, lastStreamID, opaqueData) => {
|
|
640
|
+
//console.log('http2 goaway', errorCode);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
this.#googleHomeFoyer.on('error', (error) => {
|
|
644
|
+
this?.log?.debug?.('Google Home Foyer connection error: %s', String(error));
|
|
645
|
+
try {
|
|
646
|
+
this.#googleHomeFoyer.destroy();
|
|
647
|
+
} catch {
|
|
648
|
+
// Empty
|
|
531
649
|
}
|
|
532
|
-
|
|
533
|
-
|
|
650
|
+
this.#googleHomeFoyer = undefined;
|
|
651
|
+
});
|
|
534
652
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
653
|
+
this.#googleHomeFoyer.on('close', () => {
|
|
654
|
+
clearInterval(this.#pingTimer);
|
|
655
|
+
this.#pingTimer = undefined;
|
|
656
|
+
this.#googleHomeFoyer = undefined;
|
|
657
|
+
this?.log?.debug?.('Connection closed to Google Home Foyer "%s"', this.#googleHomeFoyerAPIHost);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
539
660
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
661
|
+
let request = this.#googleHomeFoyer.request({
|
|
662
|
+
':method': 'post',
|
|
663
|
+
':path': '/' + GOOGLE_HOME_FOYER_PREFIX + service + '/' + command,
|
|
664
|
+
authorization: 'Bearer ' + this.token,
|
|
665
|
+
'content-type': 'application/grpc',
|
|
666
|
+
'user-agent': USER_AGENT,
|
|
667
|
+
te: 'trailers',
|
|
668
|
+
'request-id': crypto.randomUUID(),
|
|
669
|
+
'grpc-timeout': '10S',
|
|
544
670
|
});
|
|
545
671
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
672
|
+
request.on('data', (data) => {
|
|
673
|
+
buffer = Buffer.concat([buffer, data]);
|
|
674
|
+
while (buffer.length >= 5) {
|
|
675
|
+
let headerSize = 5;
|
|
676
|
+
let dataSize = buffer.readUInt32BE(1);
|
|
677
|
+
if (buffer.length < headerSize + dataSize) {
|
|
678
|
+
// We don't have enough data in the buffer yet to process the data
|
|
679
|
+
// so, exit loop and await more data
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
commandResponse.data.push(TraitMapResponse.decode(buffer.subarray(headerSize, headerSize + dataSize)).toJSON());
|
|
684
|
+
buffer = buffer.subarray(headerSize + dataSize);
|
|
685
|
+
}
|
|
551
686
|
});
|
|
552
|
-
}
|
|
553
687
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
'grpc-timeout': '10S',
|
|
563
|
-
});
|
|
688
|
+
request.on('trailers', (headers) => {
|
|
689
|
+
if (isNaN(Number(headers?.['grpc-status'])) === false) {
|
|
690
|
+
commandResponse.status = Number(headers['grpc-status']);
|
|
691
|
+
}
|
|
692
|
+
if (headers?.['grpc-message'] !== undefined) {
|
|
693
|
+
commandResponse.message = headers['grpc-message'];
|
|
694
|
+
}
|
|
695
|
+
});
|
|
564
696
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
break;
|
|
697
|
+
request.on('error', (error) => {
|
|
698
|
+
commandResponse.status = error.code;
|
|
699
|
+
commandResponse.message = error.message;
|
|
700
|
+
commandResponse.data = [];
|
|
701
|
+
try {
|
|
702
|
+
request.close();
|
|
703
|
+
} catch {
|
|
704
|
+
// Empty
|
|
574
705
|
}
|
|
706
|
+
});
|
|
575
707
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
708
|
+
if (request !== undefined && request?.closed === false && request?.destroyed === false) {
|
|
709
|
+
// Encode our request values, prefix with header (size of data), then send
|
|
710
|
+
let encodedData = TraitMapRequest.encode(TraitMapRequest.fromObject(values)).finish();
|
|
711
|
+
let header = Buffer.alloc(5);
|
|
712
|
+
header.writeUInt32BE(encodedData.length, 1);
|
|
713
|
+
request.write(Buffer.concat([header, encodedData]));
|
|
714
|
+
request.end();
|
|
580
715
|
|
|
581
|
-
|
|
582
|
-
if (isNaN(Number(headers?.['grpc-status'])) === false) {
|
|
583
|
-
commandResponse.status = Number(headers['grpc-status']);
|
|
584
|
-
}
|
|
585
|
-
if (headers?.['grpc-message'] !== undefined) {
|
|
586
|
-
commandResponse.message = headers['grpc-message'];
|
|
716
|
+
await EventEmitter.once(request, 'close');
|
|
587
717
|
}
|
|
588
|
-
});
|
|
589
718
|
|
|
590
|
-
|
|
719
|
+
try {
|
|
720
|
+
// No longer need this request
|
|
721
|
+
request.destroy();
|
|
722
|
+
} catch {
|
|
723
|
+
// Empty
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
591
726
|
commandResponse.status = error.code;
|
|
592
|
-
commandResponse.message = error.message;
|
|
727
|
+
commandResponse.message = String(error.message || error);
|
|
593
728
|
commandResponse.data = [];
|
|
594
|
-
request.close();
|
|
595
|
-
});
|
|
596
729
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
await EventEmitter.once(request, 'close');
|
|
730
|
+
this?.log?.debug?.('Google Home Foyer request failed: %s', commandResponse.message);
|
|
731
|
+
try {
|
|
732
|
+
this.#googleHomeFoyer?.destroy();
|
|
733
|
+
} catch {
|
|
734
|
+
// Empty
|
|
735
|
+
}
|
|
736
|
+
this.#googleHomeFoyer = undefined;
|
|
606
737
|
}
|
|
607
|
-
|
|
608
|
-
request.destroy(); // No longer need this request
|
|
609
738
|
}
|
|
610
739
|
|
|
611
740
|
return commandResponse;
|