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