165 lines
6.5 KiB
C#
165 lines
6.5 KiB
C#
using System.Text.RegularExpressions;
|
|
using Paca.Core.Models;
|
|
|
|
namespace Paca.Core.Hls;
|
|
|
|
/// <summary>m3u8(마스터/미디어) 재생목록 파서. Python parse_m3u8 의 C# 포팅.</summary>
|
|
public static class M3u8Parser
|
|
{
|
|
// KEY="VAL",KEY2=VAL 형태 속성 파싱 (소문자/언더스코어 포함)
|
|
private static readonly Regex AttrRe =
|
|
new(@"(?<k>[A-Za-z0-9_\-]+)=(\""(?<q>[^""]*)\"")|(?<k2>[A-Za-z0-9_\-]+)=(?<v>[^,]*)",
|
|
RegexOptions.Compiled);
|
|
|
|
private static Dictionary<string, string> ParseAttrs(string s)
|
|
{
|
|
var d = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (Match m in AttrRe.Matches(s))
|
|
{
|
|
var key = m.Groups["k"].Success ? m.Groups["k"].Value : m.Groups["k2"].Value;
|
|
var val = m.Groups["q"].Success ? m.Groups["q"].Value : m.Groups["v"].Value;
|
|
d[key] = val;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
private static string GetValue(this Dictionary<string, string> d, string key, string def = "") =>
|
|
d.TryGetValue(key, out var v) ? v : def;
|
|
|
|
public static ParsedPlaylist Parse(string text, Uri baseUrl)
|
|
{
|
|
var variants = new List<Variant>();
|
|
var segments = new List<Segment>();
|
|
Uri? init = null;
|
|
long? initByteRangeLen = null;
|
|
long? initByteRangeOff = null;
|
|
bool isMaster = false;
|
|
|
|
if (baseUrl == null || string.IsNullOrWhiteSpace(text) || !text.Contains("#EXTM3U", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return new ParsedPlaylist
|
|
{
|
|
Type = PlaylistType.Media,
|
|
Variants = variants,
|
|
Segments = segments,
|
|
InitSegment = init,
|
|
InitByteRangeLength = initByteRangeLen,
|
|
InitByteRangeOffset = initByteRangeOff,
|
|
};
|
|
}
|
|
|
|
Dictionary<string, string>? pendingVariant = null;
|
|
KeyInfo? curKey = null;
|
|
long seq = 0;
|
|
int idx = 0;
|
|
|
|
long? pendingByteRangeLength = null;
|
|
long? pendingByteRangeOffset = null;
|
|
long prevByteRangeOffset = 0;
|
|
long prevByteRangeLength = 0;
|
|
|
|
using var reader = new StringReader(text);
|
|
string? raw;
|
|
while ((raw = reader.ReadLine()) != null)
|
|
{
|
|
var line = raw.Trim();
|
|
if (string.IsNullOrEmpty(line)) continue;
|
|
|
|
if (line.StartsWith("#EXT-X-STREAM-INF:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
pendingVariant = ParseAttrs(line.Substring("#EXT-X-STREAM-INF:".Length));
|
|
continue;
|
|
}
|
|
if (line.StartsWith("#EXT-X-MEDIA-SEQUENCE:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = line.Split(':', 2);
|
|
if (parts.Length > 1 && long.TryParse(parts[1].Trim(), out var s) && s >= 0) seq = s;
|
|
continue;
|
|
}
|
|
if (line.StartsWith("#EXT-X-KEY:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var a = ParseAttrs(line.Substring("#EXT-X-KEY:".Length));
|
|
var method = a.GetValue("METHOD", "NONE").ToUpperInvariant();
|
|
curKey = method == "AES-128"
|
|
? new KeyInfo(
|
|
a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u) && Uri.TryCreate(baseUrl, u.Trim('"'), out var ku) ? ku : null,
|
|
a.GetValue("IV"))
|
|
: null;
|
|
continue;
|
|
}
|
|
if (line.StartsWith("#EXT-X-BYTERANGE:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var val = line.Substring("#EXT-X-BYTERANGE:".Length).Trim();
|
|
var parts = val.Split('@', 2);
|
|
if (long.TryParse(parts[0].Trim(), out var len) && len > 0)
|
|
{
|
|
pendingByteRangeLength = len;
|
|
if (parts.Length > 1 && long.TryParse(parts[1].Trim(), out var off) && off >= 0)
|
|
{
|
|
pendingByteRangeOffset = off;
|
|
prevByteRangeOffset = off;
|
|
}
|
|
else
|
|
{
|
|
pendingByteRangeOffset = prevByteRangeOffset + prevByteRangeLength;
|
|
prevByteRangeOffset = pendingByteRangeOffset.Value;
|
|
}
|
|
prevByteRangeLength = len;
|
|
}
|
|
continue;
|
|
}
|
|
if (line.StartsWith("#EXT-X-MAP:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var a = ParseAttrs(line.Substring("#EXT-X-MAP:".Length));
|
|
if (a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u) && Uri.TryCreate(baseUrl, u.Trim('"'), out var mu))
|
|
init = mu;
|
|
|
|
if (a.TryGetValue("BYTERANGE", out var br) && !string.IsNullOrEmpty(br))
|
|
{
|
|
var cleanBr = br.Trim('"');
|
|
var brParts = cleanBr.Split('@', 2);
|
|
if (long.TryParse(brParts[0].Trim(), out var bLen) && bLen > 0)
|
|
{
|
|
initByteRangeLen = bLen;
|
|
if (brParts.Length > 1 && long.TryParse(brParts[1].Trim(), out var bOff) && bOff >= 0)
|
|
{
|
|
initByteRangeOff = bOff;
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (line.StartsWith("#")) continue;
|
|
|
|
// URL 라인
|
|
if (!Uri.TryCreate(baseUrl, line, out var abs)) continue;
|
|
|
|
if (pendingVariant != null)
|
|
{
|
|
int.TryParse(pendingVariant.GetValue("BANDWIDTH", "0"), out var bw);
|
|
variants.Add(new Variant(abs, Math.Max(0, bw),
|
|
pendingVariant.GetValue("RESOLUTION"), pendingVariant.GetValue("CODECS")));
|
|
pendingVariant = null;
|
|
isMaster = true;
|
|
}
|
|
else
|
|
{
|
|
segments.Add(new Segment(abs, idx, seq, curKey, pendingByteRangeLength, pendingByteRangeOffset));
|
|
pendingByteRangeLength = null;
|
|
pendingByteRangeOffset = null;
|
|
idx++;
|
|
if (seq < long.MaxValue) seq++;
|
|
}
|
|
}
|
|
|
|
return new ParsedPlaylist
|
|
{
|
|
Type = isMaster ? PlaylistType.Master : PlaylistType.Media,
|
|
Variants = variants,
|
|
Segments = segments,
|
|
InitSegment = init,
|
|
InitByteRangeLength = initByteRangeLen,
|
|
InitByteRangeOffset = initByteRangeOff,
|
|
};
|
|
}
|
|
}
|