@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/esm/utils/ntp.js
CHANGED
|
@@ -3,54 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.toNtpTimestamp = toNtpTimestamp;
|
|
7
|
-
exports.parseNtpTimestamp = parseNtpTimestamp;
|
|
8
|
-
exports.ntpFromUnixMs = ntpFromUnixMs;
|
|
9
6
|
const config_1 = __importDefault(require("./config"));
|
|
10
|
-
const NS_PER_SEC = 1000000000n;
|
|
11
|
-
const FRAC_PER_SEC = 0x100000000n; // 2^32
|
|
12
7
|
class NTP {
|
|
13
|
-
|
|
8
|
+
timeRef = Date.now() - config_1.default.ntp_epoch * 1000;
|
|
14
9
|
timestamp() {
|
|
15
|
-
const
|
|
16
|
-
const sec =
|
|
17
|
-
const
|
|
10
|
+
const time = Date.now() - this.timeRef;
|
|
11
|
+
const sec = Math.floor(time / 1000);
|
|
12
|
+
const msec = time - sec * 1000;
|
|
13
|
+
const ntp_msec = Math.floor(msec * 4294967.296);
|
|
18
14
|
const ts = Buffer.alloc(8);
|
|
19
|
-
ts.writeUInt32BE(sec
|
|
20
|
-
ts.writeUInt32BE(
|
|
15
|
+
ts.writeUInt32BE(sec, 0);
|
|
16
|
+
ts.writeUInt32BE(ntp_msec, 4);
|
|
21
17
|
return ts;
|
|
22
18
|
}
|
|
23
|
-
/** Return the current NTP fractional component (for compatibility). */
|
|
24
19
|
getTime() {
|
|
25
|
-
const
|
|
26
|
-
|
|
20
|
+
const time = Date.now() - this.timeRef;
|
|
21
|
+
const sec = Math.floor(time / 1000);
|
|
22
|
+
const msec = time - sec * 1000;
|
|
23
|
+
const ntp_msec = Math.floor(msec * 4294967.296);
|
|
24
|
+
return ntp_msec;
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
exports.default = new NTP();
|
|
30
|
-
/** Pack various NTP timestamp representations into a uint64 bigint (sec<<32|frac). */
|
|
31
|
-
function toNtpTimestamp(input) {
|
|
32
|
-
if (typeof input === 'bigint')
|
|
33
|
-
return input;
|
|
34
|
-
if (typeof input === 'number')
|
|
35
|
-
return BigInt(input);
|
|
36
|
-
const sec = BigInt(input.sec >>> 0);
|
|
37
|
-
const frac = BigInt(input.frac >>> 0);
|
|
38
|
-
return (sec << 32n) | frac;
|
|
39
|
-
}
|
|
40
|
-
/** Parse an NTP timestamp (uint64 or sec/frac) into components. */
|
|
41
|
-
function parseNtpTimestamp(input) {
|
|
42
|
-
if (typeof input === 'bigint' || typeof input === 'number') {
|
|
43
|
-
const value = typeof input === 'bigint' ? input : BigInt(input);
|
|
44
|
-
const sec = Number(value >> 32n);
|
|
45
|
-
const frac = Number(value & 0xffffffffn);
|
|
46
|
-
return { sec, frac };
|
|
47
|
-
}
|
|
48
|
-
return { sec: input.sec, frac: input.frac };
|
|
49
|
-
}
|
|
50
|
-
/** Build an NTP timestamp from a Unix epoch (ms). */
|
|
51
|
-
function ntpFromUnixMs(unixMs) {
|
|
52
|
-
const sec = Math.floor(unixMs / 1000) + config_1.default.ntp_epoch;
|
|
53
|
-
const ms = unixMs % 1000;
|
|
54
|
-
const frac = Math.floor((ms / 1000) * Number(FRAC_PER_SEC));
|
|
55
|
-
return (BigInt(sec >>> 0) << 32n) | BigInt(frac >>> 0);
|
|
56
|
-
}
|
|
@@ -25,7 +25,6 @@ declare class Credentials {
|
|
|
25
25
|
toString(): string;
|
|
26
26
|
encrypt(message: Buffer): Buffer;
|
|
27
27
|
decrypt(message: Buffer): Buffer;
|
|
28
|
-
encryptAudio(message: Buffer, aad: Buffer | null,
|
|
29
|
-
rotateKeys(): void;
|
|
28
|
+
encryptAudio(message: Buffer, aad: Buffer | null, nonce: number): Buffer;
|
|
30
29
|
}
|
|
31
30
|
export { Credentials };
|
|
@@ -90,21 +90,11 @@ class Credentials {
|
|
|
90
90
|
}
|
|
91
91
|
return result;
|
|
92
92
|
}
|
|
93
|
-
encryptAudio(message, aad,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return Buffer.concat([cipher, tag, nonce.slice(4)]);
|
|
99
|
-
}
|
|
100
|
-
rotateKeys() {
|
|
101
|
-
const info = Buffer.from('AirPlay:Audio', 'utf-8'); // reference-style info
|
|
102
|
-
const salt = Buffer.alloc(16, 0); // deterministic salt like reference
|
|
103
|
-
const newKey = encryption_1.default.HKDF('sha256', salt, this.encryptionKey, info, 32);
|
|
104
|
-
this.writeKey = newKey;
|
|
105
|
-
this.readKey = newKey;
|
|
106
|
-
this.encryptCount = 0;
|
|
107
|
-
this.decryptCount = 0;
|
|
93
|
+
encryptAudio(message, aad, nonce) {
|
|
94
|
+
return Buffer.concat([
|
|
95
|
+
Buffer.concat(encryption_1.default.encryptAndSeal(message, aad, struct.pack("Q", nonce), this.writeKey)),
|
|
96
|
+
Buffer.from(struct.pack("Q", nonce)),
|
|
97
|
+
]);
|
|
108
98
|
}
|
|
109
99
|
}
|
|
110
100
|
exports.Credentials = Credentials;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
import { type AirplayConfig } from './utils/config';
|
|
4
|
-
import { type NtpTimestampInput } from './utils/ntp';
|
|
5
4
|
/**
|
|
6
5
|
* Configuration for a single AirPlay sender.
|
|
7
6
|
*/
|
|
@@ -31,18 +30,10 @@ export interface LoxAirplaySenderOptions {
|
|
|
31
30
|
debug?: boolean;
|
|
32
31
|
/** Optional unix ms start time for synced playback. */
|
|
33
32
|
startTimeMs?: number;
|
|
34
|
-
/** Optional absolute NTP start time (uint64: sec<<32 | frac). */
|
|
35
|
-
startTimeNtp?: NtpTimestampInput;
|
|
36
33
|
/** Logger hook for internal messages. */
|
|
37
34
|
log?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, data?: unknown) => void;
|
|
38
35
|
/** Override transport/buffer tuning without patching the module. */
|
|
39
36
|
config?: Partial<AirplayConfig>;
|
|
40
|
-
/** Optional override for per-session SSRC/device magic. */
|
|
41
|
-
deviceMagic?: number;
|
|
42
|
-
/** Optional override for resend buffer size (packets). */
|
|
43
|
-
resendBufferSize?: number;
|
|
44
|
-
/** Optional mute window (ms) after underrun to smooth recovery. */
|
|
45
|
-
underrunMuteMs?: number;
|
|
46
37
|
}
|
|
47
38
|
/**
|
|
48
39
|
* Metadata sent to receivers for UI display.
|
package/dist/index.js
CHANGED
|
@@ -42,7 +42,6 @@ const node_events_1 = require("node:events");
|
|
|
42
42
|
// Airtunes implementation (node_airtunes2 port) in src/core.
|
|
43
43
|
const index_1 = __importDefault(require("./core/index"));
|
|
44
44
|
const config_1 = __importStar(require("./utils/config"));
|
|
45
|
-
const ntp_1 = require("./utils/ntp");
|
|
46
45
|
class LoxAirplaySender extends node_events_1.EventEmitter {
|
|
47
46
|
airtunes = null;
|
|
48
47
|
deviceKey = null;
|
|
@@ -74,10 +73,6 @@ class LoxAirplaySender extends node_events_1.EventEmitter {
|
|
|
74
73
|
this.airtunes = new index_1.default({
|
|
75
74
|
packetSize: config_1.default.packet_size,
|
|
76
75
|
startTimeMs: options.startTimeMs,
|
|
77
|
-
startTimeNtp: options.startTimeNtp ? (0, ntp_1.toNtpTimestamp)(options.startTimeNtp) : undefined,
|
|
78
|
-
deviceMagic: options.deviceMagic,
|
|
79
|
-
resendBufferSize: options.resendBufferSize,
|
|
80
|
-
underrunMuteMs: options.underrunMuteMs,
|
|
81
76
|
config: options.config,
|
|
82
77
|
});
|
|
83
78
|
this.airtunes.on('device', (key, status, desc) => {
|
|
@@ -24,7 +24,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
|
|
|
24
24
|
buffers = [];
|
|
25
25
|
currentSize = 0;
|
|
26
26
|
status = WAITING;
|
|
27
|
-
hadUnderrun = false;
|
|
28
27
|
constructor(packetsInBuffer, packetSize) {
|
|
29
28
|
super();
|
|
30
29
|
this.packetPool = new packetPool_1.default(packetSize);
|
|
@@ -64,10 +63,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
|
|
|
64
63
|
this.status !== ENDED &&
|
|
65
64
|
(this.status === FILLING || this.currentSize < this.packetSize)) {
|
|
66
65
|
packet.pcm.fill(0);
|
|
67
|
-
if (!this.hadUnderrun) {
|
|
68
|
-
this.hadUnderrun = true;
|
|
69
|
-
this.emit('underrun');
|
|
70
|
-
}
|
|
71
66
|
if (this.status !== FILLING && this.status !== WAITING) {
|
|
72
67
|
this.status = FILLING;
|
|
73
68
|
this.emit('status', 'buffering');
|
|
@@ -97,9 +92,6 @@ class CircularBuffer extends node_events_1.EventEmitter {
|
|
|
97
92
|
}
|
|
98
93
|
}
|
|
99
94
|
this.currentSize -= this.packetSize;
|
|
100
|
-
if (this.hadUnderrun && this.currentSize >= this.packetSize) {
|
|
101
|
-
this.hadUnderrun = false;
|
|
102
|
-
}
|
|
103
95
|
if (this.status === ENDING && this.currentSize <= 0) {
|
|
104
96
|
this.status = ENDED;
|
|
105
97
|
this.currentSize = 0;
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -22,17 +22,7 @@ export interface AirplayConfig {
|
|
|
22
22
|
rtsp_retry_jitter_ms: number;
|
|
23
23
|
control_sync_base_delay_ms: number;
|
|
24
24
|
control_sync_jitter_ms: number;
|
|
25
|
-
use_monotonic_clock: boolean;
|
|
26
|
-
jump_forward_enabled: boolean;
|
|
27
|
-
jump_forward_threshold_ms: number;
|
|
28
|
-
jump_forward_lead_ms: number;
|
|
29
25
|
device_magic: number;
|
|
30
|
-
resend_buffer_size: number;
|
|
31
|
-
send_rtcp_sr: boolean;
|
|
32
|
-
send_rtcp_rr: boolean;
|
|
33
|
-
send_rtcp_xr: boolean;
|
|
34
|
-
underrun_mute_ms: number;
|
|
35
|
-
debug_dump: boolean;
|
|
36
26
|
ntp_epoch: number;
|
|
37
27
|
iv_base64: string;
|
|
38
28
|
rsa_aeskey_base64: string;
|
package/dist/utils/config.js
CHANGED
|
@@ -17,9 +17,9 @@ exports.config = {
|
|
|
17
17
|
coreaudio_check_period: 2000, // CoreAudio buffer level check period
|
|
18
18
|
coreaudio_preload: 352 * 2 * 2 * 50, // ~0.5s of silence pushed to CoreAudio to avoid draining AudioQueue
|
|
19
19
|
sampling_rate: 44100, // fixed by AirTunes v2
|
|
20
|
-
sync_period:
|
|
20
|
+
sync_period: 126, // UDP sync packets are sent to all AirTunes devices regularly
|
|
21
21
|
stream_latency: 200, // audio UDP packets are flushed in bursts periodically
|
|
22
|
-
rtsp_timeout:
|
|
22
|
+
rtsp_timeout: 15000, // RTSP servers are considered gone if no reply is received before the timeout
|
|
23
23
|
rtsp_heartbeat: 15000, // some RTSP (like HomePod) servers requires heartbeat.
|
|
24
24
|
rtsp_retry_attempts: 3,
|
|
25
25
|
rtsp_retry_base_ms: 300,
|
|
@@ -27,17 +27,7 @@ exports.config = {
|
|
|
27
27
|
rtsp_retry_jitter_ms: 150,
|
|
28
28
|
control_sync_base_delay_ms: 2,
|
|
29
29
|
control_sync_jitter_ms: 3,
|
|
30
|
-
use_monotonic_clock: true,
|
|
31
|
-
jump_forward_enabled: false,
|
|
32
|
-
jump_forward_threshold_ms: 180,
|
|
33
|
-
jump_forward_lead_ms: 220,
|
|
34
30
|
device_magic: (0, numUtil_1.randomInt)(9),
|
|
35
|
-
resend_buffer_size: 1000,
|
|
36
|
-
send_rtcp_sr: true,
|
|
37
|
-
send_rtcp_rr: true,
|
|
38
|
-
send_rtcp_xr: false,
|
|
39
|
-
underrun_mute_ms: 50,
|
|
40
|
-
debug_dump: false,
|
|
41
31
|
ntp_epoch: 0x83aa7e80,
|
|
42
32
|
iv_base64: 'ePRBLI0XN5ArFaaz7ncNZw',
|
|
43
33
|
rsa_aeskey_base64: 'VjVbxWcmYgbBbhwBNlCh3K0CMNtWoB844BuiHGUJT51zQS7SDpMnlbBIobsKbfEJ3SCgWHRXjYWf7VQWRYtEcfx7ejA8xDIk5PSBYTvXP5dU2QoGrSBv0leDS6uxlEWuxBq3lIxCxpWO2YswHYKJBt06Uz9P2Fq2hDUwl3qOQ8oXb0OateTKtfXEwHJMprkhsJsGDrIc5W5NJFMAo6zCiM9bGSDeH2nvTlyW6bfI/Q0v0cDGUNeY3ut6fsoafRkfpCwYId+bg3diJh+uzw5htHDyZ2sN+BFYHzEfo8iv4KDxzeya9llqg6fRNQ8d5YjpvTnoeEQ9ye9ivjkBjcAfVw',
|
package/dist/utils/ntp.d.ts
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
|
-
export type NtpTimestampInput = bigint | number | {
|
|
2
|
-
sec: number;
|
|
3
|
-
frac: number;
|
|
4
|
-
};
|
|
5
1
|
declare class NTP {
|
|
6
|
-
|
|
2
|
+
private readonly timeRef;
|
|
7
3
|
timestamp(): Buffer;
|
|
8
|
-
/** Return the current NTP fractional component (for compatibility). */
|
|
9
4
|
getTime(): number;
|
|
10
5
|
}
|
|
11
6
|
declare const _default: NTP;
|
|
12
7
|
export default _default;
|
|
13
|
-
/** Pack various NTP timestamp representations into a uint64 bigint (sec<<32|frac). */
|
|
14
|
-
export declare function toNtpTimestamp(input: NtpTimestampInput): bigint;
|
|
15
|
-
/** Parse an NTP timestamp (uint64 or sec/frac) into components. */
|
|
16
|
-
export declare function parseNtpTimestamp(input: NtpTimestampInput): {
|
|
17
|
-
sec: number;
|
|
18
|
-
frac: number;
|
|
19
|
-
};
|
|
20
|
-
/** Build an NTP timestamp from a Unix epoch (ms). */
|
|
21
|
-
export declare function ntpFromUnixMs(unixMs: number): bigint;
|
package/dist/utils/ntp.js
CHANGED
|
@@ -3,54 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.toNtpTimestamp = toNtpTimestamp;
|
|
7
|
-
exports.parseNtpTimestamp = parseNtpTimestamp;
|
|
8
|
-
exports.ntpFromUnixMs = ntpFromUnixMs;
|
|
9
6
|
const config_1 = __importDefault(require("./config"));
|
|
10
|
-
const NS_PER_SEC = 1000000000n;
|
|
11
|
-
const FRAC_PER_SEC = 0x100000000n; // 2^32
|
|
12
7
|
class NTP {
|
|
13
|
-
|
|
8
|
+
timeRef = Date.now() - config_1.default.ntp_epoch * 1000;
|
|
14
9
|
timestamp() {
|
|
15
|
-
const
|
|
16
|
-
const sec =
|
|
17
|
-
const
|
|
10
|
+
const time = Date.now() - this.timeRef;
|
|
11
|
+
const sec = Math.floor(time / 1000);
|
|
12
|
+
const msec = time - sec * 1000;
|
|
13
|
+
const ntp_msec = Math.floor(msec * 4294967.296);
|
|
18
14
|
const ts = Buffer.alloc(8);
|
|
19
|
-
ts.writeUInt32BE(sec
|
|
20
|
-
ts.writeUInt32BE(
|
|
15
|
+
ts.writeUInt32BE(sec, 0);
|
|
16
|
+
ts.writeUInt32BE(ntp_msec, 4);
|
|
21
17
|
return ts;
|
|
22
18
|
}
|
|
23
|
-
/** Return the current NTP fractional component (for compatibility). */
|
|
24
19
|
getTime() {
|
|
25
|
-
const
|
|
26
|
-
|
|
20
|
+
const time = Date.now() - this.timeRef;
|
|
21
|
+
const sec = Math.floor(time / 1000);
|
|
22
|
+
const msec = time - sec * 1000;
|
|
23
|
+
const ntp_msec = Math.floor(msec * 4294967.296);
|
|
24
|
+
return ntp_msec;
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
exports.default = new NTP();
|
|
30
|
-
/** Pack various NTP timestamp representations into a uint64 bigint (sec<<32|frac). */
|
|
31
|
-
function toNtpTimestamp(input) {
|
|
32
|
-
if (typeof input === 'bigint')
|
|
33
|
-
return input;
|
|
34
|
-
if (typeof input === 'number')
|
|
35
|
-
return BigInt(input);
|
|
36
|
-
const sec = BigInt(input.sec >>> 0);
|
|
37
|
-
const frac = BigInt(input.frac >>> 0);
|
|
38
|
-
return (sec << 32n) | frac;
|
|
39
|
-
}
|
|
40
|
-
/** Parse an NTP timestamp (uint64 or sec/frac) into components. */
|
|
41
|
-
function parseNtpTimestamp(input) {
|
|
42
|
-
if (typeof input === 'bigint' || typeof input === 'number') {
|
|
43
|
-
const value = typeof input === 'bigint' ? input : BigInt(input);
|
|
44
|
-
const sec = Number(value >> 32n);
|
|
45
|
-
const frac = Number(value & 0xffffffffn);
|
|
46
|
-
return { sec, frac };
|
|
47
|
-
}
|
|
48
|
-
return { sec: input.sec, frac: input.frac };
|
|
49
|
-
}
|
|
50
|
-
/** Build an NTP timestamp from a Unix epoch (ms). */
|
|
51
|
-
function ntpFromUnixMs(unixMs) {
|
|
52
|
-
const sec = Math.floor(unixMs / 1000) + config_1.default.ntp_epoch;
|
|
53
|
-
const ms = unixMs % 1000;
|
|
54
|
-
const frac = Math.floor((ms / 1000) * Number(FRAC_PER_SEC));
|
|
55
|
-
return (BigInt(sec >>> 0) << 32n) | BigInt(frac >>> 0);
|
|
56
|
-
}
|
package/package.json
CHANGED