homebridge-nest-accfactory 0.0.4-a → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nexustalk.js CHANGED
@@ -3,61 +3,31 @@
3
3
  //
4
4
  // Handles connection and data from Nest 'nexus' systems
5
5
  //
6
- // Code version 6/9/2024
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
+ //
8
+ // Code version 14/9/2024
7
9
  // Mark Hulskamp
8
10
  'use strict';
9
11
 
10
12
  // Define external library requirements
11
- import protoBuf from 'pbf'; // Proto buffer
13
+ import protobuf from 'protobufjs';
12
14
 
13
15
  // Define nodejs module requirements
14
16
  import { Buffer } from 'node:buffer';
15
17
  import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timers';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
16
20
  import tls from 'tls';
17
21
  import crypto from 'crypto';
22
+ import { fileURLToPath } from 'node:url';
18
23
 
19
24
  // Define our modules
20
25
  import Streamer from './streamer.js';
21
26
 
22
27
  // Define constants
23
28
  const PINGINTERVAL = 15000; // Ping interval to nexus server while stream active
24
-
25
- const CodecType = {
26
- SPEEX: 0,
27
- PCM_S16_LE: 1,
28
- H264: 2,
29
- AAC: 3,
30
- OPUS: 4,
31
- META: 5,
32
- DIRECTORS_CUT: 6,
33
- };
34
-
35
- const StreamProfile = {
36
- AVPROFILE_MOBILE_1: 1,
37
- AVPROFILE_HD_MAIN_1: 2,
38
- AUDIO_AAC: 3,
39
- AUDIO_SPEEX: 4,
40
- AUDIO_OPUS: 5,
41
- VIDEO_H264_50KBIT_L12: 6,
42
- VIDEO_H264_530KBIT_L31: 7,
43
- VIDEO_H264_100KBIT_L30: 8,
44
- VIDEO_H264_2MBIT_L40: 9,
45
- VIDEO_H264_50KBIT_L12_THUMBNAIL: 10,
46
- META: 11,
47
- DIRECTORS_CUT: 12,
48
- AUDIO_OPUS_LIVE: 13,
49
- VIDEO_H264_L31: 14,
50
- VIDEO_H264_L40: 15,
51
- };
52
-
53
- const ErrorCode = {
54
- ERROR_CAMERA_NOT_CONNECTED: 1,
55
- ERROR_ILLEGAL_PACKET: 2,
56
- ERROR_AUTHORIZATION_FAILED: 3,
57
- ERROR_NO_TRANSCODER_AVAILABLE: 4,
58
- ERROR_TRANSCODE_PROXY_ERROR: 5,
59
- ERROR_INTERNAL: 6,
60
- };
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
61
31
 
62
32
  const PacketType = {
63
33
  PING: 1,
@@ -85,48 +55,37 @@ const PacketType = {
85
55
  AUTHORIZE_REQUEST: 212,
86
56
  };
87
57
 
88
- const ProtocolVersion = {
89
- VERSION_1: 1,
90
- VERSION_2: 2,
91
- VERSION_3: 3,
92
- };
93
-
94
- const ClientType = {
95
- ANDROID: 1,
96
- IOS: 2,
97
- WEB: 3,
98
- };
99
-
100
58
  // nexusTalk object
101
59
  export default class NexusTalk extends Streamer {
102
60
  token = undefined;
103
61
  tokenType = undefined;
104
- uuid = undefined;
105
- id = undefined; // Session ID
106
- authorised = false; // Have wee been authorised
107
62
  pingTimer = undefined; // Timer object for ping interval
108
63
  stalledTimer = undefined; // Timer object for no received data
109
- packets = []; // Incoming packets
110
- messages = []; // Incoming messages
111
64
  video = {}; // Video stream details
112
65
  audio = {}; // Audio stream details
66
+ host = ''; // Host to connect to or connected too
67
+
68
+ // Internal data only for this class
69
+ #protobufNexusTalk = undefined; // Protobuf for NexusTalk
70
+ #socket = undefined; // TCP socket object
71
+ #packets = []; // Incoming packets
72
+ #messages = []; // Incoming messages
73
+ #authorised = false; // Have we been authorised
74
+ #id = undefined; // Session ID
113
75
 
114
76
  constructor(deviceData, options) {
115
77
  super(deviceData, options);
116
78
 
117
- // Store data we need from the device data passed it
118
- this.token = deviceData?.apiAccess.token;
119
- if (deviceData?.apiAccess?.key === 'Authorization') {
120
- this.tokenType = 'google';
121
- }
122
- if (deviceData?.apiAccess?.key === 'cookie') {
123
- this.tokenType = 'nest';
79
+ if (fs.existsSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto')) === true) {
80
+ protobuf.util.Long = null;
81
+ protobuf.configure();
82
+ this.#protobufNexusTalk = protobuf.loadSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto'));
124
83
  }
125
- this.uuid = deviceData?.uuid;
126
- this.host = deviceData?.streaming_host; // Host we'll connect to
127
84
 
128
- this.pendingHost = null;
129
- this.weDidClose = true; // Flag if we did the socket close gracefully
85
+ // Store data we need from the device data passed it
86
+ this.token = deviceData?.apiAccess?.token;
87
+ this.tokenType = deviceData?.apiAccess?.oauth2 !== undefined ? 'google' : 'nest';
88
+ this.host = deviceData?.streaming_host; // Host we'll connect to
130
89
 
131
90
  // If specified option to start buffering, kick off
132
91
  if (typeof options?.buffer === 'boolean' && options.buffer === true) {
@@ -140,7 +99,7 @@ export default class NexusTalk extends Streamer {
140
99
  this.pingTimer = clearInterval(this.pingTimer);
141
100
  this.stalledTimer = clearInterval(this.stalledTimer);
142
101
 
143
- this.id = undefined; // No session ID yet
102
+ this.#id = undefined; // No session ID yet
144
103
 
145
104
  if (this.online === true && this.videoEnabled === true) {
146
105
  if (typeof host === 'undefined' || host === null) {
@@ -148,47 +107,37 @@ export default class NexusTalk extends Streamer {
148
107
  host = this.host;
149
108
  }
150
109
 
151
- if (this.pendingHost !== null) {
152
- host = this.pendingHost;
153
- this.pendingHost = null;
154
- }
155
-
156
110
  this?.log?.debug && this.log.debug('Starting connection to "%s"', host);
157
111
 
158
- this.socket = tls.connect({ host: host, port: 1443 }, () => {
112
+ this.#socket = tls.connect({ host: host, port: 1443 }, () => {
159
113
  // Opened connection to Nexus server, so now need to authenticate ourselves
160
114
  this?.log?.debug && this.log.debug('Connection established to "%s"', host);
161
115
 
162
- this.socket.setKeepAlive(true); // Keep socket connection alive
116
+ this.#socket.setKeepAlive(true); // Keep socket connection alive
163
117
  this.host = host; // update internal host name since we've connected
118
+ this.connected = true;
164
119
  this.#Authenticate(false);
165
120
  });
166
121
 
167
- this.socket.on('error', () => {});
122
+ this.#socket.on('error', () => {});
168
123
 
169
- this.socket.on('end', () => {});
124
+ this.#socket.on('end', () => {});
170
125
 
171
- this.socket.on('data', (data) => {
126
+ this.#socket.on('data', (data) => {
172
127
  this.#handleNexusData(data);
173
128
  });
174
129
 
175
- this.socket.on('close', (hadError) => {
176
- if (hadError === true) {
177
- //
178
- }
179
- let normalClose = this.weDidClose; // Cache this, so can reset it below before we take action
130
+ this.#socket.on('close', (hadError) => {
131
+ this?.log?.debug && this.log.debug('Connection closed to "%s"', host);
180
132
 
181
133
  this.stalledTimer = clearTimeout(this.stalledTimer); // Clear stalled timer
182
134
  this.pingTimer = clearInterval(this.pingTimer); // Clear ping timer
183
- this.authorised = false; // Since connection close, we can't be authorised anymore
184
- this.socket = null; // Clear socket object
185
- this.id = undefined; // Not an active session anymore
135
+ this.#authorised = false; // Since connection close, we can't be authorised anymore
136
+ this.#socket = undefined; // Clear socket object
137
+ this.connected = false;
138
+ this.#id = undefined; // Not an active session anymore
186
139
 
187
- this.weDidClose = false; // Reset closed flag
188
-
189
- this?.log?.debug && this.log.debug('Connection closed to "%s"', host);
190
-
191
- if (normalClose === false && this.haveOutputs() === true) {
140
+ if (hadError === true && this.haveOutputs() === true) {
192
141
  // We still have either active buffering occuring or output streams running
193
142
  // so attempt to restart connection to existing host
194
143
  this.connect(host);
@@ -199,20 +148,19 @@ export default class NexusTalk extends Streamer {
199
148
 
200
149
  close(stopStreamFirst) {
201
150
  // Close an authenicated socket stream gracefully
202
- if (this.socket !== null) {
151
+ if (this.#socket !== undefined) {
203
152
  if (stopStreamFirst === true) {
204
153
  // Send a notifcation to nexus we're finished playback
205
154
  this.#stopNexusData();
206
155
  }
207
- this.socket.destroy();
156
+ this.#socket.destroy();
208
157
  }
209
158
 
210
- this.socket = null;
211
- this.id = undefined; // Not an active session anymore
212
- this.packets = [];
213
- this.messages = [];
214
-
215
- this.weDidClose = true; // Flag we did the socket close
159
+ this.connected = false;
160
+ this.#socket = undefined;
161
+ this.#id = undefined; // Not an active session anymore
162
+ this.#packets = [];
163
+ this.#messages = [];
216
164
  }
217
165
 
218
166
  update(deviceData) {
@@ -224,62 +172,84 @@ export default class NexusTalk extends Streamer {
224
172
  // access token has changed so re-authorise
225
173
  this.token = deviceData.apiAccess.token;
226
174
 
227
- if (this.socket !== null) {
175
+ if (this.#socket !== undefined) {
228
176
  this.#Authenticate(true); // Update authorisation only if connected
229
177
  }
230
178
  }
231
179
 
180
+ if (this.host !== deviceData.streaming_host) {
181
+ this.host = deviceData.streaming_host;
182
+ this?.log?.debug && this.log.debug('New host has been requested for connection. Host requested is "%s"', this.host);
183
+ }
184
+
232
185
  // Let our parent handle the remaining updates
233
186
  super.update(deviceData);
234
187
  }
235
188
 
236
189
  talkingAudio(talkingData) {
237
190
  // Encode audio packet for sending to camera
238
- let audioBuffer = new protoBuf();
239
- audioBuffer.writeBytesField(1, talkingData); // audio data
240
- audioBuffer.writeVarintField(2, this.id); // session ID
241
- audioBuffer.writeVarintField(3, CodecType.SPEEX); // codec
242
- audioBuffer.writeVarintField(4, 16000); // sample rate, 16k
243
- //audioBuffer.writeVarintField(5, ????); // Latency measure tag. What does this do?
244
- this.#sendMessage(PacketType.AUDIO_PAYLOAD, audioBuffer.finish());
191
+ if (typeof talkingData === 'object' && this.#protobufNexusTalk !== undefined) {
192
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AudioPayload');
193
+ if (TraitMap !== null) {
194
+ let encodedData = TraitMap.encode(
195
+ TraitMap.fromObject({
196
+ payload: talkingData,
197
+ sessionId: this.#id,
198
+ codec: 'SPEEX',
199
+ sampleRate: 16000,
200
+ }),
201
+ ).finish();
202
+ this.#sendMessage(PacketType.AUDIO_PAYLOAD, encodedData);
203
+ }
204
+ }
245
205
  }
246
206
 
247
207
  #startNexusData() {
248
- if (this.videoEnabled === false || this.online === false) {
208
+ if (this.videoEnabled === false || this.online === false || this.#protobufNexusTalk === undefined) {
249
209
  return;
250
210
  }
251
211
 
252
212
  // Setup streaming profiles
253
213
  // We'll use the highest profile as the main, with others for fallback
254
- let otherProfiles = [];
255
- otherProfiles.push(StreamProfile.VIDEO_H264_530KBIT_L31); // Medium quality
256
- otherProfiles.push(StreamProfile.VIDEO_H264_100KBIT_L30); // Low quality
214
+ let otherProfiles = ['VIDEO_H264_530KBIT_L31', 'VIDEO_H264_100KBIT_L30'];
257
215
 
258
216
  if (this.audioEnabled === true) {
259
217
  // Include AAC profile if audio is enabled on camera
260
- otherProfiles.push(StreamProfile.AUDIO_AAC);
218
+ otherProfiles.push('AUDIO_AAC');
261
219
  }
262
220
 
263
- let startBuffer = new protoBuf();
264
- startBuffer.writeVarintField(1, Math.floor(Math.random() * (100 - 1) + 1)); // Random session ID between 1 and 100);
265
- startBuffer.writeVarintField(2, StreamProfile.VIDEO_H264_2MBIT_L40); // Default profile. ie: high quality
266
- otherProfiles.forEach((otherProfile) => {
267
- startBuffer.writeVarintField(6, otherProfile); // Other supported profiles
268
- });
269
-
270
- this.#sendMessage(PacketType.START_PLAYBACK, startBuffer.finish());
221
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.StartPlayback');
222
+ if (TraitMap !== null) {
223
+ let encodedData = TraitMap.encode(
224
+ TraitMap.fromObject({
225
+ sessionId: Math.floor(Math.random() * (100 - 1) + 1),
226
+ profile: 'VIDEO_H264_2MBIT_L40',
227
+ otherProfiles: otherProfiles,
228
+ profileNotFoundAction: 'REDIRECT',
229
+ }),
230
+ ).finish();
231
+ this.#sendMessage(PacketType.START_PLAYBACK, encodedData);
232
+ }
271
233
  }
272
234
 
273
235
  #stopNexusData() {
274
- let stopBuffer = new protoBuf();
275
- stopBuffer.writeVarintField(1, this.id); // Session ID
276
- this.#sendMessage(PacketType.STOP_PLAYBACK, stopBuffer.finish());
236
+ if (this.#id !== undefined && this.#protobufNexusTalk !== undefined) {
237
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.StopPlayback');
238
+ if (TraitMap !== null) {
239
+ let encodedData = TraitMap.encode(
240
+ TraitMap.fromObject({
241
+ sessionId: this.#id,
242
+ }),
243
+ ).finish();
244
+ this.#sendMessage(PacketType.STOP_PLAYBACK, encodedData);
245
+ }
246
+ }
277
247
  }
278
248
 
279
249
  #sendMessage(type, data) {
280
- if (this.socket === null || this.socket.readyState !== 'open' || (type !== PacketType.HELLO && this.authorised === false)) {
250
+ if (this.#socket?.readyState !== 'open' || (type !== PacketType.HELLO && this.#authorised === false)) {
281
251
  // We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
282
- this.messages.push({ type: type, data: data });
252
+ this.#messages.push({ type: type, data: data });
283
253
  return;
284
254
  }
285
255
 
@@ -296,61 +266,60 @@ export default class NexusTalk extends Streamer {
296
266
  }
297
267
 
298
268
  // write our composed message out to the socket back to NexusTalk
299
- this.socket.write(Buffer.concat([header, Buffer.from(data)]), () => {
269
+ this.#socket.write(Buffer.concat([header, Buffer.from(data)]), () => {
300
270
  // Message sent. Don't do anything?
301
271
  });
302
272
  }
303
273
 
304
274
  #Authenticate(reauthorise) {
305
275
  // Authenticate over created socket connection
306
- let tokenBuffer = new protoBuf();
307
- let helloBuffer = new protoBuf();
276
+ if (this.#protobufNexusTalk !== undefined) {
277
+ this.#authorised = false; // We're nolonger authorised
278
+
279
+ let authoriseRequest = null;
280
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AuthoriseRequest');
281
+ if (TraitMap !== null) {
282
+ authoriseRequest = TraitMap.encode(
283
+ TraitMap.fromObject(
284
+ this.tokenType === 'nest' ? { sessionToken: this.token } : this.tokenType === 'google' ? { oliveToken: this.token } : {},
285
+ ),
286
+ ).finish();
287
+ }
308
288
 
309
- this.authorised = false; // We're nolonger authorised
289
+ if (reauthorise === true && authoriseRequest !== null) {
290
+ // Request to re-authorise only
291
+ this?.log?.debug && this.log.debug('Re-authentication requested to "%s"', this.host);
292
+ this.#sendMessage(PacketType.AUTHORIZE_REQUEST, authoriseRequest);
293
+ }
310
294
 
311
- if (this.tokenType === 'nest') {
312
- tokenBuffer.writeStringField(1, this.token); // Tag 1, session token, Nest auth accounts
313
- helloBuffer.writeStringField(4, this.token); // Tag 4, session token, Nest auth accounts
314
- }
315
- if (this.tokenType === 'google') {
316
- tokenBuffer.writeStringField(4, this.token); // Tag 4, olive token, Google auth accounts
317
- helloBuffer.writeBytesField(12, tokenBuffer.finish()); // Tag 12, olive token, Google auth accounts
318
- }
319
- if (typeof reauthorise === 'boolean' && reauthorise === true) {
320
- // Request to re-authorise only
321
- this?.log?.debug && this.log.debug('Re-authentication requested to "%s"', this.host);
322
- this.#sendMessage(PacketType.AUTHORIZE_REQUEST, tokenBuffer.finish());
323
- } else {
324
- // This isn't a re-authorise request, so perform 'Hello' packet
325
- this?.log?.debug && this.log.debug('Performing authentication to "%s"', this.host);
326
- helloBuffer.writeVarintField(1, ProtocolVersion.VERSION_3);
327
- helloBuffer.writeStringField(2, this.uuid.split('.')[1]); // UUID should be 'quartz.xxxxxx'. We want the xxxxxx part
328
- helloBuffer.writeBooleanField(3, false); // Doesnt required a connected camera
329
- helloBuffer.writeStringField(6, crypto.randomUUID()); // Random UUID for this connection attempt
330
- helloBuffer.writeStringField(7, 'Nest/5.75.0 (iOScom.nestlabs.jasper.release) os=17.4.1');
331
- helloBuffer.writeVarintField(9, ClientType.IOS);
332
- this.#sendMessage(PacketType.HELLO, helloBuffer.finish());
295
+ if (reauthorise === false && authoriseRequest !== null) {
296
+ // This isn't a re-authorise request, so perform 'Hello' packet
297
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Hello');
298
+ if (TraitMap !== null) {
299
+ this?.log?.debug && this.log.debug('Performing authentication to "%s"', this.host);
300
+
301
+ let encodedData = TraitMap.encode(
302
+ TraitMap.fromObject({
303
+ protocolVersion: 'VERSION_3',
304
+ uuid: this.uuid.split(/[._]+/)[1],
305
+ requireConnectedCamera: false,
306
+ userAgent: USERAGENT,
307
+ deviceId: crypto.randomUUID(),
308
+ ClientType: 'IOS',
309
+ authoriseRequest: authoriseRequest,
310
+ }),
311
+ ).finish();
312
+ this.#sendMessage(PacketType.HELLO, encodedData);
313
+ }
314
+ }
333
315
  }
334
316
  }
335
317
 
336
318
  #handleRedirect(payload) {
337
319
  let redirectToHost = undefined;
338
- if (typeof payload === 'object') {
339
- // Payload parameter is an object, we'll assume its a payload packet
340
- // Decode redirect packet to determine new host
341
- let packet = payload.readFields(
342
- (tag, obj, protoBuf) => {
343
- if (tag === 1) {
344
- obj.new_host = protoBuf.readString(); // new host
345
- }
346
- if (tag === 2) {
347
- obj.is_transcode = protoBuf.readBoolean();
348
- }
349
- },
350
- { new_host: '', is_transcode: false },
351
- );
352
-
353
- redirectToHost = packet.new_host;
320
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
321
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Redirect').decode(payload).toJSON();
322
+ redirectToHost = decodedMessage?.newHost;
354
323
  }
355
324
  if (typeof payload === 'string') {
356
325
  // Payload parameter is a string, we'll assume this is a direct hostname
@@ -364,316 +333,157 @@ export default class NexusTalk extends Streamer {
364
333
  this?.log?.debug && this.log.debug('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
365
334
 
366
335
  // Setup listener for socket close event. Once socket is closed, we'll perform the redirect
367
- this.socket &&
368
- this.socket.on('close', () => {
336
+ this.#socket &&
337
+ this.#socket.on('close', () => {
369
338
  this.connect(redirectToHost); // Connect to new host
370
339
  });
371
340
  this.close(true); // Close existing socket
372
341
  }
373
342
 
374
343
  #handlePlaybackBegin(payload) {
375
- // Decode playback begin packet
376
- let packet = payload.readFields(
377
- (tag, obj, protoBuf) => {
378
- if (tag === 1) {
379
- obj.session_id = protoBuf.readVarint();
380
- }
381
- if (tag === 2) {
382
- obj.channels.push(
383
- protoBuf.readFields(
384
- (tag, obj, protoBuf) => {
385
- if (tag === 1) {
386
- obj.channel_id = protoBuf.readVarint();
387
- }
388
- if (tag === 2) {
389
- obj.codec_type = protoBuf.readVarint();
390
- }
391
- if (tag === 3) {
392
- obj.sample_rate = protoBuf.readVarint();
393
- }
394
- if (tag === 4) {
395
- obj.private_data.push(protoBuf.readBytes());
396
- }
397
- if (tag === 5) {
398
- obj.start_time = protoBuf.readDouble();
399
- }
400
- if (tag === 6) {
401
- obj.udp_ssrc = protoBuf.readVarint();
402
- }
403
- if (tag === 7) {
404
- obj.rtp_start_time = protoBuf.readVarint();
405
- }
406
- if (tag === 8) {
407
- obj.profile = protoBuf.readVarint();
408
- }
409
- },
410
- { channel_id: 0, codec_type: 0, sample_rate: 0, private_data: [], start_time: 0, udp_ssrc: 0, rtp_start_time: 0, profile: 3 },
411
- protoBuf.readVarint() + protoBuf.pos,
412
- ),
413
- );
414
- }
415
- if (tag === 3) {
416
- obj.srtp_master_key = protoBuf.readBytes();
417
- }
418
- if (tag === 4) {
419
- obj.srtp_master_salt = protoBuf.readBytes();
420
- }
421
- if (tag === 5) {
422
- obj.fec_k_val = protoBuf.readVarint();
423
- }
424
- if (tag === 6) {
425
- obj.fec_n_val = protoBuf.readVarint();
426
- }
427
- },
428
- { session_id: 0, channels: [], srtp_master_key: null, srtp_master_salt: null, fec_k_val: 0, fec_n_val: 0 },
429
- );
430
-
431
- packet.channels &&
432
- packet.channels.forEach((stream) => {
344
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
345
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackBegin').decode(payload).toJSON();
346
+ decodedMessage.channels.forEach((stream) => {
433
347
  // Find which channels match our video and audio streams
434
- if (stream.codec_type === CodecType.H264) {
348
+ if (stream.codecType === 'H264') {
435
349
  this.video = {
436
- channel_id: stream.channel_id,
437
- start_time: Date.now() + stream.start_time,
438
- sample_rate: stream.sample_rate,
350
+ channel_id: stream.channelId,
351
+ start_time: Date.now() + stream.startTime,
352
+ sample_rate: stream.sampleRate,
439
353
  timestamp_delta: 0,
440
354
  };
441
355
  }
442
- if (stream.codec_type === CodecType.AAC || stream.codec_type === CodecType.OPUS || stream.codec_type === CodecType.SPEEX) {
356
+ if (stream.codecType === 'AAC' || stream.codecType === 'OPUS' || stream.codecType === 'SPEEX') {
443
357
  this.audio = {
444
- channel_id: stream.channel_id,
445
- start_time: Date.now() + stream.start_time,
446
- sample_rate: stream.sample_rate,
358
+ channel_id: stream.channelId,
359
+ start_time: Date.now() + stream.startTime,
360
+ sample_rate: stream.sampleRate,
447
361
  timestamp_delta: 0,
448
362
  };
449
363
  }
450
364
  });
451
365
 
452
- // Since this is the beginning of playback, clear any active buffers contents
453
- this.id = packet.session_id;
454
- this.packets = [];
455
- this.messages = [];
366
+ // Since this is the beginning of playback, clear any active buffers contents
367
+ this.#id = decodedMessage.sessionId;
368
+ this.#packets = [];
369
+ this.#messages = [];
456
370
 
457
- this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.id);
371
+ this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.#id);
372
+ }
458
373
  }
459
374
 
460
375
  #handlePlaybackPacket(payload) {
461
376
  // Decode playback packet
462
- let packet = payload.readFields(
463
- (tag, obj, protoBuf) => {
464
- if (tag === 1) {
465
- obj.session_id = protoBuf.readVarint();
466
- }
467
- if (tag === 2) {
468
- obj.channel_id = protoBuf.readVarint();
469
- }
470
- if (tag === 3) {
471
- obj.timestamp_delta = protoBuf.readSVarint();
472
- }
473
- if (tag === 4) {
474
- obj.payload = protoBuf.readBytes();
475
- }
476
- if (tag === 5) {
477
- obj.latency_rtp_sequence = protoBuf.readVarint();
478
- }
479
- if (tag === 6) {
480
- obj.latency_rtp_ssrc = protoBuf.readVarint();
481
- }
482
- if (tag === 7) {
483
- obj.directors_cut_regions.push(
484
- protoBuf.readFields(
485
- (tag, obj, protoBuf) => {
486
- if (tag === 1) {
487
- obj.id = protoBuf.readVarint();
488
- }
489
- if (tag === 2) {
490
- obj.left = protoBuf.readVarint();
491
- }
492
- if (tag === 3) {
493
- obj.right = protoBuf.readVarint();
494
- }
495
- if (tag === 4) {
496
- obj.top = protoBuf.readVarint();
497
- }
498
- if (tag === 5) {
499
- obj.bottom = protoBuf.readVarint();
500
- }
501
- },
502
- {
503
- // Defaults
504
- id: 0,
505
- left: 0,
506
- right: 0,
507
- top: 0,
508
- bottom: 0,
509
- },
510
- protoBuf.readVarint() + protoBuf.pos,
511
- ),
512
- );
513
- }
514
- },
515
- {
516
- // Defaults
517
- session_id: 0,
518
- channel_id: 0,
519
- timestamp_delta: 0,
520
- payload: null,
521
- latency_rtp_sequence: 0,
522
- latency_rtp_ssrc: 0,
523
- directors_cut_regions: [],
524
- },
525
- );
526
-
527
- // Setup up a timeout to monitor for no packets recieved in a certain period
528
- // If its trigger, we'll attempt to restart the stream and/or connection
529
- // <-- testing to see how often this occurs first
530
- this.stalledTimer = clearTimeout(this.stalledTimer);
531
- this.stalledTimer = setTimeout(() => {
532
- this?.log?.debug && this.log.debug('We have not received any data from nexus in the past "%s" seconds. Attempting restart', 8);
533
-
534
- // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
535
- this.socket &&
536
- this.socket.on('close', () => {
537
- this.connect(this.host); // try reconnection
538
- });
539
- this.close(false); // Close existing socket
540
- }, 8000);
541
-
542
- // Handle video packet
543
- if (packet.channel_id === this.video.channel_id) {
544
- this.video.timestamp_delta += packet.timestamp_delta;
545
- this.addToOutput('video', this.video.start_time + this.video.timestamp_delta, packet.payload);
546
- }
377
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
378
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackPacket').decode(payload).toJSON();
379
+
380
+ // Setup up a timeout to monitor for no packets recieved in a certain period
381
+ // If its trigger, we'll attempt to restart the stream and/or connection
382
+ // <-- testing to see how often this occurs first
383
+ this.stalledTimer = clearTimeout(this.stalledTimer);
384
+ this.stalledTimer = setTimeout(() => {
385
+ this?.log?.debug && this.log.debug('We have not received any data from nexus in the past "%s" seconds. Attempting restart', 8);
386
+
387
+ // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
388
+ this.#socket &&
389
+ this.#socket.on('close', () => {
390
+ this.connect(); // try reconnection
391
+ });
392
+ this.close(false); // Close existing socket
393
+ }, 8000);
394
+
395
+ // Handle video packet
396
+ if (decodedMessage.channelId === this.video.channel_id) {
397
+ this.video.timestamp_delta += decodedMessage.timestampDelta;
398
+ this.addToOutput('video', this.video.start_time + this.video.timestamp_delta, Buffer.from(decodedMessage.payload, 'base64'));
399
+ }
547
400
 
548
- // Handle audio packet
549
- if (packet.channel_id === this.audio.channel_id) {
550
- this.audio.timestamp_delta += packet.timestamp_delta;
551
- this.addToOutput('audio', this.audio.start_time + this.audio.timestamp_delta, packet.payload);
401
+ // Handle audio packet
402
+ if (decodedMessage.channelId === this.audio.channel_id) {
403
+ this.audio.timestamp_delta += decodedMessage.timestampDelta;
404
+ this.addToOutput('audio', this.audio.start_time + this.audio.timestamp_delta, Buffer.from(decodedMessage.payload, 'base64'));
405
+ }
552
406
  }
553
407
  }
554
408
 
555
409
  #handlePlaybackEnd(payload) {
556
410
  // Decode playpack ended packet
557
- let packet = payload.readFields(
558
- (tag, obj, protoBuf) => {
559
- if (tag === 1) {
560
- obj.session_id = protoBuf.readVarint();
561
- }
562
- if (tag === 2) {
563
- obj.reason = protoBuf.readVarint();
564
- }
565
- },
566
- { session_id: 0, reason: 0 },
567
- );
411
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
412
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackEnd').decode(payload).toJSON();
568
413
 
569
- if (this.id !== null && packet.reason === 0) {
570
- // Normal playback ended ie: when we stopped playback
571
- this?.log?.debug && this.log.debug('Playback ended on "%s"', this.host);
572
- }
573
-
574
- if (packet.reason !== 0) {
575
- // Error during playback, so we'll attempt to restart by reconnection to host
576
- this?.log?.debug && this.log.debug('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, packet.reason);
414
+ if (this.#id !== undefined && decodedMessage.reason === 'USER_ENDED_SESSION') {
415
+ // Normal playback ended ie: when we stopped playback
416
+ this?.log?.debug && this.log.debug('Playback ended on "%s"', this.host);
417
+ }
577
418
 
578
- // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
579
- this.socket &&
580
- this.socket.on('close', () => {
581
- this.connect(this.host); // try reconnection to existing host
582
- });
583
- this.close(false); // Close existing socket
419
+ if (decodedMessage.reason !== 'USER_ENDED_SESSION') {
420
+ // Error during playback, so we'll attempt to restart by reconnection to host
421
+ this?.log?.debug &&
422
+ this.log.debug('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
423
+
424
+ // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
425
+ this.#socket &&
426
+ this.#socket.on('close', () => {
427
+ this.connect(); // try reconnection to existing host
428
+ });
429
+ this.close(false); // Close existing socket
430
+ }
584
431
  }
585
432
  }
586
433
 
587
434
  #handleNexusError(payload) {
588
435
  // Decode error packet
589
- let packet = payload.readFields(
590
- (tag, obj, protoBuf) => {
591
- if (tag === 1) {
592
- obj.code = protoBuf.readVarint();
593
- }
594
- if (tag === 2) {
595
- obj.message = protoBuf.readString();
596
- }
597
- },
598
- { code: 1, message: '' },
599
- );
600
-
601
- if (packet.code === ErrorCode.ERROR_AUTHORIZATION_FAILED) {
602
- // NexusStreamer Updating authentication
603
- this.#Authenticate(true); // Update authorisation only
604
- } else {
605
- // NexusStreamer Error, packet.message contains the message
606
- this?.log?.debug && this.log.debug('Error', packet.message);
436
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
437
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Error').decode(payload).toJSON();
438
+ if (decodedMessage.code === 'ERROR_AUTHORIZATION_FAILED') {
439
+ // NexusStreamer Updating authentication
440
+ this.#Authenticate(true); // Update authorisation only
441
+ } else {
442
+ // NexusStreamer Error, packet.message contains the message
443
+ this?.log?.debug && this.log.debug('Error', decodedMessage.message);
444
+ }
607
445
  }
608
446
  }
609
447
 
610
448
  #handleTalkbackBegin(payload) {
611
449
  // Decode talk begin packet
612
- let packet = payload.readFields(
613
- (tag, obj, protoBuf) => {
614
- if (tag === 1) {
615
- obj.user_id = protoBuf.readString();
616
- }
617
- if (tag === 2) {
618
- obj.session_id = protoBuf.readVarint();
619
- }
620
- if (tag === 3) {
621
- obj.quick_action_id = protoBuf.readVarint();
622
- }
623
- if (tag === 4) {
624
- obj.device_id = protoBuf.readString();
625
- }
626
- },
627
- { user_id: '', session_id: 0, quick_action_id: 0, device_id: '' },
628
- );
629
-
630
- this?.log?.debug && this.log.debug('Talkback started on "%s"', packet.device_id);
450
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
451
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackBegin').decode(payload).toJSON();
452
+ this?.log?.debug && this.log.debug('Talkback started to uuid "%s" with id of "%s"', this.uuid, decodedMessage.deviceId);
453
+ }
631
454
  }
632
455
 
633
456
  #handleTalkbackEnd(payload) {
634
457
  // Decode talk end packet
635
- let packet = payload.readFields(
636
- (tag, obj, protoBuf) => {
637
- if (tag === 1) {
638
- obj.user_id = protoBuf.readString();
639
- }
640
- if (tag === 2) {
641
- obj.session_id = protoBuf.readVarint();
642
- }
643
- if (tag === 3) {
644
- obj.quick_action_id = protoBuf.readVarint();
645
- }
646
- if (tag === 4) {
647
- obj.device_id = protoBuf.readString();
648
- }
649
- },
650
- { user_id: '', session_id: 0, quick_action_id: 0, device_id: '' },
651
- );
652
-
653
- this?.log?.debug && this.log.debug('Talkback ended on "%s"', packet.device_id);
458
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
459
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackEnd').decode(payload).toJSON();
460
+ this?.log?.debug && this.log.debug('Talkback ended from uuid "%s" with id of "%s"', this.uuid, decodedMessage.deviceId);
461
+ }
654
462
  }
655
463
 
656
464
  #handleNexusData(data) {
657
465
  // Process the rawdata from our socket connection and convert into nexus packets to take action against
658
- this.packets = this.packets.length === 0 ? data : Buffer.concat([this.packets, data]);
466
+ this.#packets = this.#packets.length === 0 ? data : Buffer.concat([this.#packets, data]);
659
467
 
660
- while (this.packets.length >= 3) {
468
+ while (this.#packets.length >= 3) {
661
469
  let headerSize = 3;
662
- let packetType = this.packets.readUInt8(0);
663
- let packetSize = this.packets.readUInt16BE(1);
470
+ let packetType = this.#packets.readUInt8(0);
471
+ let packetSize = this.#packets.readUInt16BE(1);
664
472
 
665
473
  if (packetType === PacketType.LONG_PLAYBACK_PACKET) {
666
474
  headerSize = 5;
667
- packetSize = this.packets.readUInt32BE(1);
475
+ packetSize = this.#packets.readUInt32BE(1);
668
476
  }
669
477
 
670
- if (this.packets.length < headerSize + packetSize) {
478
+ if (this.#packets.length < headerSize + packetSize) {
671
479
  // We dont have enough data in the buffer yet to process the full packet
672
480
  // so, exit loop and await more data
673
481
  break;
674
482
  }
675
483
 
676
- let protoBufPayload = new protoBuf(this.packets.slice(headerSize, headerSize + packetSize));
484
+ let protoBufPayload = this.#packets.subarray(headerSize, headerSize + packetSize);
485
+ this.#packets = this.#packets.subarray(headerSize + packetSize);
486
+
677
487
  switch (packetType) {
678
488
  case PacketType.PING: {
679
489
  break;
@@ -681,8 +491,8 @@ export default class NexusTalk extends Streamer {
681
491
 
682
492
  case PacketType.OK: {
683
493
  // process any pending messages we have stored
684
- this.authorised = true; // OK message, means we're connected and authorised to Nexus
685
- for (let message = this.messages.shift(); message; message = this.messages.shift()) {
494
+ this.#authorised = true; // OK message, means we're connected and authorised to Nexus
495
+ for (let message = this.#messages.shift(); message; message = this.#messages.shift()) {
686
496
  this.#sendMessage(message.type, message.data);
687
497
  }
688
498
 
@@ -733,9 +543,6 @@ export default class NexusTalk extends Streamer {
733
543
  break;
734
544
  }
735
545
  }
736
-
737
- // Remove the section of data we've just processed from our pending buffer
738
- this.packets = this.packets.slice(headerSize + packetSize);
739
546
  }
740
547
  }
741
548
  }