recker 1.0.19 → 1.0.20-next.9796be9
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/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/core/client.d.ts +2 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +6 -7
- package/dist/plugins/hls.d.ts +90 -17
- package/dist/plugins/hls.d.ts.map +1 -1
- package/dist/plugins/hls.js +343 -173
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +4 -0
- package/dist/testing/mock-hls-server.d.ts +81 -0
- package/dist/testing/mock-hls-server.d.ts.map +1 -0
- package/dist/testing/mock-hls-server.js +381 -0
- package/dist/testing/mock-http-server.d.ts +100 -0
- package/dist/testing/mock-http-server.d.ts.map +1 -0
- package/dist/testing/mock-http-server.js +298 -0
- package/dist/testing/mock-sse-server.d.ts +77 -0
- package/dist/testing/mock-sse-server.d.ts.map +1 -0
- package/dist/testing/mock-sse-server.js +291 -0
- package/dist/testing/mock-websocket-server.d.ts +78 -0
- package/dist/testing/mock-websocket-server.d.ts.map +1 -0
- package/dist/testing/mock-websocket-server.js +316 -0
- package/dist/transport/undici.d.ts +1 -1
- package/dist/transport/undici.d.ts.map +1 -1
- package/dist/transport/undici.js +10 -4
- package/package.json +3 -1
package/dist/plugins/hls.js
CHANGED
|
@@ -1,225 +1,395 @@
|
|
|
1
1
|
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
function parseAttributes(line, prefix) {
|
|
5
|
+
const attrs = {};
|
|
6
|
+
const content = line.substring(prefix.length);
|
|
7
|
+
const regex = /([A-Z0-9-]+)=(?:"([^"]*)"|([^,]*))/g;
|
|
8
|
+
let match;
|
|
9
|
+
while ((match = regex.exec(content)) !== null) {
|
|
10
|
+
attrs[match[1]] = match[2] ?? match[3];
|
|
11
|
+
}
|
|
12
|
+
return attrs;
|
|
13
|
+
}
|
|
14
|
+
function resolveUrl(url, baseUrl) {
|
|
15
|
+
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const base = new URL(baseUrl);
|
|
20
|
+
const basePath = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1);
|
|
21
|
+
return new URL(url, base.origin + basePath).toString();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseMasterPlaylist(content, baseUrl) {
|
|
28
|
+
const lines = content.split('\n');
|
|
29
|
+
const variants = [];
|
|
30
|
+
let pendingVariant = {};
|
|
31
|
+
for (const rawLine of lines) {
|
|
32
|
+
const line = rawLine.trim();
|
|
33
|
+
if (!line)
|
|
34
|
+
continue;
|
|
35
|
+
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
|
36
|
+
const attrs = parseAttributes(line, '#EXT-X-STREAM-INF:');
|
|
37
|
+
pendingVariant = {
|
|
38
|
+
bandwidth: attrs.BANDWIDTH ? parseInt(attrs.BANDWIDTH, 10) : undefined,
|
|
39
|
+
resolution: attrs.RESOLUTION,
|
|
40
|
+
codecs: attrs.CODECS,
|
|
41
|
+
name: attrs.NAME,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
else if (!line.startsWith('#') && pendingVariant.bandwidth !== undefined) {
|
|
45
|
+
variants.push({
|
|
46
|
+
...pendingVariant,
|
|
47
|
+
url: resolveUrl(line, baseUrl),
|
|
48
|
+
});
|
|
49
|
+
pendingVariant = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { variants, isMaster: true };
|
|
53
|
+
}
|
|
54
|
+
function parseMediaPlaylist(content, baseUrl) {
|
|
5
55
|
const lines = content.split('\n');
|
|
6
56
|
const segments = [];
|
|
7
|
-
let currentDuration = 0;
|
|
8
|
-
let mediaSequence = 0;
|
|
9
57
|
let targetDuration = 5;
|
|
58
|
+
let mediaSequence = 0;
|
|
59
|
+
let discontinuitySequence = 0;
|
|
10
60
|
let endList = false;
|
|
61
|
+
let playlistType;
|
|
62
|
+
let currentDuration = 0;
|
|
11
63
|
let currentKey;
|
|
12
|
-
|
|
13
|
-
|
|
64
|
+
let currentDiscontinuity = false;
|
|
65
|
+
let currentProgramDateTime;
|
|
66
|
+
let segmentIndex = 0;
|
|
67
|
+
for (const rawLine of lines) {
|
|
68
|
+
const line = rawLine.trim();
|
|
14
69
|
if (!line)
|
|
15
70
|
continue;
|
|
16
71
|
if (line.startsWith('#EXT-X-TARGETDURATION:')) {
|
|
17
|
-
targetDuration =
|
|
72
|
+
targetDuration = parseInt(line.split(':')[1], 10);
|
|
18
73
|
}
|
|
19
74
|
else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
|
|
20
75
|
mediaSequence = parseInt(line.split(':')[1], 10);
|
|
21
76
|
}
|
|
22
|
-
else if (line.startsWith('#EXT-X-
|
|
77
|
+
else if (line.startsWith('#EXT-X-DISCONTINUITY-SEQUENCE:')) {
|
|
78
|
+
discontinuitySequence = parseInt(line.split(':')[1], 10);
|
|
79
|
+
}
|
|
80
|
+
else if (line.startsWith('#EXT-X-PLAYLIST-TYPE:')) {
|
|
81
|
+
playlistType = line.split(':')[1];
|
|
82
|
+
}
|
|
83
|
+
else if (line === '#EXT-X-ENDLIST') {
|
|
23
84
|
endList = true;
|
|
24
85
|
}
|
|
86
|
+
else if (line === '#EXT-X-DISCONTINUITY') {
|
|
87
|
+
currentDiscontinuity = true;
|
|
88
|
+
}
|
|
89
|
+
else if (line.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) {
|
|
90
|
+
currentProgramDateTime = new Date(line.split(':').slice(1).join(':'));
|
|
91
|
+
}
|
|
25
92
|
else if (line.startsWith('#EXT-X-KEY:')) {
|
|
26
|
-
const attrs = line
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
93
|
+
const attrs = parseAttributes(line, '#EXT-X-KEY:');
|
|
94
|
+
if (attrs.METHOD === 'NONE') {
|
|
95
|
+
currentKey = undefined;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
31
98
|
currentKey = {
|
|
32
|
-
method:
|
|
33
|
-
uri:
|
|
34
|
-
iv:
|
|
99
|
+
method: attrs.METHOD,
|
|
100
|
+
uri: attrs.URI ? resolveUrl(attrs.URI, baseUrl) : undefined,
|
|
101
|
+
iv: attrs.IV,
|
|
35
102
|
};
|
|
36
103
|
}
|
|
37
104
|
}
|
|
38
105
|
else if (line.startsWith('#EXTINF:')) {
|
|
39
|
-
const durationStr = line.substring(8).split(','
|
|
106
|
+
const durationStr = line.substring(8).split(',')[0];
|
|
40
107
|
currentDuration = parseFloat(durationStr);
|
|
41
108
|
}
|
|
42
109
|
else if (!line.startsWith('#')) {
|
|
43
|
-
let url = line;
|
|
44
|
-
if (!url.startsWith('http')) {
|
|
45
|
-
try {
|
|
46
|
-
const base = new URL(baseUrl);
|
|
47
|
-
const basePath = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1);
|
|
48
|
-
url = new URL(url, base.origin + basePath).toString();
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
110
|
segments.push({
|
|
54
|
-
url,
|
|
111
|
+
url: resolveUrl(line, baseUrl),
|
|
55
112
|
duration: currentDuration,
|
|
56
|
-
sequence:
|
|
57
|
-
key: currentKey
|
|
113
|
+
sequence: mediaSequence + segmentIndex,
|
|
114
|
+
key: currentKey,
|
|
115
|
+
discontinuity: currentDiscontinuity,
|
|
116
|
+
programDateTime: currentProgramDateTime,
|
|
58
117
|
});
|
|
118
|
+
segmentIndex++;
|
|
119
|
+
currentDiscontinuity = false;
|
|
120
|
+
currentProgramDateTime = undefined;
|
|
59
121
|
}
|
|
60
122
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
123
|
+
return {
|
|
124
|
+
segments,
|
|
125
|
+
targetDuration,
|
|
126
|
+
mediaSequence,
|
|
127
|
+
endList,
|
|
128
|
+
playlistType,
|
|
129
|
+
discontinuitySequence,
|
|
130
|
+
};
|
|
65
131
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const bwMatch = line.match(/BANDWIDTH=(\d+)/);
|
|
77
|
-
const resMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
|
78
|
-
if (bwMatch)
|
|
79
|
-
currentBandwidth = parseInt(bwMatch[1], 10);
|
|
80
|
-
if (resMatch)
|
|
81
|
-
currentResolution = resMatch[1];
|
|
82
|
-
}
|
|
83
|
-
else if (!line.startsWith('#')) {
|
|
84
|
-
let url = line;
|
|
85
|
-
if (!url.startsWith('http')) {
|
|
86
|
-
try {
|
|
87
|
-
const base = new URL(baseUrl);
|
|
88
|
-
const basePath = base.pathname.substring(0, base.pathname.lastIndexOf('/') + 1);
|
|
89
|
-
url = new URL(url, base.origin + basePath).toString();
|
|
90
|
-
}
|
|
91
|
-
catch { }
|
|
92
|
-
}
|
|
93
|
-
variants.push({
|
|
94
|
-
url,
|
|
95
|
-
bandwidth: currentBandwidth,
|
|
96
|
-
resolution: currentResolution
|
|
97
|
-
});
|
|
98
|
-
}
|
|
132
|
+
function isMasterPlaylist(content) {
|
|
133
|
+
return content.includes('#EXT-X-STREAM-INF');
|
|
134
|
+
}
|
|
135
|
+
function selectVariant(variants, quality) {
|
|
136
|
+
if (!variants.length) {
|
|
137
|
+
throw new Error('No variants found in master playlist');
|
|
138
|
+
}
|
|
139
|
+
const sorted = [...variants].sort((a, b) => (a.bandwidth ?? 0) - (b.bandwidth ?? 0));
|
|
140
|
+
if (quality === 'lowest') {
|
|
141
|
+
return sorted[0];
|
|
99
142
|
}
|
|
100
|
-
|
|
143
|
+
if (quality === 'highest' || quality === undefined) {
|
|
144
|
+
return sorted[sorted.length - 1];
|
|
145
|
+
}
|
|
146
|
+
if (quality.resolution) {
|
|
147
|
+
const match = variants.find((v) => v.resolution === quality.resolution);
|
|
148
|
+
if (match)
|
|
149
|
+
return match;
|
|
150
|
+
}
|
|
151
|
+
if (quality.bandwidth) {
|
|
152
|
+
const target = quality.bandwidth;
|
|
153
|
+
return sorted.reduce((prev, curr) => {
|
|
154
|
+
const prevDiff = Math.abs((prev.bandwidth ?? 0) - target);
|
|
155
|
+
const currDiff = Math.abs((curr.bandwidth ?? 0) - target);
|
|
156
|
+
return currDiff < prevDiff ? curr : prev;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return sorted[sorted.length - 1];
|
|
101
160
|
}
|
|
102
|
-
export
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
if (initialManifest.includes('#EXT-X-STREAM-INF')) {
|
|
125
|
-
const variants = parseVariants(initialManifest, currentManifestUrl);
|
|
126
|
-
if (variants.length > 0) {
|
|
127
|
-
let selected = variants[variants.length - 1];
|
|
128
|
-
if (options.onVariantSelected) {
|
|
129
|
-
const userSelected = options.onVariantSelected(variants, selected);
|
|
130
|
-
if (userSelected)
|
|
131
|
-
selected = userSelected;
|
|
132
|
-
}
|
|
133
|
-
currentManifestUrl = selected.url;
|
|
134
|
-
info(`Master playlist detected. Switching to variant: ${currentManifestUrl}`);
|
|
135
|
-
}
|
|
161
|
+
export class HlsPromise {
|
|
162
|
+
client;
|
|
163
|
+
manifestUrl;
|
|
164
|
+
options;
|
|
165
|
+
seenSequences = new Set();
|
|
166
|
+
downloadedBytes = 0;
|
|
167
|
+
downloadedSegments = 0;
|
|
168
|
+
startTime = 0;
|
|
169
|
+
aborted = false;
|
|
170
|
+
abortController = new AbortController();
|
|
171
|
+
constructor(client, manifestUrl, options = {}) {
|
|
172
|
+
this.client = client;
|
|
173
|
+
this.manifestUrl = manifestUrl;
|
|
174
|
+
this.options = {
|
|
175
|
+
mode: 'merge',
|
|
176
|
+
format: 'ts',
|
|
177
|
+
concurrency: 5,
|
|
178
|
+
...options,
|
|
179
|
+
};
|
|
180
|
+
if (this.options.format !== 'ts') {
|
|
181
|
+
throw new Error(`Format '${this.options.format}' requires ffmpeg. Use format: 'ts' or install ffmpeg.`);
|
|
136
182
|
}
|
|
137
183
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
184
|
+
get [Symbol.toStringTag]() {
|
|
185
|
+
return 'HlsPromise';
|
|
141
186
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
then(onfulfilled, onrejected) {
|
|
188
|
+
return Promise.reject(new Error('HlsPromise requires .download(), .stream(), or .pipe() to execute')).then(onfulfilled, onrejected);
|
|
189
|
+
}
|
|
190
|
+
catch(onrejected) {
|
|
191
|
+
return this.then(null, onrejected);
|
|
192
|
+
}
|
|
193
|
+
finally(onfinally) {
|
|
194
|
+
return this.then(() => {
|
|
195
|
+
onfinally?.();
|
|
196
|
+
}, () => {
|
|
197
|
+
onfinally?.();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
cancel() {
|
|
201
|
+
this.aborted = true;
|
|
202
|
+
this.abortController.abort();
|
|
203
|
+
}
|
|
204
|
+
async download(dest) {
|
|
205
|
+
this.startTime = Date.now();
|
|
206
|
+
const mediaPlaylistUrl = await this.resolveMediaPlaylist();
|
|
207
|
+
if (this.options.mode === 'chunks') {
|
|
208
|
+
await this.downloadChunks(mediaPlaylistUrl, dest);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (typeof dest !== 'string') {
|
|
212
|
+
throw new Error('Merge mode requires a string path, not a function');
|
|
153
213
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
filtered.push(seg);
|
|
214
|
+
await this.downloadMerged(mediaPlaylistUrl, dest);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async *stream() {
|
|
218
|
+
this.startTime = Date.now();
|
|
219
|
+
const mediaPlaylistUrl = await this.resolveMediaPlaylist();
|
|
220
|
+
const isLive = this.isLiveMode();
|
|
221
|
+
const maxDuration = this.getMaxDuration();
|
|
222
|
+
while (!this.aborted) {
|
|
223
|
+
const playlist = await this.fetchMediaPlaylist(mediaPlaylistUrl);
|
|
224
|
+
const newSegments = playlist.segments.filter((s) => !this.seenSequences.has(s.sequence));
|
|
225
|
+
for (const segment of newSegments) {
|
|
226
|
+
if (this.aborted)
|
|
227
|
+
break;
|
|
228
|
+
if (maxDuration && Date.now() - this.startTime > maxDuration) {
|
|
229
|
+
return;
|
|
171
230
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
231
|
+
this.seenSequences.add(segment.sequence);
|
|
232
|
+
const data = await this.downloadSegment(segment);
|
|
233
|
+
const segmentData = {
|
|
234
|
+
sequence: segment.sequence,
|
|
235
|
+
duration: segment.duration,
|
|
236
|
+
data,
|
|
237
|
+
url: segment.url,
|
|
238
|
+
downloadedAt: new Date(),
|
|
239
|
+
};
|
|
240
|
+
this.downloadedSegments++;
|
|
241
|
+
this.downloadedBytes += data.byteLength;
|
|
242
|
+
this.emitProgress(playlist, isLive);
|
|
243
|
+
if (this.options.onSegment) {
|
|
244
|
+
await this.options.onSegment(segmentData);
|
|
245
|
+
}
|
|
246
|
+
yield segmentData;
|
|
183
247
|
}
|
|
184
248
|
if (!isLive || playlist.endList) {
|
|
185
|
-
recording = false;
|
|
186
249
|
break;
|
|
187
250
|
}
|
|
188
|
-
if (maxDuration
|
|
189
|
-
info('Max duration reached. Stopping.');
|
|
190
|
-
recording = false;
|
|
251
|
+
if (maxDuration && Date.now() - this.startTime > maxDuration) {
|
|
191
252
|
break;
|
|
192
253
|
}
|
|
193
|
-
const
|
|
194
|
-
await
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
254
|
+
const pollInterval = Math.max(1000, (playlist.targetDuration * 1000) / 2);
|
|
255
|
+
await this.sleep(pollInterval);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async pipe(writable) {
|
|
259
|
+
try {
|
|
260
|
+
for await (const segment of this.stream()) {
|
|
261
|
+
const canContinue = writable.write(segment.data);
|
|
262
|
+
if (!canContinue) {
|
|
263
|
+
await new Promise((resolve) => writable.once('drain', resolve));
|
|
264
|
+
}
|
|
201
265
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
if ('end' in writable && typeof writable.end === 'function') {
|
|
269
|
+
writable.end();
|
|
205
270
|
}
|
|
206
271
|
}
|
|
207
272
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
273
|
+
async info() {
|
|
274
|
+
const content = await this.client.get(this.manifestUrl).text();
|
|
275
|
+
if (isMasterPlaylist(content)) {
|
|
276
|
+
const master = parseMasterPlaylist(content, this.manifestUrl);
|
|
277
|
+
const selectedVariant = selectVariant(master.variants, this.options.quality);
|
|
278
|
+
const playlistContent = await this.client.get(selectedVariant.url).text();
|
|
279
|
+
const playlist = parseMediaPlaylist(playlistContent, selectedVariant.url);
|
|
280
|
+
const totalDuration = playlist.endList
|
|
281
|
+
? playlist.segments.reduce((sum, s) => sum + s.duration, 0)
|
|
282
|
+
: undefined;
|
|
283
|
+
return {
|
|
284
|
+
master,
|
|
285
|
+
playlist,
|
|
286
|
+
selectedVariant,
|
|
287
|
+
isLive: !playlist.endList,
|
|
288
|
+
totalDuration,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const playlist = parseMediaPlaylist(content, this.manifestUrl);
|
|
292
|
+
const totalDuration = playlist.endList
|
|
293
|
+
? playlist.segments.reduce((sum, s) => sum + s.duration, 0)
|
|
294
|
+
: undefined;
|
|
295
|
+
return {
|
|
296
|
+
playlist,
|
|
297
|
+
isLive: !playlist.endList,
|
|
298
|
+
totalDuration,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
async resolveMediaPlaylist() {
|
|
302
|
+
const content = await this.client.get(this.manifestUrl).text();
|
|
303
|
+
if (!isMasterPlaylist(content)) {
|
|
304
|
+
return this.manifestUrl;
|
|
305
|
+
}
|
|
306
|
+
const master = parseMasterPlaylist(content, this.manifestUrl);
|
|
307
|
+
const variant = selectVariant(master.variants, this.options.quality);
|
|
308
|
+
return variant.url;
|
|
309
|
+
}
|
|
310
|
+
async fetchMediaPlaylist(url) {
|
|
311
|
+
const content = await this.client.get(url).text();
|
|
312
|
+
return parseMediaPlaylist(content, url);
|
|
313
|
+
}
|
|
314
|
+
async downloadSegment(segment) {
|
|
315
|
+
if (segment.key && segment.key.method !== 'NONE') {
|
|
316
|
+
throw new Error(`Encrypted HLS (${segment.key.method}) requires ffmpeg. Use unencrypted streams or install ffmpeg.`);
|
|
317
|
+
}
|
|
318
|
+
const response = await this.client.get(segment.url, {
|
|
319
|
+
headers: this.options.headers,
|
|
320
|
+
signal: this.abortController.signal,
|
|
321
|
+
});
|
|
322
|
+
const blob = await response.blob();
|
|
323
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
324
|
+
}
|
|
325
|
+
async downloadMerged(playlistUrl, outputPath) {
|
|
326
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
327
|
+
const output = createWriteStream(outputPath);
|
|
328
|
+
try {
|
|
329
|
+
await this.pipe(output);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
output.destroy();
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async downloadChunks(playlistUrl, dest) {
|
|
337
|
+
const getPath = typeof dest === 'string'
|
|
338
|
+
? (seg) => join(dest, `segment-${seg.sequence}.ts`)
|
|
339
|
+
: dest;
|
|
340
|
+
const baseDir = typeof dest === 'string' ? dest : dirname(getPath({ sequence: 0, duration: 0, url: '' }));
|
|
341
|
+
await mkdir(baseDir, { recursive: true });
|
|
342
|
+
for await (const segment of this.stream()) {
|
|
343
|
+
const filePath = getPath({
|
|
344
|
+
sequence: segment.sequence,
|
|
345
|
+
duration: segment.duration,
|
|
346
|
+
url: segment.url,
|
|
347
|
+
});
|
|
348
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
349
|
+
const output = createWriteStream(filePath);
|
|
350
|
+
await new Promise((resolve, reject) => {
|
|
351
|
+
output.write(segment.data, (err) => {
|
|
352
|
+
if (err)
|
|
353
|
+
reject(err);
|
|
354
|
+
else {
|
|
355
|
+
output.end();
|
|
356
|
+
resolve();
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
});
|
|
217
360
|
}
|
|
218
|
-
dest.end();
|
|
219
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
220
|
-
info(`Saved to ${outputPath}`);
|
|
221
361
|
}
|
|
222
|
-
|
|
223
|
-
|
|
362
|
+
isLiveMode() {
|
|
363
|
+
return this.options.live === true || typeof this.options.live === 'object';
|
|
224
364
|
}
|
|
365
|
+
getMaxDuration() {
|
|
366
|
+
if (typeof this.options.live === 'object' && this.options.live.duration) {
|
|
367
|
+
return this.options.live.duration;
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
emitProgress(playlist, isLive) {
|
|
372
|
+
if (!this.options.onProgress)
|
|
373
|
+
return;
|
|
374
|
+
this.options.onProgress({
|
|
375
|
+
downloadedSegments: this.downloadedSegments,
|
|
376
|
+
totalSegments: isLive ? undefined : playlist.segments.length,
|
|
377
|
+
downloadedBytes: this.downloadedBytes,
|
|
378
|
+
currentSegment: Math.max(...this.seenSequences),
|
|
379
|
+
isLive,
|
|
380
|
+
elapsed: Date.now() - this.startTime,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
sleep(ms) {
|
|
384
|
+
return new Promise((resolve) => {
|
|
385
|
+
const timeout = setTimeout(resolve, ms);
|
|
386
|
+
this.abortController.signal.addEventListener('abort', () => {
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
resolve();
|
|
389
|
+
}, { once: true });
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export function hls(client, manifestUrl, options = {}) {
|
|
394
|
+
return new HlsPromise(client, manifestUrl, options);
|
|
225
395
|
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -2,4 +2,12 @@ export { MockClient, MockTransport, createMockClient, installGlobalMock, uninsta
|
|
|
2
2
|
export type { MockResponseOptions, MockInterceptOptions, } from './mock.js';
|
|
3
3
|
export { MockUDPServer, createMockUDPServer, } from './mock-udp-server.js';
|
|
4
4
|
export type { MockUDPServerOptions, ReceivedMessage, } from './mock-udp-server.js';
|
|
5
|
+
export { MockHlsServer, createMockHlsVod, createMockHlsLive, createMockHlsMultiQuality, } from './mock-hls-server.js';
|
|
6
|
+
export type { MockHlsServerOptions, MockHlsVariant, MockHlsSegment, MockHlsStats, } from './mock-hls-server.js';
|
|
7
|
+
export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocket-server.js';
|
|
8
|
+
export type { MockWebSocketServerOptions, MockWebSocketClient, MockWebSocketMessage, MockWebSocketStats, } from './mock-websocket-server.js';
|
|
9
|
+
export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
|
|
10
|
+
export type { MockSSEServerOptions, SSEEvent, MockSSEClient, MockSSEStats, } from './mock-sse-server.js';
|
|
11
|
+
export { MockHttpServer, createMockHttpServer, } from './mock-http-server.js';
|
|
12
|
+
export type { MockHttpServerOptions, MockHttpResponse, MockHttpRequest, MockHttpHandler, MockHttpStats, } from './mock-http-server.js';
|
|
5
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAqCA,OAAO,EACL,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,SAAS,GACV,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,aAAa,EACb,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,oBAAoB,EACpB,eAAe,GAChB,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAqCA,OAAO,EACL,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,SAAS,GACV,MAAM,WAAW,CAAC;AAEnB,YAAY,EACV,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,WAAW,CAAC;AAGnB,OAAO,EACL,aAAa,EACb,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,oBAAoB,EACpB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,oBAAoB,EACpB,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EACV,0BAA0B,EAC1B,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,aAAa,EACb,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,YAAY,GACb,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,cAAc,EACd,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EACV,qBAAqB,EACrB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,uBAAuB,CAAC"}
|
package/dist/testing/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { MockClient, MockTransport, createMockClient, installGlobalMock, uninstallGlobalMock, MockAgent, } from './mock.js';
|
|
2
2
|
export { MockUDPServer, createMockUDPServer, } from './mock-udp-server.js';
|
|
3
|
+
export { MockHlsServer, createMockHlsVod, createMockHlsLive, createMockHlsMultiQuality, } from './mock-hls-server.js';
|
|
4
|
+
export { MockWebSocketServer, createMockWebSocketServer, } from './mock-websocket-server.js';
|
|
5
|
+
export { MockSSEServer, createMockSSEServer, } from './mock-sse-server.js';
|
|
6
|
+
export { MockHttpServer, createMockHttpServer, } from './mock-http-server.js';
|