@corewire/airplay 0.0.0 → 1.0.0-beta.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 +3638 -0
- package/dist/mrp.d.mts +687 -0
- package/dist/mrp.mjs +2112 -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 +76 -18
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { ConnectionClosedError, ConnectionTimeoutError, InvalidResponseError, TimeoutError, reporter } from "@corewire/core";
|
|
2
|
+
import { Plist } from "@corewire/core/encoding";
|
|
3
|
+
import { Connection } from "@corewire/core/transport";
|
|
4
|
+
|
|
5
|
+
//#region src/const.ts
|
|
6
|
+
/** RTSP port of an AirPlay receiver when discovery did not say otherwise. */
|
|
7
|
+
const DEFAULT_PORT = 7e3;
|
|
8
|
+
/** Timeout in milliseconds for HTTP and RTSP requests during setup and control. */
|
|
9
|
+
const HTTP_TIMEOUT = 6e3;
|
|
10
|
+
/** Interval in milliseconds of the `/feedback` keepalive a receiver expects. */
|
|
11
|
+
const FEEDBACK_INTERVAL = 2e3;
|
|
12
|
+
/** Default bytes per sample per channel, for 16-bit PCM. */
|
|
13
|
+
const AUDIO_BYTES_PER_CHANNEL = 2;
|
|
14
|
+
/** Default number of audio channels, for stereo. */
|
|
15
|
+
const AUDIO_CHANNELS = 2;
|
|
16
|
+
/** Default sample rate in Hz, for CD quality. */
|
|
17
|
+
const AUDIO_SAMPLE_RATE = 44100;
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/rtsp/encoding.ts
|
|
21
|
+
const MAX_HEADER_LENGTH = 64 * 1024;
|
|
22
|
+
const MAX_BODY_LENGTH = 16 * 1024 * 1024;
|
|
23
|
+
/** Serializes an RTSP or HTTP response, with `Content-Length` set from the body. */
|
|
24
|
+
function buildResponse(options) {
|
|
25
|
+
const { status, statusText, headers: extraHeaders = {}, body, protocol = "RTSP/1.0" } = options;
|
|
26
|
+
const headers = {
|
|
27
|
+
...extraHeaders,
|
|
28
|
+
"Content-Length": body?.byteLength ?? 0
|
|
29
|
+
};
|
|
30
|
+
const headerLines = [
|
|
31
|
+
`${protocol} ${status} ${statusText}`,
|
|
32
|
+
...Object.entries(headers).map(([name, value]) => `${name}: ${value}`),
|
|
33
|
+
"",
|
|
34
|
+
""
|
|
35
|
+
].join("\r\n");
|
|
36
|
+
if (body && body.byteLength > 0) return Buffer.concat([Buffer.from(headerLines), body]);
|
|
37
|
+
return Buffer.from(headerLines);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses an RTSP or HTTP request from the start of a buffer that fills as
|
|
41
|
+
* TCP data arrives.
|
|
42
|
+
*
|
|
43
|
+
* @returns The request, or `null` while the headers or body are incomplete.
|
|
44
|
+
* @throws Error if the request line is malformed.
|
|
45
|
+
*/
|
|
46
|
+
function parseRequest(buffer) {
|
|
47
|
+
const headerLength = buffer.indexOf("\r\n\r\n");
|
|
48
|
+
if (headerLength > MAX_HEADER_LENGTH || headerLength === -1 && buffer.length > MAX_HEADER_LENGTH) throw new Error("RTSP headers exceed maximum length");
|
|
49
|
+
if (headerLength === -1) return null;
|
|
50
|
+
const { headers, method, path } = parseRequestHeaders(buffer.subarray(0, headerLength));
|
|
51
|
+
const contentLength = parseContentLength(headers);
|
|
52
|
+
const requestLength = headerLength + 4 + contentLength;
|
|
53
|
+
if (buffer.byteLength < requestLength) return null;
|
|
54
|
+
return {
|
|
55
|
+
headers,
|
|
56
|
+
method,
|
|
57
|
+
path,
|
|
58
|
+
body: buffer.subarray(headerLength + 4, requestLength),
|
|
59
|
+
requestLength
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Parses an RTSP or HTTP response from the start of a buffer that fills as
|
|
64
|
+
* TCP data arrives.
|
|
65
|
+
*
|
|
66
|
+
* @returns The response, or `null` while the headers or body are incomplete.
|
|
67
|
+
* @throws Error if the status line is malformed.
|
|
68
|
+
*/
|
|
69
|
+
function parseResponse(buffer) {
|
|
70
|
+
const headerLength = buffer.indexOf("\r\n\r\n");
|
|
71
|
+
if (headerLength > MAX_HEADER_LENGTH || headerLength === -1 && buffer.length > MAX_HEADER_LENGTH) throw new Error("RTSP headers exceed maximum length");
|
|
72
|
+
if (headerLength === -1) return null;
|
|
73
|
+
const { headers, status, statusText } = parseResponseHeaders(buffer.subarray(0, headerLength));
|
|
74
|
+
const contentLength = parseContentLength(headers);
|
|
75
|
+
const responseLength = headerLength + 4 + contentLength;
|
|
76
|
+
if (buffer.byteLength < responseLength) return null;
|
|
77
|
+
const body = buffer.subarray(headerLength + 4, responseLength);
|
|
78
|
+
return {
|
|
79
|
+
response: new Response(status === 204 || status === 205 || status === 304 ? null : new Uint8Array(body), {
|
|
80
|
+
status,
|
|
81
|
+
statusText,
|
|
82
|
+
headers
|
|
83
|
+
}),
|
|
84
|
+
responseLength
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parses header lines, without the request or status line. Names become
|
|
89
|
+
* capitalized (`content-length` to `Content-Length`), so lookups ignore the
|
|
90
|
+
* sender's casing. Lines without a colon are skipped.
|
|
91
|
+
*/
|
|
92
|
+
function parseHeaders(lines) {
|
|
93
|
+
const headers = {};
|
|
94
|
+
for (let i = 0; i < lines.length; i++) {
|
|
95
|
+
const colon = lines[i].indexOf(":");
|
|
96
|
+
if (colon <= 0) continue;
|
|
97
|
+
const rawName = lines[i].substring(0, colon).trim();
|
|
98
|
+
const name = rawName.toLowerCase() === "cseq" ? "CSeq" : rawName.toLowerCase().replace(/(^|-)(\w)/g, (_, prefix, char) => prefix + char.toUpperCase());
|
|
99
|
+
const value = lines[i].substring(colon + 1).trim();
|
|
100
|
+
if (name === "Content-Length" && headers[name] !== void 0 && headers[name] !== value) throw new Error("Conflicting Content-Length headers");
|
|
101
|
+
headers[name] = value;
|
|
102
|
+
}
|
|
103
|
+
return headers;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Parses the header section of a request, with a first line such as
|
|
107
|
+
* `METHOD /path RTSP/1.0` or `METHOD /path HTTP/1.1`.
|
|
108
|
+
*
|
|
109
|
+
* @param buffer - The header bytes, without the `\r\n\r\n` delimiter.
|
|
110
|
+
* @throws Error if the request line does not match.
|
|
111
|
+
*/
|
|
112
|
+
function parseRequestHeaders(buffer) {
|
|
113
|
+
const lines = buffer.toString("utf8").split("\r\n");
|
|
114
|
+
const rawRequest = lines[0].match(/^(\S+)\s+(\S+)\s+(?:RTSP|HTTP)\/[\d.]+$/);
|
|
115
|
+
if (!rawRequest) throw new Error(`Invalid RTSP/HTTP request line: ${lines[0]}`);
|
|
116
|
+
const method = rawRequest[1];
|
|
117
|
+
const path = rawRequest[2];
|
|
118
|
+
return {
|
|
119
|
+
headers: parseHeaders(lines.slice(1)),
|
|
120
|
+
method,
|
|
121
|
+
path
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parses the header section of a response, with a first line such as
|
|
126
|
+
* `RTSP/1.0 200 OK` or `HTTP/1.1 200 OK`.
|
|
127
|
+
*
|
|
128
|
+
* @param buffer - The header bytes, without the `\r\n\r\n` delimiter.
|
|
129
|
+
* @throws Error if the status line does not match.
|
|
130
|
+
*/
|
|
131
|
+
function parseResponseHeaders(buffer) {
|
|
132
|
+
const lines = buffer.toString("utf8").split("\r\n");
|
|
133
|
+
const rawStatus = lines[0].match(/(HTTP|RTSP)\/[\d.]+\s+(\d+)\s+(.+)/);
|
|
134
|
+
if (!rawStatus) throw new Error(`Invalid RTSP/HTTP response line: ${lines[0]}`);
|
|
135
|
+
const status = Number(rawStatus[2]);
|
|
136
|
+
const statusText = rawStatus[3];
|
|
137
|
+
return {
|
|
138
|
+
headers: parseHeaders(lines.slice(1)),
|
|
139
|
+
status,
|
|
140
|
+
statusText
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function parseContentLength(headers) {
|
|
144
|
+
const value = headers["Content-Length"];
|
|
145
|
+
if (value === void 0) return 0;
|
|
146
|
+
const length = Number(value);
|
|
147
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(length) || length > MAX_BODY_LENGTH) throw new Error("Invalid or excessive Content-Length");
|
|
148
|
+
return length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/rtsp/client.ts
|
|
153
|
+
/** Buffer size at which the buffers reset, against memory exhaustion. */
|
|
154
|
+
const MAX_BUFFER_SIZE = 2 * 1024 * 1024;
|
|
155
|
+
/**
|
|
156
|
+
* RTSP over TCP with CSeq-based response tracking and binary-plist serialization of object bodies.
|
|
157
|
+
* Keeps ciphertext and plaintext separate to handle partial TCP delivery.
|
|
158
|
+
* Override {@link transformIncoming}, {@link transformOutgoing} and {@link getDefaultHeaders} for encryption and protocol headers.
|
|
159
|
+
*/
|
|
160
|
+
var RtspClient = class extends Connection {
|
|
161
|
+
/** Decrypted plaintext waiting to be parsed as responses. */
|
|
162
|
+
#buffer = Buffer.alloc(0);
|
|
163
|
+
/** Raw TCP data waiting for {@link transformIncoming}. */
|
|
164
|
+
#encryptedBuffer = Buffer.alloc(0);
|
|
165
|
+
#cseq = 0;
|
|
166
|
+
#requests = /* @__PURE__ */ new Map();
|
|
167
|
+
constructor(context, address, port) {
|
|
168
|
+
super(context, address, port);
|
|
169
|
+
this.on("closed", (reason, error) => this.#onClose(reason, error));
|
|
170
|
+
this.on("data", (data) => this.#onData(data));
|
|
171
|
+
this.on("error", (error) => this.#onError(error));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Headers on every request, after CSeq and before the headers of
|
|
175
|
+
* {@link ExchangeOptions.headers}. A subclass adds its session headers
|
|
176
|
+
* here, such as DACP-ID and Active-Remote.
|
|
177
|
+
*/
|
|
178
|
+
getDefaultHeaders() {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Decrypts incoming TCP data before parsing; a subclass overrides it.
|
|
183
|
+
*
|
|
184
|
+
* @returns The plaintext, with the consumed byte count when not all input
|
|
185
|
+
* was used, or `false` until a complete encrypted block arrived.
|
|
186
|
+
*/
|
|
187
|
+
transformIncoming(data) {
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
/** Encrypts a formatted request before it goes to the socket; a subclass overrides it. */
|
|
191
|
+
transformOutgoing(data) {
|
|
192
|
+
return data;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Sends a request with CSeq tracking and a response timeout. Object bodies become binary plists.
|
|
196
|
+
*
|
|
197
|
+
* @throws TimeoutError if no response arrives in time.
|
|
198
|
+
* @throws InvalidResponseError for non-OK status unless `allowError` is set.
|
|
199
|
+
*/
|
|
200
|
+
async exchange(method, path, options = {}) {
|
|
201
|
+
const { contentType, headers: extraHeaders = {}, allowError = false, protocol = "RTSP/1.0", timeout = HTTP_TIMEOUT } = options;
|
|
202
|
+
let { body } = options;
|
|
203
|
+
const cseq = this.#cseq++;
|
|
204
|
+
const headers = {
|
|
205
|
+
"CSeq": cseq,
|
|
206
|
+
...this.getDefaultHeaders(),
|
|
207
|
+
...extraHeaders
|
|
208
|
+
};
|
|
209
|
+
let bodyBuffer;
|
|
210
|
+
if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
211
|
+
bodyBuffer = Buffer.from(Plist.serialize(body));
|
|
212
|
+
headers["Content-Type"] = "application/x-apple-binary-plist";
|
|
213
|
+
} else if (body) {
|
|
214
|
+
bodyBuffer = typeof body === "string" ? Buffer.from(body) : body;
|
|
215
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
216
|
+
} else if (contentType) headers["Content-Type"] = contentType;
|
|
217
|
+
if (bodyBuffer) headers["Content-Length"] = bodyBuffer.length;
|
|
218
|
+
else headers["Content-Length"] = 0;
|
|
219
|
+
const headerLines = [
|
|
220
|
+
`${method} ${path} ${protocol}`,
|
|
221
|
+
...Object.entries(headers).map(([name, value]) => `${name}: ${value}`),
|
|
222
|
+
"",
|
|
223
|
+
""
|
|
224
|
+
].join("\r\n");
|
|
225
|
+
const raw = bodyBuffer ? Buffer.concat([Buffer.from(headerLines), bodyBuffer]) : Buffer.from(headerLines);
|
|
226
|
+
const data = this.transformOutgoing(Buffer.from(raw));
|
|
227
|
+
this.context.logger.net("[rtsp]", method, path, `cseq=${cseq}`);
|
|
228
|
+
if (reporter.tapsTraffic) this.context.logger.traffic("rtsp", "out", `${method} ${path} cseq=${cseq}`, {
|
|
229
|
+
headers,
|
|
230
|
+
body: decodeBody(String(headers["Content-Type"] ?? ""), bodyBuffer)
|
|
231
|
+
}, raw);
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const timer = setTimeout(() => {
|
|
234
|
+
this.#requests.delete(cseq);
|
|
235
|
+
reject(new TimeoutError(`No response to CSeq ${cseq} (${path})`));
|
|
236
|
+
}, timeout);
|
|
237
|
+
this.#requests.set(cseq, {
|
|
238
|
+
resolve: (response) => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
if (!allowError && !response.ok) reject(new InvalidResponseError(`RTSP error: ${response.status} ${response.statusText}`));
|
|
241
|
+
else resolve(response);
|
|
242
|
+
},
|
|
243
|
+
reject: (error) => {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
reject(error);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
this.write(data);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
this.#requests.delete(cseq);
|
|
253
|
+
reject(err);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Resets both buffers and rejects every pending request: with a {@link ConnectionTimeoutError}
|
|
259
|
+
* after a timeout, with the error that broke the connection, or with a {@link ConnectionClosedError}.
|
|
260
|
+
*/
|
|
261
|
+
#onClose(reason, error) {
|
|
262
|
+
this.#buffer = Buffer.alloc(0);
|
|
263
|
+
this.#encryptedBuffer = Buffer.alloc(0);
|
|
264
|
+
for (const [cseq, { reject }] of this.#requests) {
|
|
265
|
+
reject(reason === "timeout" ? new ConnectionTimeoutError() : error ?? new ConnectionClosedError("Connection closed."));
|
|
266
|
+
this.#requests.delete(cseq);
|
|
267
|
+
}
|
|
268
|
+
this.context.logger.net("[rtsp]", "onRtspClose()", reason);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Collects TCP data in the encrypted buffer, decrypts it with
|
|
272
|
+
* {@link transformIncoming} into the plaintext buffer and matches each
|
|
273
|
+
* complete response to its request by CSeq. Past {@link MAX_BUFFER_SIZE},
|
|
274
|
+
* both buffers reset and every pending request is rejected.
|
|
275
|
+
*/
|
|
276
|
+
#onData(data) {
|
|
277
|
+
try {
|
|
278
|
+
this.#encryptedBuffer = Buffer.concat([this.#encryptedBuffer, data]);
|
|
279
|
+
if (this.#encryptedBuffer.byteLength > MAX_BUFFER_SIZE) {
|
|
280
|
+
this.context.logger.error("[rtsp]", `Buffer exceeded max size (${this.#encryptedBuffer.byteLength} bytes), resetting.`);
|
|
281
|
+
this.#encryptedBuffer = Buffer.alloc(0);
|
|
282
|
+
this.#buffer = Buffer.alloc(0);
|
|
283
|
+
const err = /* @__PURE__ */ new Error("Buffer overflow: exceeded maximum buffer size");
|
|
284
|
+
for (const [cseq, { reject }] of this.#requests) {
|
|
285
|
+
reject(err);
|
|
286
|
+
this.#requests.delete(cseq);
|
|
287
|
+
}
|
|
288
|
+
this.emit("error", err);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const transformed = this.transformIncoming(this.#encryptedBuffer);
|
|
292
|
+
if (transformed === false) return;
|
|
293
|
+
const result = Buffer.isBuffer(transformed) ? {
|
|
294
|
+
data: transformed,
|
|
295
|
+
consumed: this.#encryptedBuffer.length
|
|
296
|
+
} : transformed;
|
|
297
|
+
this.#encryptedBuffer = Buffer.from(this.#encryptedBuffer.subarray(result.consumed));
|
|
298
|
+
this.#buffer = Buffer.concat([this.#buffer, result.data]);
|
|
299
|
+
while (this.#buffer.byteLength > 0) {
|
|
300
|
+
const result = parseResponse(this.#buffer);
|
|
301
|
+
if (result === null) return;
|
|
302
|
+
if (reporter.tapsTraffic) this.#tapResponse(result.response, this.#buffer.subarray(0, result.responseLength));
|
|
303
|
+
this.#buffer = this.#buffer.subarray(result.responseLength);
|
|
304
|
+
const cseqHeader = result.response.headers.get("CSeq");
|
|
305
|
+
const cseq = cseqHeader ? parseInt(cseqHeader, 10) : -1;
|
|
306
|
+
if (this.#requests.has(cseq)) {
|
|
307
|
+
const { resolve } = this.#requests.get(cseq);
|
|
308
|
+
this.#requests.delete(cseq);
|
|
309
|
+
resolve(result.response);
|
|
310
|
+
} else this.context.logger.warn("[rtsp]", `Unexpected response for CSeq ${cseq}`);
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
this.#encryptedBuffer = Buffer.alloc(0);
|
|
314
|
+
this.#buffer = Buffer.alloc(0);
|
|
315
|
+
this.context.logger.error("[rtsp]", "onRtspData()", err);
|
|
316
|
+
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Reports a response to the traffic sink. The body is cut from the frame, because reading it
|
|
321
|
+
* off the `Response` would consume it before the caller gets to.
|
|
322
|
+
*
|
|
323
|
+
* @param bytes - The plaintext response as it was framed.
|
|
324
|
+
*/
|
|
325
|
+
#tapResponse(response, bytes) {
|
|
326
|
+
const frame = Buffer.from(bytes);
|
|
327
|
+
const headers = Object.fromEntries(response.headers.entries());
|
|
328
|
+
const bodyStart = frame.indexOf("\r\n\r\n") + 4;
|
|
329
|
+
const body = decodeBody(headers["content-type"] ?? "", frame.subarray(bodyStart));
|
|
330
|
+
this.context.logger.traffic("rtsp", "in", `${response.status} ${response.statusText} cseq=${headers["cseq"] ?? "?"}`, {
|
|
331
|
+
headers,
|
|
332
|
+
body
|
|
333
|
+
}, frame);
|
|
334
|
+
}
|
|
335
|
+
/** Rejects every pending request with the socket error. */
|
|
336
|
+
#onError(err) {
|
|
337
|
+
for (const [cseq, { reject }] of this.#requests) {
|
|
338
|
+
reject(err);
|
|
339
|
+
this.#requests.delete(cseq);
|
|
340
|
+
}
|
|
341
|
+
this.context.logger.error("[rtsp]", "onRtspError()", err);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Turns a body into something readable for the traffic sink: a parsed plist, text, or nothing
|
|
346
|
+
* when it is binary the sink already gets as bytes.
|
|
347
|
+
*/
|
|
348
|
+
function decodeBody(contentType, body) {
|
|
349
|
+
if (!body || body.byteLength === 0) return;
|
|
350
|
+
try {
|
|
351
|
+
if (contentType.includes("plist")) return decodeNestedPlist(Plist.parse(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)));
|
|
352
|
+
if (contentType.startsWith("text/") || contentType.includes("sdp") || contentType.includes("parameters")) return body.toString("utf8");
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
/** Decodes a binary plist in `params.data`, as AirPlay play queue commands carry one. */
|
|
356
|
+
function decodeNestedPlist(value) {
|
|
357
|
+
const params = value?.params;
|
|
358
|
+
const data = params?.data;
|
|
359
|
+
if (!(data instanceof ArrayBuffer) || data.byteLength < 8 || Buffer.from(data, 0, 8).toString("latin1") !== "bplist00") return value;
|
|
360
|
+
try {
|
|
361
|
+
return {
|
|
362
|
+
...value,
|
|
363
|
+
params: {
|
|
364
|
+
...params,
|
|
365
|
+
data: Plist.parse(data)
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
} catch {
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
//#endregion
|
|
374
|
+
export { AUDIO_BYTES_PER_CHANNEL as a, DEFAULT_PORT as c, parseResponse as i, FEEDBACK_INTERVAL as l, buildResponse as n, AUDIO_CHANNELS as o, parseRequest as r, AUDIO_SAMPLE_RATE as s, RtspClient as t, HTTP_TIMEOUT as u };
|
package/dist/rtsp.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as RtspRequest, c as parseRequest, i as Method, l as parseResponse, n as RtspClient, o as RtspResponse, r as BuildResponseOptions, s as buildResponse, t as ExchangeOptions } from "./index-BlcOBfSz.mjs";
|
|
2
|
+
export { type BuildResponseOptions, type ExchangeOptions, type Method, RtspClient, type RtspRequest, type RtspResponse, buildResponse, parseRequest, parseResponse };
|
package/dist/rtsp.mjs
ADDED