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;
task.startRange = hlsSegment.startRange;
task.endRange = hlsSegment.endRange;
}
// parse EXT-X-BYTERANGE to get range header
if (headers.containsKey('range')) {
String range = headers['range'] ?? '';
if (range.startsWith("bytes=")) range = range.substring(6);
List rangeList = range.split('-');
if (rangeList.length == 2) {
task.startRange = int.tryParse(rangeList[0]) ?? 0;
task.endRange = int.tryParse(rangeList[1]);
}
}
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 = '';
int lastEndRange = 0;
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;
}
// parse EXT-X-BYTERANGE to get range header
int startRange = 0;
int? endRange;
if (lastLine.startsWith("#EXT-X-BYTERANGE")) {
final reg = RegExp(r'#EXT-X-BYTERANGE:(\d+)(?:@(\d+))?');
final match = reg.firstMatch(line);
if (match != null) {
if (match.groupCount == 2) {
int offset = int.tryParse(match.group(1)!) ?? 0;
startRange = int.tryParse(match.group(2)!) ?? 0;
endRange = offset == 0 ? null : startRange + offset;
} else if (match.groupCount == 1) {
startRange = lastEndRange;
int offset = int.tryParse(match.group(1)!) ?? 0;
endRange = offset == 0 ? null : startRange + offset;
lastEndRange = endRange ?? 0;
}
}
}
concurrentAdd(
HlsSegment(
url: hlsLine,
key: task.hlsKey!,
startRange: startRange,
endRange: endRange,
),
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';
}
// return contentRange and contentLength to video player which parse from EXT-X-BYTERANGE
String contentRange = "";
String contentLength = "";
if (task.endRange != null) {
contentRange = 'bytes=${task.startRange}-${task.endRange!}';
contentLength = (task.endRange! - task.startRange + 1).toString();
}
String responseHeaders = <String>[
contentRange.isEmpty
? 'HTTP/1.1 200 OK'
: 'HTTP/1.1 206 Partial Content',
'Content-Type: $contentType',
'Connection: keep-alive',
if (contentType == 'video/MP2T') 'Accept-Ranges: bytes',
if (contentRange.isNotEmpty) 'Content-Range: $contentRange',
if (contentLength.isNotEmpty) 'Content-Length: $contentLength',
].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');
}
}