homebridge-nest-accfactory 0.0.4-a → 0.0.5

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 11/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,36 @@ 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
62
  id = undefined; // Session ID
106
- authorised = false; // Have wee been authorised
107
63
  pingTimer = undefined; // Timer object for ping interval
108
64
  stalledTimer = undefined; // Timer object for no received data
109
- packets = []; // Incoming packets
110
- messages = []; // Incoming messages
111
65
  video = {}; // Video stream details
112
66
  audio = {}; // Audio stream details
113
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
+
114
75
  constructor(deviceData, options) {
115
76
  super(deviceData, options);
116
77
 
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';
78
+ if (fs.existsSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto')) === true) {
79
+ protobuf.util.Long = null;
80
+ protobuf.configure();
81
+ this.#protobufNexusTalk = protobuf.loadSync(path.resolve(__dirname + '/protobuf/nest/nexustalk.proto'));
124
82
  }
125
- this.uuid = deviceData?.uuid;
126
- this.host = deviceData?.streaming_host; // Host we'll connect to
127
83
 
128
- this.pendingHost = null;
129
- this.weDidClose = true; // Flag if we did the socket close gracefully
84
+ // Store data we need from the device data passed it
85
+ this.token = deviceData?.apiAccess?.token;
86
+ this.tokenType = deviceData?.apiAccess?.oauth2 !== undefined ? 'google' : 'nest';
87
+ this.host = deviceData?.streaming_host; // Host we'll connect to
130
88
 
131
89
  // If specified option to start buffering, kick off
132
90
  if (typeof options?.buffer === 'boolean' && options.buffer === true) {
@@ -148,47 +106,37 @@ export default class NexusTalk extends Streamer {
148
106
  host = this.host;
149
107
  }
150
108
 
151
- if (this.pendingHost !== null) {
152
- host = this.pendingHost;
153
- this.pendingHost = null;
154
- }
155
-
156
109
  this?.log?.debug && this.log.debug('Starting connection to "%s"', host);
157
110
 
158
- this.socket = tls.connect({ host: host, port: 1443 }, () => {
111
+ this.#socket = tls.connect({ host: host, port: 1443 }, () => {
159
112
  // Opened connection to Nexus server, so now need to authenticate ourselves
160
113
  this?.log?.debug && this.log.debug('Connection established to "%s"', host);
161
114
 
162
- this.socket.setKeepAlive(true); // Keep socket connection alive
115
+ this.#socket.setKeepAlive(true); // Keep socket connection alive
163
116
  this.host = host; // update internal host name since we've connected
117
+ this.connected = true;
164
118
  this.#Authenticate(false);
165
119
  });
166
120
 
167
- this.socket.on('error', () => {});
121
+ this.#socket.on('error', () => {});
168
122
 
169
- this.socket.on('end', () => {});
123
+ this.#socket.on('end', () => {});
170
124
 
171
- this.socket.on('data', (data) => {
125
+ this.#socket.on('data', (data) => {
172
126
  this.#handleNexusData(data);
173
127
  });
174
128
 
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
129
+ this.#socket.on('close', (hadError) => {
130
+ this?.log?.debug && this.log.debug('Connection closed to "%s"', host);
180
131
 
181
132
  this.stalledTimer = clearTimeout(this.stalledTimer); // Clear stalled timer
182
133
  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
134
+ this.#authorised = false; // Since connection close, we can't be authorised anymore
135
+ this.#socket = undefined; // Clear socket object
136
+ this.connected = false;
185
137
  this.id = undefined; // Not an active session anymore
186
138
 
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) {
139
+ if (hadError === true && this.haveOutputs() === true) {
192
140
  // We still have either active buffering occuring or output streams running
193
141
  // so attempt to restart connection to existing host
194
142
  this.connect(host);
@@ -199,20 +147,19 @@ export default class NexusTalk extends Streamer {
199
147
 
200
148
  close(stopStreamFirst) {
201
149
  // Close an authenicated socket stream gracefully
202
- if (this.socket !== null) {
150
+ if (this.#socket !== null) {
203
151
  if (stopStreamFirst === true) {
204
152
  // Send a notifcation to nexus we're finished playback
205
153
  this.#stopNexusData();
206
154
  }
207
- this.socket.destroy();
155
+ this.#socket.destroy();
208
156
  }
209
157
 
210
- this.socket = null;
158
+ this.connected = false;
159
+ this.#socket = undefined;
211
160
  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
161
+ this.#packets = [];
162
+ this.#messages = [];
216
163
  }
217
164
 
218
165
  update(deviceData) {
@@ -224,7 +171,7 @@ export default class NexusTalk extends Streamer {
224
171
  // access token has changed so re-authorise
225
172
  this.token = deviceData.apiAccess.token;
226
173
 
227
- if (this.socket !== null) {
174
+ if (this.#socket !== null) {
228
175
  this.#Authenticate(true); // Update authorisation only if connected
229
176
  }
230
177
  }
@@ -235,51 +182,68 @@ export default class NexusTalk extends Streamer {
235
182
 
236
183
  talkingAudio(talkingData) {
237
184
  // 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());
185
+ if (typeof talkingData === 'object' && this.#protobufNexusTalk !== undefined) {
186
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.StartPlayback');
187
+ if (TraitMap !== null) {
188
+ let encodedData = TraitMap.encode(
189
+ TraitMap.fromObject({
190
+ payload: talkingData,
191
+ sessionId: this.id,
192
+ codec: 'SPEEX',
193
+ sampleRate: 16000,
194
+ }),
195
+ ).finish();
196
+ this.#sendMessage(PacketType.AUDIO_PAYLOAD, encodedData);
197
+ }
198
+ }
245
199
  }
246
200
 
247
201
  #startNexusData() {
248
- if (this.videoEnabled === false || this.online === false) {
202
+ if (this.videoEnabled === false || this.online === false || this.#protobufNexusTalk === undefined) {
249
203
  return;
250
204
  }
251
205
 
252
206
  // Setup streaming profiles
253
207
  // 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
208
+ let otherProfiles = ['VIDEO_H264_530KBIT_L31', 'VIDEO_H264_100KBIT_L30'];
257
209
 
258
210
  if (this.audioEnabled === true) {
259
211
  // Include AAC profile if audio is enabled on camera
260
- otherProfiles.push(StreamProfile.AUDIO_AAC);
212
+ otherProfiles.push('AUDIO_AAC');
261
213
  }
262
214
 
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());
215
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.StartPlayback');
216
+ if (TraitMap !== null) {
217
+ let encodedData = TraitMap.encode(
218
+ TraitMap.fromObject({
219
+ sessionId: Math.floor(Math.random() * (100 - 1) + 1),
220
+ profile: 'VIDEO_H264_2MBIT_L40',
221
+ otherProfiles: otherProfiles,
222
+ profileNotFoundAction: 'REDIRECT',
223
+ }),
224
+ ).finish();
225
+ this.#sendMessage(PacketType.START_PLAYBACK, encodedData);
226
+ }
271
227
  }
272
228
 
273
229
  #stopNexusData() {
274
- let stopBuffer = new protoBuf();
275
- stopBuffer.writeVarintField(1, this.id); // Session ID
276
- this.#sendMessage(PacketType.STOP_PLAYBACK, stopBuffer.finish());
230
+ if (this.id !== undefined && this.#protobufNexusTalk !== undefined) {
231
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.StopPlayback');
232
+ if (TraitMap !== null) {
233
+ let encodedData = TraitMap.encode(
234
+ TraitMap.fromObject({
235
+ sessionId: this.id,
236
+ }),
237
+ ).finish();
238
+ this.#sendMessage(PacketType.STOP_PLAYBACK, encodedData);
239
+ }
240
+ }
277
241
  }
278
242
 
279
243
  #sendMessage(type, data) {
280
- if (this.socket === null || this.socket.readyState !== 'open' || (type !== PacketType.HELLO && this.authorised === false)) {
244
+ if (this.#socket === null || this.#socket.readyState !== 'open' || (type !== PacketType.HELLO && this.#authorised === false)) {
281
245
  // We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
282
- this.messages.push({ type: type, data: data });
246
+ this.#messages.push({ type: type, data: data });
283
247
  return;
284
248
  }
285
249
 
@@ -296,61 +260,60 @@ export default class NexusTalk extends Streamer {
296
260
  }
297
261
 
298
262
  // write our composed message out to the socket back to NexusTalk
299
- this.socket.write(Buffer.concat([header, Buffer.from(data)]), () => {
263
+ this.#socket.write(Buffer.concat([header, Buffer.from(data)]), () => {
300
264
  // Message sent. Don't do anything?
301
265
  });
302
266
  }
303
267
 
304
268
  #Authenticate(reauthorise) {
305
269
  // Authenticate over created socket connection
306
- let tokenBuffer = new protoBuf();
307
- let helloBuffer = new protoBuf();
270
+ if (this.#protobufNexusTalk !== undefined) {
271
+ this.#authorised = false; // We're nolonger authorised
272
+
273
+ let authoriseRequest = null;
274
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.AuthoriseRequest');
275
+ if (TraitMap !== null) {
276
+ authoriseRequest = TraitMap.encode(
277
+ TraitMap.fromObject(
278
+ this.tokenType === 'nest' ? { sessionToken: this.token } : this.tokenType === 'google' ? { oliveToken: this.token } : {},
279
+ ),
280
+ ).finish();
281
+ }
308
282
 
309
- this.authorised = false; // We're nolonger authorised
283
+ if (reauthorise === true && authoriseRequest !== null) {
284
+ // Request to re-authorise only
285
+ this?.log?.debug && this.log.debug('Re-authentication requested to "%s"', this.host);
286
+ this.#sendMessage(PacketType.AUTHORIZE_REQUEST, authoriseRequest);
287
+ }
310
288
 
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());
289
+ if (reauthorise === false && authoriseRequest !== null) {
290
+ // This isn't a re-authorise request, so perform 'Hello' packet
291
+ let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Hello');
292
+ if (TraitMap !== null) {
293
+ this?.log?.debug && this.log.debug('Performing authentication to "%s"', this.host);
294
+
295
+ let encodedData = TraitMap.encode(
296
+ TraitMap.fromObject({
297
+ protocolVersion: 'VERSION_3',
298
+ uuid: this.uuid.split(/[._]+/)[1],
299
+ requireConnectedCamera: false,
300
+ userAgent: USERAGENT,
301
+ deviceId: crypto.randomUUID(),
302
+ ClientType: 'IOS',
303
+ authoriseRequest: authoriseRequest,
304
+ }),
305
+ ).finish();
306
+ this.#sendMessage(PacketType.HELLO, encodedData);
307
+ }
308
+ }
333
309
  }
334
310
  }
335
311
 
336
312
  #handleRedirect(payload) {
337
313
  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;
314
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
315
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Redirect').decode(payload).toJSON();
316
+ redirectToHost = decodedMessage?.newHost;
354
317
  }
355
318
  if (typeof payload === 'string') {
356
319
  // Payload parameter is a string, we'll assume this is a direct hostname
@@ -364,316 +327,157 @@ export default class NexusTalk extends Streamer {
364
327
  this?.log?.debug && this.log.debug('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
365
328
 
366
329
  // Setup listener for socket close event. Once socket is closed, we'll perform the redirect
367
- this.socket &&
368
- this.socket.on('close', () => {
330
+ this.#socket &&
331
+ this.#socket.on('close', () => {
369
332
  this.connect(redirectToHost); // Connect to new host
370
333
  });
371
334
  this.close(true); // Close existing socket
372
335
  }
373
336
 
374
337
  #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) => {
338
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
339
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackBegin').decode(payload).toJSON();
340
+ decodedMessage.channels.forEach((stream) => {
433
341
  // Find which channels match our video and audio streams
434
- if (stream.codec_type === CodecType.H264) {
342
+ if (stream.codecType === 'H264') {
435
343
  this.video = {
436
- channel_id: stream.channel_id,
437
- start_time: Date.now() + stream.start_time,
438
- sample_rate: stream.sample_rate,
344
+ channel_id: stream.channelId,
345
+ start_time: Date.now() + stream.startTime,
346
+ sample_rate: stream.sampleRate,
439
347
  timestamp_delta: 0,
440
348
  };
441
349
  }
442
- if (stream.codec_type === CodecType.AAC || stream.codec_type === CodecType.OPUS || stream.codec_type === CodecType.SPEEX) {
350
+ if (stream.codecType === 'AAC' || stream.codecType === 'OPUS' || stream.codecType === 'SPEEX') {
443
351
  this.audio = {
444
- channel_id: stream.channel_id,
445
- start_time: Date.now() + stream.start_time,
446
- sample_rate: stream.sample_rate,
352
+ channel_id: stream.channelId,
353
+ start_time: Date.now() + stream.startTime,
354
+ sample_rate: stream.sampleRate,
447
355
  timestamp_delta: 0,
448
356
  };
449
357
  }
450
358
  });
451
359
 
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 = [];
360
+ // Since this is the beginning of playback, clear any active buffers contents
361
+ this.id = decodedMessage.sessionId;
362
+ this.#packets = [];
363
+ this.#messages = [];
456
364
 
457
- this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.id);
365
+ this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.id);
366
+ }
458
367
  }
459
368
 
460
369
  #handlePlaybackPacket(payload) {
461
370
  // 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
- }
371
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
372
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackPacket').decode(payload).toJSON();
373
+
374
+ // Setup up a timeout to monitor for no packets recieved in a certain period
375
+ // If its trigger, we'll attempt to restart the stream and/or connection
376
+ // <-- testing to see how often this occurs first
377
+ this.stalledTimer = clearTimeout(this.stalledTimer);
378
+ this.stalledTimer = setTimeout(() => {
379
+ this?.log?.debug && this.log.debug('We have not received any data from nexus in the past "%s" seconds. Attempting restart', 8);
380
+
381
+ // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
382
+ this.#socket &&
383
+ this.#socket.on('close', () => {
384
+ this.connect(this.host); // try reconnection
385
+ });
386
+ this.close(false); // Close existing socket
387
+ }, 8000);
388
+
389
+ // Handle video packet
390
+ if (decodedMessage.channelId === this.video.channel_id) {
391
+ this.video.timestamp_delta += decodedMessage.timestampDelta;
392
+ this.addToOutput('video', this.video.start_time + this.video.timestamp_delta, Buffer.from(decodedMessage.payload, 'base64'));
393
+ }
547
394
 
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);
395
+ // Handle audio packet
396
+ if (decodedMessage.channelId === this.audio.channel_id) {
397
+ this.audio.timestamp_delta += decodedMessage.timestampDelta;
398
+ this.addToOutput('audio', this.audio.start_time + this.audio.timestamp_delta, Buffer.from(decodedMessage.payload, 'base64'));
399
+ }
552
400
  }
553
401
  }
554
402
 
555
403
  #handlePlaybackEnd(payload) {
556
404
  // 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
- );
405
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
406
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.PlaybackEnd').decode(payload).toJSON();
568
407
 
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);
408
+ if (this.id !== null && decodedMessage.reason === 'USER_ENDED_SESSION') {
409
+ // Normal playback ended ie: when we stopped playback
410
+ this?.log?.debug && this.log.debug('Playback ended on "%s"', this.host);
411
+ }
577
412
 
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
413
+ if (decodedMessage.reason !== 'USER_ENDED_SESSION') {
414
+ // Error during playback, so we'll attempt to restart by reconnection to host
415
+ this?.log?.debug &&
416
+ this.log.debug('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
417
+
418
+ // Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
419
+ this.#socket &&
420
+ this.#socket.on('close', () => {
421
+ this.connect(this.host); // try reconnection to existing host
422
+ });
423
+ this.close(false); // Close existing socket
424
+ }
584
425
  }
585
426
  }
586
427
 
587
428
  #handleNexusError(payload) {
588
429
  // 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);
430
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
431
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Error').decode(payload).toJSON();
432
+ if (decodedMessage.code === 'ERROR_AUTHORIZATION_FAILED') {
433
+ // NexusStreamer Updating authentication
434
+ this.#Authenticate(true); // Update authorisation only
435
+ } else {
436
+ // NexusStreamer Error, packet.message contains the message
437
+ this?.log?.debug && this.log.debug('Error', decodedMessage.message);
438
+ }
607
439
  }
608
440
  }
609
441
 
610
442
  #handleTalkbackBegin(payload) {
611
443
  // 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);
444
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
445
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackBegin').decode(payload).toJSON();
446
+ this?.log?.debug && this.log.debug('Talkback started on "%s"', decodedMessage.deviceId);
447
+ }
631
448
  }
632
449
 
633
450
  #handleTalkbackEnd(payload) {
634
451
  // 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);
452
+ if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
453
+ let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackEnd').decode(payload).toJSON();
454
+ this?.log?.debug && this.log.debug('Talkback ended on "%s"', decodedMessage.device_id);
455
+ }
654
456
  }
655
457
 
656
458
  #handleNexusData(data) {
657
459
  // 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]);
460
+ this.#packets = this.#packets.length === 0 ? data : Buffer.concat([this.#packets, data]);
659
461
 
660
- while (this.packets.length >= 3) {
462
+ while (this.#packets.length >= 3) {
661
463
  let headerSize = 3;
662
- let packetType = this.packets.readUInt8(0);
663
- let packetSize = this.packets.readUInt16BE(1);
464
+ let packetType = this.#packets.readUInt8(0);
465
+ let packetSize = this.#packets.readUInt16BE(1);
664
466
 
665
467
  if (packetType === PacketType.LONG_PLAYBACK_PACKET) {
666
468
  headerSize = 5;
667
- packetSize = this.packets.readUInt32BE(1);
469
+ packetSize = this.#packets.readUInt32BE(1);
668
470
  }
669
471
 
670
- if (this.packets.length < headerSize + packetSize) {
472
+ if (this.#packets.length < headerSize + packetSize) {
671
473
  // We dont have enough data in the buffer yet to process the full packet
672
474
  // so, exit loop and await more data
673
475
  break;
674
476
  }
675
477
 
676
- let protoBufPayload = new protoBuf(this.packets.slice(headerSize, headerSize + packetSize));
478
+ let protoBufPayload = this.#packets.subarray(headerSize, headerSize + packetSize);
479
+ this.#packets = this.#packets.subarray(headerSize + packetSize);
480
+
677
481
  switch (packetType) {
678
482
  case PacketType.PING: {
679
483
  break;
@@ -681,8 +485,8 @@ export default class NexusTalk extends Streamer {
681
485
 
682
486
  case PacketType.OK: {
683
487
  // 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()) {
488
+ this.#authorised = true; // OK message, means we're connected and authorised to Nexus
489
+ for (let message = this.#messages.shift(); message; message = this.#messages.shift()) {
686
490
  this.#sendMessage(message.type, message.data);
687
491
  }
688
492
 
@@ -733,9 +537,6 @@ export default class NexusTalk extends Streamer {
733
537
  break;
734
538
  }
735
539
  }
736
-
737
- // Remove the section of data we've just processed from our pending buffer
738
- this.packets = this.packets.slice(headerSize + packetSize);
739
540
  }
740
541
  }
741
542
  }