@corewire/airplay 0.0.0 → 1.0.0-next.1
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 +30 -1
- package/dist/chunk-w6R9maHv.mjs +18 -0
- package/dist/describe-CAEDGJLu.mjs +12457 -0
- package/dist/identifiers-CsXXbYkW.mjs +67 -0
- package/dist/index-BlcOBfSz.d.mts +87 -0
- package/dist/index-YqIv0vCR.d.mts +135 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.mjs +3637 -0
- package/dist/mrp.d.mts +687 -0
- package/dist/mrp.mjs +2102 -0
- package/dist/raop.d.mts +344 -0
- package/dist/raop.mjs +1248 -0
- package/dist/rtsp-BWFaYW5H.mjs +374 -0
- package/dist/rtsp.d.mts +2 -0
- package/dist/rtsp.mjs +3 -0
- package/dist/session-oDF9A9as.d.mts +13915 -0
- package/package.json +72 -18
package/dist/raop.d.mts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { n as TimingServer, r as AudioSource } from "./index-YqIv0vCR.mjs";
|
|
2
|
+
import { n as RtspClient } from "./index-BlcOBfSz.mjs";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { CloseReason, Context } from "@corewire/core/transport";
|
|
5
|
+
import { Socket } from "node:dgram";
|
|
6
|
+
import { DiscoverOptions, DiscoveryResult } from "@corewire/core/discovery";
|
|
7
|
+
|
|
8
|
+
//#region src/raop/types.d.ts
|
|
9
|
+
/** Where a RAOP receiver is. */
|
|
10
|
+
type RaopTarget = {
|
|
11
|
+
readonly address: string; /** RTSP port of the RAOP service; 7000 when omitted. */
|
|
12
|
+
readonly port?: number; /** Tags the logs, usually the mDNS name. Defaults to the address. */
|
|
13
|
+
readonly id?: string; /** The RAOP TXT record; the encryption, metadata and audio properties come from it. */
|
|
14
|
+
readonly txt?: Readonly<Record<string, string>>;
|
|
15
|
+
};
|
|
16
|
+
/** The track a stream shows on the receiver. */
|
|
17
|
+
type MediaMetadata = {
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly artist: string;
|
|
20
|
+
readonly album: string; /** Duration in seconds. */
|
|
21
|
+
readonly duration: number; /** Album artwork as JPEG or PNG. */
|
|
22
|
+
readonly artwork?: Buffer;
|
|
23
|
+
};
|
|
24
|
+
type RaopPlaybackInfo = {
|
|
25
|
+
readonly metadata: MediaMetadata; /** Playback position in audio frames. */
|
|
26
|
+
readonly position: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* State shared by the RAOP streaming parts and updated in place: RTP state,
|
|
30
|
+
* audio format and the ports from RTSP SETUP.
|
|
31
|
+
*/
|
|
32
|
+
type StreamContext = {
|
|
33
|
+
/** Sample rate in Hz. */sampleRate: number;
|
|
34
|
+
channels: number; /** Bytes per sample per channel. */
|
|
35
|
+
bytesPerChannel: number; /** RTP sequence number, wraps at 16 bits. */
|
|
36
|
+
rtpseq: number; /** Initial RTP timestamp for the RECORD and FLUSH headers. */
|
|
37
|
+
rtptime: number; /** Timestamp of the latest frame sent. */
|
|
38
|
+
headTs: number; /** Latency in audio frames, usually 2 seconds. */
|
|
39
|
+
latency: number; /** Receiver audio data port from SETUP. */
|
|
40
|
+
serverPort: number; /** Receiver control port for sync and retransmits. */
|
|
41
|
+
controlPort: number;
|
|
42
|
+
rtspSession: string; /** Volume in dBFS. */
|
|
43
|
+
volume: number; /** Playback position in audio frames. */
|
|
44
|
+
position: number; /** Bytes per audio packet payload. */
|
|
45
|
+
packetSize: number; /** Bytes per frame: channels * bytesPerChannel. */
|
|
46
|
+
frameSize: number; /** Silence frames sent after the source ran out. */
|
|
47
|
+
paddingSent: number; /** Resets the RTP state, the padding counter and the position for a new stream. */
|
|
48
|
+
reset(): void;
|
|
49
|
+
};
|
|
50
|
+
/** The protocol steps to set up, feed and tear down an audio stream. */
|
|
51
|
+
interface StreamProtocol {
|
|
52
|
+
/**
|
|
53
|
+
* Sends RTSP ANNOUNCE and SETUP to negotiate the ports.
|
|
54
|
+
*
|
|
55
|
+
* @param timingPort - Local NTP timing server port.
|
|
56
|
+
* @param controlPort - Local control channel port.
|
|
57
|
+
*/
|
|
58
|
+
setup(timingPort: number, controlPort: number): Promise<void>;
|
|
59
|
+
/** Starts the periodic feedback requests that keep the session alive. */
|
|
60
|
+
startFeedback(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Sends one audio packet.
|
|
63
|
+
*
|
|
64
|
+
* @returns The sequence number and the full packet, for the backlog.
|
|
65
|
+
*/
|
|
66
|
+
sendAudioPacket(transport: Socket, header: Buffer, audio: Buffer): Promise<[number, Buffer]>;
|
|
67
|
+
/** Stops the feedback and releases the session. */
|
|
68
|
+
teardown(): void;
|
|
69
|
+
}
|
|
70
|
+
/** Local RAOP port bindings. */
|
|
71
|
+
interface Settings {
|
|
72
|
+
protocols: {
|
|
73
|
+
raop: {
|
|
74
|
+
/** Control channel port; 0 picks a free one. */controlPort: number; /** NTP timing server port; 0 picks a free one. */
|
|
75
|
+
timingPort: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Encryption types a RAOP receiver supports, from the `et` TXT field. */
|
|
80
|
+
declare enum EncryptionType {
|
|
81
|
+
/** The TXT record has no encryption field. */
|
|
82
|
+
Unknown = 0,
|
|
83
|
+
Unencrypted = 1,
|
|
84
|
+
/** The receiver supports MFi-SAP encryption, as the AirPort Express does. */
|
|
85
|
+
MFiSAP = 2
|
|
86
|
+
}
|
|
87
|
+
/** Metadata types a RAOP receiver supports, from the `md` TXT field. */
|
|
88
|
+
declare enum MetadataType {
|
|
89
|
+
NotSupported = 0,
|
|
90
|
+
/** The receiver shows title, artist and album. */
|
|
91
|
+
Text = 1,
|
|
92
|
+
Artwork = 2,
|
|
93
|
+
/** The receiver shows progress and duration. */
|
|
94
|
+
Progress = 4
|
|
95
|
+
}
|
|
96
|
+
/** How {@link connect} reaches the receiver and what a stream shows by default. */
|
|
97
|
+
type RaopConnectOptions = {
|
|
98
|
+
/** NTP timing server the receiver synchronizes with; SETUP announces its port. */readonly timing: TimingServer; /** Logger to use. Built from {@link RaopTarget.id} when omitted. */
|
|
99
|
+
readonly context?: Context; /** Shown on the receiver while a stream comes without metadata. Title `Streaming with CoreWire`, artist `CoreWire` and album `AirPlay` when omitted. */
|
|
100
|
+
readonly metadata?: Partial<Pick<MediaMetadata, "title" | "artist" | "album">>; /** Answers a digest challenge on ANNOUNCE from a receiver with a password. */
|
|
101
|
+
readonly password?: string; /** Ends the session, or interrupts the handshake, when aborted. */
|
|
102
|
+
readonly signal?: AbortSignal;
|
|
103
|
+
};
|
|
104
|
+
type RaopStreamOptions = {
|
|
105
|
+
/** Track metadata to show on the receiver; the defaults of {@link RaopConnectOptions.metadata} when omitted. */readonly metadata?: MediaMetadata; /** Volume in dBFS, -144 (silent) to 0, sent after FLUSH. The receiver keeps its own when omitted. */
|
|
106
|
+
readonly volumeDb?: number;
|
|
107
|
+
};
|
|
108
|
+
/** A running RAOP stream. */
|
|
109
|
+
type RaopPlayback = {
|
|
110
|
+
/**
|
|
111
|
+
* Settles when streaming is over: resolves once the source ran out and TEARDOWN went out,
|
|
112
|
+
* rejects when the stream failed.
|
|
113
|
+
*/
|
|
114
|
+
readonly ended: Promise<void>; /** Stops the source; the stream winds down and sends TEARDOWN. */
|
|
115
|
+
stop(): void;
|
|
116
|
+
};
|
|
117
|
+
type RaopEventMap = {
|
|
118
|
+
/** The session ended, see {@link CloseReason}. */closed: [reason: CloseReason, error?: Error]; /** A stream started, with the metadata it shows. */
|
|
119
|
+
playing: [playbackInfo: RaopPlaybackInfo]; /** A stream ended. */
|
|
120
|
+
stopped: [];
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/raop/rtspClient.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* RTSP client for the RAOP commands: ANNOUNCE, SETUP, RECORD, SET_PARAMETER,
|
|
126
|
+
* FLUSH and TEARDOWN, plus auth-setup, digest authentication, metadata and
|
|
127
|
+
* artwork.
|
|
128
|
+
*/
|
|
129
|
+
declare class RaopRtspClient extends RtspClient {
|
|
130
|
+
#private;
|
|
131
|
+
/** Active-Remote identifier for DACP remote control. */
|
|
132
|
+
get activeRemoteId(): string;
|
|
133
|
+
/** DACP identifier for remote control discovery. */
|
|
134
|
+
get dacpId(): string;
|
|
135
|
+
/** RTSP session identifier for session-scoped requests. */
|
|
136
|
+
get rtspSessionId(): string;
|
|
137
|
+
/** Numeric session identifier in the RTSP URI path. */
|
|
138
|
+
get sessionId(): number;
|
|
139
|
+
/** RTSP URI of this session, in the form `rtsp://<localIp>/<sessionId>`. */
|
|
140
|
+
get uri(): string;
|
|
141
|
+
get connection(): {
|
|
142
|
+
localIp: string;
|
|
143
|
+
remoteIp: string;
|
|
144
|
+
};
|
|
145
|
+
constructor(context: Context, address: string, port: number);
|
|
146
|
+
/** Headers on every request: the DACP identifiers, User-Agent and, after a challenge, digest authorization. */
|
|
147
|
+
protected override getDefaultHeaders(): Record<string, string | number>;
|
|
148
|
+
/**
|
|
149
|
+
* Fetches the device info plist from `/info`.
|
|
150
|
+
*
|
|
151
|
+
* @returns The info dictionary, or an empty object on failure.
|
|
152
|
+
*/
|
|
153
|
+
info(): Promise<Record<string, unknown>>;
|
|
154
|
+
/**
|
|
155
|
+
* Sends the Curve25519 public key to `/auth-setup`, which an AirPort
|
|
156
|
+
* Express with MFi-SAP encryption requires.
|
|
157
|
+
*/
|
|
158
|
+
authSetup(): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Sends ANNOUNCE with an SDP body for the audio format. On a 401
|
|
161
|
+
* challenge with a password given, it retries with HTTP Digest
|
|
162
|
+
* authentication.
|
|
163
|
+
*
|
|
164
|
+
* @param bytesPerChannel - Bytes per sample per channel, 2 for 16-bit.
|
|
165
|
+
* @param sampleRate - Sample rate in Hz.
|
|
166
|
+
*/
|
|
167
|
+
announce(bytesPerChannel: number, channels: number, sampleRate: number, password?: string): Promise<Response>;
|
|
168
|
+
/**
|
|
169
|
+
* Sends SETUP to negotiate the transport (ports, protocol).
|
|
170
|
+
*
|
|
171
|
+
* @param headers - Extra headers, for example Transport.
|
|
172
|
+
* @returns The response, with the receiver's ports in its Transport header.
|
|
173
|
+
*/
|
|
174
|
+
setup(headers?: Record<string, string>, body?: Buffer | string | Record<string, unknown>): Promise<Response>;
|
|
175
|
+
/**
|
|
176
|
+
* Sends RECORD to start playback on the receiver.
|
|
177
|
+
*
|
|
178
|
+
* @param headers - Usually Range, Session and RTP-Info.
|
|
179
|
+
*/
|
|
180
|
+
record(headers?: Record<string, string>): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Sends FLUSH to clear the receiver's audio buffer and move playback to
|
|
183
|
+
* the RTP position in the headers.
|
|
184
|
+
*
|
|
185
|
+
* @param options - Headers with Session and RTP-Info.
|
|
186
|
+
*/
|
|
187
|
+
flush(options: {
|
|
188
|
+
headers: Record<string, string>;
|
|
189
|
+
}): Promise<void>;
|
|
190
|
+
/**
|
|
191
|
+
* Sends a `text/parameters` SET_PARAMETER, for volume, progress and other
|
|
192
|
+
* scalar values.
|
|
193
|
+
*
|
|
194
|
+
* @param name - For example `volume` or `progress`.
|
|
195
|
+
*/
|
|
196
|
+
setParameter(name: string, value: string): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Sends title, artist, album and duration as DAAP-tagged SET_PARAMETER.
|
|
199
|
+
*
|
|
200
|
+
* @param rtpseq - RTP sequence number for the RTP-Info header.
|
|
201
|
+
* @param rtptime - RTP timestamp for the RTP-Info header.
|
|
202
|
+
*/
|
|
203
|
+
setMetadata(session: string, rtpseq: number, rtptime: number, metadata: MediaMetadata): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Sends album artwork as SET_PARAMETER. The magic bytes tell PNG apart;
|
|
206
|
+
* anything else goes as JPEG.
|
|
207
|
+
*
|
|
208
|
+
* @param rtpseq - RTP sequence number for the RTP-Info header.
|
|
209
|
+
* @param rtptime - RTP timestamp for the RTP-Info header.
|
|
210
|
+
*/
|
|
211
|
+
setArtwork(session: string, rtpseq: number, rtptime: number, artwork: Buffer): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Posts to `/feedback`, which keeps the session alive during streaming.
|
|
214
|
+
*
|
|
215
|
+
* @param allowError - Resolves on an HTTP error status instead of throwing.
|
|
216
|
+
*/
|
|
217
|
+
feedback(allowError?: boolean): Promise<Response>;
|
|
218
|
+
/** Sends TEARDOWN to end the streaming session. */
|
|
219
|
+
teardown(session: string): Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/raop/streamClient.d.ts
|
|
223
|
+
type EventMap = {
|
|
224
|
+
readonly playing: [playbackInfo: RaopPlaybackInfo]; /** The stream ended, at the end of the source or through {@link StreamClient.stop}. */
|
|
225
|
+
readonly stopped: [];
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Streams RAOP audio: negotiates encryption and metadata, sets up the control
|
|
229
|
+
* channel, sends PCM packets in real time, publishes metadata and artwork and
|
|
230
|
+
* tears down. When sending falls behind, it bursts extra packets to catch up.
|
|
231
|
+
*/
|
|
232
|
+
declare class StreamClient extends EventEmitter<EventMap> {
|
|
233
|
+
#private;
|
|
234
|
+
/** The `/info` dictionary fetched during initialization. */
|
|
235
|
+
get info(): Record<string, unknown>;
|
|
236
|
+
/** The playback info, with the fallback metadata when the caller gave none. */
|
|
237
|
+
get playbackInfo(): RaopPlaybackInfo;
|
|
238
|
+
/**
|
|
239
|
+
* @param missingMetadata - Shown on the receiver when a stream comes without metadata.
|
|
240
|
+
*/
|
|
241
|
+
constructor(context: Context, rtsp: RaopRtspClient, streamContext: StreamContext, protocol: StreamProtocol, settings: Settings, timingServer: TimingServer, missingMetadata: MediaMetadata);
|
|
242
|
+
/** Closes the control channel and its UDP socket. */
|
|
243
|
+
close(): void;
|
|
244
|
+
/**
|
|
245
|
+
* Reads the receiver's capabilities, binds the control channel, fetches
|
|
246
|
+
* `/info`, sends auth-setup when needed and runs the protocol SETUP.
|
|
247
|
+
*
|
|
248
|
+
* @param properties - The receiver's mDNS TXT record.
|
|
249
|
+
*/
|
|
250
|
+
initialize(properties: Map<string, string>): Promise<void>;
|
|
251
|
+
/**
|
|
252
|
+
* Stops the streaming loop after the current packet. {@link sendAudio}
|
|
253
|
+
* returns once the teardown is done.
|
|
254
|
+
*/
|
|
255
|
+
stop(): void;
|
|
256
|
+
/**
|
|
257
|
+
* Sets the receiver volume with SET_PARAMETER.
|
|
258
|
+
*
|
|
259
|
+
* @param volume - Volume in dBFS.
|
|
260
|
+
*/
|
|
261
|
+
setVolume(volume: number): Promise<void>;
|
|
262
|
+
/**
|
|
263
|
+
* Streams PCM after setting up metadata, volume and RTSP RECORD.
|
|
264
|
+
* On completion or error, clears the backlog, tears down RTSP, closes UDP and emits `stopped`.
|
|
265
|
+
*
|
|
266
|
+
* @param volumeDb - Initial volume in dBFS, -144 to 0; the receiver keeps its own when omitted.
|
|
267
|
+
* @param onStreaming - Runs once the setup is done and the audio starts to flow.
|
|
268
|
+
*/
|
|
269
|
+
sendAudio(source: AudioSource, metadata?: MediaMetadata, volumeDb?: number, onStreaming?: () => void): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/raop/session.d.ts
|
|
273
|
+
/**
|
|
274
|
+
* Connects to a RAOP (AirPlay 1 audio) receiver and returns a ready session: the RTSP connection,
|
|
275
|
+
* the control channel, `GET /info`, `/auth-setup` for an AirPort, then ANNOUNCE and SETUP.
|
|
276
|
+
*
|
|
277
|
+
* @throws {ConnectionClosedError} When the signal aborts during the handshake.
|
|
278
|
+
*/
|
|
279
|
+
declare function connect(target: RaopTarget, options: RaopConnectOptions): Promise<RaopSession>;
|
|
280
|
+
/**
|
|
281
|
+
* One RAOP connection to a receiver. It lives once: it ends exactly once, with a reason, and then
|
|
282
|
+
* its {@link signal} aborts and a running stream stops. One stream runs at a time.
|
|
283
|
+
*/
|
|
284
|
+
declare class RaopSession extends EventEmitter<RaopEventMap> {
|
|
285
|
+
#private;
|
|
286
|
+
/** Why the session ended, or undefined while it is open. */
|
|
287
|
+
get closeReason(): CloseReason | undefined;
|
|
288
|
+
get context(): Context;
|
|
289
|
+
/** The `/info` plist of the receiver; empty when it did not answer. */
|
|
290
|
+
get info(): Record<string, unknown>;
|
|
291
|
+
get isOpen(): boolean;
|
|
292
|
+
/** Aborts with a {@link ConnectionClosedError} when the session ends. */
|
|
293
|
+
get signal(): AbortSignal;
|
|
294
|
+
get target(): RaopTarget;
|
|
295
|
+
/** The volume last sent to the receiver, in dBFS. */
|
|
296
|
+
get volumeDb(): number;
|
|
297
|
+
/** Use {@link connect}; a session starts from a finished handshake. */
|
|
298
|
+
constructor(target: RaopTarget, context: Context, rtsp: RaopRtspClient, streamClient: StreamClient, streamContext: StreamContext, signal?: AbortSignal);
|
|
299
|
+
/** Stops a running stream and closes the connection, without TEARDOWN of an idle session. */
|
|
300
|
+
close(): Promise<void>;
|
|
301
|
+
/** Sends a volume in dBFS, -144 (silent) to 0, with SET_PARAMETER. */
|
|
302
|
+
setVolumeDb(volumeDb: number): Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* Streams PCM audio: metadata, RECORD, FLUSH and the volume, then the audio over RTP. Resolves
|
|
305
|
+
* once the audio flows; {@link RaopPlayback.ended} settles when the source ran out and TEARDOWN
|
|
306
|
+
* went out. The source is stopped in every case.
|
|
307
|
+
*
|
|
308
|
+
* @throws {PlaybackError} When a stream is already running.
|
|
309
|
+
*/
|
|
310
|
+
stream(source: AudioSource, options?: RaopStreamOptions): Promise<RaopPlayback>;
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/raop/discovery.d.ts
|
|
314
|
+
declare const RAOP_SERVICE = "_raop._tcp.local";
|
|
315
|
+
/** Finds the RAOP (AirPlay 1 audio) receivers on the local network. */
|
|
316
|
+
declare function discover(options?: DiscoverOptions): Promise<DiscoveryResult[]>;
|
|
317
|
+
/** The connect target for a discovered receiver. */
|
|
318
|
+
declare function toTarget(result: DiscoveryResult): RaopTarget;
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/raop/utils.d.ts
|
|
321
|
+
/**
|
|
322
|
+
* Converts a volume percentage to dBFS for the RAOP `volume` SET_PARAMETER.
|
|
323
|
+
*
|
|
324
|
+
* @param volume - Volume percentage, from 0 (silent) to 100 (full).
|
|
325
|
+
* @returns Volume in dBFS, from -144 (mute) to 0 (full).
|
|
326
|
+
*/
|
|
327
|
+
declare function pctToDbfs(volume: number): number;
|
|
328
|
+
/**
|
|
329
|
+
* Reads the encryption types from the `et` field of the mDNS TXT record.
|
|
330
|
+
*/
|
|
331
|
+
declare function getEncryptionTypes(properties: Map<string, string>): EncryptionType;
|
|
332
|
+
/**
|
|
333
|
+
* Reads the metadata types from the `md` field of the mDNS TXT record.
|
|
334
|
+
*/
|
|
335
|
+
declare function getMetadataTypes(properties: Map<string, string>): MetadataType;
|
|
336
|
+
/**
|
|
337
|
+
* Reads the audio format from the mDNS TXT record, with CD quality
|
|
338
|
+
* (44100 Hz, 2 channels, 16-bit) for missing fields.
|
|
339
|
+
*
|
|
340
|
+
* @returns `[sampleRate, channels, bytesPerChannel]`.
|
|
341
|
+
*/
|
|
342
|
+
declare function getAudioProperties(properties: Map<string, string>): [number, number, number];
|
|
343
|
+
//#endregion
|
|
344
|
+
export { EncryptionType, type MediaMetadata, MetadataType, RAOP_SERVICE, type RaopConnectOptions, type RaopEventMap, type RaopPlayback, type RaopPlaybackInfo, RaopSession, type RaopStreamOptions, type RaopTarget, connect, discover, getAudioProperties, getEncryptionTypes, getMetadataTypes, pctToDbfs, toTarget };
|