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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +176 -0
- package/README.md +121 -0
- package/config.schema.json +107 -0
- package/dist/HomeKitDevice.js +441 -0
- package/dist/HomeKitHistory.js +2835 -0
- package/dist/camera.js +1276 -0
- package/dist/doorbell.js +122 -0
- package/dist/index.js +35 -0
- package/dist/nexustalk.js +741 -0
- package/dist/protect.js +240 -0
- package/dist/protobuf/google/rpc/status.proto +91 -0
- package/dist/protobuf/google/rpc/stream_body.proto +26 -0
- package/dist/protobuf/google/trait/product/camera.proto +53 -0
- package/dist/protobuf/googlehome/foyer.proto +208 -0
- package/dist/protobuf/nest/messages.proto +8 -0
- package/dist/protobuf/nest/services/apigateway.proto +107 -0
- package/dist/protobuf/nest/trait/audio.proto +7 -0
- package/dist/protobuf/nest/trait/cellular.proto +313 -0
- package/dist/protobuf/nest/trait/debug.proto +37 -0
- package/dist/protobuf/nest/trait/detector.proto +41 -0
- package/dist/protobuf/nest/trait/diagnostics.proto +87 -0
- package/dist/protobuf/nest/trait/firmware.proto +221 -0
- package/dist/protobuf/nest/trait/guest.proto +105 -0
- package/dist/protobuf/nest/trait/history.proto +345 -0
- package/dist/protobuf/nest/trait/humanlibrary.proto +19 -0
- package/dist/protobuf/nest/trait/hvac.proto +1353 -0
- package/dist/protobuf/nest/trait/input.proto +29 -0
- package/dist/protobuf/nest/trait/lighting.proto +61 -0
- package/dist/protobuf/nest/trait/located.proto +193 -0
- package/dist/protobuf/nest/trait/media.proto +68 -0
- package/dist/protobuf/nest/trait/network.proto +352 -0
- package/dist/protobuf/nest/trait/occupancy.proto +373 -0
- package/dist/protobuf/nest/trait/olive.proto +15 -0
- package/dist/protobuf/nest/trait/pairing.proto +85 -0
- package/dist/protobuf/nest/trait/product/camera.proto +283 -0
- package/dist/protobuf/nest/trait/product/detect.proto +67 -0
- package/dist/protobuf/nest/trait/product/doorbell.proto +18 -0
- package/dist/protobuf/nest/trait/product/guard.proto +59 -0
- package/dist/protobuf/nest/trait/product/protect.proto +344 -0
- package/dist/protobuf/nest/trait/promonitoring.proto +14 -0
- package/dist/protobuf/nest/trait/resourcedirectory.proto +32 -0
- package/dist/protobuf/nest/trait/safety.proto +119 -0
- package/dist/protobuf/nest/trait/security.proto +516 -0
- package/dist/protobuf/nest/trait/selftest.proto +78 -0
- package/dist/protobuf/nest/trait/sensor.proto +291 -0
- package/dist/protobuf/nest/trait/service.proto +46 -0
- package/dist/protobuf/nest/trait/structure.proto +85 -0
- package/dist/protobuf/nest/trait/system.proto +51 -0
- package/dist/protobuf/nest/trait/test.proto +15 -0
- package/dist/protobuf/nest/trait/ui.proto +65 -0
- package/dist/protobuf/nest/trait/user.proto +98 -0
- package/dist/protobuf/nest/trait/voiceassistant.proto +30 -0
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +83 -0
- package/dist/protobuf/nestlabs/gateway/v1.proto +273 -0
- package/dist/protobuf/nestlabs/gateway/v2.proto +96 -0
- package/dist/protobuf/nestlabs/history/v1.proto +73 -0
- package/dist/protobuf/root.proto +64 -0
- package/dist/protobuf/wdl-event-importance.proto +11 -0
- package/dist/protobuf/wdl.proto +450 -0
- package/dist/protobuf/weave/common.proto +144 -0
- package/dist/protobuf/weave/trait/audio.proto +12 -0
- package/dist/protobuf/weave/trait/auth.proto +22 -0
- package/dist/protobuf/weave/trait/description.proto +32 -0
- package/dist/protobuf/weave/trait/heartbeat.proto +38 -0
- package/dist/protobuf/weave/trait/locale.proto +20 -0
- package/dist/protobuf/weave/trait/network.proto +24 -0
- package/dist/protobuf/weave/trait/pairing.proto +8 -0
- package/dist/protobuf/weave/trait/peerdevices.proto +18 -0
- package/dist/protobuf/weave/trait/power.proto +86 -0
- package/dist/protobuf/weave/trait/schedule.proto +76 -0
- package/dist/protobuf/weave/trait/security.proto +343 -0
- package/dist/protobuf/weave/trait/telemetry/tunnel.proto +37 -0
- package/dist/protobuf/weave/trait/time.proto +16 -0
- package/dist/res/Nest_camera_connecting.h264 +0 -0
- package/dist/res/Nest_camera_connecting.jpg +0 -0
- package/dist/res/Nest_camera_off.h264 +0 -0
- package/dist/res/Nest_camera_off.jpg +0 -0
- package/dist/res/Nest_camera_offline.h264 +0 -0
- package/dist/res/Nest_camera_offline.jpg +0 -0
- package/dist/res/Nest_camera_transfer.jpg +0 -0
- package/dist/streamer.js +344 -0
- package/dist/system.js +3112 -0
- package/dist/tempsensor.js +99 -0
- package/dist/thermostat.js +1026 -0
- package/dist/weather.js +205 -0
- package/dist/webrtc.js +55 -0
- 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
|
+
}
|