parse method
Parses the request and returns the data to the socket.
Handles M3U8 playlist and segment requests, replacing URLs with local proxy URLs.
Returns true if parsing and response succeed, otherwise false.
Implementation
@override
Future<bool> parse(
Socket socket,
Uri uri,
Map<String, String> headers,
) async {
try {
DownloadTask task = DownloadTask(
uri: uri,
hlsKey: uri.generateMd5,
headers: headers,
);
HlsSegment? hlsSegment = findSegmentByUri(uri);
if (hlsSegment != null) task.hlsKey = hlsSegment.key;
Uint8List? data = await cache(task);
// if the task has been added, wait for the download to complete
bool exitUri = VideoProxy.downloadManager.isUrlExit(task.url);
if (exitUri) {
while (data == null) {
await Future.delayed(const Duration(milliseconds: 100));
data = await cache(task);
}
}
if (data == null) {
concurrentLoop(hlsSegment, headers);
task.priority += 10;
data = await download(task);
}
if (data == null) return false;
String contentType = 'application/octet-stream';
if (VideoProxy.urlMatcherImpl.matchM3u8(task.uri)) {
// Parse and rewrite M3U8 playlist lines for local proxying
List<String> lines = readLineFromUint8List(data);
String lastLine = '';
StringBuffer buffer = StringBuffer();
for (String line in lines) {
String hlsLine = line.trim();
String? parseUri;
if (hlsLine.startsWith("#EXT-X-KEY") ||
hlsLine.startsWith("#EXT-X-MEDIA")) {
Match? match = RegExp(r'URI="([^"]+)"').firstMatch(hlsLine);
if (match != null && match.groupCount >= 1) {
parseUri = match.group(1);
if (parseUri != null) {
String newUri = parseUri.startsWith('http')
? parseUri.toLocalUrl()
: '$parseUri${parseUri.contains('?') ? '&' : '?'}'
'origin=${base64Url.encode(utf8.encode(uri.origin))}';
line = hlsLine.replaceAll(parseUri, newUri);
}
}
}
if (lastLine.startsWith("#EXTINF") ||
lastLine.startsWith("#EXT-X-BYTERANGE") ||
lastLine.startsWith("#EXT-X-STREAM-INF")) {
if (!line.startsWith("#EXT")) {
line = line.toSafeUrl();
line = line.startsWith('http')
? line.toLocalUrl()
: '$line${line.contains('?') ? '&' : '?'}'
'origin=${base64Url.encode(utf8.encode(uri.origin))}';
}
}
// Add HLS segment to download list
if (hlsLine.startsWith("#EXT-X-KEY") ||
hlsLine.startsWith("#EXT-X-MEDIA")) {
if (parseUri != null) {
if (!parseUri.startsWith('http')) {
int relativePath = 0;
while (hlsLine.startsWith("../")) {
hlsLine = hlsLine.substring(3);
relativePath++;
}
parseUri = '${uri.pathPrefix(relativePath)}/' + parseUri;
}
concurrentAdd(
HlsSegment(url: parseUri, key: task.hlsKey!),
headers,
);
}
}
if (lastLine.startsWith("#EXTINF") ||
lastLine.startsWith("#EXT-X-BYTERANGE") ||
lastLine.startsWith("#EXT-X-STREAM-INF")) {
if (!line.startsWith("#EXT")) {
if (!hlsLine.startsWith('http')) {
int relativePath = 0;
// when hlsLine is relative path
while (hlsLine.startsWith("../")) {
hlsLine = hlsLine.substring(3);
relativePath++;
}
// when hlsLine start with /, and prefix contain hlsLine
String prefix = '${uri.pathPrefix(relativePath)}/';
if (hlsLine.startsWith("/")) {
List<String> split = hlsLine.split("/");
List<String> result = [];
for (var item in split) {
if (prefix.contains(item)) continue;
result.add(item);
}
hlsLine = result.join("/");
}
hlsLine = prefix + hlsLine;
}
concurrentAdd(
HlsSegment(url: hlsLine, key: task.hlsKey!),
headers,
);
}
}
buffer.write('$line\r\n');
lastLine = line;
}
data = Uint8List.fromList(buffer.toString().codeUnits);
contentType = 'application/vnd.apple.mpegurl';
} else if (VideoProxy.urlMatcherImpl.matchM3u8Key(task.uri)) {
contentType = 'application/octet-stream';
} else if (VideoProxy.urlMatcherImpl.matchM3u8Segment(task.uri)) {
contentType = 'video/MP2T';
}
String responseHeaders = <String>[
'HTTP/1.1 200 OK',
'Content-Type: $contentType',
'Connection: keep-alive',
if (contentType == 'video/MP2T') 'Accept-Ranges: bytes',
].join('\r\n');
await socket.append(responseHeaders);
await socket.append(data);
await socket.flush();
logD('Return request data: $uri');
return true;
} catch (e) {
logW('[UrlParserM3U8] ⚠ ⚠ ⚠ parse socket close: $e');
return false;
} finally {
await socket.close();
logD('Connection closed\n');
}
}