@lox-audioserver/node-airplay-sender 0.4.4 → 0.4.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/README.md +2 -8
- package/dist/core/audioOut.d.ts +1 -14
- package/dist/core/audioOut.js +16 -152
- package/dist/core/deviceAirtunes.d.ts +2 -4
- package/dist/core/deviceAirtunes.js +18 -51
- package/dist/core/devices.js +2 -14
- package/dist/core/index.d.ts +3 -10
- package/dist/core/index.js +5 -13
- package/dist/core/rtsp.js +15 -51
- package/dist/core/udpServers.d.ts +1 -19
- package/dist/core/udpServers.js +3 -94
- package/dist/esm/core/audioOut.js +16 -152
- package/dist/esm/core/deviceAirtunes.js +18 -51
- package/dist/esm/core/devices.js +2 -14
- package/dist/esm/core/index.js +5 -13
- package/dist/esm/core/rtsp.js +15 -51
- package/dist/esm/core/udpServers.js +3 -94
- package/dist/esm/homekit/credentials.js +5 -15
- package/dist/esm/index.js +0 -5
- package/dist/esm/utils/circularBuffer.js +0 -8
- package/dist/esm/utils/config.js +2 -12
- package/dist/esm/utils/ntp.js +12 -41
- package/dist/homekit/credentials.d.ts +1 -2
- package/dist/homekit/credentials.js +5 -15
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -5
- package/dist/utils/circularBuffer.d.ts +0 -1
- package/dist/utils/circularBuffer.js +0 -8
- package/dist/utils/config.d.ts +0 -10
- package/dist/utils/config.js +2 -12
- package/dist/utils/ntp.d.ts +1 -15
- package/dist/utils/ntp.js +12 -41
- package/package.json +1 -1
package/dist/core/rtsp.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Client = Client;
|
|
4
|
-
// @ts-nocheck
|
|
5
4
|
var net = require('net'), nodeCrypto = require('crypto'), events = require('events'), util = require('util'), fs = require('fs'), dgram = require('dgram');
|
|
6
5
|
const ntp = require('../utils/ntp').default ?? require('../utils/ntp');
|
|
7
6
|
const config = require('../utils/config').default ?? require('../utils/config');
|
|
@@ -93,6 +92,7 @@ function Client(volume, password, audioOut, options) {
|
|
|
93
92
|
this.activeRemote = nu.randomInt(9).toString().toUpperCase();
|
|
94
93
|
this.dacpId = "04F8191D99BEC6E9";
|
|
95
94
|
this.session = null;
|
|
95
|
+
this.readySent = false;
|
|
96
96
|
this.timeout = null;
|
|
97
97
|
this.volume = volume;
|
|
98
98
|
this.progress = 0;
|
|
@@ -152,7 +152,6 @@ function Client(volume, password, audioOut, options) {
|
|
|
152
152
|
this.homekitver = this.transient ? "4" : "3";
|
|
153
153
|
this.metadataReady = false;
|
|
154
154
|
this.connectAttempts = 0;
|
|
155
|
-
this.lastBackoff = 0;
|
|
156
155
|
}
|
|
157
156
|
util.inherits(Client, events.EventEmitter);
|
|
158
157
|
exports.default = { Client };
|
|
@@ -222,8 +221,7 @@ Client.prototype.startHandshake = function (udpServers, host, port) {
|
|
|
222
221
|
const baseBackOff = Math.min(config.rtsp_retry_base_ms * Math.pow(2, nextAttempt - 1), config.rtsp_retry_max_ms);
|
|
223
222
|
const jitter = Math.random() * config.rtsp_retry_jitter_ms;
|
|
224
223
|
const backOff = baseBackOff + jitter;
|
|
225
|
-
this.
|
|
226
|
-
if (this.debug || config.debug_dump)
|
|
224
|
+
if (this.debug)
|
|
227
225
|
this.logLine?.('rtsp_retry', { attempt: nextAttempt, backOff, code: err?.code });
|
|
228
226
|
setTimeout(() => {
|
|
229
227
|
this.startTimeout();
|
|
@@ -242,14 +240,8 @@ Client.prototype.startHandshake = function (udpServers, host, port) {
|
|
|
242
240
|
this.cleanup('rtsp_socket', err?.code);
|
|
243
241
|
});
|
|
244
242
|
this.socket.on('end', () => {
|
|
245
|
-
if (this.debug)
|
|
246
|
-
this.logLine?.('
|
|
247
|
-
lastStatus: rtsp_methods[this.status + 1] ?? this.status,
|
|
248
|
-
cseq: this.cseq,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
this.connectAttempts = 0;
|
|
252
|
-
this.lastBackoff = 0;
|
|
243
|
+
if (this.debug)
|
|
244
|
+
this.logLine?.('block2');
|
|
253
245
|
this.cleanup('disconnected');
|
|
254
246
|
});
|
|
255
247
|
};
|
|
@@ -289,13 +281,6 @@ Client.prototype.setVolume = function (volume, callback) {
|
|
|
289
281
|
this.sendNextRequest();
|
|
290
282
|
};
|
|
291
283
|
Client.prototype.setProgress = function (progress, duration, callback) {
|
|
292
|
-
if (this.airplay2) {
|
|
293
|
-
this.progress = progress;
|
|
294
|
-
this.duration = duration;
|
|
295
|
-
if (typeof callback === 'function')
|
|
296
|
-
callback();
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
284
|
if (this.status !== PLAYING)
|
|
300
285
|
return;
|
|
301
286
|
let normProgress = progress;
|
|
@@ -341,12 +326,6 @@ Client.prototype.sendHeartBeat = function (callback) {
|
|
|
341
326
|
this.sendNextRequest();
|
|
342
327
|
};
|
|
343
328
|
Client.prototype.setTrackInfo = function (name, artist, album, callback) {
|
|
344
|
-
if (this.airplay2) {
|
|
345
|
-
this.trackInfo = { name, artist, album };
|
|
346
|
-
if (typeof callback === 'function')
|
|
347
|
-
callback();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
329
|
if (this.status !== PLAYING)
|
|
351
330
|
return;
|
|
352
331
|
if (name != this.trackInfo?.name || artist != this.trackInfo?.artist || album != this.trackInfo?.album) {
|
|
@@ -424,7 +403,6 @@ Client.prototype.cleanup = function (type, msg) {
|
|
|
424
403
|
this.seed = null;
|
|
425
404
|
this.credentials = null;
|
|
426
405
|
// this.password = null;
|
|
427
|
-
this.connectAttempts = 0;
|
|
428
406
|
this.removeAllListeners();
|
|
429
407
|
if (this.timeout) {
|
|
430
408
|
clearTimeout(this.timeout);
|
|
@@ -840,9 +818,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
840
818
|
request += 'Connection: keep-alive\r\n';
|
|
841
819
|
}
|
|
842
820
|
request += 'Apple-Challenge: SdX9kFJVxgKVMFof/Znj4Q\r\n\r\n';
|
|
843
|
-
this.socket.write(Buffer.from(request, 'utf-8'));
|
|
844
|
-
request = '';
|
|
845
|
-
this.status = PLAYING;
|
|
846
821
|
break;
|
|
847
822
|
case OPTIONS2:
|
|
848
823
|
request = '';
|
|
@@ -1089,10 +1064,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
1089
1064
|
//this.logLine?.(request);
|
|
1090
1065
|
break;
|
|
1091
1066
|
case SETPROGRESS:
|
|
1092
|
-
if (this.airplay2) {
|
|
1093
|
-
this.status = PLAYING;
|
|
1094
|
-
break;
|
|
1095
|
-
}
|
|
1096
1067
|
function hms(seconds) {
|
|
1097
1068
|
const h = Math.floor(seconds / 3600);
|
|
1098
1069
|
const m = Math.floor((seconds % 3600) / 60);
|
|
@@ -1113,10 +1084,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
1113
1084
|
//this.logLine?.(request);
|
|
1114
1085
|
break;
|
|
1115
1086
|
case SETDAAP:
|
|
1116
|
-
if (this.airplay2) {
|
|
1117
|
-
this.status = PLAYING;
|
|
1118
|
-
break;
|
|
1119
|
-
}
|
|
1120
1087
|
let daapenc = true;
|
|
1121
1088
|
//daapenc = true
|
|
1122
1089
|
var name = this.daapEncode('minm', this.trackInfo.name, daapenc);
|
|
@@ -1165,10 +1132,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
1165
1132
|
return;
|
|
1166
1133
|
}
|
|
1167
1134
|
this.startTimeout();
|
|
1168
|
-
if (config.debug_dump) {
|
|
1169
|
-
// eslint-disable-next-line no-console
|
|
1170
|
-
console.debug('rtsp_req', { status: rtsp_methods[this.status] ?? this.status, len: request.length });
|
|
1171
|
-
}
|
|
1172
1135
|
if (this.encryptedChannel && this.credentials) {
|
|
1173
1136
|
this.socket.write(this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')])));
|
|
1174
1137
|
}
|
|
@@ -1225,10 +1188,6 @@ function parseAuthenticate(auth, field) {
|
|
|
1225
1188
|
Client.prototype.processData = function (blob, rawData) {
|
|
1226
1189
|
this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
|
|
1227
1190
|
var response = parseResponse2(blob, this), headers = response.headers || {};
|
|
1228
|
-
if (config.debug_dump) {
|
|
1229
|
-
// eslint-disable-next-line no-console
|
|
1230
|
-
console.debug('rtsp_res', { code: response.code, status: rtsp_methods[this.status + 1], len: rawData.length });
|
|
1231
|
-
}
|
|
1232
1191
|
if (this.debug) {
|
|
1233
1192
|
try {
|
|
1234
1193
|
if ((rawData.toString()).includes("bplist00")) {
|
|
@@ -1469,9 +1428,7 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1469
1428
|
return;
|
|
1470
1429
|
}
|
|
1471
1430
|
// this.logLine?.("DEBUG: Device Proof=" + this.deviceProof.toString('hex'));
|
|
1472
|
-
|
|
1473
|
-
this.srp.checkM2(this.deviceProof);
|
|
1474
|
-
}
|
|
1431
|
+
this.srp.checkM2(this.deviceProof);
|
|
1475
1432
|
if (this.transient == true) {
|
|
1476
1433
|
this.credentials = new Credentials("sdsds", "", "", "", this.seed);
|
|
1477
1434
|
this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
|
|
@@ -1660,11 +1617,18 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1660
1617
|
case RECORD:
|
|
1661
1618
|
this.metadataReady = true;
|
|
1662
1619
|
this.emit('pair_success');
|
|
1663
|
-
if (
|
|
1620
|
+
if (this.airplay2) {
|
|
1621
|
+
// AirPlay2 may not send FLUSH after SETPROGRESS; ensure session exists and start relay once.
|
|
1622
|
+
this.session = this.session ?? "1";
|
|
1623
|
+
if (!this.readySent) {
|
|
1624
|
+
this.readySent = true;
|
|
1625
|
+
this.emit('ready');
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1664
1629
|
this.session = this.session ?? "1";
|
|
1665
1630
|
this.emit('ready');
|
|
1666
1631
|
}
|
|
1667
|
-
;
|
|
1668
1632
|
this.status = SETVOLUME;
|
|
1669
1633
|
break;
|
|
1670
1634
|
case SETVOLUME:
|
|
@@ -1685,7 +1649,7 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1685
1649
|
}
|
|
1686
1650
|
break;
|
|
1687
1651
|
case SETPROGRESS:
|
|
1688
|
-
//
|
|
1652
|
+
// Keep PLAYING to avoid forcing FLUSH on every progress update.
|
|
1689
1653
|
this.status = PLAYING;
|
|
1690
1654
|
break;
|
|
1691
1655
|
case SETDAAP:
|
|
@@ -3,21 +3,6 @@ type ControlSyncTarget = {
|
|
|
3
3
|
host: string;
|
|
4
4
|
controlPort: number;
|
|
5
5
|
};
|
|
6
|
-
type SenderReportCounters = {
|
|
7
|
-
rtpTimestamp: number;
|
|
8
|
-
ntp: Buffer;
|
|
9
|
-
packetCount: number;
|
|
10
|
-
octetCount: number;
|
|
11
|
-
};
|
|
12
|
-
type ReceiverReport = {
|
|
13
|
-
ssrc?: number;
|
|
14
|
-
};
|
|
15
|
-
type ExtendedReport = {
|
|
16
|
-
ntp?: Buffer;
|
|
17
|
-
ssrc?: number;
|
|
18
|
-
lastRr?: number;
|
|
19
|
-
delaySinceLastRr?: number;
|
|
20
|
-
};
|
|
21
6
|
/**
|
|
22
7
|
* Manages control/timing UDP sockets used by RAOP for resend requests and clock sync.
|
|
23
8
|
* Binds ports for both endpoints and emits events with socket info.
|
|
@@ -36,9 +21,6 @@ export default class UDPServers extends EventEmitter {
|
|
|
36
21
|
/**
|
|
37
22
|
* Send an RTCP sync packet to a receiver to align playback.
|
|
38
23
|
*/
|
|
39
|
-
sendControlSync(seq: number, dev: ControlSyncTarget
|
|
40
|
-
private buildSenderReport;
|
|
41
|
-
private buildReceiverReport;
|
|
42
|
-
private buildExtendedReport;
|
|
24
|
+
sendControlSync(seq: number, dev: ControlSyncTarget): void;
|
|
43
25
|
}
|
|
44
26
|
export {};
|
package/dist/core/udpServers.js
CHANGED
|
@@ -133,112 +133,21 @@ class UDPServers extends node_events_1.EventEmitter {
|
|
|
133
133
|
/**
|
|
134
134
|
* Send an RTCP sync packet to a receiver to align playback.
|
|
135
135
|
*/
|
|
136
|
-
sendControlSync(seq, dev
|
|
136
|
+
sendControlSync(seq, dev) {
|
|
137
137
|
if (this.status !== BOUND || !this.control.socket)
|
|
138
138
|
return;
|
|
139
139
|
const packet = Buffer.alloc(20);
|
|
140
140
|
packet.writeUInt16BE(0x80d4, 0);
|
|
141
141
|
packet.writeUInt16BE(0x0007, 2);
|
|
142
|
-
packet.writeUInt32BE((0, numUtil_1.low32)(
|
|
142
|
+
packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet), 4);
|
|
143
143
|
const ntpTime = ntp_1.default.timestamp();
|
|
144
144
|
ntpTime.copy(packet, 8);
|
|
145
|
-
packet.writeUInt32BE((0, numUtil_1.low32)(
|
|
145
|
+
packet.writeUInt32BE((0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + config_1.default.sampling_rate * 2), 16);
|
|
146
146
|
const delay = Math.max(0, config_1.default.control_sync_base_delay_ms +
|
|
147
147
|
Math.random() * config_1.default.control_sync_jitter_ms);
|
|
148
148
|
setTimeout(() => {
|
|
149
|
-
if (config_1.default.debug_dump) {
|
|
150
|
-
// eslint-disable-next-line no-console
|
|
151
|
-
console.debug('rtcp_sync', { seq, tsOffsetFrames, host: dev.host, port: dev.controlPort });
|
|
152
|
-
}
|
|
153
149
|
this.control.socket?.send(packet, 0, packet.length, dev.controlPort, dev.host);
|
|
154
|
-
if (sr && config_1.default.send_rtcp_sr) {
|
|
155
|
-
const srPacket = this.buildSenderReport(sr);
|
|
156
|
-
if (config_1.default.debug_dump) {
|
|
157
|
-
// eslint-disable-next-line no-console
|
|
158
|
-
console.debug('rtcp_sr', { ssrc: config_1.default.device_magic, rtp: sr.rtpTimestamp, packets: sr.packetCount });
|
|
159
|
-
}
|
|
160
|
-
this.control.socket?.send(srPacket, 0, srPacket.length, dev.controlPort, dev.host);
|
|
161
|
-
}
|
|
162
|
-
if (config_1.default.send_rtcp_rr) {
|
|
163
|
-
const rrPacket = this.buildReceiverReport(rr);
|
|
164
|
-
if (config_1.default.debug_dump) {
|
|
165
|
-
// eslint-disable-next-line no-console
|
|
166
|
-
console.debug('rtcp_rr', { ssrc: config_1.default.device_magic });
|
|
167
|
-
}
|
|
168
|
-
this.control.socket?.send(rrPacket, 0, rrPacket.length, dev.controlPort, dev.host);
|
|
169
|
-
}
|
|
170
|
-
if (xr && config_1.default.send_rtcp_xr) {
|
|
171
|
-
const xrPacket = this.buildExtendedReport(xr);
|
|
172
|
-
if (config_1.default.debug_dump) {
|
|
173
|
-
// eslint-disable-next-line no-console
|
|
174
|
-
console.debug('rtcp_xr', { ssrc: config_1.default.device_magic });
|
|
175
|
-
}
|
|
176
|
-
this.control.socket?.send(xrPacket, 0, xrPacket.length, dev.controlPort, dev.host);
|
|
177
|
-
}
|
|
178
150
|
}, delay);
|
|
179
151
|
}
|
|
180
|
-
buildSenderReport(counters) {
|
|
181
|
-
const sr = Buffer.alloc(28);
|
|
182
|
-
// V=2, P=0, RC=0
|
|
183
|
-
sr.writeUInt8(0x80, 0);
|
|
184
|
-
// PT=200 (SR)
|
|
185
|
-
sr.writeUInt8(200, 1);
|
|
186
|
-
// length in 32-bit words minus 1 -> 6 words (28 bytes) => 6
|
|
187
|
-
sr.writeUInt16BE(6, 2);
|
|
188
|
-
// SSRC
|
|
189
|
-
sr.writeUInt32BE(config_1.default.device_magic, 4);
|
|
190
|
-
// NTP timestamp
|
|
191
|
-
counters.ntp.copy(sr, 8, 0, 8);
|
|
192
|
-
// RTP timestamp
|
|
193
|
-
sr.writeUInt32BE((0, numUtil_1.low32)(counters.rtpTimestamp), 16);
|
|
194
|
-
// packet count
|
|
195
|
-
sr.writeUInt32BE((0, numUtil_1.low32)(counters.packetCount), 20);
|
|
196
|
-
// octet count
|
|
197
|
-
sr.writeUInt32BE((0, numUtil_1.low32)(counters.octetCount), 24);
|
|
198
|
-
return sr;
|
|
199
|
-
}
|
|
200
|
-
buildReceiverReport(rr) {
|
|
201
|
-
const packet = Buffer.alloc(8);
|
|
202
|
-
// V=2, P=0, RC=0
|
|
203
|
-
packet.writeUInt8(0x80, 0);
|
|
204
|
-
// PT=201 (RR)
|
|
205
|
-
packet.writeUInt8(201, 1);
|
|
206
|
-
// length = 1 (8 bytes / 4 - 1)
|
|
207
|
-
packet.writeUInt16BE(1, 2);
|
|
208
|
-
// SSRC
|
|
209
|
-
packet.writeUInt32BE(rr?.ssrc ?? config_1.default.device_magic, 4);
|
|
210
|
-
return packet;
|
|
211
|
-
}
|
|
212
|
-
buildExtendedReport(xr) {
|
|
213
|
-
// XR with RRT (Receiver Reference Time) and optional DLRR.
|
|
214
|
-
const rrtBlock = Buffer.alloc(12);
|
|
215
|
-
// BT=4 (RRT), reserved+block length=2 (8 octets following header)
|
|
216
|
-
rrtBlock.writeUInt8(4, 0);
|
|
217
|
-
rrtBlock.writeUInt8(0, 1);
|
|
218
|
-
rrtBlock.writeUInt16BE(2, 2);
|
|
219
|
-
(xr?.ntp ?? ntp_1.default.timestamp()).copy(rrtBlock, 4, 0, 8);
|
|
220
|
-
let dlrrBlock = null;
|
|
221
|
-
if (typeof xr?.lastRr === 'number' || typeof xr?.delaySinceLastRr === 'number') {
|
|
222
|
-
dlrrBlock = Buffer.alloc(12);
|
|
223
|
-
// BT=5 (DLRR)
|
|
224
|
-
dlrrBlock.writeUInt8(5, 0);
|
|
225
|
-
dlrrBlock.writeUInt8(0, 1);
|
|
226
|
-
dlrrBlock.writeUInt16BE(3, 2); // block length (3 words = 12 bytes following header)
|
|
227
|
-
dlrrBlock.writeUInt32BE(xr?.lastRr ?? 0, 4);
|
|
228
|
-
dlrrBlock.writeUInt32BE(xr?.delaySinceLastRr ?? 0, 8);
|
|
229
|
-
}
|
|
230
|
-
const blocks = dlrrBlock ? Buffer.concat([rrtBlock, dlrrBlock]) : rrtBlock;
|
|
231
|
-
const packet = Buffer.alloc(8 + blocks.length);
|
|
232
|
-
// V=2, P=0, RC=0
|
|
233
|
-
packet.writeUInt8(0x80, 0);
|
|
234
|
-
// PT=207 (XR)
|
|
235
|
-
packet.writeUInt8(207, 1);
|
|
236
|
-
// length in 32-bit words minus 1
|
|
237
|
-
packet.writeUInt16BE(packet.length / 4 - 1, 2);
|
|
238
|
-
// SSRC
|
|
239
|
-
packet.writeUInt32BE(xr?.ssrc ?? config_1.default.device_magic, 4);
|
|
240
|
-
blocks.copy(packet, 8);
|
|
241
|
-
return packet;
|
|
242
|
-
}
|
|
243
152
|
}
|
|
244
153
|
exports.default = UDPServers;
|
|
@@ -1,194 +1,69 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
6
|
const node_events_1 = require("node:events");
|
|
40
|
-
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
41
7
|
const config_1 = __importDefault(require("../utils/config"));
|
|
42
8
|
const numUtil_1 = require("../utils/numUtil");
|
|
43
|
-
const ntp_1 = __importStar(require("../utils/ntp"));
|
|
44
9
|
const SEQ_NUM_WRAP = Math.pow(2, 16);
|
|
45
|
-
const FRAC_PER_SEC = 0x1_0000_0000;
|
|
46
10
|
/**
|
|
47
11
|
* Generates RTP timestamps and sequence, pulling PCM/ALAC packets from a circular buffer.
|
|
48
12
|
* Emits `packet` events for devices and sync requests (`need_sync`) at intervals.
|
|
49
13
|
*/
|
|
50
14
|
class AudioOut extends node_events_1.EventEmitter {
|
|
51
15
|
lastSeq = -1;
|
|
52
|
-
lastWireSeq = 0;
|
|
53
16
|
hasAirTunes = false;
|
|
54
|
-
rtpTimeRef =
|
|
17
|
+
rtpTimeRef = Date.now();
|
|
55
18
|
startTimeMs;
|
|
56
|
-
startTimeNtp;
|
|
57
19
|
latencyFrames = 0;
|
|
58
20
|
latencyApplied = false;
|
|
59
|
-
seqOffset = 0;
|
|
60
|
-
tsOffset = 0;
|
|
61
|
-
syncOffsetFrames = 0;
|
|
62
|
-
deviceMagic = 0;
|
|
63
|
-
packetCount = 0;
|
|
64
|
-
octetCount = 0;
|
|
65
|
-
frameDurationMs = (config_1.default.frames_per_packet / config_1.default.sampling_rate) * 1000;
|
|
66
|
-
muteUntilMs = 0;
|
|
67
21
|
/**
|
|
68
22
|
* Begin pulling from the buffer and emitting packets at the configured cadence.
|
|
69
23
|
* @param devices Device manager for sync events.
|
|
70
24
|
* @param circularBuffer PCM/ALAC buffer.
|
|
71
25
|
* @param startTimeMs Optional unix ms to align playback.
|
|
72
|
-
* @param startTimeNtp Optional NTP uint64 (sec<<32|frac) to align playback.
|
|
73
26
|
*/
|
|
74
|
-
init(devices, circularBuffer, startTimeMs
|
|
27
|
+
init(devices, circularBuffer, startTimeMs) {
|
|
75
28
|
this.startTimeMs =
|
|
76
29
|
typeof startTimeMs === 'number' && Number.isFinite(startTimeMs)
|
|
77
30
|
? startTimeMs
|
|
78
31
|
: undefined;
|
|
79
|
-
this.
|
|
80
|
-
this.seqOffset = Math.floor(Math.random() * SEQ_NUM_WRAP);
|
|
81
|
-
this.tsOffset = Math.floor(Math.random() * 0xffffffff);
|
|
82
|
-
this.syncOffsetFrames = this.tsOffset / config_1.default.frames_per_packet;
|
|
83
|
-
this.deviceMagic = typeof deviceMagic === 'number' ? deviceMagic : config_1.default.device_magic;
|
|
84
|
-
if (typeof underrunMuteMs === 'number') {
|
|
85
|
-
config_1.default.underrun_mute_ms = underrunMuteMs;
|
|
86
|
-
}
|
|
87
|
-
circularBuffer.on('underrun', () => this.handleUnderrun());
|
|
88
|
-
const monoNow = node_perf_hooks_1.performance.now();
|
|
89
|
-
const wallToMonoOffset = Date.now() - monoNow;
|
|
90
|
-
if (typeof this.startTimeNtp === 'bigint' || typeof this.startTimeNtp === 'number') {
|
|
91
|
-
const { sec, frac } = (0, ntp_1.parseNtpTimestamp)(this.startTimeNtp);
|
|
92
|
-
const unixMs = (sec - config_1.default.ntp_epoch) * 1000 + Math.floor((frac * 1000) / FRAC_PER_SEC);
|
|
93
|
-
const nowMs = config_1.default.use_monotonic_clock ? monoNow : Date.now();
|
|
94
|
-
const delta = unixMs - nowMs;
|
|
95
|
-
// If target is too close/past, clamp to now.
|
|
96
|
-
this.rtpTimeRef = delta > -config_1.default.stream_latency ? unixMs - wallToMonoOffset : nowMs;
|
|
97
|
-
}
|
|
98
|
-
else if (this.startTimeMs !== undefined) {
|
|
99
|
-
const nowMs = config_1.default.use_monotonic_clock ? monoNow : Date.now();
|
|
100
|
-
const delta = this.startTimeMs - nowMs;
|
|
101
|
-
this.rtpTimeRef = delta > -config_1.default.stream_latency ? this.startTimeMs - wallToMonoOffset : nowMs;
|
|
102
|
-
}
|
|
103
|
-
else if (config_1.default.use_monotonic_clock) {
|
|
104
|
-
this.rtpTimeRef = monoNow;
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
this.rtpTimeRef = Date.now();
|
|
108
|
-
}
|
|
32
|
+
this.rtpTimeRef = this.startTimeMs ?? Date.now();
|
|
109
33
|
devices.on('airtunes_devices', (hasAirTunes) => {
|
|
110
34
|
this.hasAirTunes = hasAirTunes;
|
|
111
35
|
});
|
|
112
36
|
devices.on('need_sync', () => {
|
|
113
|
-
this.emit('need_sync',
|
|
37
|
+
this.emit('need_sync', this.lastSeq);
|
|
114
38
|
});
|
|
115
|
-
const syncEvery = config_1.default.sync_period && config_1.default.sync_period > 0
|
|
116
|
-
? config_1.default.sync_period
|
|
117
|
-
: Math.max(1, Math.round(config_1.default.sampling_rate / config_1.default.frames_per_packet));
|
|
118
39
|
const sendPacket = (seq) => {
|
|
119
|
-
const wireSeq = (this.seqOffset + seq) % SEQ_NUM_WRAP;
|
|
120
40
|
const packet = circularBuffer.readPacket();
|
|
121
|
-
packet.seq =
|
|
122
|
-
packet.timestamp = (0, numUtil_1.low32)(
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
if (this.hasAirTunes && seq % syncEvery === 0) {
|
|
126
|
-
this.emit('need_sync', {
|
|
127
|
-
seq: wireSeq,
|
|
128
|
-
tsOffsetFrames: this.syncOffsetFrames,
|
|
129
|
-
rtcp: {
|
|
130
|
-
rtpTimestamp: packet.timestamp,
|
|
131
|
-
ntp: ntp_1.default.timestamp(),
|
|
132
|
-
packetCount: this.packetCount,
|
|
133
|
-
octetCount: this.octetCount,
|
|
134
|
-
xr: { ntp: ntp_1.default.timestamp() },
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
const nowMs = config_1.default.use_monotonic_clock ? node_perf_hooks_1.performance.now() : Date.now();
|
|
41
|
+
packet.seq = seq % SEQ_NUM_WRAP;
|
|
42
|
+
packet.timestamp = (0, numUtil_1.low32)(seq * config_1.default.frames_per_packet + 2 * config_1.default.sampling_rate);
|
|
43
|
+
if (this.hasAirTunes && seq % config_1.default.sync_period === 0) {
|
|
44
|
+
this.emit('need_sync', seq);
|
|
138
45
|
const expectedTimeMs = this.rtpTimeRef +
|
|
139
|
-
((
|
|
140
|
-
const deltaMs =
|
|
141
|
-
this.emit('metrics', { type: 'sync', seq
|
|
46
|
+
((seq * config_1.default.frames_per_packet) / config_1.default.sampling_rate) * 1000;
|
|
47
|
+
const deltaMs = Date.now() - expectedTimeMs;
|
|
48
|
+
this.emit('metrics', { type: 'sync', seq, deltaMs, latencyFrames: this.latencyFrames });
|
|
142
49
|
}
|
|
143
50
|
this.emit('packet', packet);
|
|
144
|
-
if (this.muteUntilMs > 0) {
|
|
145
|
-
const nowMsSend = config_1.default.use_monotonic_clock ? node_perf_hooks_1.performance.now() : Date.now();
|
|
146
|
-
if (nowMsSend < this.muteUntilMs) {
|
|
147
|
-
packet.pcm.fill(0);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
this.muteUntilMs = 0;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
51
|
packet.release();
|
|
154
|
-
this.lastWireSeq = wireSeq;
|
|
155
52
|
};
|
|
156
53
|
const syncAudio = () => {
|
|
157
|
-
const
|
|
158
|
-
const elapsed = nowMs - this.rtpTimeRef;
|
|
54
|
+
const elapsed = Date.now() - this.rtpTimeRef;
|
|
159
55
|
if (elapsed < 0) {
|
|
160
56
|
setTimeout(syncAudio, Math.min(config_1.default.stream_latency, Math.abs(elapsed)));
|
|
161
57
|
return;
|
|
162
58
|
}
|
|
163
|
-
|
|
164
|
-
// If we're lagging behind significantly, jump forward to avoid long hitches.
|
|
165
|
-
if (config_1.default.jump_forward_enabled) {
|
|
166
|
-
const expectedTimeMs = this.rtpTimeRef + currentSeq * this.frameDurationMs;
|
|
167
|
-
const deltaMs = nowMs - expectedTimeMs;
|
|
168
|
-
if (deltaMs > config_1.default.jump_forward_threshold_ms) {
|
|
169
|
-
const jumpSeq = Math.ceil((config_1.default.jump_forward_lead_ms * config_1.default.sampling_rate) /
|
|
170
|
-
(config_1.default.frames_per_packet * 1000));
|
|
171
|
-
const newSeq = currentSeq + jumpSeq;
|
|
172
|
-
this.rtpTimeRef = nowMs - newSeq * this.frameDurationMs;
|
|
173
|
-
this.lastSeq = newSeq - 1;
|
|
174
|
-
currentSeq = newSeq;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
59
|
+
const currentSeq = Math.floor((elapsed * config_1.default.sampling_rate) / (config_1.default.frames_per_packet * 1000));
|
|
177
60
|
for (let i = this.lastSeq + 1; i <= currentSeq; i += 1) {
|
|
178
61
|
sendPacket(i);
|
|
179
62
|
}
|
|
180
63
|
this.lastSeq = currentSeq;
|
|
181
64
|
setTimeout(syncAudio, config_1.default.stream_latency);
|
|
182
65
|
};
|
|
183
|
-
|
|
184
|
-
const nowMs = config_1.default.use_monotonic_clock ? node_perf_hooks_1.performance.now() : Date.now();
|
|
185
|
-
const delayMs = Math.max(0, this.rtpTimeRef - nowMs);
|
|
186
|
-
if (delayMs > 0) {
|
|
187
|
-
setTimeout(syncAudio, delayMs);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
syncAudio();
|
|
191
|
-
}
|
|
66
|
+
syncAudio();
|
|
192
67
|
}
|
|
193
68
|
/**
|
|
194
69
|
* Apply latency (in audio frames) when aligning start time.
|
|
@@ -198,23 +73,12 @@ class AudioOut extends node_events_1.EventEmitter {
|
|
|
198
73
|
return;
|
|
199
74
|
}
|
|
200
75
|
this.latencyFrames = latencyFrames;
|
|
201
|
-
if (
|
|
76
|
+
if (this.startTimeMs === undefined || this.latencyApplied) {
|
|
202
77
|
return;
|
|
203
78
|
}
|
|
204
79
|
const latencyMs = (this.latencyFrames / config_1.default.sampling_rate) * 1000;
|
|
205
|
-
this.rtpTimeRef
|
|
80
|
+
this.rtpTimeRef = this.startTimeMs - latencyMs;
|
|
206
81
|
this.latencyApplied = true;
|
|
207
82
|
}
|
|
208
|
-
handleUnderrun() {
|
|
209
|
-
const nowMs = config_1.default.use_monotonic_clock ? node_perf_hooks_1.performance.now() : Date.now();
|
|
210
|
-
const currentSeq = Math.max(0, this.lastSeq);
|
|
211
|
-
const currentFrames = currentSeq * config_1.default.frames_per_packet;
|
|
212
|
-
const offsetMs = (currentFrames / config_1.default.sampling_rate) * 1000;
|
|
213
|
-
this.rtpTimeRef = nowMs - offsetMs;
|
|
214
|
-
if (config_1.default.underrun_mute_ms > 0) {
|
|
215
|
-
this.muteUntilMs = nowMs + config_1.default.underrun_mute_ms;
|
|
216
|
-
}
|
|
217
|
-
this.emit('underrun');
|
|
218
|
-
}
|
|
219
83
|
}
|
|
220
84
|
exports.default = AudioOut;
|