@lox-audioserver/node-airplay-sender 0.4.3 → 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 +27 -37
- 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 +27 -37
- 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
|
};
|
|
@@ -411,7 +403,6 @@ Client.prototype.cleanup = function (type, msg) {
|
|
|
411
403
|
this.seed = null;
|
|
412
404
|
this.credentials = null;
|
|
413
405
|
// this.password = null;
|
|
414
|
-
this.connectAttempts = 0;
|
|
415
406
|
this.removeAllListeners();
|
|
416
407
|
if (this.timeout) {
|
|
417
408
|
clearTimeout(this.timeout);
|
|
@@ -827,9 +818,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
827
818
|
request += 'Connection: keep-alive\r\n';
|
|
828
819
|
}
|
|
829
820
|
request += 'Apple-Challenge: SdX9kFJVxgKVMFof/Znj4Q\r\n\r\n';
|
|
830
|
-
this.socket.write(Buffer.from(request, 'utf-8'));
|
|
831
|
-
request = '';
|
|
832
|
-
this.status = PLAYING;
|
|
833
821
|
break;
|
|
834
822
|
case OPTIONS2:
|
|
835
823
|
request = '';
|
|
@@ -1144,10 +1132,6 @@ Client.prototype.sendNextRequest = async function (di) {
|
|
|
1144
1132
|
return;
|
|
1145
1133
|
}
|
|
1146
1134
|
this.startTimeout();
|
|
1147
|
-
if (config.debug_dump) {
|
|
1148
|
-
// eslint-disable-next-line no-console
|
|
1149
|
-
console.debug('rtsp_req', { status: rtsp_methods[this.status] ?? this.status, len: request.length });
|
|
1150
|
-
}
|
|
1151
1135
|
if (this.encryptedChannel && this.credentials) {
|
|
1152
1136
|
this.socket.write(this.credentials.encrypt(Buffer.concat([Buffer.from(request, 'utf-8')])));
|
|
1153
1137
|
}
|
|
@@ -1204,10 +1188,6 @@ function parseAuthenticate(auth, field) {
|
|
|
1204
1188
|
Client.prototype.processData = function (blob, rawData) {
|
|
1205
1189
|
this.logLine?.('Receiving request:', this.hostip, rtsp_methods[this.status + 1]);
|
|
1206
1190
|
var response = parseResponse2(blob, this), headers = response.headers || {};
|
|
1207
|
-
if (config.debug_dump) {
|
|
1208
|
-
// eslint-disable-next-line no-console
|
|
1209
|
-
console.debug('rtsp_res', { code: response.code, status: rtsp_methods[this.status + 1], len: rawData.length });
|
|
1210
|
-
}
|
|
1211
1191
|
if (this.debug) {
|
|
1212
1192
|
try {
|
|
1213
1193
|
if ((rawData.toString()).includes("bplist00")) {
|
|
@@ -1448,9 +1428,7 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1448
1428
|
return;
|
|
1449
1429
|
}
|
|
1450
1430
|
// this.logLine?.("DEBUG: Device Proof=" + this.deviceProof.toString('hex'));
|
|
1451
|
-
|
|
1452
|
-
this.srp.checkM2(this.deviceProof);
|
|
1453
|
-
}
|
|
1431
|
+
this.srp.checkM2(this.deviceProof);
|
|
1454
1432
|
if (this.transient == true) {
|
|
1455
1433
|
this.credentials = new Credentials("sdsds", "", "", "", this.seed);
|
|
1456
1434
|
this.credentials.writeKey = enc.HKDF("sha512", Buffer.from("Control-Salt"), this.srp.computeK(), Buffer.from("Control-Write-Encryption-Key"), 32);
|
|
@@ -1639,27 +1617,39 @@ Client.prototype.processData = function (blob, rawData) {
|
|
|
1639
1617
|
case RECORD:
|
|
1640
1618
|
this.metadataReady = true;
|
|
1641
1619
|
this.emit('pair_success');
|
|
1642
|
-
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 {
|
|
1643
1629
|
this.session = this.session ?? "1";
|
|
1644
1630
|
this.emit('ready');
|
|
1645
1631
|
}
|
|
1646
|
-
;
|
|
1647
1632
|
this.status = SETVOLUME;
|
|
1648
1633
|
break;
|
|
1649
1634
|
case SETVOLUME:
|
|
1650
|
-
if (
|
|
1651
|
-
this.
|
|
1652
|
-
this.duration = 2000000;
|
|
1653
|
-
this.sentFakeProgess = true;
|
|
1654
|
-
this.status = SETPROGRESS;
|
|
1635
|
+
if (this.airplay2) {
|
|
1636
|
+
this.status = PLAYING;
|
|
1655
1637
|
}
|
|
1656
1638
|
else {
|
|
1657
|
-
this.
|
|
1639
|
+
if (!this.sentFakeProgess) {
|
|
1640
|
+
this.progress = 10;
|
|
1641
|
+
this.duration = 2000000;
|
|
1642
|
+
this.sentFakeProgess = true;
|
|
1643
|
+
this.status = SETPROGRESS;
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
this.status = PLAYING;
|
|
1647
|
+
}
|
|
1648
|
+
;
|
|
1658
1649
|
}
|
|
1659
|
-
;
|
|
1660
1650
|
break;
|
|
1661
1651
|
case SETPROGRESS:
|
|
1662
|
-
//
|
|
1652
|
+
// Keep PLAYING to avoid forcing FLUSH on every progress update.
|
|
1663
1653
|
this.status = PLAYING;
|
|
1664
1654
|
break;
|
|
1665
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;
|