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