@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.
@@ -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
- /** Convert monotonic clock to NTP epoch (1900-01-01) seconds + fractional. */
8
+ timeRef = Date.now() - config_1.default.ntp_epoch * 1000;
14
9
  timestamp() {
15
- const nowNs = process.hrtime.bigint();
16
- const sec = Number(nowNs / NS_PER_SEC) + config_1.default.ntp_epoch;
17
- const frac = Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
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 >>> 0, 0);
20
- ts.writeUInt32BE(frac >>> 0, 4);
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 nowNs = process.hrtime.bigint();
26
- return Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
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, seq: number): Buffer;
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, seq) {
94
- const nonce = Buffer.alloc(12, 0);
95
- nonce.writeUInt16BE(seq & 0xffff, 4);
96
- const [cipher, tag] = encryption_1.default.encryptAndSeal(message, aad, nonce, this.writeKey);
97
- // Reference appends auth tag and nonce (without leading zeros)
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) => {
@@ -14,7 +14,6 @@ export default class CircularBuffer extends EventEmitter {
14
14
  private buffers;
15
15
  private currentSize;
16
16
  private status;
17
- private hadUnderrun;
18
17
  constructor(packetsInBuffer: number, packetSize: number);
19
18
  /**
20
19
  * Write a PCM/ALAC chunk into the buffer.
@@ -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;
@@ -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;
@@ -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: 0, // UDP sync packets are sent to all AirTunes devices regularly (0 = derive from sample rate)
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: 2147483647, // RTSP servers are considered gone if no reply is received before the timeout (legacy default)
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',
@@ -1,21 +1,7 @@
1
- export type NtpTimestampInput = bigint | number | {
2
- sec: number;
3
- frac: number;
4
- };
5
1
  declare class NTP {
6
- /** Convert monotonic clock to NTP epoch (1900-01-01) seconds + fractional. */
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
- /** Convert monotonic clock to NTP epoch (1900-01-01) seconds + fractional. */
8
+ timeRef = Date.now() - config_1.default.ntp_epoch * 1000;
14
9
  timestamp() {
15
- const nowNs = process.hrtime.bigint();
16
- const sec = Number(nowNs / NS_PER_SEC) + config_1.default.ntp_epoch;
17
- const frac = Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
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 >>> 0, 0);
20
- ts.writeUInt32BE(frac >>> 0, 4);
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 nowNs = process.hrtime.bigint();
26
- return Number(((nowNs % NS_PER_SEC) * FRAC_PER_SEC) / NS_PER_SEC);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lox-audioserver/node-airplay-sender",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "AirPlay sender (RAOP/AirPlay 1; AirPlay 2 control/auth, best-effort audio)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",