homebridge-nest-accfactory 0.2.11 → 0.3.1
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 +28 -0
- package/README.md +14 -7
- package/config.schema.json +118 -0
- package/dist/HomeKitDevice.js +203 -77
- package/dist/HomeKitHistory.js +1 -1
- package/dist/config.js +207 -0
- package/dist/devices.js +118 -0
- package/dist/index.js +2 -1
- package/dist/nexustalk.js +46 -48
- package/dist/{camera.js → plugins/camera.js} +216 -241
- package/dist/{doorbell.js → plugins/doorbell.js} +32 -30
- package/dist/plugins/floodlight.js +91 -0
- package/dist/plugins/heatlink.js +17 -0
- package/dist/{protect.js → plugins/protect.js} +26 -43
- package/dist/{tempsensor.js → plugins/tempsensor.js} +15 -19
- package/dist/{thermostat.js → plugins/thermostat.js} +426 -383
- package/dist/{weather.js → plugins/weather.js} +26 -60
- package/dist/protobuf/nest/services/apigateway.proto +31 -1
- package/dist/protobuf/nest/trait/firmware.proto +207 -89
- package/dist/protobuf/nest/trait/hvac.proto +1052 -312
- package/dist/protobuf/nest/trait/located.proto +51 -8
- package/dist/protobuf/nest/trait/network.proto +366 -36
- package/dist/protobuf/nest/trait/occupancy.proto +145 -17
- package/dist/protobuf/nest/trait/product/protect.proto +57 -43
- package/dist/protobuf/nest/trait/resourcedirectory.proto +8 -0
- package/dist/protobuf/nest/trait/sensor.proto +7 -1
- package/dist/protobuf/nest/trait/service.proto +3 -1
- package/dist/protobuf/nest/trait/structure.proto +60 -14
- package/dist/protobuf/nest/trait/ui.proto +41 -1
- package/dist/protobuf/nest/trait/user.proto +6 -1
- package/dist/protobuf/nest/trait/voiceassistant.proto +2 -1
- package/dist/protobuf/nestlabs/eventingapi/v1.proto +20 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/protobuf/wdl.proto +18 -2
- package/dist/protobuf/weave/common.proto +2 -1
- package/dist/protobuf/weave/trait/heartbeat.proto +41 -1
- package/dist/protobuf/weave/trait/power.proto +1 -0
- package/dist/protobuf/weave/trait/security.proto +10 -1
- package/dist/streamer.js +74 -78
- package/dist/system.js +1213 -1264
- package/dist/webrtc.js +39 -34
- package/package.json +11 -11
- package/dist/floodlight.js +0 -97
package/dist/config.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Configuration validation and processing
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 2025.06.15
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define nodejs module requirements
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import crypto from 'node:crypto';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
import child_process from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
// Define constants
|
|
16
|
+
const FFMPEG_VERSION = '6.0.0';
|
|
17
|
+
const ACCOUNT_TYPE = {
|
|
18
|
+
NEST: 'Nest',
|
|
19
|
+
GOOGLE: 'Google',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function processConfig(config, log) {
|
|
23
|
+
let options = (config.options = typeof config?.options === 'object' ? config.options : {});
|
|
24
|
+
|
|
25
|
+
options.eveHistory = config.options?.eveHistory === true;
|
|
26
|
+
options.weather = config.options?.weather === true;
|
|
27
|
+
options.hksv = config.options?.hksv === true;
|
|
28
|
+
options.exclude = config.options?.exclude === true;
|
|
29
|
+
|
|
30
|
+
options.elevation =
|
|
31
|
+
isNaN(config.options?.elevation) === false && Number(config.options.elevation) >= 0 && Number(config.options.elevation) <= 8848
|
|
32
|
+
? Number(config.options.elevation)
|
|
33
|
+
: 0;
|
|
34
|
+
|
|
35
|
+
// Controls what APIs we use, default is to use both Nest and protobuf APIs
|
|
36
|
+
options.useNestAPI = config.options?.useNestAPI === true || config.options?.useNestAPI === undefined;
|
|
37
|
+
options.useGoogleAPI = config.options?.useGoogleAPI === true || config.options?.useGoogleAPI === undefined;
|
|
38
|
+
|
|
39
|
+
// Get configuration for max number of concurrent 'live view' streams. For HomeKit Secure Video, this will always be 1
|
|
40
|
+
options.maxStreams = isNaN(config.options?.maxStreams) === false ? Number(config.options.maxStreams) : 2;
|
|
41
|
+
|
|
42
|
+
// Check if a ffmpeg binary exist via a specific path in configuration OR /usr/local/bin
|
|
43
|
+
options.ffmpeg = {};
|
|
44
|
+
options.ffmpeg.debug = config.options?.ffmpegDebug === true;
|
|
45
|
+
options.ffmpeg.binary = path.resolve(
|
|
46
|
+
typeof config.options?.ffmpegPath === 'string' && config.options.ffmpegPath !== '' ? config.options.ffmpegPath : '/usr/local/bin',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// If the path doesn't include 'ffmpeg' on the end, we'll add it here
|
|
50
|
+
if (options.ffmpeg.binary.endsWith('/ffmpeg') === false) {
|
|
51
|
+
options.ffmpeg.binary += '/ffmpeg';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
options.ffmpeg.version = undefined;
|
|
55
|
+
options.ffmpeg.libspeex = false;
|
|
56
|
+
options.ffmpeg.libopus = false;
|
|
57
|
+
options.ffmpeg.libx264 = false;
|
|
58
|
+
options.ffmpeg.libfdk_aac = false;
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(options.ffmpeg.binary) === false) {
|
|
61
|
+
// If we flag ffmpegPath as undefined, no video streaming/record support enabled for camers/doorbells
|
|
62
|
+
log?.warn?.('Specified ffmpeg binary "%s" was not found', options.ffmpeg.binary);
|
|
63
|
+
log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
|
|
64
|
+
options.ffmpeg.binary = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fs.existsSync(options.ffmpeg.binary) === true) {
|
|
68
|
+
let ffmpegProcess = child_process.spawnSync(options.ffmpeg.binary, ['-version'], {
|
|
69
|
+
env: process.env,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (ffmpegProcess.stdout !== null) {
|
|
73
|
+
let stdout = ffmpegProcess.stdout.toString();
|
|
74
|
+
|
|
75
|
+
// Determine what libraries ffmpeg is compiled with
|
|
76
|
+
options.ffmpeg.version = stdout.match(/(?:ffmpeg version:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)(.*?)/gim)?.[0];
|
|
77
|
+
options.ffmpeg.libspeex = stdout.includes('--enable-libspeex') === true;
|
|
78
|
+
options.ffmpeg.libopus = stdout.includes('--enable-libopus') === true;
|
|
79
|
+
options.ffmpeg.libx264 = stdout.includes('--enable-libx264') === true;
|
|
80
|
+
options.ffmpeg.libfdk_aac = stdout.includes('--enable-libfdk-aac') === true;
|
|
81
|
+
|
|
82
|
+
let versionTooOld =
|
|
83
|
+
options.ffmpeg.version?.localeCompare(FFMPEG_VERSION, undefined, {
|
|
84
|
+
numeric: true,
|
|
85
|
+
sensitivity: 'case',
|
|
86
|
+
caseFirst: 'upper',
|
|
87
|
+
}) === -1;
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
versionTooOld ||
|
|
91
|
+
options.ffmpeg.libspeex === false ||
|
|
92
|
+
options.ffmpeg.libopus === false ||
|
|
93
|
+
options.ffmpeg.libx264 === false ||
|
|
94
|
+
options.ffmpeg.libfdk_aac === false
|
|
95
|
+
) {
|
|
96
|
+
log?.warn?.('ffmpeg binary "%s" does not meet the minimum support requirements', options.ffmpeg.binary);
|
|
97
|
+
|
|
98
|
+
if (versionTooOld) {
|
|
99
|
+
log?.warn?.('Minimum binary version is "%s", however the installed version is "%s"', FFMPEG_VERSION, options.ffmpeg.version);
|
|
100
|
+
log?.warn?.('Stream video/recording from camera/doorbells will be unavailable');
|
|
101
|
+
options.ffmpeg.binary = undefined; // No ffmpeg since below min version
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!options.ffmpeg.libspeex && options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac) {
|
|
105
|
+
log?.warn?.('Missing libspeex in ffmpeg binary, talkback on certain camera/doorbells will be unavailable');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
|
|
109
|
+
log?.warn?.('Missing libfdk_aac and libopus in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.ffmpeg.libx264 && !options.ffmpeg.libfdk_aac) {
|
|
113
|
+
log?.warn?.('Missing libfdk_aac in ffmpeg binary, audio from camera/doorbells will be unavailable');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (options.ffmpeg.libx264 && options.ffmpeg.libfdk_aac && !options.ffmpeg.libopus) {
|
|
117
|
+
log?.warn?.('Missing libopus in ffmpeg binary, audio (including talkback) from certain camera/doorbells will be unavailable');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!options.ffmpeg.libx264) {
|
|
121
|
+
log?.warn?.('Missing libx264 in ffmpeg binary, stream video/recording from camera/doorbells will be unavailable');
|
|
122
|
+
options.ffmpeg.binary = undefined; // No ffmpeg since we do not have all the required libraries
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (options.ffmpeg.binary !== undefined) {
|
|
129
|
+
log?.success?.('Found valid ffmpeg binary in %s', options.ffmpeg.binary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Process per device configuration(s)
|
|
133
|
+
if (config?.devices === undefined) {
|
|
134
|
+
config.devices = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (config?.devices !== undefined && Array.isArray(config.devices) === false) {
|
|
138
|
+
// If the devices section is a JSON oject keyed by the devices serial number, convert to devices array object
|
|
139
|
+
let newDeviceArray = [];
|
|
140
|
+
for (const [serialNumber, props] of Object.entries(config.devices)) {
|
|
141
|
+
newDeviceArray.push({
|
|
142
|
+
serialNumber,
|
|
143
|
+
...props,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
config.devices = newDeviceArray;
|
|
147
|
+
|
|
148
|
+
// Alert user to changed configuration for them to update config
|
|
149
|
+
log?.warn?.('');
|
|
150
|
+
log?.warn?.('NOTICE');
|
|
151
|
+
log?.warn?.('> The per device configuration contains legacy options. Please review the readme at the link below');
|
|
152
|
+
log?.warn?.('> Consider updating your configuration file as the mapping from legacy to current per device configuration maybe removed');
|
|
153
|
+
log?.warn?.('> https://github.com/n0rt0nthec4t/homebridge-nest-accfactory/blob/main/src/README.md');
|
|
154
|
+
log?.warn?.('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return config;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildConnections(config) {
|
|
161
|
+
let connections = {};
|
|
162
|
+
|
|
163
|
+
Object.keys(config).forEach((key) => {
|
|
164
|
+
let section = config[key];
|
|
165
|
+
|
|
166
|
+
if (typeof section?.access_token === 'string' && section.access_token !== '') {
|
|
167
|
+
let fieldTest = section?.fieldTest === true;
|
|
168
|
+
connections[crypto.randomUUID()] = {
|
|
169
|
+
name: key,
|
|
170
|
+
type: ACCOUNT_TYPE.NEST,
|
|
171
|
+
authorised: false,
|
|
172
|
+
access_token: section.access_token,
|
|
173
|
+
fieldTest,
|
|
174
|
+
referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
|
|
175
|
+
restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
|
|
176
|
+
cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
177
|
+
protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
typeof section?.issuetoken === 'string' &&
|
|
183
|
+
section.issuetoken !== '' &&
|
|
184
|
+
typeof section?.cookie === 'string' &&
|
|
185
|
+
section.cookie !== ''
|
|
186
|
+
) {
|
|
187
|
+
let fieldTest = section?.fieldTest === true;
|
|
188
|
+
connections[crypto.randomUUID()] = {
|
|
189
|
+
name: key,
|
|
190
|
+
type: ACCOUNT_TYPE.GOOGLE,
|
|
191
|
+
authorised: false,
|
|
192
|
+
issuetoken: section.issuetoken,
|
|
193
|
+
cookie: section.cookie,
|
|
194
|
+
fieldTest,
|
|
195
|
+
referer: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
|
|
196
|
+
restAPIHost: fieldTest ? 'home.ft.nest.com' : 'home.nest.com',
|
|
197
|
+
cameraAPIHost: fieldTest ? 'camera.home.ft.nest.com' : 'camera.home.nest.com',
|
|
198
|
+
protobufAPIHost: fieldTest ? 'grpc-web.ft.nest.com' : 'grpc-web.production.nest.com',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return connections;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Define exports
|
|
207
|
+
export { ACCOUNT_TYPE, processConfig, buildConnections };
|
package/dist/devices.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Device support loader
|
|
2
|
+
// Part of homebridge-nest-accfactory
|
|
3
|
+
//
|
|
4
|
+
// Code version 2025.06.15
|
|
5
|
+
// Mark Hulskamp
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Define nodejs module requirements
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import url from 'node:url';
|
|
12
|
+
|
|
13
|
+
// Import our modules
|
|
14
|
+
import HomeKitDevice from './HomeKitDevice.js';
|
|
15
|
+
|
|
16
|
+
// Define constants
|
|
17
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
18
|
+
const DEVICE_TYPE = Object.freeze({
|
|
19
|
+
THERMOSTAT: 'Thermostat',
|
|
20
|
+
TEMPSENSOR: 'TemperatureSensor',
|
|
21
|
+
SMOKESENSOR: 'Protect',
|
|
22
|
+
CAMERA: 'Camera',
|
|
23
|
+
DOORBELL: 'Doorbell',
|
|
24
|
+
FLOODLIGHT: 'FloodlightCamera',
|
|
25
|
+
WEATHER: 'Weather',
|
|
26
|
+
HEATLINK: 'Heatlink',
|
|
27
|
+
LOCK: 'Lock',
|
|
28
|
+
ALARM: 'Alarm',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function loadDeviceModules(log, pluginDir = '') {
|
|
32
|
+
let baseDir = path.join(__dirname, pluginDir);
|
|
33
|
+
let deviceMap = new Map();
|
|
34
|
+
let files = (await fs.readdir(baseDir)).sort();
|
|
35
|
+
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
if (file.endsWith('.js') === false) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
let module = await import(url.pathToFileURL(path.join(baseDir, file)).href);
|
|
43
|
+
let exportsToCheck = Object.values(module);
|
|
44
|
+
|
|
45
|
+
for (const exported of exportsToCheck) {
|
|
46
|
+
if (typeof exported !== 'function') {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let proto = Object.getPrototypeOf(exported);
|
|
51
|
+
while (proto !== undefined && proto.name !== '') {
|
|
52
|
+
if (proto === HomeKitDevice) {
|
|
53
|
+
if (
|
|
54
|
+
typeof exported.TYPE !== 'string' ||
|
|
55
|
+
exported.TYPE === '' ||
|
|
56
|
+
typeof exported.VERSION !== 'string' ||
|
|
57
|
+
exported.VERSION === ''
|
|
58
|
+
) {
|
|
59
|
+
log?.warn?.('Skipping device module %s (missing TYPE or VERSION)', file);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (deviceMap.has(exported.TYPE) === false) {
|
|
64
|
+
deviceMap.set(exported.TYPE, exported);
|
|
65
|
+
log?.info?.('Loaded device module "%s" (v%s)', exported.TYPE, exported.VERSION);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
proto = Object.getPrototypeOf(proto);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log?.warn?.('Failed to load device support file "%s": %s', file, error.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return deviceMap;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getDeviceHKCategory(type) {
|
|
83
|
+
let category = 1; // Categories.OTHER
|
|
84
|
+
|
|
85
|
+
if (type === DEVICE_TYPE.LOCK) {
|
|
86
|
+
category = 6; // Categories.DOOR_LOCK
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === DEVICE_TYPE.THERMOSTAT) {
|
|
90
|
+
category = 9; // Categories.THERMOSTAT
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
type === DEVICE_TYPE.TEMPSENSOR ||
|
|
95
|
+
type === DEVICE_TYPE.HEATLINK ||
|
|
96
|
+
type === DEVICE_TYPE.SMOKESENSOR ||
|
|
97
|
+
type === DEVICE_TYPE.WEATHER
|
|
98
|
+
) {
|
|
99
|
+
category = 10; // Categories.SENSOR
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (type === DEVICE_TYPE.ALARM) {
|
|
103
|
+
category = 11; // Categories.SECURITY_SYSTEM
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (type === DEVICE_TYPE.CAMERA || type === DEVICE_TYPE.FLOODLIGHT) {
|
|
107
|
+
category = 17; // Categories.IP_CAMERA
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (type === DEVICE_TYPE.DOORBELL) {
|
|
111
|
+
category = 18; // Categories.VIDEO_DOORBELL
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return category;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Define exports
|
|
118
|
+
export { DEVICE_TYPE, loadDeviceModules, getDeviceHKCategory };
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
// Nest Temp Sensors (1st gen)
|
|
11
11
|
// Nest Cameras (Cam Indoor, IQ Indoor, Outdoor, IQ Outdoor, Cam with Floodlight)
|
|
12
12
|
// Nest Doorbells (wired 1st gen)
|
|
13
|
+
// Nest HeatLink
|
|
13
14
|
//
|
|
14
15
|
// The accessory supports authentication to Nest/Google using either a Nest account OR Google (migrated Nest account) account.
|
|
15
16
|
// "preliminary" support for using FieldTest account types also.
|
|
16
17
|
//
|
|
17
18
|
// Supports both Nest REST and Protobuf APIs for communication
|
|
18
19
|
//
|
|
19
|
-
// Code version
|
|
20
|
+
// Code version 2025.06.05
|
|
20
21
|
// Mark Hulskamp
|
|
21
22
|
'use strict';
|
|
22
23
|
|
package/dist/nexustalk.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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
7
|
//
|
|
8
|
-
// Code version
|
|
8
|
+
// Code version 2025.06.15
|
|
9
9
|
// Mark Hulskamp
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
@@ -25,11 +25,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
import Streamer from './streamer.js';
|
|
26
26
|
|
|
27
27
|
// Define constants
|
|
28
|
-
const
|
|
29
|
-
const
|
|
28
|
+
const PING_INTERVAL = 15000; // Ping interval to nexus server while stream active
|
|
29
|
+
const USER_AGENT = 'Nest/5.78.0 (iOScom.nestlabs.jasper.release) os=18.0'; // User Agent string
|
|
30
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Make a defined for JS __dirname
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const PACKET_TYPE = {
|
|
33
33
|
PING: 1,
|
|
34
34
|
HELLO: 100,
|
|
35
35
|
PING_CAMERA: 101,
|
|
@@ -56,7 +56,7 @@ const PacketType = {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// Blank audio in AAC format, mono channel @48000
|
|
59
|
-
const
|
|
59
|
+
const AAC_MONO_48000_BLANK = Buffer.from([
|
|
60
60
|
0xff, 0xf1, 0x4c, 0x40, 0x03, 0x9f, 0xfc, 0xde, 0x02, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x39, 0x2e, 0x31, 0x38, 0x2e, 0x31, 0x30, 0x30,
|
|
61
61
|
0x00, 0x02, 0x30, 0x40, 0x0e,
|
|
62
62
|
]);
|
|
@@ -68,7 +68,7 @@ export default class NexusTalk extends Streamer {
|
|
|
68
68
|
pingTimer = undefined; // Timer object for ping interval
|
|
69
69
|
stalledTimer = undefined; // Timer object for no received data
|
|
70
70
|
host = ''; // Host to connect to or connected too
|
|
71
|
-
blankAudio =
|
|
71
|
+
blankAudio = AAC_MONO_48000_BLANK;
|
|
72
72
|
video = {}; // Video stream details once connected
|
|
73
73
|
audio = {}; // Audio stream details once connected
|
|
74
74
|
|
|
@@ -123,11 +123,11 @@ export default class NexusTalk extends Streamer {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
this.connected = false; // Starting connection
|
|
126
|
-
this?.log?.debug
|
|
126
|
+
this?.log?.debug?.('Connection started to "%s"', host);
|
|
127
127
|
|
|
128
128
|
this.#socket = tls.connect({ host: host, port: 1443 }, () => {
|
|
129
129
|
// Opened connection to Nexus server, so now need to authenticate ourselves
|
|
130
|
-
this?.log?.debug
|
|
130
|
+
this?.log?.debug?.('Connection established to "%s"', host);
|
|
131
131
|
|
|
132
132
|
this.#socket.setKeepAlive(true); // Keep socket connection alive
|
|
133
133
|
this.host = host; // update internal host name since we've connected
|
|
@@ -144,7 +144,7 @@ export default class NexusTalk extends Streamer {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
this.#socket.on('close', (hadError) => {
|
|
147
|
-
this?.log?.debug
|
|
147
|
+
this?.log?.debug?.('Connection closed to "%s"', host);
|
|
148
148
|
|
|
149
149
|
clearInterval(this.pingTimer);
|
|
150
150
|
clearTimeout(this.stalledTimer);
|
|
@@ -199,7 +199,7 @@ export default class NexusTalk extends Streamer {
|
|
|
199
199
|
|
|
200
200
|
if (this.host !== deviceData.streaming_host) {
|
|
201
201
|
this.host = deviceData.streaming_host;
|
|
202
|
-
this?.log?.debug
|
|
202
|
+
this?.log?.debug?.('New host has been requested for connection. Host requested is "%s"', this.host);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
// Let our parent handle the remaining updates
|
|
@@ -219,7 +219,7 @@ export default class NexusTalk extends Streamer {
|
|
|
219
219
|
sampleRate: 16000,
|
|
220
220
|
}),
|
|
221
221
|
).finish();
|
|
222
|
-
this.#sendMessage(
|
|
222
|
+
this.#sendMessage(PACKET_TYPE.AUDIO_PAYLOAD, encodedData);
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -248,7 +248,7 @@ export default class NexusTalk extends Streamer {
|
|
|
248
248
|
profileNotFoundAction: 'REDIRECT',
|
|
249
249
|
}),
|
|
250
250
|
).finish();
|
|
251
|
-
this.#sendMessage(
|
|
251
|
+
this.#sendMessage(PACKET_TYPE.START_PLAYBACK, encodedData);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -261,13 +261,13 @@ export default class NexusTalk extends Streamer {
|
|
|
261
261
|
sessionId: this.#id,
|
|
262
262
|
}),
|
|
263
263
|
).finish();
|
|
264
|
-
this.#sendMessage(
|
|
264
|
+
this.#sendMessage(PACKET_TYPE.STOP_PLAYBACK, encodedData);
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
#sendMessage(type, data) {
|
|
270
|
-
if (this.#socket?.readyState !== 'open' || (type !==
|
|
270
|
+
if (this.#socket?.readyState !== 'open' || (type !== PACKET_TYPE.HELLO && this.#authorised === false)) {
|
|
271
271
|
// We're not connect and/or authorised yet, so 'cache' message for processing once this occurs
|
|
272
272
|
this.#messages.push({ type: type, data: data });
|
|
273
273
|
return;
|
|
@@ -275,11 +275,11 @@ export default class NexusTalk extends Streamer {
|
|
|
275
275
|
|
|
276
276
|
// Create nexusTalk message header
|
|
277
277
|
let header = Buffer.alloc(3);
|
|
278
|
-
if (type !==
|
|
278
|
+
if (type !== PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
279
279
|
header.writeUInt8(type, 0);
|
|
280
280
|
header.writeUInt16BE(data.length, 1);
|
|
281
281
|
}
|
|
282
|
-
if (type ===
|
|
282
|
+
if (type === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
283
283
|
header = Buffer.alloc(5);
|
|
284
284
|
header.writeUInt8(type, 0);
|
|
285
285
|
header.writeUInt32BE(data.length, 1);
|
|
@@ -308,28 +308,28 @@ export default class NexusTalk extends Streamer {
|
|
|
308
308
|
|
|
309
309
|
if (reauthorise === true && authoriseRequest !== null) {
|
|
310
310
|
// Request to re-authorise only
|
|
311
|
-
this?.log?.debug
|
|
312
|
-
this.#sendMessage(
|
|
311
|
+
this?.log?.debug?.('Re-authentication requested to "%s"', this.host);
|
|
312
|
+
this.#sendMessage(PACKET_TYPE.AUTHORIZE_REQUEST, authoriseRequest);
|
|
313
313
|
}
|
|
314
314
|
|
|
315
315
|
if (reauthorise === false && authoriseRequest !== null) {
|
|
316
316
|
// This isn't a re-authorise request, so perform 'Hello' packet
|
|
317
317
|
let TraitMap = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.Hello');
|
|
318
318
|
if (TraitMap !== null) {
|
|
319
|
-
this?.log?.debug
|
|
319
|
+
this?.log?.debug?.('Performing authentication to "%s"', this.host);
|
|
320
320
|
|
|
321
321
|
let encodedData = TraitMap.encode(
|
|
322
322
|
TraitMap.fromObject({
|
|
323
323
|
protocolVersion: 'VERSION_3',
|
|
324
324
|
uuid: this.uuid.split(/[._]+/)[1],
|
|
325
325
|
requireConnectedCamera: false,
|
|
326
|
-
|
|
326
|
+
USER_AGENT: USER_AGENT,
|
|
327
327
|
deviceId: crypto.randomUUID(),
|
|
328
328
|
clientType: 'IOS',
|
|
329
329
|
authoriseRequest: authoriseRequest,
|
|
330
330
|
}),
|
|
331
331
|
).finish();
|
|
332
|
-
this.#sendMessage(
|
|
332
|
+
this.#sendMessage(PACKET_TYPE.HELLO, encodedData);
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
}
|
|
@@ -350,7 +350,7 @@ export default class NexusTalk extends Streamer {
|
|
|
350
350
|
return;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
this?.log?.debug
|
|
353
|
+
this?.log?.debug?.('Redirect requested from "%s" to "%s"', this.host, redirectToHost);
|
|
354
354
|
|
|
355
355
|
// Setup listener for socket close event. Once socket is closed, we'll perform the redirect
|
|
356
356
|
this.#socket &&
|
|
@@ -389,7 +389,7 @@ export default class NexusTalk extends Streamer {
|
|
|
389
389
|
this.#packets = [];
|
|
390
390
|
this.#messages = [];
|
|
391
391
|
|
|
392
|
-
this?.log?.debug
|
|
392
|
+
this?.log?.debug?.('Playback started from "%s" with session ID "%s"', this.host, this.#id);
|
|
393
393
|
}
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -403,12 +403,11 @@ export default class NexusTalk extends Streamer {
|
|
|
403
403
|
// <-- testing to see how often this occurs first
|
|
404
404
|
clearTimeout(this.stalledTimer);
|
|
405
405
|
this.stalledTimer = setTimeout(() => {
|
|
406
|
-
this?.log?.debug
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
);
|
|
406
|
+
this?.log?.debug?.(
|
|
407
|
+
'We have not received any data from nexus in the past "%s" seconds for uuid "%s". Attempting restart',
|
|
408
|
+
10,
|
|
409
|
+
this.uuid,
|
|
410
|
+
);
|
|
412
411
|
|
|
413
412
|
// Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
|
|
414
413
|
this.#socket &&
|
|
@@ -437,13 +436,12 @@ export default class NexusTalk extends Streamer {
|
|
|
437
436
|
|
|
438
437
|
if (this.#id !== undefined && decodedMessage.reason === 'USER_ENDED_SESSION') {
|
|
439
438
|
// Normal playback ended ie: when we stopped playback
|
|
440
|
-
this?.log?.debug
|
|
439
|
+
this?.log?.debug?.('Playback ended on "%s"', this.host);
|
|
441
440
|
}
|
|
442
441
|
|
|
443
442
|
if (decodedMessage.reason !== 'USER_ENDED_SESSION') {
|
|
444
443
|
// Error during playback, so we'll attempt to restart by reconnection to host
|
|
445
|
-
this?.log?.debug
|
|
446
|
-
this.log.debug('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
|
|
444
|
+
this?.log?.debug?.('Playback ended on "%s" with error "%s". Attempting reconnection', this.host, decodedMessage.reason);
|
|
447
445
|
|
|
448
446
|
// Setup listener for socket close event. Once socket is closed, we'll perform the re-connection
|
|
449
447
|
this.#socket &&
|
|
@@ -464,7 +462,7 @@ export default class NexusTalk extends Streamer {
|
|
|
464
462
|
this.#Authenticate(true); // Update authorisation only
|
|
465
463
|
} else {
|
|
466
464
|
// NexusStreamer Error, packet.message contains the message
|
|
467
|
-
this?.log?.debug
|
|
465
|
+
this?.log?.debug?.('Error', decodedMessage.message);
|
|
468
466
|
}
|
|
469
467
|
}
|
|
470
468
|
}
|
|
@@ -474,7 +472,7 @@ export default class NexusTalk extends Streamer {
|
|
|
474
472
|
if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
|
|
475
473
|
//let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackBegin').decode(payload).toJSON();
|
|
476
474
|
this.audio.talking = true;
|
|
477
|
-
this?.log?.debug
|
|
475
|
+
this?.log?.debug?.('Talking started on uuid "%s"', this.uuid);
|
|
478
476
|
}
|
|
479
477
|
}
|
|
480
478
|
|
|
@@ -483,7 +481,7 @@ export default class NexusTalk extends Streamer {
|
|
|
483
481
|
if (typeof payload === 'object' && this.#protobufNexusTalk !== undefined) {
|
|
484
482
|
//let decodedMessage = this.#protobufNexusTalk.lookup('nest.nexustalk.v1.TalkbackEnd').decode(payload).toJSON();
|
|
485
483
|
this.audio.talking = false;
|
|
486
|
-
this?.log?.debug
|
|
484
|
+
this?.log?.debug?.('Talking ended on uuid "%s"', this.uuid);
|
|
487
485
|
}
|
|
488
486
|
}
|
|
489
487
|
|
|
@@ -496,7 +494,7 @@ export default class NexusTalk extends Streamer {
|
|
|
496
494
|
let packetType = this.#packets.readUInt8(0);
|
|
497
495
|
let packetSize = this.#packets.readUInt16BE(1);
|
|
498
496
|
|
|
499
|
-
if (packetType ===
|
|
497
|
+
if (packetType === PACKET_TYPE.LONG_PLAYBACK_PACKET) {
|
|
500
498
|
headerSize = 5;
|
|
501
499
|
packetSize = this.#packets.readUInt32BE(1);
|
|
502
500
|
}
|
|
@@ -511,11 +509,11 @@ export default class NexusTalk extends Streamer {
|
|
|
511
509
|
this.#packets = this.#packets.subarray(headerSize + packetSize);
|
|
512
510
|
|
|
513
511
|
switch (packetType) {
|
|
514
|
-
case
|
|
512
|
+
case PACKET_TYPE.PING: {
|
|
515
513
|
break;
|
|
516
514
|
}
|
|
517
515
|
|
|
518
|
-
case
|
|
516
|
+
case PACKET_TYPE.OK: {
|
|
519
517
|
// process any pending messages we have stored
|
|
520
518
|
this.#authorised = true; // OK message, means we're connected and authorised to Nexus
|
|
521
519
|
for (let message = this.#messages.shift(); message; message = this.#messages.shift()) {
|
|
@@ -525,46 +523,46 @@ export default class NexusTalk extends Streamer {
|
|
|
525
523
|
// Periodically send PING message to keep stream alive
|
|
526
524
|
clearInterval(this.pingTimer);
|
|
527
525
|
this.pingTimer = setInterval(() => {
|
|
528
|
-
this.#sendMessage(
|
|
529
|
-
},
|
|
526
|
+
this.#sendMessage(PACKET_TYPE.PING, Buffer.alloc(0));
|
|
527
|
+
}, PING_INTERVAL);
|
|
530
528
|
|
|
531
529
|
// Start processing data
|
|
532
530
|
this.#startNexusData();
|
|
533
531
|
break;
|
|
534
532
|
}
|
|
535
533
|
|
|
536
|
-
case
|
|
534
|
+
case PACKET_TYPE.ERROR: {
|
|
537
535
|
this.#handleNexusError(protoBufPayload);
|
|
538
536
|
break;
|
|
539
537
|
}
|
|
540
538
|
|
|
541
|
-
case
|
|
539
|
+
case PACKET_TYPE.PLAYBACK_BEGIN: {
|
|
542
540
|
this.#handlePlaybackBegin(protoBufPayload);
|
|
543
541
|
break;
|
|
544
542
|
}
|
|
545
543
|
|
|
546
|
-
case
|
|
544
|
+
case PACKET_TYPE.PLAYBACK_END: {
|
|
547
545
|
this.#handlePlaybackEnd(protoBufPayload);
|
|
548
546
|
break;
|
|
549
547
|
}
|
|
550
548
|
|
|
551
|
-
case
|
|
552
|
-
case
|
|
549
|
+
case PACKET_TYPE.PLAYBACK_PACKET:
|
|
550
|
+
case PACKET_TYPE.LONG_PLAYBACK_PACKET: {
|
|
553
551
|
this.#handlePlaybackPacket(protoBufPayload);
|
|
554
552
|
break;
|
|
555
553
|
}
|
|
556
554
|
|
|
557
|
-
case
|
|
555
|
+
case PACKET_TYPE.REDIRECT: {
|
|
558
556
|
this.#handleRedirect(protoBufPayload);
|
|
559
557
|
break;
|
|
560
558
|
}
|
|
561
559
|
|
|
562
|
-
case
|
|
560
|
+
case PACKET_TYPE.TALKBACK_BEGIN: {
|
|
563
561
|
this.#handleTalkbackBegin(protoBufPayload);
|
|
564
562
|
break;
|
|
565
563
|
}
|
|
566
564
|
|
|
567
|
-
case
|
|
565
|
+
case PACKET_TYPE.TALKBACK_END: {
|
|
568
566
|
this.#handleTalkbackEnd(protoBufPayload);
|
|
569
567
|
break;
|
|
570
568
|
}
|