Baseline: WPF/Avalonia/Cli/Core + plans (BROWSER_FEATURES_PLAN v2, MOBILE_PLAN v1)
This commit is contained in:
commit
21b7c4d8bd
60 changed files with 6581 additions and 0 deletions
101
VideoDownloader.Core/Hls/M3u8Parser.cs
Normal file
101
VideoDownloader.Core/Hls/M3u8Parser.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using VideoDownloader.Core.Models;
|
||||
|
||||
namespace VideoDownloader.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-Z0-9\-]+)=(\""(?<q>[^""]*)\"")|(?<k2>[A-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;
|
||||
bool isMaster = false;
|
||||
|
||||
Dictionary<string, string>? pendingVariant = null;
|
||||
KeyInfo? curKey = null;
|
||||
long seq = 0;
|
||||
int idx = 0;
|
||||
|
||||
foreach (var raw in text.Split('\n'))
|
||||
{
|
||||
var line = raw.Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
if (line.StartsWith("#EXT-X-STREAM-INF:"))
|
||||
{
|
||||
pendingVariant = ParseAttrs(line.Substring("#EXT-X-STREAM-INF:".Length));
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#EXT-X-MEDIA-SEQUENCE:"))
|
||||
{
|
||||
if (long.TryParse(line.Split(':', 2)[1].Trim(), out var s)) seq = s;
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#EXT-X-KEY:"))
|
||||
{
|
||||
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) ? new Uri(baseUrl, u) : null,
|
||||
a.GetValue("IV"))
|
||||
: null;
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#EXT-X-MAP:"))
|
||||
{
|
||||
var a = ParseAttrs(line.Substring("#EXT-X-MAP:".Length));
|
||||
if (a.TryGetValue("URI", out var u) && !string.IsNullOrEmpty(u))
|
||||
init = new Uri(baseUrl, u);
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#")) continue;
|
||||
|
||||
// URL 라인
|
||||
var abs = new Uri(baseUrl, line);
|
||||
if (pendingVariant != null)
|
||||
{
|
||||
int.TryParse(pendingVariant.GetValue("BANDWIDTH", "0"), out var bw);
|
||||
variants.Add(new Variant(abs, bw,
|
||||
pendingVariant.GetValue("RESOLUTION"), pendingVariant.GetValue("CODECS")));
|
||||
pendingVariant = null;
|
||||
isMaster = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
segments.Add(new Segment(abs, idx, seq, curKey));
|
||||
idx++; seq++;
|
||||
}
|
||||
}
|
||||
|
||||
return new ParsedPlaylist
|
||||
{
|
||||
Type = isMaster ? PlaylistType.Master : PlaylistType.Media,
|
||||
Variants = variants,
|
||||
Segments = segments,
|
||||
InitSegment = init,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue