@basmilius/apple-raop 0.9.16 → 0.9.18
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 +6 -4
- package/dist/index.mjs +39 -133
- package/package.json +5 -4
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,
|
|
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
|
|
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:
|
|
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
|
|
3
|
+
import { DAAP, NTP, Plist } from "@basmilius/apple-encoding";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import {
|
|
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
|
|
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 =
|
|
308
|
-
this.on("
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
384
|
+
await this.exchange("RECORD", this.uri, { headers });
|
|
380
385
|
}
|
|
381
386
|
async flush(options) {
|
|
382
|
-
await this
|
|
387
|
+
await this.exchange("FLUSH", this.uri, { headers: options.headers });
|
|
383
388
|
}
|
|
384
389
|
async setParameter(name, value) {
|
|
385
|
-
await this
|
|
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
|
|
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
|
|
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
|
|
424
|
+
return await this.exchange("POST", "/feedback", { allowError });
|
|
420
425
|
}
|
|
421
426
|
async teardown(session) {
|
|
422
|
-
await this
|
|
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
|
|
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.
|
|
4
|
+
"version": "0.9.18",
|
|
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.
|
|
50
|
-
"@basmilius/apple-encoding": "0.9.
|
|
51
|
-
"@basmilius/apple-encryption": "0.9.
|
|
49
|
+
"@basmilius/apple-common": "0.9.18",
|
|
50
|
+
"@basmilius/apple-encoding": "0.9.18",
|
|
51
|
+
"@basmilius/apple-encryption": "0.9.18",
|
|
52
|
+
"@basmilius/apple-rtsp": "0.9.18"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@types/bun": "^1.3.11",
|