homebridge-nest-accfactory 0.3.0 → 0.3.2
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 +31 -0
- package/README.md +31 -25
- package/config.schema.json +46 -22
- package/dist/HomeKitDevice.js +523 -281
- package/dist/HomeKitHistory.js +357 -341
- package/dist/config.js +69 -87
- package/dist/consts.js +160 -0
- package/dist/devices.js +40 -48
- package/dist/ffmpeg.js +297 -0
- package/dist/index.js +3 -3
- package/dist/nexustalk.js +182 -149
- package/dist/plugins/camera.js +1164 -933
- package/dist/plugins/doorbell.js +26 -32
- package/dist/plugins/floodlight.js +11 -24
- package/dist/plugins/heatlink.js +411 -5
- package/dist/plugins/lock.js +309 -0
- package/dist/plugins/protect.js +240 -71
- package/dist/plugins/tempsensor.js +159 -35
- package/dist/plugins/thermostat.js +891 -455
- package/dist/plugins/weather.js +128 -33
- package/dist/protobuf/nest/services/apigateway.proto +1 -1
- package/dist/protobuf/nestlabs/gateway/v2.proto +1 -1
- package/dist/protobuf/root.proto +1 -0
- package/dist/rtpmuxer.js +186 -0
- package/dist/streamer.js +490 -248
- package/dist/system.js +1741 -2868
- package/dist/utils.js +327 -0
- package/dist/webrtc.js +358 -229
- package/package.json +19 -16
package/dist/plugins/weather.js
CHANGED
|
@@ -6,22 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
// Define our modules
|
|
8
8
|
import HomeKitDevice from '../HomeKitDevice.js';
|
|
9
|
+
import { processCommonData, adjustTemperature, crc24 } from '../utils.js';
|
|
10
|
+
|
|
11
|
+
// Define constants
|
|
12
|
+
import { DATA_SOURCE, DEVICE_TYPE, NESTLABS_MAC_PREFIX } from '../consts.js';
|
|
9
13
|
|
|
10
14
|
export default class NestWeather extends HomeKitDevice {
|
|
11
15
|
static TYPE = 'Weather';
|
|
12
|
-
static VERSION = '2025.
|
|
16
|
+
static VERSION = '2025.08.04'; // Code version
|
|
13
17
|
|
|
14
18
|
batteryService = undefined;
|
|
15
19
|
airPressureService = undefined;
|
|
16
20
|
temperatureService = undefined;
|
|
17
21
|
humidityService = undefined;
|
|
18
22
|
|
|
19
|
-
constructor(accessory, api, log, eventEmitter, deviceData) {
|
|
20
|
-
super(accessory, api, log, eventEmitter, deviceData);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
23
|
// Class functions
|
|
24
|
-
|
|
24
|
+
onAdd() {
|
|
25
25
|
// Setup temperature service if not already present on the accessory
|
|
26
26
|
this.temperatureService = this.addHKService(this.hap.Service.TemperatureSensor, '', 1);
|
|
27
27
|
this.temperatureService.setPrimaryService();
|
|
@@ -29,13 +29,14 @@ export default class NestWeather extends HomeKitDevice {
|
|
|
29
29
|
// Setup humidity service if not already present on the accessory
|
|
30
30
|
this.humidityService = this.addHKService(this.hap.Service.HumiditySensor, '', 1);
|
|
31
31
|
|
|
32
|
-
// Setup battery service if not already present on the accessory
|
|
32
|
+
// Setup battery service if not already present on the accessory (required for EveHome support)
|
|
33
33
|
this.batteryService = this.addHKService(this.hap.Service.Battery, '', 1);
|
|
34
34
|
this.batteryService.setHiddenService(true);
|
|
35
35
|
|
|
36
36
|
// Add custom weather service and characteristics if they have been defined
|
|
37
37
|
if (this.hap.Service?.EveAirPressureSensor !== undefined) {
|
|
38
|
-
|
|
38
|
+
// This will be linked to the Eve app if configured to do so
|
|
39
|
+
this.airPressureService = this.addHKService(this.hap.Service.EveAirPressureSensor, '', 1, {});
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
if (this.hap.Characteristic?.ForecastDay !== undefined) {
|
|
@@ -60,22 +61,22 @@ export default class NestWeather extends HomeKitDevice {
|
|
|
60
61
|
this.addHKCharacteristic(this.temperatureService, this.hap.Characteristic.SunsetTime);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
// Setup linkage to EveHome app if configured todo so
|
|
64
|
-
if (
|
|
65
|
-
this.deviceData?.eveHistory === true &&
|
|
66
|
-
this.airPressureService !== undefined &&
|
|
67
|
-
typeof this.historyService?.linkToEveHome === 'function'
|
|
68
|
-
) {
|
|
69
|
-
this.historyService.linkToEveHome(this.airPressureService, {
|
|
70
|
-
description: this.deviceData.description,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
// Extra setup details for output
|
|
75
65
|
this.deviceData?.elevation !== undefined && this.postSetupDetail('Elevation of ' + this.deviceData.elevation + 'm');
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
onRemove() {
|
|
69
|
+
this.accessory.removeService(this.temperatureService);
|
|
70
|
+
this.accessory.removeService(this.humidityService);
|
|
71
|
+
this.accessory.removeService(this.batteryService);
|
|
72
|
+
this.accessory.removeService(this.airPressureService);
|
|
73
|
+
this.temperatureService = undefined;
|
|
74
|
+
this.humidityService = undefined;
|
|
75
|
+
this.batteryService = undefined;
|
|
76
|
+
this.airPressureService = undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onUpdate(deviceData) {
|
|
79
80
|
if (
|
|
80
81
|
typeof deviceData !== 'object' ||
|
|
81
82
|
this.temperatureService === undefined ||
|
|
@@ -97,7 +98,7 @@ export default class NestWeather extends HomeKitDevice {
|
|
|
97
98
|
this.humidityService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity);
|
|
98
99
|
|
|
99
100
|
if (this.airPressureService !== undefined) {
|
|
100
|
-
//this.airPressureService.updateCharacteristic(this.hap.Characteristic.EveAirPressure, 0); // Where from??
|
|
101
|
+
// this.airPressureService.updateCharacteristic(this.hap.Characteristic.EveAirPressure, 0); // Where from??
|
|
101
102
|
this.airPressureService.updateCharacteristic(this.hap.Characteristic.EveElevation, deviceData.elevation);
|
|
102
103
|
}
|
|
103
104
|
|
|
@@ -155,17 +156,111 @@ export default class NestWeather extends HomeKitDevice {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// If we have the history service running, record temperature and humity every 5mins
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
temperature: deviceData.current_temperature,
|
|
164
|
-
humidity: deviceData.current_humidity,
|
|
165
|
-
pressure: 0,
|
|
166
|
-
},
|
|
167
|
-
300,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
159
|
+
this.history(
|
|
160
|
+
this.airPressureService,
|
|
161
|
+
{ temperature: deviceData.current_temperature, humidity: deviceData.current_humidity, pressure: 0 },
|
|
162
|
+
{ timegap: 300, force: true },
|
|
163
|
+
);
|
|
170
164
|
}
|
|
171
165
|
}
|
|
166
|
+
|
|
167
|
+
// Function to process our RAW Nest or Google for this device type
|
|
168
|
+
export function processRawData(log, rawData, config, deviceType = undefined) {
|
|
169
|
+
if (
|
|
170
|
+
rawData === null ||
|
|
171
|
+
typeof rawData !== 'object' ||
|
|
172
|
+
rawData?.constructor !== Object ||
|
|
173
|
+
typeof config !== 'object' ||
|
|
174
|
+
config?.constructor !== Object
|
|
175
|
+
) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Process data for any structure(s) for both Nest and Protobuf API data
|
|
180
|
+
// We use this to created virtual weather station(s) for each structure that has location data
|
|
181
|
+
let devices = {};
|
|
182
|
+
Object.entries(rawData)
|
|
183
|
+
.filter(
|
|
184
|
+
([key]) => (key.startsWith('structure.') === true || key.startsWith('STRUCTURE_') === true) && config?.options?.weather === true, // Only if weather enabled
|
|
185
|
+
)
|
|
186
|
+
.forEach(([object_key, value]) => {
|
|
187
|
+
let tempDevice = {};
|
|
188
|
+
try {
|
|
189
|
+
// For a Google API source data, we use the Nest API structure ID. This will ensure we generate the same serial number
|
|
190
|
+
// This should prevent two 'wether' objects from being created
|
|
191
|
+
// Nest API uses the structure id only
|
|
192
|
+
let serialNumber =
|
|
193
|
+
value?.source === DATA_SOURCE.GOOGLE &&
|
|
194
|
+
typeof value.value?.structure_info?.rtsStructureId === 'string' &&
|
|
195
|
+
value.value.structure_info.rtsStructureId !== ''
|
|
196
|
+
? value.value.structure_info.rtsStructureId.trim() // Google API STRUCTURE_xxx, translated Nest API structure.xxx
|
|
197
|
+
: value?.source === DATA_SOURCE.NEST
|
|
198
|
+
? object_key // Nest API structure.xxx
|
|
199
|
+
: undefined;
|
|
200
|
+
|
|
201
|
+
let description =
|
|
202
|
+
value?.source === DATA_SOURCE.GOOGLE
|
|
203
|
+
? (value.value?.structure_location?.city?.value?.trim() ?? '') !== '' &&
|
|
204
|
+
(value.value?.structure_location?.state?.value?.trim() ?? '') !== ''
|
|
205
|
+
? value.value.structure_location.city.value.trim() + ' - ' + value.value.structure_location.state.value.trim()
|
|
206
|
+
: (value.value?.structure_info?.name?.trim() ?? '') !== ''
|
|
207
|
+
? value.value.structure_info.name.trim()
|
|
208
|
+
: undefined
|
|
209
|
+
: value?.source === DATA_SOURCE.NEST
|
|
210
|
+
? (value.value?.city?.trim() ?? '') !== '' && (value.value?.state?.trim() ?? '') !== ''
|
|
211
|
+
? value.value.city.trim() + ' - ' + value.value.state.trim()
|
|
212
|
+
: (value.value?.name?.trim() ?? '') !== ''
|
|
213
|
+
? value.value.name.trim()
|
|
214
|
+
: undefined
|
|
215
|
+
: undefined;
|
|
216
|
+
|
|
217
|
+
if (serialNumber !== undefined && description !== undefined) {
|
|
218
|
+
tempDevice = processCommonData(
|
|
219
|
+
object_key,
|
|
220
|
+
{
|
|
221
|
+
type: DEVICE_TYPE.WEATHER,
|
|
222
|
+
model: 'Weather',
|
|
223
|
+
softwareVersion: NestWeather.VERSION, // We'll use our class version here now
|
|
224
|
+
serialNumber: NESTLABS_MAC_PREFIX + crc24(serialNumber.toUpperCase()).toUpperCase(),
|
|
225
|
+
description: description,
|
|
226
|
+
current_temperature:
|
|
227
|
+
isNaN(value.value?.weather?.current_temperature) === false
|
|
228
|
+
? adjustTemperature(Number(value.value.weather.current_temperature), 'C', 'C', true)
|
|
229
|
+
: 0.0,
|
|
230
|
+
current_humidity: isNaN(value.value?.weather?.current_humidity) === false ? Number(value.value.weather.current_humidity) : 0,
|
|
231
|
+
condition: value.value?.weather?.condition?.trim() ?? '',
|
|
232
|
+
wind_direction: value.value?.weather?.wind_direction?.trim() ?? '',
|
|
233
|
+
wind_speed: isNaN(value.value?.weather?.wind_speed) === false ? Number(value.value.weather.wind_speed) : 0,
|
|
234
|
+
sunrise: isNaN(value.value?.weather?.sunrise) === false ? Number(value.value.weather.sunrise) : '---',
|
|
235
|
+
sunset: isNaN(value.value?.weather?.sunset) === false ? Number(value.value.weather.sunset) : '---',
|
|
236
|
+
station: value.value?.weather?.station?.trim() ?? '',
|
|
237
|
+
forecast: value.value?.weather?.forecast?.trim() ?? '',
|
|
238
|
+
},
|
|
239
|
+
config,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
// eslint-disable-next-line no-unused-vars
|
|
243
|
+
} catch (error) {
|
|
244
|
+
log?.debug?.('Error processing weather data for "%s"', object_key);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (
|
|
248
|
+
Object.entries(tempDevice).length !== 0 &&
|
|
249
|
+
typeof devices[tempDevice.serialNumber] === 'undefined' &&
|
|
250
|
+
(deviceType === undefined || (typeof deviceType === 'string' && deviceType !== '' && tempDevice.type === deviceType))
|
|
251
|
+
) {
|
|
252
|
+
let deviceOptions = config?.devices?.find(
|
|
253
|
+
(device) => device?.serialNumber?.toUpperCase?.() === tempDevice?.serialNumber?.toUpperCase?.(),
|
|
254
|
+
);
|
|
255
|
+
// Insert any extra options we've read in from configuration file for this device
|
|
256
|
+
tempDevice.eveHistory = config.options.eveHistory === true || deviceOptions?.eveHistory === true;
|
|
257
|
+
tempDevice.elevation =
|
|
258
|
+
isNaN(deviceOptions?.elevation) === false && Number(deviceOptions?.elevation) >= 0 && Number(deviceOptions?.elevation) <= 8848
|
|
259
|
+
? Number(deviceOptions?.elevation)
|
|
260
|
+
: config.options.elevation;
|
|
261
|
+
devices[tempDevice.serialNumber] = tempDevice; // Store processed device
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return devices;
|
|
266
|
+
}
|
package/dist/protobuf/root.proto
CHANGED
|
@@ -37,5 +37,6 @@ import "weave/trait/power.proto";
|
|
|
37
37
|
import "weave/trait/schedule.proto";
|
|
38
38
|
import "weave/trait/security.proto";
|
|
39
39
|
import "weave/trait/time.proto";
|
|
40
|
+
import "nest/services/apigateway.proto";
|
|
40
41
|
import "nestlabs/gateway/v2.proto";
|
|
41
42
|
import "nestlabs/history/v1.proto";
|
package/dist/rtpmuxer.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// rtpmuxer.js
|
|
2
|
+
// Unified RTP Muxer + Stream Engine with FFmpeg support
|
|
3
|
+
// Part of homebridge-nest-accfactory
|
|
4
|
+
//
|
|
5
|
+
// Code version 2025.07.04
|
|
6
|
+
// Mark Hulskamp
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Define nodejs module requirements
|
|
10
|
+
import dgram from 'dgram';
|
|
11
|
+
import { Writable } from 'stream';
|
|
12
|
+
import { Buffer } from 'node:buffer';
|
|
13
|
+
import { setInterval, clearInterval } from 'node:timers';
|
|
14
|
+
|
|
15
|
+
// Define constants
|
|
16
|
+
const LOG_LEVELS = {
|
|
17
|
+
INFO: 'info',
|
|
18
|
+
SUCCESS: 'success',
|
|
19
|
+
WARN: 'warn',
|
|
20
|
+
ERROR: 'error',
|
|
21
|
+
DEBUG: 'debug',
|
|
22
|
+
};
|
|
23
|
+
const RTP_PACKET_HEADER_SIZE = 12;
|
|
24
|
+
|
|
25
|
+
export default class RTPMuxer {
|
|
26
|
+
static RTP_PORT_START = 50000;
|
|
27
|
+
static RTP_PORT_END = 51000;
|
|
28
|
+
static SAMPLE_RATE_VIDEO = 90000;
|
|
29
|
+
static SAMPLE_RATE_AUDIO = 48000;
|
|
30
|
+
static PAYLOAD_TYPE_H264 = 96;
|
|
31
|
+
static PAYLOAD_TYPE_OPUS = 111;
|
|
32
|
+
|
|
33
|
+
static STREAM_TYPE = {
|
|
34
|
+
BUFFER: 'buffer',
|
|
35
|
+
LIVE: 'live',
|
|
36
|
+
RECORD: 'record',
|
|
37
|
+
TALK: 'talk',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
log = undefined; // Logging function object
|
|
41
|
+
|
|
42
|
+
#udpServer = undefined; // UDP server for RTP packets
|
|
43
|
+
#port = undefined; // UDP port for RTP packets
|
|
44
|
+
#outputSessions = new Map(); // Output sessions for RTP streams
|
|
45
|
+
#buffer = []; // Buffer for RTP packets
|
|
46
|
+
#bufferDuration = 5000;
|
|
47
|
+
#bufferTimer = undefined; // Timer for buffer cleanup
|
|
48
|
+
#ffmpeg = undefined; // FFmpeg instance for processing RTP streams
|
|
49
|
+
|
|
50
|
+
constructor(options) {
|
|
51
|
+
// Setup logger object if passed as option
|
|
52
|
+
if (Object.values(LOG_LEVELS).every((fn) => typeof options?.log?.[fn] === 'function')) {
|
|
53
|
+
this.log = options.log;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.#ffmpeg = options.ffmpeg; // pass instance of FFmpeg from ffmpeg.js
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async start() {
|
|
60
|
+
this.#port = await this.#allocatePort();
|
|
61
|
+
this.#udpServer = dgram.createSocket('udp4');
|
|
62
|
+
this.#udpServer.on('message', (msg) => this.#handleRTP(msg));
|
|
63
|
+
this.#udpServer.bind(this.#port);
|
|
64
|
+
this.#startBufferLoop();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
stop(uuid) {
|
|
68
|
+
if (this.#udpServer) {
|
|
69
|
+
this.#udpServer.close();
|
|
70
|
+
this.#udpServer = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
clearInterval(this.#bufferTimer);
|
|
74
|
+
this.#outputSessions.clear();
|
|
75
|
+
|
|
76
|
+
this.#ffmpeg?.killAllSessions?.(uuid);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getPort() {
|
|
80
|
+
return this.#port;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getSDP(kind) {
|
|
84
|
+
let sdp = '';
|
|
85
|
+
if (kind === 'video') {
|
|
86
|
+
sdp += 'm=video ' + this.#port + ' RTP/AVP ' + RTPMuxer.PAYLOAD_TYPE_H264 + '\r\n';
|
|
87
|
+
sdp += 'a=rtpmap:' + RTPMuxer.PAYLOAD_TYPE_H264 + ' H264/' + RTPMuxer.SAMPLE_RATE_VIDEO + '\r\n';
|
|
88
|
+
} else if (kind === 'audio') {
|
|
89
|
+
sdp += 'm=audio ' + this.#port + ' RTP/AVP ' + RTPMuxer.PAYLOAD_TYPE_OPUS + '\r\n';
|
|
90
|
+
sdp += 'a=rtpmap:' + RTPMuxer.PAYLOAD_TYPE_OPUS + ' opus/' + RTPMuxer.SAMPLE_RATE_AUDIO + '/2\r\n';
|
|
91
|
+
}
|
|
92
|
+
return sdp;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
attachOutput(sessionID, writableStream, options = {}) {
|
|
96
|
+
this.#outputSessions.set(sessionID, {
|
|
97
|
+
stream: writableStream,
|
|
98
|
+
kind: options.kind,
|
|
99
|
+
isRecording: options.isRecording === true,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
detachOutput(sessionID) {
|
|
104
|
+
this.#outputSessions.delete(sessionID);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getWritableStream(type) {
|
|
108
|
+
return new Writable({
|
|
109
|
+
write: (chunk, encoding, callback) => {
|
|
110
|
+
if (type === RTPMuxer.STREAM_TYPE.BUFFER) {
|
|
111
|
+
this.#buffer.push({ time: Date.now(), data: chunk });
|
|
112
|
+
}
|
|
113
|
+
for (let session of this.#outputSessions.values()) {
|
|
114
|
+
if (session.kind === type || session.kind === undefined) {
|
|
115
|
+
session.stream.write(chunk);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
callback();
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getBufferedPackets(kind) {
|
|
124
|
+
let now = Date.now();
|
|
125
|
+
return this.#buffer.filter((p) => p.kind === kind && now - p.timestamp <= this.#bufferDuration).map((p) => p.packet);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
startSession(uuid, sessionID, args, sessionType = 'live', errorCallback, pipeCount = 4) {
|
|
129
|
+
return this.#ffmpeg?.createSession?.(uuid, sessionID, args, sessionType, errorCallback, pipeCount);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stopSession(uuid, sessionID, sessionType = 'live') {
|
|
133
|
+
return this.#ffmpeg?.killSession?.(uuid, sessionID, sessionType);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
processRTP(packet) {
|
|
137
|
+
this.#handleRTP(packet);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#handleRTP(packet) {
|
|
141
|
+
if (Buffer.isBuffer(packet) === false || packet.length < RTP_PACKET_HEADER_SIZE) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let payloadType = packet[1] & 0x7f;
|
|
146
|
+
let kind = payloadType === RTPMuxer.PAYLOAD_TYPE_H264 ? 'video' : payloadType === RTPMuxer.PAYLOAD_TYPE_OPUS ? 'audio' : undefined;
|
|
147
|
+
if (kind === undefined) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let copy = Buffer.from(packet);
|
|
152
|
+
this.#buffer.push({ kind, timestamp: Date.now(), packet: copy });
|
|
153
|
+
|
|
154
|
+
for (let session of this.#outputSessions.values()) {
|
|
155
|
+
if (session.kind === kind || session.kind === undefined) {
|
|
156
|
+
session.stream.write(copy);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#startBufferLoop() {
|
|
162
|
+
this.#bufferTimer = setInterval(() => {
|
|
163
|
+
let now = Date.now();
|
|
164
|
+
this.#buffer = this.#buffer.filter((p) => now - p.timestamp <= this.#bufferDuration);
|
|
165
|
+
}, 1000);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async #allocatePort() {
|
|
169
|
+
for (let port = RTPMuxer.RTP_PORT_START; port <= RTPMuxer.RTP_PORT_END; port += 2) {
|
|
170
|
+
try {
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
let socket = dgram.createSocket('udp4');
|
|
173
|
+
socket.once('error', reject);
|
|
174
|
+
socket.bind(port, () => {
|
|
175
|
+
socket.close();
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
return port;
|
|
180
|
+
} catch {
|
|
181
|
+
// try next port
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw new Error('No available UDP port');
|
|
185
|
+
}
|
|
186
|
+
}
|