@basmilius/apple-raop 0.9.16 → 0.9.17

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/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Socket } from "node:dgram";
2
2
  import { EventEmitter } from "node:events";
3
- import { AudioSource, Connection, Context, DiscoveryResult, TimingServer } from "@basmilius/apple-common";
3
+ import { AudioSource, Context, DiscoveryResult, TimingServer } from "@basmilius/apple-common";
4
+ import { RtspClient } from "@basmilius/apple-rtsp";
4
5
 
5
6
  //#region src/types.d.ts
6
7
  type MediaMetadata = {
@@ -101,7 +102,7 @@ declare class ControlClient extends EventEmitter {
101
102
  }
102
103
  //#endregion
103
104
  //#region src/rtspClient.d.ts
104
- declare class RtspClient extends Connection<{}> {
105
+ declare class RaopRtspClient extends RtspClient {
105
106
  #private;
106
107
  get activeRemoteId(): string;
107
108
  get dacpId(): string;
@@ -113,6 +114,7 @@ declare class RtspClient extends Connection<{}> {
113
114
  remoteIp: string;
114
115
  };
115
116
  constructor(context: Context, address: string, port: number);
117
+ protected getDefaultHeaders(): Record<string, string | number>;
116
118
  info(): Promise<Record<string, unknown>>;
117
119
  authSetup(): Promise<void>;
118
120
  announce(bytesPerChannel: number, channels: number, sampleRate: number, password?: string): Promise<Response>;
@@ -152,7 +154,7 @@ declare class StreamClient extends EventEmitter<EventMap$1> {
152
154
  #private;
153
155
  get info(): Record<string, unknown>;
154
156
  get playbackInfo(): PlaybackInfo;
155
- constructor(context: Context, rtsp: RtspClient, streamContext: StreamContext, protocol: StreamProtocol, settings: Settings, timingServer: TimingServer);
157
+ constructor(context: Context, rtsp: RaopRtspClient, streamContext: StreamContext, protocol: StreamProtocol, settings: Settings, timingServer: TimingServer);
156
158
  close(): void;
157
159
  initialize(properties: Map<string, string>): Promise<void>;
158
160
  stop(): void;
@@ -185,4 +187,4 @@ declare class RaopClient extends EventEmitter<EventMap> {
185
187
  static discover(deviceId: string, timingServer: TimingServer): Promise<RaopClient>;
186
188
  }
187
189
  //#endregion
188
- export { AudioPacketHeader, ControlClient, EncryptionType, MediaMetadata, MetadataType, PacketFifo, PlaybackInfo, RaopClient, RaopListener, RetransmitRequest, RtspClient, Settings, Statistics, StreamClient, StreamContext, type StreamOptions, StreamProtocol, SyncPacket, decodeRetransmitRequest, getAudioProperties, getEncryptionTypes, getMetadataTypes, pctToDbfs };
190
+ export { AudioPacketHeader, ControlClient, EncryptionType, MediaMetadata, MetadataType, PacketFifo, PlaybackInfo, RaopClient, RaopListener, RetransmitRequest, RaopRtspClient as RtspClient, Settings, Statistics, StreamClient, StreamContext, type StreamOptions, StreamProtocol, SyncPacket, decodeRetransmitRequest, getAudioProperties, getEncryptionTypes, getMetadataTypes, pctToDbfs };
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createSocket } from "node:dgram";
2
2
  import { EventEmitter } from "node:events";
3
- import { DAAP, NTP, Plist, RTSP } from "@basmilius/apple-encoding";
3
+ import { DAAP, NTP, Plist } from "@basmilius/apple-encoding";
4
4
  import { createHash } from "node:crypto";
5
- import { Connection, Context, Discovery, HTTP_TIMEOUT, generateActiveRemoteId, generateDacpId, generateSessionId, waitFor } from "@basmilius/apple-common";
5
+ import { Context, Discovery, generateActiveRemoteId, generateDacpId, generateSessionId, waitFor } from "@basmilius/apple-common";
6
+ import { RtspClient } from "@basmilius/apple-rtsp";
6
7
 
7
8
  //#region src/types.ts
8
9
  let EncryptionType = /* @__PURE__ */ function(EncryptionType) {
@@ -253,9 +254,6 @@ function getDigestPayload(method, uri, info) {
253
254
  const response = createHash("md5").update(`${ha1}:${info.nonce}:${ha2}`).digest("hex");
254
255
  return `Digest username="${info.username}", realm="${info.realm}", nonce="${info.nonce}", uri="${uri}", response="${response}"`;
255
256
  }
256
- function generateRandomSessionId() {
257
- return Math.floor(Math.random() * 4294967295);
258
- }
259
257
  function buildAnnouncePayload(options) {
260
258
  return [
261
259
  "v=0",
@@ -268,7 +266,7 @@ function buildAnnouncePayload(options) {
268
266
  `a=fmtp:96 ${FRAMES_PER_PACKET$1} 0 ${options.bitsPerChannel} 40 10 14 ${options.channels} 255 0 0 ${options.sampleRate}`
269
267
  ].join("\r\n") + "\r\n";
270
268
  }
271
- var RtspClient = class extends Connection {
269
+ var RaopRtspClient = class extends RtspClient {
272
270
  get activeRemoteId() {
273
271
  return this.#activeRemoteId;
274
272
  }
@@ -295,41 +293,48 @@ var RtspClient = class extends Connection {
295
293
  #rtspSessionId;
296
294
  #sessionId;
297
295
  #localIp = "0.0.0.0";
298
- #buffer = Buffer.alloc(0);
299
- #cseq = 0;
300
296
  #digestInfo;
301
- #requests = /* @__PURE__ */ new Map();
302
297
  constructor(context, address, port) {
303
298
  super(context, address, port);
304
299
  this.#activeRemoteId = generateActiveRemoteId();
305
300
  this.#dacpId = generateDacpId();
306
301
  this.#rtspSessionId = generateSessionId();
307
- this.#sessionId = generateRandomSessionId();
308
- this.on("close", this.#onClose.bind(this));
309
- this.on("data", this.#onData.bind(this));
310
- this.on("error", this.#onError.bind(this));
311
- this.on("timeout", this.#onTimeout.bind(this));
312
- this.on("connect", this.#onConnect.bind(this));
302
+ this.#sessionId = Math.floor(Math.random() * 4294967295);
303
+ this.on("connect", () => {
304
+ this.#localIp = "0.0.0.0";
305
+ });
306
+ }
307
+ getDefaultHeaders() {
308
+ const headers = {
309
+ "DACP-ID": this.#dacpId,
310
+ "Active-Remote": this.#activeRemoteId,
311
+ "Client-Instance": this.#dacpId,
312
+ "User-Agent": USER_AGENT
313
+ };
314
+ if (this.#digestInfo) headers["Authorization"] = getDigestPayload("", this.uri, this.#digestInfo);
315
+ return headers;
313
316
  }
314
317
  async info() {
315
318
  try {
316
- const response = await this.#exchange("GET", "/info", { allowError: true });
319
+ const response = await this.exchange("GET", "/info", { allowError: true });
317
320
  if (response.ok) {
318
321
  const buffer = Buffer.from(await response.arrayBuffer());
319
322
  if (buffer.length > 0) try {
320
323
  return Plist.parse(buffer.buffer);
321
- } catch {
324
+ } catch (err) {
325
+ this.context.logger.warn("[raop-rtsp]", "Failed to parse info plist", err);
322
326
  return {};
323
327
  }
324
328
  }
325
329
  return {};
326
- } catch {
330
+ } catch (err) {
331
+ this.context.logger.warn("[raop-rtsp]", "Failed to get device info", err);
327
332
  return {};
328
333
  }
329
334
  }
330
335
  async authSetup() {
331
336
  const body = Buffer.concat([AUTH_SETUP_UNENCRYPTED, CURVE25519_PUB_KEY]);
332
- await this.#exchange("POST", "/auth-setup", {
337
+ await this.exchange("POST", "/auth-setup", {
333
338
  contentType: "application/octet-stream",
334
339
  body,
335
340
  protocol: "HTTP/1.1"
@@ -344,7 +349,7 @@ var RtspClient = class extends Connection {
344
349
  channels,
345
350
  sampleRate
346
351
  });
347
- let response = await this.#exchange("ANNOUNCE", void 0, {
352
+ let response = await this.exchange("ANNOUNCE", this.uri, {
348
353
  contentType: "application/sdp",
349
354
  body,
350
355
  allowError: !!password
@@ -360,7 +365,7 @@ var RtspClient = class extends Connection {
360
365
  password,
361
366
  nonce: parts[3]
362
367
  };
363
- response = await this.#exchange("ANNOUNCE", void 0, {
368
+ response = await this.exchange("ANNOUNCE", this.uri, {
364
369
  contentType: "application/sdp",
365
370
  body
366
371
  });
@@ -370,19 +375,19 @@ var RtspClient = class extends Connection {
370
375
  return response;
371
376
  }
372
377
  async setup(headers, body) {
373
- return await this.#exchange("SETUP", void 0, {
378
+ return await this.exchange("SETUP", this.uri, {
374
379
  headers,
375
380
  body
376
381
  });
377
382
  }
378
383
  async record(headers) {
379
- await this.#exchange("RECORD", void 0, { headers });
384
+ await this.exchange("RECORD", this.uri, { headers });
380
385
  }
381
386
  async flush(options) {
382
- await this.#exchange("FLUSH", void 0, { headers: options.headers });
387
+ await this.exchange("FLUSH", this.uri, { headers: options.headers });
383
388
  }
384
389
  async setParameter(name, value) {
385
- await this.#exchange("SET_PARAMETER", void 0, {
390
+ await this.exchange("SET_PARAMETER", this.uri, {
386
391
  contentType: "text/parameters",
387
392
  body: `${name}: ${value}`
388
393
  });
@@ -394,7 +399,7 @@ var RtspClient = class extends Connection {
394
399
  album: metadata.album,
395
400
  duration: metadata.duration
396
401
  });
397
- await this.#exchange("SET_PARAMETER", void 0, {
402
+ await this.exchange("SET_PARAMETER", this.uri, {
398
403
  contentType: "application/x-dmap-tagged",
399
404
  headers: {
400
405
  "Session": session,
@@ -406,7 +411,7 @@ var RtspClient = class extends Connection {
406
411
  async setArtwork(session, rtpseq, rtptime, artwork) {
407
412
  let contentType = "image/jpeg";
408
413
  if (artwork[0] === 137 && artwork[1] === 80) contentType = "image/png";
409
- await this.#exchange("SET_PARAMETER", void 0, {
414
+ await this.exchange("SET_PARAMETER", this.uri, {
410
415
  contentType,
411
416
  headers: {
412
417
  "Session": session,
@@ -416,111 +421,10 @@ var RtspClient = class extends Connection {
416
421
  });
417
422
  }
418
423
  async feedback(allowError = false) {
419
- return await this.#exchange("POST", "/feedback", { allowError });
424
+ return await this.exchange("POST", "/feedback", { allowError });
420
425
  }
421
426
  async teardown(session) {
422
- await this.#exchange("TEARDOWN", void 0, { headers: { "Session": session } });
423
- }
424
- async #exchange(method, uri, options = {}) {
425
- const { contentType, headers: extraHeaders = {}, allowError = false, protocol = "RTSP/1.0", timeout = HTTP_TIMEOUT } = options;
426
- let { body } = options;
427
- const cseq = this.#cseq++;
428
- const targetUri = uri ?? this.uri;
429
- const headers = {
430
- "CSeq": cseq,
431
- "DACP-ID": this.#dacpId,
432
- "Active-Remote": this.#activeRemoteId,
433
- "Client-Instance": this.#dacpId,
434
- "User-Agent": USER_AGENT
435
- };
436
- if (this.#digestInfo) headers["Authorization"] = getDigestPayload(method, targetUri, this.#digestInfo);
437
- Object.assign(headers, extraHeaders);
438
- if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
439
- headers["Content-Type"] = "application/x-apple-binary-plist";
440
- body = Buffer.from(Plist.serialize(body));
441
- } else if (contentType) headers["Content-Type"] = contentType;
442
- let bodyBuffer;
443
- if (body) {
444
- bodyBuffer = typeof body === "string" ? Buffer.from(body) : body;
445
- headers["Content-Length"] = bodyBuffer.length;
446
- } else headers["Content-Length"] = 0;
447
- const headerLines = [
448
- `${method} ${targetUri} ${protocol}`,
449
- ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
450
- "",
451
- ""
452
- ].join("\r\n");
453
- const data = bodyBuffer ? Buffer.concat([Buffer.from(headerLines), bodyBuffer]) : Buffer.from(headerLines);
454
- this.context.logger.net("[rtsp]", method, targetUri, `cseq=${cseq}`);
455
- return new Promise((resolve, reject) => {
456
- this.#requests.set(cseq, {
457
- resolve,
458
- reject
459
- });
460
- const timer = setTimeout(() => {
461
- this.#requests.delete(cseq);
462
- reject(/* @__PURE__ */ new Error(`No response to CSeq ${cseq} (${targetUri})`));
463
- }, timeout);
464
- this.write(data);
465
- const originalResolve = resolve;
466
- this.#requests.set(cseq, {
467
- resolve: (response) => {
468
- clearTimeout(timer);
469
- if (!allowError && !response.ok) reject(/* @__PURE__ */ new Error(`RTSP error: ${response.status} ${response.statusText}`));
470
- else originalResolve(response);
471
- },
472
- reject: (error) => {
473
- clearTimeout(timer);
474
- reject(error);
475
- }
476
- });
477
- });
478
- }
479
- #onConnect() {
480
- this.#localIp = "0.0.0.0";
481
- }
482
- #onClose() {
483
- this.#buffer = Buffer.alloc(0);
484
- for (const [cseq, { reject }] of this.#requests) {
485
- reject(/* @__PURE__ */ new Error("Connection closed"));
486
- this.#requests.delete(cseq);
487
- }
488
- this.context.logger.net("[rtsp]", "#onClose()");
489
- }
490
- #onData(data) {
491
- try {
492
- this.#buffer = Buffer.concat([this.#buffer, data]);
493
- while (this.#buffer.byteLength > 0) {
494
- const result = RTSP.makeResponse(this.#buffer);
495
- if (result === null) return;
496
- this.#buffer = this.#buffer.subarray(result.responseLength);
497
- const cseqHeader = result.response.headers.get("CSeq");
498
- const cseq = cseqHeader ? parseInt(cseqHeader, 10) : -1;
499
- if (this.#requests.has(cseq)) {
500
- const { resolve } = this.#requests.get(cseq);
501
- this.#requests.delete(cseq);
502
- resolve(result.response);
503
- } else this.context.logger.warn("[rtsp]", `Unexpected response for CSeq ${cseq}`);
504
- }
505
- } catch (err) {
506
- this.context.logger.error("[rtsp]", "#onData()", err);
507
- this.emit("error", err);
508
- }
509
- }
510
- #onError(err) {
511
- for (const [cseq, { reject }] of this.#requests) {
512
- reject(err);
513
- this.#requests.delete(cseq);
514
- }
515
- this.context.logger.error("[rtsp]", "#onError()", err);
516
- }
517
- #onTimeout() {
518
- const err = /* @__PURE__ */ new Error("Connection timed out");
519
- for (const [cseq, { reject }] of this.#requests) {
520
- reject(err);
521
- this.#requests.delete(cseq);
522
- }
523
- this.context.logger.net("[rtsp]", "#onTimeout()");
427
+ await this.exchange("TEARDOWN", this.uri, { headers: { "Session": session } });
524
428
  }
525
429
  };
526
430
 
@@ -833,7 +737,7 @@ var RaopClient = class RaopClient extends EventEmitter {
833
737
  }
834
738
  static async create(discoveryResult, timingServer) {
835
739
  const context = new Context(discoveryResult.id);
836
- const rtsp = new RtspClient(context, discoveryResult.address, discoveryResult.service.port);
740
+ const rtsp = new RaopRtspClient(context, discoveryResult.address, discoveryResult.service.port);
837
741
  await rtsp.connect();
838
742
  const streamContext = createStreamContext();
839
743
  streamContext.rtspSession = rtsp.rtspSessionId;
@@ -879,7 +783,9 @@ var RaopStreamProtocol = class {
879
783
  this.#feedbackInterval = setInterval(async () => {
880
784
  try {
881
785
  await this.#rtsp.feedback(true);
882
- } catch {}
786
+ } catch (err) {
787
+ this.#rtsp.context.logger.warn("[raop]", "Feedback failed", err);
788
+ }
883
789
  }, 2e3);
884
790
  }
885
791
  async sendAudioPacket(transport, header, audio) {
@@ -924,4 +830,4 @@ function createStreamContext() {
924
830
  }
925
831
 
926
832
  //#endregion
927
- export { AudioPacketHeader, ControlClient, EncryptionType, MetadataType, PacketFifo, RaopClient, RtspClient, Statistics, StreamClient, SyncPacket, decodeRetransmitRequest, getAudioProperties, getEncryptionTypes, getMetadataTypes, pctToDbfs };
833
+ export { AudioPacketHeader, ControlClient, EncryptionType, MetadataType, PacketFifo, RaopClient, RaopRtspClient as RtspClient, Statistics, StreamClient, SyncPacket, decodeRetransmitRequest, getAudioProperties, getEncryptionTypes, getMetadataTypes, pctToDbfs };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@basmilius/apple-raop",
3
3
  "description": "Implementation of Apple's RAOP protocol in Node.js.",
4
- "version": "0.9.16",
4
+ "version": "0.9.17",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -46,9 +46,10 @@
46
46
  }
47
47
  },
48
48
  "dependencies": {
49
- "@basmilius/apple-common": "0.9.16",
50
- "@basmilius/apple-encoding": "0.9.16",
51
- "@basmilius/apple-encryption": "0.9.16"
49
+ "@basmilius/apple-common": "0.9.17",
50
+ "@basmilius/apple-encoding": "0.9.17",
51
+ "@basmilius/apple-encryption": "0.9.17",
52
+ "@basmilius/apple-rtsp": "0.9.17"
52
53
  },
53
54
  "devDependencies": {
54
55
  "@types/bun": "^1.3.11",