forked from edideaur/monochrome
-
-
Notifications
You must be signed in to change notification settings - Fork 421
Expand file tree
/
Copy pathhls-downloader.js
More file actions
104 lines (81 loc) · 3.49 KB
/
Copy pathhls-downloader.js
File metadata and controls
104 lines (81 loc) · 3.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
import { SegmentedDownloadProgress } from './progressEvents';
import { getProxyUrl } from './proxy-utils';
export class HlsDownloader {
constructor() {}
async downloadHlsStream(masterUrl, options = {}) {
const { onProgress, signal } = options;
const response = await fetch(masterUrl, { signal });
const masterText = await response.text();
const variantUrl = this.getBestVariantUrl(masterUrl, masterText);
const mediaResponse = await fetch(variantUrl, { signal });
const mediaText = await mediaResponse.text();
const segments = this.parseMediaPlaylist(variantUrl, mediaText);
if (segments.length === 0) {
throw new Error('No segments found in HLS playlist');
}
const chunks = [];
let downloadedBytes = 0;
const totalSegments = segments.length;
for (let i = 0; i < totalSegments; i++) {
if (signal?.aborted) throw new Error('AbortError');
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, undefined, i, totalSegments));
const segmentUrl = segments[i];
const segmentResponse = await fetch(segmentUrl, { signal });
if (!segmentResponse.ok) {
throw new Error(`Failed to fetch segment ${i}: ${segmentResponse.status}`);
}
const chunk = await segmentResponse.arrayBuffer();
chunks.push(chunk);
downloadedBytes += chunk.byteLength;
onProgress?.(new SegmentedDownloadProgress(downloadedBytes, undefined, i + 1, totalSegments));
}
const mimeType = segments[0].endsWith('.m4s') || segments[0].includes('mp4') ? 'video/mp4' : 'video/mp2t';
return new Blob(chunks, { type: mimeType });
}
getBestVariantUrl(masterUrl, masterText) {
if (!masterText.includes('#EXT-X-STREAM-INF')) {
return masterUrl;
}
const lines = masterText.split('\n');
const variants = [];
let currentVariant = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('#EXT-X-STREAM-INF:')) {
const bandwidthMatch = trimmed.match(/BANDWIDTH=(\d+)/);
const resolutionMatch = trimmed.match(/RESOLUTION=(\d+x\d+)/);
currentVariant = {
bandwidth: bandwidthMatch ? parseInt(bandwidthMatch[1], 10) : 0,
resolution: resolutionMatch ? resolutionMatch[1] : 'unknown',
};
} else if (trimmed && !trimmed.startsWith('#')) {
if (currentVariant) {
currentVariant.url = this.resolveUrl(masterUrl, trimmed);
variants.push(currentVariant);
currentVariant = null;
}
}
}
if (variants.length === 0) return masterUrl;
variants.sort((a, b) => b.bandwidth - a.bandwidth);
return variants[0].url;
}
parseMediaPlaylist(mediaUrl, mediaText) {
const lines = mediaText.split('\n');
const segments = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
segments.push(this.resolveUrl(mediaUrl, trimmed));
}
}
return segments;
}
resolveUrl(baseUrl, relativeUrl) {
try {
return new URL(relativeUrl, baseUrl).href;
} catch {
return relativeUrl;
}
}
}