homebridge-nest-accfactory 0.3.0 → 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 +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- 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 +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- 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 +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- package/package.json +19 -16
package/dist/nexustalk.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Credit to https://github.com/Brandawg93/homebridge-nest-cam for the work on the Nest Camera comms code on which this is based
|
|
7
7
|
//
|
|
8
|
-
// Code version 2025.
|
|
8
|
+
// Code version 2025.07.30
|
|
9
9
|
// Mark Hulskamp
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
@@ -19,17 +19,16 @@ import fs from 'node:fs';
|
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import tls from 'tls';
|
|
21
21
|
import crypto from 'crypto';
|
|
22
|
-
import { fileURLToPath } from 'node:url';
|
|
23
22
|
|
|
24
23
|
// Define our modules
|
|
25
24
|
import Streamer from './streamer.js';
|
|
26
25
|
|
|
27
26
|
// Define constants
|
|
28
|
-
|
|
29
|
-
const USERAGENT = 'Nest/5.78.0 (iOScom.nestlabs.jasper.release) os=18.0'; // User Agent string
|
|
30
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
27
|
+
import { USER_AGENT, __dirname } from './consts.js';
|
|
31
28
|
|
|
32
|
-
const
|
|
29
|
+
const PING_INTERVAL = 15000; // Ping interval to nexus server while stream active
|
|
30
|
+
|
|
31
|
+
const PACKET_TYPE = {
|
|
33
32
|
PING: 1,
|
|
34
33
|
HELLO: 100,
|
|
35
34
|
PING_CAMERA: 101,
|
|
@@ -56,19 +55,17 @@ const PacketType = {
|
|
|
56
55
|
};
|
|
57
56
|
|
|
58
57
|
// Blank audio in AAC format, mono channel @48000
|
|
59
|
-
const
|
|
58
|
+
const AAC_MONO_48000_BLANK = Buffer.from([
|
|
60
59
|
0xff, 0xf1, 0x4c, 0x40, 0x03, 0x9f, 0xfc, 0xde, 0x02, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x39, 0x2e, 0x31, 0x38, 0x2e, 0x31, 0x30, 0x30,
|
|
61
60
|
0x00, 0x02, 0x30, 0x40, 0x0e,
|
|
62
61
|
]);
|
|
63
62
|
|
|
64
63
|
// nexusTalk object
|
|
65
64
|
export default class NexusTalk extends Streamer {
|
|
65
|
+
streaming_host = undefined; // Main nexustalk streaming host
|
|
66
66
|
token = undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
stalledTimer = undefined; // Timer object for no received data
|
|
70
|
-
host = ''; // Host to connect to or connected too
|
|
71
|
-
blankAudio = AACMONO48000BLANK;
|
|
67
|
+
useGoogleAuth = false; // Nest vs google auth
|
|
68
|
+
blankAudio = AAC_MONO_48000_BLANK;
|
|
72
69
|
video = {}; // Video stream details once connected
|
|
73
70
|
audio = {}; // Audio stream details once connected
|
|
74
71
|
|
|
@@ -79,11 +76,23 @@ export default class NexusTalk extends Streamer {
|
|
|
79
76
|
#messages = []; // Incoming messages
|
|
80
77
|
#authorised = false; // Have we been authorised
|
|
81
78
|
#id = undefined; // Session ID
|
|
79
|
+
#host = undefined; // Current host connected to
|
|
80
|
+
#pingTimer = undefined; // Timer object for ping interval
|
|
81
|
+
#stalledTimer = undefined; // Timer object for no received data
|
|
82
|
+
|
|
83
|
+
// Codecs being used for video, audio and talking
|
|
84
|
+
get codecs() {
|
|
85
|
+
return {
|
|
86
|
+
video: Streamer.CODEC_TYPE.H264, // Video codec
|
|
87
|
+
audio: Streamer.CODEC_TYPE.AAC, // Audio codec
|
|
88
|
+
talkback: Streamer.CODEC_TYPE.SPEEX, // Talkback codec
|
|
89
|
+
};
|
|
90
|
+
}
|
|
82
91
|
|
|
83
|
-
constructor(deviceData, options) {
|
|
84
|
-
super(deviceData, options);
|
|
92
|
+
constructor(uuid, deviceData, options) {
|
|
93
|
+
super(uuid, deviceData, options);
|
|
85
94
|
|
|
86
|
-
if (fs.existsSync(path.
|
|
95
|
+
if (fs.existsSync(path.join(__dirname, 'protobuf/nest/nexustalk.proto')) === true) {
|
|
87
96
|
protobuf.util.Long = null;
|
|
88
97
|
protobuf.configure();
|
|
89
98
|
this.#protobufNexusTalk = protobuf.loadSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto'));
|
|
@@ -91,85 +100,86 @@ export default class NexusTalk extends Streamer {
|
|
|
91
100
|
|
|
92
101
|
// Store data we need from the device data passed it
|
|
93
102
|
this.token = deviceData?.apiAccess?.token;
|
|
94
|
-
this.
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
// Set our streamer codec types
|
|
98
|
-
this.codecs = {
|
|
99
|
-
video: 'h264',
|
|
100
|
-
audio: 'aac',
|
|
101
|
-
talk: 'speex',
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// If specified option to start buffering, kick off
|
|
105
|
-
if (options?.buffer === true) {
|
|
106
|
-
this.startBuffering();
|
|
107
|
-
}
|
|
103
|
+
this.streaming_host = deviceData?.streaming_host; // Host we'll connect to
|
|
104
|
+
this.useGoogleAuth = typeof deviceData?.apiAccess?.oauth2 === 'string' && deviceData?.apiAccess?.oauth2 !== '';
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
// Class functions
|
|
111
|
-
connect(host) {
|
|
108
|
+
async connect(host) {
|
|
112
109
|
// Clear any timers we have running
|
|
113
|
-
clearInterval(this
|
|
114
|
-
clearTimeout(this
|
|
115
|
-
this
|
|
116
|
-
this
|
|
110
|
+
clearInterval(this.#pingTimer);
|
|
111
|
+
clearTimeout(this.#stalledTimer);
|
|
112
|
+
this.#pingTimer = undefined;
|
|
113
|
+
this.#stalledTimer = undefined;
|
|
117
114
|
this.#id = undefined; // No session ID yet
|
|
118
115
|
|
|
119
116
|
if (this.online === true && this.videoEnabled === true) {
|
|
120
117
|
if (typeof host === 'undefined' || host === null) {
|
|
121
118
|
// No host parameter passed in, so we'll set this to our internally stored host
|
|
122
|
-
host = this.
|
|
119
|
+
host = this.streaming_host;
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
this.connected = false; // Starting connection
|
|
126
123
|
this?.log?.debug?.('Connection started to "%s"', host);
|
|
124
|
+
this.#host = host; // Update internal host name since we’re about to connect
|
|
125
|
+
|
|
126
|
+
// Wrap tls.connect() in a Promise so we can await the TLS handshake
|
|
127
|
+
try {
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
this.#socket = tls.connect({ host: host, port: 1443 }, () => {
|
|
130
|
+
// Opened connection to Nexus server, so now need to authenticate ourselves
|
|
131
|
+
this?.log?.debug?.('Connection established to "%s"', host);
|
|
132
|
+
|
|
133
|
+
this.#socket.setKeepAlive(true); // Keep socket connection alive
|
|
134
|
+
this.connected = true;
|
|
135
|
+
this.#Authenticate(false); // Send authentication request
|
|
136
|
+
resolve(); // Allow await connect() to continue
|
|
137
|
+
});
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.connected = true;
|
|
135
|
-
this.#Authenticate(false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
this.#socket.on('error', () => {});
|
|
139
|
+
this.#socket.on('error', (err) => {
|
|
140
|
+
// TLS error (could be refused, timeout, etc.)
|
|
141
|
+
this?.log?.warn?.('TLS error on connect to "%s": %s', host, err?.message || err);
|
|
142
|
+
this.connected = undefined;
|
|
143
|
+
reject(err);
|
|
144
|
+
});
|
|
139
145
|
|
|
140
|
-
|
|
146
|
+
this.#socket.on('end', () => {});
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
this.#socket.on('data', (data) => {
|
|
149
|
+
this.#handleNexusData(data);
|
|
150
|
+
});
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
152
|
+
this.#socket.on('close', (hadError) => {
|
|
153
|
+
this?.log?.debug?.('Connection closed to "%s"', host);
|
|
154
|
+
|
|
155
|
+
clearInterval(this.#pingTimer);
|
|
156
|
+
clearTimeout(this.#stalledTimer);
|
|
157
|
+
this.#pingTimer = undefined;
|
|
158
|
+
this.#stalledTimer = undefined;
|
|
159
|
+
this.#authorised = false; // Since connection closed, we can't be authorised anymore
|
|
160
|
+
this.#socket = undefined; // Clear socket object
|
|
161
|
+
this.connected = undefined;
|
|
162
|
+
this.#id = undefined; // Not an active session anymore
|
|
163
|
+
|
|
164
|
+
if (hadError === true && (this.isStreaming() === true || this.isBuffering() === true)) {
|
|
165
|
+
// We still have either active buffering occurring or output streams running
|
|
166
|
+
// so attempt to restart connection to existing host
|
|
167
|
+
this.connect(host);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
this?.log?.error?.('Failed to connect to "%s": %s', host, String(error));
|
|
173
|
+
}
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
close(stopStreamFirst) {
|
|
177
|
+
async close(stopStreamFirst) {
|
|
168
178
|
// Close an authenicated socket stream gracefully
|
|
169
179
|
if (this.#socket !== undefined) {
|
|
170
180
|
if (stopStreamFirst === true) {
|
|
171
181
|
// Send a notifcation to nexus we're finished playback
|
|
172
|
-
this.#stopNexusData();
|
|
182
|
+
await this.#stopNexusData();
|
|
173
183
|
}
|
|
174
184
|
this.#socket.destroy();
|
|
175
185
|
}
|
|
@@ -181,15 +191,17 @@ export default class NexusTalk extends Streamer {
|
|
|
181
191
|
this.#messages = [];
|
|
182
192
|
this.video = {};
|
|
183
193
|
this.audio = {};
|
|
194
|
+
this.#host = undefined; // No longer connected to this host
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
|
|
197
|
+
async onUpdate(deviceData) {
|
|
187
198
|
if (typeof deviceData !== 'object') {
|
|
188
199
|
return;
|
|
189
200
|
}
|
|
190
201
|
|
|
191
|
-
if (deviceData.apiAccess.token !== this.token) {
|
|
202
|
+
if (deviceData?.apiAccess?.token !== undefined && deviceData.apiAccess.token !== this.token) {
|
|
192
203
|
// access token has changed so re-authorise
|
|
204
|
+
this?.log?.debug?.('Access token has changed for uuid "%s". Updating token', this.nest_google_uuid);
|
|
193
205
|
this.token = deviceData.apiAccess.token;
|
|
194
206
|
|
|
195
207
|
if (this.#socket !== undefined) {
|
|
@@ -197,30 +209,38 @@ export default class NexusTalk extends Streamer {
|
|
|
197
209
|
}
|
|
198
210
|
}
|
|
199
211
|
|
|
200
|
-
if (this.
|
|
201
|
-
this.
|
|
202
|
-
this?.log?.debug?.('New host has been requested for connection. Host requested is "%s"', this.host);
|
|
203
|
-
}
|
|
212
|
+
if (deviceData?.streaming_host !== undefined && this.streaming_host !== deviceData.streaming_host) {
|
|
213
|
+
this.streaming_host = deviceData.streaming_host;
|
|
204
214
|
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
if (this.isStreaming() === true || this.isBuffering() === true) {
|
|
216
|
+
this?.log?.debug?.('New host has been requested for connection. Host requested is "%s"', this.streaming_host);
|
|
217
|
+
|
|
218
|
+
// Setup listener for socket close event. Once socket is closed, we'll perform the redirect
|
|
219
|
+
this.#socket?.on?.('close', () => {
|
|
220
|
+
this.connect(this.streaming_host); // Connect to new host
|
|
221
|
+
});
|
|
222
|
+
this.close(true); // Close existing socket
|
|
223
|
+
}
|
|
224
|
+
}
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
|
|
227
|
+
sendTalkback(talkingBuffer) {
|
|
228
|
+
if (Buffer.isBuffer(talkingBuffer) === false || this.#protobufNexusTalk === undefined || this.#id === undefined) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
210
232
|
// Encode audio packet for sending to camera
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.#sendMessage(PacketType.AUDIO_PAYLOAD, encodedData);
|
|
223
|
-
}
|
|
233
|
+
let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AudioPayload');
|
|
234
|
+
if (TraitMap !== null) {
|
|
235
|
+
let encodedData = TraitMap.encode(
|
|
236
|
+
TraitMap.fromObject({
|
|
237
|
+
payload: talkingBuffer,
|
|
238
|
+
sessionId: this.#id,
|
|
239
|
+
codec: Streamer.CODEC_TYPE.SPEEX,
|
|
240
|
+
sampleRate: 16000,
|
|
241
|
+
}),
|
|
242
|
+
).finish();
|
|
243
|
+
this.#sendMessage(PACKET_TYPE.AUDIO_PAYLOAD, encodedData);
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
|
|
@@ -248,7 +268,7 @@ export default class NexusTalk extends Streamer {
|
|
|
248
268
|
profileNotFoundAction: 'REDIRECT',
|
|
249
269
|
}),
|
|
250
270
|
).finish();
|
|
251
|
-
this.#sendMessage(
|
|
271
|
+
this.#sendMessage(PACKET_TYPE.START_PLAYBACK, encodedData);
|
|
252
272
|
}
|
|
253
273
|
}
|
|
254
274
|
|
|
@@ -261,13 +281,13 @@ export default class NexusTalk extends Streamer {
|
|
|
261
281
|
sessionId: this.#id,
|
|
262
282
|
}),
|
|
263
283
|
).finish();
|
|
264
|
-
this.#sendMessage(
|
|
284
|
+
this.#sendMessage(PACKET_TYPE.STOP_PLAYBACK, encodedData);
|
|
265
285
|
}
|
|
266
286
|
}
|
|
267
287
|
}
|
|
268
288
|
|
|
269
289
|
#sendMessage(type, data) {
|
|
270
|
-
if (this.#socket?.readyState !== 'open' || (type !==
|
|
290
|
+
if (this.#socket?.readyState !== 'open' || (type !== PACKET_TYPE.HELLO && this.#authorised === false)) {
|
|
271
291
|
// We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
|
|
272
292
|
this.#messages.push({ type: type, data: data });
|
|
273
293
|
return;
|
|
@@ -275,11 +295,11 @@ export default class NexusTalk extends Streamer {
|
|
|
275
295
|
|
|
276
296
|
// Create nexusTalk message header
|
|
277
297
|
let header = Buffer.alloc(3);
|
|
278
|
-
if (type !==
|
|
298
|
+
if (type !== PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
279
299
|
header.writeUInt8(type, 0);
|
|
280
300
|
header.writeUInt16BE(data.length, 1);
|
|
281
301
|
}
|
|
282
|
-
if (type ===
|
|
302
|
+
if (type === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
283
303
|
header = Buffer.alloc(5);
|
|
284
304
|
header.writeUInt8(type, 0);
|
|
285
305
|
header.writeUInt32BE(data.length, 1);
|
|
@@ -300,36 +320,34 @@ export default class NexusTalk extends Streamer {
|
|
|
300
320
|
let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AuthoriseRequest');
|
|
301
321
|
if (TraitMap !== null) {
|
|
302
322
|
authoriseRequest = TraitMap.encode(
|
|
303
|
-
TraitMap.fromObject(
|
|
304
|
-
this.tokenType === 'nest' ? { sessionToken: this.token } : this.tokenType === 'google' ? { oliveToken: this.token } : {},
|
|
305
|
-
),
|
|
323
|
+
TraitMap.fromObject(this.useGoogleAuth === true ? { oliveToken: this.token } : { sessionToken: this.token }),
|
|
306
324
|
).finish();
|
|
307
325
|
}
|
|
308
326
|
|
|
309
327
|
if (reauthorise === true && authoriseRequest !== null) {
|
|
310
328
|
// Request to re-authorise only
|
|
311
|
-
this?.log?.debug?.('Re-authentication requested to "%s"', this
|
|
312
|
-
this.#sendMessage(
|
|
329
|
+
this?.log?.debug?.('Re-authentication requested to "%s"', this.#host);
|
|
330
|
+
this.#sendMessage(PACKET_TYPE.AUTHORIZE_REQUEST, authoriseRequest);
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
if (reauthorise === false && authoriseRequest !== null) {
|
|
316
334
|
// This isn't a re-authorise request, so perform 'Hello' packet
|
|
317
335
|
let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Hello');
|
|
318
336
|
if (TraitMap !== null) {
|
|
319
|
-
this?.log?.debug?.('Performing authentication to "%s"', this
|
|
337
|
+
this?.log?.debug?.('Performing authentication to "%s"', this.#host);
|
|
320
338
|
|
|
321
339
|
let encodedData = TraitMap.encode(
|
|
322
340
|
TraitMap.fromObject({
|
|
323
341
|
protocolVersion: 'VERSION_3',
|
|
324
|
-
uuid: this.
|
|
342
|
+
uuid: this.nest_google_uuid.split(/[._]+/)[1],
|
|
325
343
|
requireConnectedCamera: false,
|
|
326
|
-
|
|
344
|
+
USER_AGENT: USER_AGENT,
|
|
327
345
|
deviceId: crypto.randomUUID(),
|
|
328
346
|
clientType: 'IOS',
|
|
329
347
|
authoriseRequest: authoriseRequest,
|
|
330
348
|
}),
|
|
331
349
|
).finish();
|
|
332
|
-
this.#sendMessage(
|
|
350
|
+
this.#sendMessage(PACKET_TYPE.HELLO, encodedData);
|
|
333
351
|
}
|
|
334
352
|
}
|
|
335
353
|
}
|
|
@@ -350,13 +368,12 @@ export default class NexusTalk extends Streamer {
|
|
|
350
368
|
return;
|
|
351
369
|
}
|
|
352
370
|
|
|
353
|
-
this?.log?.debug?.('Redirect requested from "%s" to "%s"', this
|
|
371
|
+
this?.log?.debug?.('Redirect requested from "%s" to "%s"', this.#host, redirectToHost);
|
|
354
372
|
|
|
355
373
|
// Setup listener for socket close event. Once socket is closed, we'll perform the redirect
|
|
356
|
-
this.#socket
|
|
357
|
-
this
|
|
358
|
-
|
|
359
|
-
});
|
|
374
|
+
this.#socket?.on?.('close', () => {
|
|
375
|
+
this.connect(redirectToHost); // Connect to new host
|
|
376
|
+
});
|
|
360
377
|
this.close(true); // Close existing socket
|
|
361
378
|
}
|
|
362
379
|
|
|
@@ -368,18 +385,17 @@ export default class NexusTalk extends Streamer {
|
|
|
368
385
|
if (stream.codecType === this.codecs.video.toUpperCase()) {
|
|
369
386
|
this.video = {
|
|
370
387
|
id: stream.channelId,
|
|
371
|
-
|
|
388
|
+
baseTimestamp: stream.startTimestamp,
|
|
389
|
+
baseTime: Date.now(),
|
|
372
390
|
sampleRate: stream.sampleRate,
|
|
373
|
-
timeStamp: 0,
|
|
374
391
|
};
|
|
375
392
|
}
|
|
376
393
|
if (stream.codecType === this.codecs.audio.toUpperCase()) {
|
|
377
394
|
this.audio = {
|
|
378
395
|
id: stream.channelId,
|
|
379
|
-
|
|
396
|
+
baseTimestamp: stream.startTimestamp,
|
|
397
|
+
baseTime: Date.now(),
|
|
380
398
|
sampleRate: stream.sampleRate,
|
|
381
|
-
timeStamp: 0,
|
|
382
|
-
talking: false,
|
|
383
399
|
};
|
|
384
400
|
}
|
|
385
401
|
});
|
|
@@ -389,11 +405,26 @@ export default class NexusTalk extends Streamer {
|
|
|
389
405
|
this.#packets = [];
|
|
390
406
|
this.#messages = [];
|
|
391
407
|
|
|
392
|
-
this?.log?.debug?.('Playback started from "%s" with session ID "%s"', this
|
|
408
|
+
this?.log?.debug?.('Playback started from "%s" with session ID "%s"', this.#host, this.#id);
|
|
393
409
|
}
|
|
394
410
|
}
|
|
395
411
|
|
|
396
412
|
#handlePlaybackPacket(payload) {
|
|
413
|
+
const calculateTimestamp = (delta, stream) => {
|
|
414
|
+
if (
|
|
415
|
+
typeof delta !== 'number' ||
|
|
416
|
+
typeof stream?.sampleRate !== 'number' ||
|
|
417
|
+
stream?.baseTime === undefined ||
|
|
418
|
+
stream?.baseTimestamp === undefined
|
|
419
|
+
) {
|
|
420
|
+
return Date.now();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let deltaTicks = stream.baseTimestamp + delta - stream.baseTimestamp;
|
|
424
|
+
let deltaMs = (deltaTicks / stream.sampleRate) * 1000;
|
|
425
|
+
return stream.baseTime + deltaMs;
|
|
426
|
+
};
|
|
427
|
+
|
|
397
428
|
// Decode playback packet
|
|
398
429
|
if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
|
|
399
430
|
let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackPacket').decode(payload).toJSON();
|
|
@@ -401,30 +432,33 @@ export default class NexusTalk extends Streamer {
|
|
|
401
432
|
// Setup up a timeout to monitor for no packets recieved in a certain period
|
|
402
433
|
// If its trigger, we'll attempt to restart the stream and/or connection
|
|
403
434
|
// <-- testing to see how often this occurs first
|
|
404
|
-
clearTimeout(this
|
|
405
|
-
this
|
|
435
|
+
clearTimeout(this.#stalledTimer);
|
|
436
|
+
this.#stalledTimer = setTimeout(() => {
|
|
406
437
|
this?.log?.debug?.(
|
|
407
438
|
'We have not received any data from nexus in the past "%s" seconds for uuid "%s". Attempting restart',
|
|
408
439
|
10,
|
|
409
|
-
this.
|
|
440
|
+
this.nest_google_uuid,
|
|
410
441
|
);
|
|
411
442
|
|
|
412
443
|
// Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
|
|
413
|
-
this.#socket
|
|
414
|
-
this
|
|
415
|
-
|
|
416
|
-
});
|
|
444
|
+
this.#socket?.on?.('close', () => {
|
|
445
|
+
this.connect(); // try reconnection
|
|
446
|
+
});
|
|
417
447
|
this.close(false); // Close existing socket
|
|
418
448
|
}, 10000);
|
|
419
449
|
|
|
450
|
+
// Timestamps are rolling — incremented from startTime using timestampDelta per packet
|
|
451
|
+
|
|
420
452
|
// Handle video packet
|
|
421
453
|
if (decodedMessage?.channelId !== undefined && decodedMessage.channelId === this.video?.id) {
|
|
422
|
-
|
|
454
|
+
let ts = calculateTimestamp(decodedMessage.timestampDelta, this.video);
|
|
455
|
+
this.add(Streamer.PACKET_TYPE.VIDEO, Buffer.from(decodedMessage.payload, 'base64'), ts);
|
|
423
456
|
}
|
|
424
457
|
|
|
425
458
|
// Handle audio packet
|
|
426
459
|
if (decodedMessage?.channelId !== undefined && decodedMessage.channelId === this.audio?.id) {
|
|
427
|
-
|
|
460
|
+
let ts = calculateTimestamp(decodedMessage.timestampDelta, this.audio);
|
|
461
|
+
this.add(Streamer.PACKET_TYPE.AUDIO, Buffer.from(decodedMessage.payload, 'base64'), ts);
|
|
428
462
|
}
|
|
429
463
|
}
|
|
430
464
|
}
|
|
@@ -436,18 +470,17 @@ export default class NexusTalk extends Streamer {
|
|
|
436
470
|
|
|
437
471
|
if (this.#id !== undefined && decodedMessage.reason === 'USER_ENDED_SESSION') {
|
|
438
472
|
// Normal playback ended ie: when we stopped playback
|
|
439
|
-
this?.log?.debug?.('Playback ended on "%s"', this
|
|
473
|
+
this?.log?.debug?.('Playback ended on "%s"', this.#host);
|
|
440
474
|
}
|
|
441
475
|
|
|
442
476
|
if (decodedMessage.reason !== 'USER_ENDED_SESSION') {
|
|
443
477
|
// Error during playback, so we'll attempt to restart by reconnection to host
|
|
444
|
-
this?.log?.debug?.('Playback ended on "%s" with error "%s". Attempting reconnection', this
|
|
478
|
+
this?.log?.debug?.('Playback ended on "%s" with error "%s". Attempting reconnection', this.#host, decodedMessage.reason);
|
|
445
479
|
|
|
446
480
|
// Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
|
|
447
|
-
this.#socket
|
|
448
|
-
this
|
|
449
|
-
|
|
450
|
-
});
|
|
481
|
+
this.#socket?.on?.('close', () => {
|
|
482
|
+
this.connect(); // try reconnection to existing host
|
|
483
|
+
});
|
|
451
484
|
this.close(false); // Close existing socket
|
|
452
485
|
}
|
|
453
486
|
}
|
|
@@ -472,7 +505,7 @@ export default class NexusTalk extends Streamer {
|
|
|
472
505
|
if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
|
|
473
506
|
//let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackBegin').decode(payload).toJSON();
|
|
474
507
|
this.audio.talking = true;
|
|
475
|
-
this?.log?.debug?.('Talking started on uuid "%s"', this.
|
|
508
|
+
this?.log?.debug?.('Talking started on uuid "%s"', this.nest_google_uuid);
|
|
476
509
|
}
|
|
477
510
|
}
|
|
478
511
|
|
|
@@ -481,7 +514,7 @@ export default class NexusTalk extends Streamer {
|
|
|
481
514
|
if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
|
|
482
515
|
//let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackEnd').decode(payload).toJSON();
|
|
483
516
|
this.audio.talking = false;
|
|
484
|
-
this?.log?.debug?.('Talking ended on uuid "%s"', this.
|
|
517
|
+
this?.log?.debug?.('Talking ended on uuid "%s"', this.nest_google_uuid);
|
|
485
518
|
}
|
|
486
519
|
}
|
|
487
520
|
|
|
@@ -494,7 +527,7 @@ export default class NexusTalk extends Streamer {
|
|
|
494
527
|
let packetType = this.#packets.readUInt8(0);
|
|
495
528
|
let packetSize = this.#packets.readUInt16BE(1);
|
|
496
529
|
|
|
497
|
-
if (packetType ===
|
|
530
|
+
if (packetType === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
498
531
|
headerSize = 5;
|
|
499
532
|
packetSize = this.#packets.readUInt32BE(1);
|
|
500
533
|
}
|
|
@@ -509,11 +542,11 @@ export default class NexusTalk extends Streamer {
|
|
|
509
542
|
this.#packets = this.#packets.subarray(headerSize + packetSize);
|
|
510
543
|
|
|
511
544
|
switch (packetType) {
|
|
512
|
-
case
|
|
545
|
+
case PACKET_TYPE.PING: {
|
|
513
546
|
break;
|
|
514
547
|
}
|
|
515
548
|
|
|
516
|
-
case
|
|
549
|
+
case PACKET_TYPE.OK: {
|
|
517
550
|
// process any pending messages we have stored
|
|
518
551
|
this.#authorised = true; // OK message, means we're connected and authorised to Nexus
|
|
519
552
|
for (let message = this.#messages.shift(); message; message = this.#messages.shift()) {
|
|
@@ -521,48 +554,48 @@ export default class NexusTalk extends Streamer {
|
|
|
521
554
|
}
|
|
522
555
|
|
|
523
556
|
// Periodically send PING message to keep stream alive
|
|
524
|
-
clearInterval(this
|
|
525
|
-
this
|
|
526
|
-
this.#sendMessage(
|
|
527
|
-
},
|
|
557
|
+
clearInterval(this.#pingTimer);
|
|
558
|
+
this.#pingTimer = setInterval(() => {
|
|
559
|
+
this.#sendMessage(PACKET_TYPE.PING, Buffer.alloc(0));
|
|
560
|
+
}, PING_INTERVAL);
|
|
528
561
|
|
|
529
562
|
// Start processing data
|
|
530
563
|
this.#startNexusData();
|
|
531
564
|
break;
|
|
532
565
|
}
|
|
533
566
|
|
|
534
|
-
case
|
|
567
|
+
case PACKET_TYPE.ERROR: {
|
|
535
568
|
this.#handleNexusError(protoBufPayload);
|
|
536
569
|
break;
|
|
537
570
|
}
|
|
538
571
|
|
|
539
|
-
case
|
|
572
|
+
case PACKET_TYPE.PLAYBACK_BEGIN: {
|
|
540
573
|
this.#handlePlaybackBegin(protoBufPayload);
|
|
541
574
|
break;
|
|
542
575
|
}
|
|
543
576
|
|
|
544
|
-
case
|
|
577
|
+
case PACKET_TYPE.PLAYBACK_END: {
|
|
545
578
|
this.#handlePlaybackEnd(protoBufPayload);
|
|
546
579
|
break;
|
|
547
580
|
}
|
|
548
581
|
|
|
549
|
-
case
|
|
550
|
-
case
|
|
582
|
+
case PACKET_TYPE.PLAYBACK_PACKET:
|
|
583
|
+
case PACKET_TYPE.LONG_PLAYBACK_PACKET: {
|
|
551
584
|
this.#handlePlaybackPacket(protoBufPayload);
|
|
552
585
|
break;
|
|
553
586
|
}
|
|
554
587
|
|
|
555
|
-
case
|
|
588
|
+
case PACKET_TYPE.REDIRECT: {
|
|
556
589
|
this.#handleRedirect(protoBufPayload);
|
|
557
590
|
break;
|
|
558
591
|
}
|
|
559
592
|
|
|
560
|
-
case
|
|
593
|
+
case PACKET_TYPE.TALKBACK_BEGIN: {
|
|
561
594
|
this.#handleTalkbackBegin(protoBufPayload);
|
|
562
595
|
break;
|
|
563
596
|
}
|
|
564
597
|
|
|
565
|
-
case
|
|
598
|
+
case PACKET_TYPE.TALKBACK_END: {
|
|
566
599
|
this.#handleTalkbackEnd(protoBufPayload);
|
|
567
600
|
break;
|
|
568
601
|
}
|