homebridge-nest-accfactory 0.0.4-a

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +176 -0
  3. package/README.md +121 -0
  4. package/config.schema.json +107 -0
  5. package/dist/HomeKitDevice.js +441 -0
  6. package/dist/HomeKitHistory.js +2835 -0
  7. package/dist/camera.js +1276 -0
  8. package/dist/doorbell.js +122 -0
  9. package/dist/index.js +35 -0
  10. package/dist/nexustalk.js +741 -0
  11. package/dist/protect.js +240 -0
  12. package/dist/protobuf/google/rpc/status.proto +91 -0
  13. package/dist/protobuf/google/rpc/stream_body.proto +26 -0
  14. package/dist/protobuf/google/trait/product/camera.proto +53 -0
  15. package/dist/protobuf/googlehome/foyer.proto +208 -0
  16. package/dist/protobuf/nest/messages.proto +8 -0
  17. package/dist/protobuf/nest/services/apigateway.proto +107 -0
  18. package/dist/protobuf/nest/trait/audio.proto +7 -0
  19. package/dist/protobuf/nest/trait/cellular.proto +313 -0
  20. package/dist/protobuf/nest/trait/debug.proto +37 -0
  21. package/dist/protobuf/nest/trait/detector.proto +41 -0
  22. package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
  23. package/dist/protobuf/nest/trait/firmware.proto +221 -0
  24. package/dist/protobuf/nest/trait/guest.proto +105 -0
  25. package/dist/protobuf/nest/trait/history.proto +345 -0
  26. package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
  27. package/dist/protobuf/nest/trait/hvac.proto +1353 -0
  28. package/dist/protobuf/nest/trait/input.proto +29 -0
  29. package/dist/protobuf/nest/trait/lighting.proto +61 -0
  30. package/dist/protobuf/nest/trait/located.proto +193 -0
  31. package/dist/protobuf/nest/trait/media.proto +68 -0
  32. package/dist/protobuf/nest/trait/network.proto +352 -0
  33. package/dist/protobuf/nest/trait/occupancy.proto +373 -0
  34. package/dist/protobuf/nest/trait/olive.proto +15 -0
  35. package/dist/protobuf/nest/trait/pairing.proto +85 -0
  36. package/dist/protobuf/nest/trait/product/camera.proto +283 -0
  37. package/dist/protobuf/nest/trait/product/detect.proto +67 -0
  38. package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
  39. package/dist/protobuf/nest/trait/product/guard.proto +59 -0
  40. package/dist/protobuf/nest/trait/product/protect.proto +344 -0
  41. package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
  42. package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
  43. package/dist/protobuf/nest/trait/safety.proto +119 -0
  44. package/dist/protobuf/nest/trait/security.proto +516 -0
  45. package/dist/protobuf/nest/trait/selftest.proto +78 -0
  46. package/dist/protobuf/nest/trait/sensor.proto +291 -0
  47. package/dist/protobuf/nest/trait/service.proto +46 -0
  48. package/dist/protobuf/nest/trait/structure.proto +85 -0
  49. package/dist/protobuf/nest/trait/system.proto +51 -0
  50. package/dist/protobuf/nest/trait/test.proto +15 -0
  51. package/dist/protobuf/nest/trait/ui.proto +65 -0
  52. package/dist/protobuf/nest/trait/user.proto +98 -0
  53. package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
  54. package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
  55. package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
  56. package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
  57. package/dist/protobuf/nestlabs/history/v1.proto +73 -0
  58. package/dist/protobuf/root.proto +64 -0
  59. package/dist/protobuf/wdl-event-importance.proto +11 -0
  60. package/dist/protobuf/wdl.proto +450 -0
  61. package/dist/protobuf/weave/common.proto +144 -0
  62. package/dist/protobuf/weave/trait/audio.proto +12 -0
  63. package/dist/protobuf/weave/trait/auth.proto +22 -0
  64. package/dist/protobuf/weave/trait/description.proto +32 -0
  65. package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
  66. package/dist/protobuf/weave/trait/locale.proto +20 -0
  67. package/dist/protobuf/weave/trait/network.proto +24 -0
  68. package/dist/protobuf/weave/trait/pairing.proto +8 -0
  69. package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
  70. package/dist/protobuf/weave/trait/power.proto +86 -0
  71. package/dist/protobuf/weave/trait/schedule.proto +76 -0
  72. package/dist/protobuf/weave/trait/security.proto +343 -0
  73. package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
  74. package/dist/protobuf/weave/trait/time.proto +16 -0
  75. package/dist/res/Nest_camera_connecting.h264 +0 -0
  76. package/dist/res/Nest_camera_connecting.jpg +0 -0
  77. package/dist/res/Nest_camera_off.h264 +0 -0
  78. package/dist/res/Nest_camera_off.jpg +0 -0
  79. package/dist/res/Nest_camera_offline.h264 +0 -0
  80. package/dist/res/Nest_camera_offline.jpg +0 -0
  81. package/dist/res/Nest_camera_transfer.jpg +0 -0
  82. package/dist/streamer.js +344 -0
  83. package/dist/system.js +3112 -0
  84. package/dist/tempsensor.js +99 -0
  85. package/dist/thermostat.js +1026 -0
  86. package/dist/weather.js +205 -0
  87. package/dist/webrtc.js +55 -0
  88. package/package.json +66 -0
@@ -0,0 +1,741 @@
1
+ // NexusTalk
2
+ // Part of homebridge-nest-accfactory
3
+ //
4
+ // Handles connection and data from Nest 'nexus' systems
5
+ //
6
+ // Code version 6/9/2024
7
+ // Mark Hulskamp
8
+ 'use strict';
9
+
10
+ // Define external library requirements
11
+ import protoBuf from 'pbf'; // Proto buffer
12
+
13
+ // Define nodejs module requirements
14
+ import { Buffer } from 'node:buffer';
15
+ import { setInterval, clearInterval, setTimeout, clearTimeout } from 'node:timers';
16
+ import tls from 'tls';
17
+ import crypto from 'crypto';
18
+
19
+ // Define our modules
20
+ import Streamer from './streamer.js';
21
+
22
+ // Define constants
23
+ 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
+ };
61
+
62
+ const PacketType = {
63
+ PING: 1,
64
+ HELLO: 100,
65
+ PING_CAMERA: 101,
66
+ AUDIO_PAYLOAD: 102,
67
+ START_PLAYBACK: 103,
68
+ STOP_PLAYBACK: 104,
69
+ CLOCK_SYNC_ECHO: 105,
70
+ LATENCY_MEASURE: 106,
71
+ TALKBACK_LATENCY: 107,
72
+ METADATA_REQUEST: 108,
73
+ OK: 200,
74
+ ERROR: 201,
75
+ PLAYBACK_BEGIN: 202,
76
+ PLAYBACK_END: 203,
77
+ PLAYBACK_PACKET: 204,
78
+ LONG_PLAYBACK_PACKET: 205,
79
+ CLOCK_SYNC: 206,
80
+ REDIRECT: 207,
81
+ TALKBACK_BEGIN: 208,
82
+ TALKBACK_END: 209,
83
+ METADATA: 210,
84
+ METADATA_ERROR: 211,
85
+ AUTHORIZE_REQUEST: 212,
86
+ };
87
+
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
+ // nexusTalk object
101
+ export default class NexusTalk extends Streamer {
102
+ token = undefined;
103
+ tokenType = undefined;
104
+ uuid = undefined;
105
+ id = undefined; // Session ID
106
+ authorised = false; // Have wee been authorised
107
+ pingTimer = undefined; // Timer object for ping interval
108
+ stalledTimer = undefined; // Timer object for no received data
109
+ packets = []; // Incoming packets
110
+ messages = []; // Incoming messages
111
+ video = {}; // Video stream details
112
+ audio = {}; // Audio stream details
113
+
114
+ constructor(deviceData, options) {
115
+ super(deviceData, options);
116
+
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';
124
+ }
125
+ this.uuid = deviceData?.uuid;
126
+ this.host = deviceData?.streaming_host; // Host we'll connect to
127
+
128
+ this.pendingHost = null;
129
+ this.weDidClose = true; // Flag if we did the socket close gracefully
130
+
131
+ // If specified option to start buffering, kick off
132
+ if (typeof options?.buffer === 'boolean' && options.buffer === true) {
133
+ this.startBuffering();
134
+ }
135
+ }
136
+
137
+ // Class functions
138
+ connect(host) {
139
+ // Clear any timers we have running
140
+ this.pingTimer = clearInterval(this.pingTimer);
141
+ this.stalledTimer = clearInterval(this.stalledTimer);
142
+
143
+ this.id = undefined; // No session ID yet
144
+
145
+ if (this.online === true && this.videoEnabled === true) {
146
+ if (typeof host === 'undefined' || host === null) {
147
+ // No host parameter passed in, so we'll set this to our internally stored host
148
+ host = this.host;
149
+ }
150
+
151
+ if (this.pendingHost !== null) {
152
+ host = this.pendingHost;
153
+ this.pendingHost = null;
154
+ }
155
+
156
+ this?.log?.debug && this.log.debug('Starting connection to "%s"', host);
157
+
158
+ this.socket = tls.connect({ host: host, port: 1443 }, () => {
159
+ // Opened connection to Nexus server, so now need to authenticate ourselves
160
+ this?.log?.debug && this.log.debug('Connection established to "%s"', host);
161
+
162
+ this.socket.setKeepAlive(true); // Keep socket connection alive
163
+ this.host = host; // update internal host name since we've connected
164
+ this.#Authenticate(false);
165
+ });
166
+
167
+ this.socket.on('error', () => {});
168
+
169
+ this.socket.on('end', () => {});
170
+
171
+ this.socket.on('data', (data) => {
172
+ this.#handleNexusData(data);
173
+ });
174
+
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
180
+
181
+ this.stalledTimer = clearTimeout(this.stalledTimer); // Clear stalled timer
182
+ 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
186
+
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) {
192
+ // We still have either active buffering occuring or output streams running
193
+ // so attempt to restart connection to existing host
194
+ this.connect(host);
195
+ }
196
+ });
197
+ }
198
+ }
199
+
200
+ close(stopStreamFirst) {
201
+ // Close an authenicated socket stream gracefully
202
+ if (this.socket !== null) {
203
+ if (stopStreamFirst === true) {
204
+ // Send a notifcation to nexus we're finished playback
205
+ this.#stopNexusData();
206
+ }
207
+ this.socket.destroy();
208
+ }
209
+
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
216
+ }
217
+
218
+ update(deviceData) {
219
+ if (typeof deviceData !== 'object') {
220
+ return;
221
+ }
222
+
223
+ if (deviceData.apiAccess.token !== this.token) {
224
+ // access token has changed so re-authorise
225
+ this.token = deviceData.apiAccess.token;
226
+
227
+ if (this.socket !== null) {
228
+ this.#Authenticate(true); // Update authorisation only if connected
229
+ }
230
+ }
231
+
232
+ // Let our parent handle the remaining updates
233
+ super.update(deviceData);
234
+ }
235
+
236
+ talkingAudio(talkingData) {
237
+ // 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());
245
+ }
246
+
247
+ #startNexusData() {
248
+ if (this.videoEnabled === false || this.online === false) {
249
+ return;
250
+ }
251
+
252
+ // Setup streaming profiles
253
+ // 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
257
+
258
+ if (this.audioEnabled === true) {
259
+ // Include AAC profile if audio is enabled on camera
260
+ otherProfiles.push(StreamProfile.AUDIO_AAC);
261
+ }
262
+
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());
271
+ }
272
+
273
+ #stopNexusData() {
274
+ let stopBuffer = new protoBuf();
275
+ stopBuffer.writeVarintField(1, this.id); // Session ID
276
+ this.#sendMessage(PacketType.STOP_PLAYBACK, stopBuffer.finish());
277
+ }
278
+
279
+ #sendMessage(type, data) {
280
+ if (this.socket === null || this.socket.readyState !== 'open' || (type !== PacketType.HELLO && this.authorised === false)) {
281
+ // We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
282
+ this.messages.push({ type: type, data: data });
283
+ return;
284
+ }
285
+
286
+ // Create nexusTalk message header
287
+ let header = Buffer.alloc(3);
288
+ if (type !== PacketType.LONG_PLAYBACK_PACKET) {
289
+ header.writeUInt8(type, 0);
290
+ header.writeUInt16BE(data.length, 1);
291
+ }
292
+ if (type === PacketType.LONG_PLAYBACK_PACKET) {
293
+ header = Buffer.alloc(5);
294
+ header.writeUInt8(type, 0);
295
+ header.writeUInt32BE(data.length, 1);
296
+ }
297
+
298
+ // write our composed message out to the socket back to NexusTalk
299
+ this.socket.write(Buffer.concat([header, Buffer.from(data)]), () => {
300
+ // Message sent. Don't do anything?
301
+ });
302
+ }
303
+
304
+ #Authenticate(reauthorise) {
305
+ // Authenticate over created socket connection
306
+ let tokenBuffer = new protoBuf();
307
+ let helloBuffer = new protoBuf();
308
+
309
+ this.authorised = false; // We're nolonger authorised
310
+
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());
333
+ }
334
+ }
335
+
336
+ #handleRedirect(payload) {
337
+ 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;
354
+ }
355
+ if (typeof payload === 'string') {
356
+ // Payload parameter is a string, we'll assume this is a direct hostname
357
+ redirectToHost = payload;
358
+ }
359
+
360
+ if (typeof redirectToHost !== 'string' || redirectToHost === '') {
361
+ return;
362
+ }
363
+
364
+ this?.log?.debug && this.log.debug('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
365
+
366
+ // Setup listener for socket close event. Once socket is closed, we'll perform the redirect
367
+ this.socket &&
368
+ this.socket.on('close', () => {
369
+ this.connect(redirectToHost); // Connect to new host
370
+ });
371
+ this.close(true); // Close existing socket
372
+ }
373
+
374
+ #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) => {
433
+ // Find which channels match our video and audio streams
434
+ if (stream.codec_type === CodecType.H264) {
435
+ this.video = {
436
+ channel_id: stream.channel_id,
437
+ start_time: Date.now() + stream.start_time,
438
+ sample_rate: stream.sample_rate,
439
+ timestamp_delta: 0,
440
+ };
441
+ }
442
+ if (stream.codec_type === CodecType.AAC || stream.codec_type === CodecType.OPUS || stream.codec_type === CodecType.SPEEX) {
443
+ this.audio = {
444
+ channel_id: stream.channel_id,
445
+ start_time: Date.now() + stream.start_time,
446
+ sample_rate: stream.sample_rate,
447
+ timestamp_delta: 0,
448
+ };
449
+ }
450
+ });
451
+
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 = [];
456
+
457
+ this?.log?.debug && this.log.debug('Playback started from "%s" with session ID "%s"', this.host, this.id);
458
+ }
459
+
460
+ #handlePlaybackPacket(payload) {
461
+ // 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
+ }
547
+
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);
552
+ }
553
+ }
554
+
555
+ #handlePlaybackEnd(payload) {
556
+ // 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
+ );
568
+
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);
577
+
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
584
+ }
585
+ }
586
+
587
+ #handleNexusError(payload) {
588
+ // 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);
607
+ }
608
+ }
609
+
610
+ #handleTalkbackBegin(payload) {
611
+ // 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);
631
+ }
632
+
633
+ #handleTalkbackEnd(payload) {
634
+ // 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);
654
+ }
655
+
656
+ #handleNexusData(data) {
657
+ // 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]);
659
+
660
+ while (this.packets.length >= 3) {
661
+ let headerSize = 3;
662
+ let packetType = this.packets.readUInt8(0);
663
+ let packetSize = this.packets.readUInt16BE(1);
664
+
665
+ if (packetType === PacketType.LONG_PLAYBACK_PACKET) {
666
+ headerSize = 5;
667
+ packetSize = this.packets.readUInt32BE(1);
668
+ }
669
+
670
+ if (this.packets.length < headerSize + packetSize) {
671
+ // We dont have enough data in the buffer yet to process the full packet
672
+ // so, exit loop and await more data
673
+ break;
674
+ }
675
+
676
+ let protoBufPayload = new protoBuf(this.packets.slice(headerSize, headerSize + packetSize));
677
+ switch (packetType) {
678
+ case PacketType.PING: {
679
+ break;
680
+ }
681
+
682
+ case PacketType.OK: {
683
+ // 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()) {
686
+ this.#sendMessage(message.type, message.data);
687
+ }
688
+
689
+ // Periodically send PING message to keep stream alive
690
+ this.pingTimer = clearInterval(this.pingTimer);
691
+ this.pingTimer = setInterval(() => {
692
+ this.#sendMessage(PacketType.PING, Buffer.alloc(0));
693
+ }, PINGINTERVAL);
694
+
695
+ // Start processing data
696
+ this.#startNexusData();
697
+ break;
698
+ }
699
+
700
+ case PacketType.ERROR: {
701
+ this.#handleNexusError(protoBufPayload);
702
+ break;
703
+ }
704
+
705
+ case PacketType.PLAYBACK_BEGIN: {
706
+ this.#handlePlaybackBegin(protoBufPayload);
707
+ break;
708
+ }
709
+
710
+ case PacketType.PLAYBACK_END: {
711
+ this.#handlePlaybackEnd(protoBufPayload);
712
+ break;
713
+ }
714
+
715
+ case PacketType.PLAYBACK_PACKET:
716
+ case PacketType.LONG_PLAYBACK_PACKET: {
717
+ this.#handlePlaybackPacket(protoBufPayload);
718
+ break;
719
+ }
720
+
721
+ case PacketType.REDIRECT: {
722
+ this.#handleRedirect(protoBufPayload);
723
+ break;
724
+ }
725
+
726
+ case PacketType.TALKBACK_BEGIN: {
727
+ this.#handleTalkbackBegin(protoBufPayload);
728
+ break;
729
+ }
730
+
731
+ case PacketType.TALKBACK_END: {
732
+ this.#handleTalkbackEnd(protoBufPayload);
733
+ break;
734
+ }
735
+ }
736
+
737
+ // Remove the section of data we've just processed from our pending buffer
738
+ this.packets = this.packets.slice(headerSize + packetSize);
739
+ }
740
+ }
741
+ }