381 lines
12 KiB
C#
381 lines
12 KiB
C#
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using EmbyToolbox.Models;
|
|
|
|
namespace EmbyToolbox.Services;
|
|
|
|
/// <summary>Разбор JSON ffprobe в <see cref="MediaAnalysisResult"/>.</summary>
|
|
public static class MediaAnalysisParser
|
|
{
|
|
public static MediaAnalysisResult? TryParse(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(json);
|
|
return ParseRoot(doc.RootElement);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static MediaAnalysisResult ParseRoot(JsonElement root)
|
|
{
|
|
var format = string.Empty;
|
|
var name = string.Empty;
|
|
long? formatBitRateBps = null;
|
|
double? durationSec = null;
|
|
if (root.TryGetProperty("format", out var fmt) && fmt.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (fmt.TryGetProperty("format_name", out var fn) && fn.ValueKind == JsonValueKind.String)
|
|
{
|
|
format = fn.GetString() ?? string.Empty;
|
|
}
|
|
if (fmt.TryGetProperty("format_long_name", out var fl) && fl.ValueKind == JsonValueKind.String)
|
|
{
|
|
name = fl.GetString() ?? string.Empty;
|
|
}
|
|
if (fmt.TryGetProperty("duration", out var d))
|
|
{
|
|
durationSec = ParseDuration(d);
|
|
}
|
|
|
|
formatBitRateBps = ParseLong(fmt, "bit_rate");
|
|
}
|
|
|
|
var streams = new List<MediaStreamInfo>();
|
|
if (root.TryGetProperty("streams", out var st) && st.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var e in st.EnumerateArray())
|
|
{
|
|
if (e.ValueKind == JsonValueKind.Object)
|
|
{
|
|
var s = TryParseStream(e);
|
|
if (s is not null)
|
|
{
|
|
streams.Add(s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var primaryVideoBitrate = streams
|
|
.Where(x => x.Kind == MediaStreamKind.Video)
|
|
.OrderByDescending(static v => ((long)(v.Width ?? 0)) * (v.Height ?? 0))
|
|
.ThenByDescending(static v => v.IsDefault ? 1 : 0)
|
|
.Select(static v => v.BitRateBps)
|
|
.FirstOrDefault();
|
|
|
|
return new MediaAnalysisResult
|
|
{
|
|
ContainerFormat = format,
|
|
FormatName = string.IsNullOrEmpty(name) ? format : name,
|
|
FormatBitRateBps = formatBitRateBps,
|
|
DurationSeconds = durationSec,
|
|
AllStreams = streams,
|
|
VideoStreams = streams.Where(x => x.Kind == MediaStreamKind.Video).ToList(),
|
|
AudioStreams = streams.Where(x => x.Kind == MediaStreamKind.Audio).ToList(),
|
|
SubtitleStreams = streams.Where(x => x.Kind == MediaStreamKind.Subtitle).ToList(),
|
|
DataStreams = streams.Where(x => x.Kind == MediaStreamKind.Data).ToList(),
|
|
SourceVideoBitrateBps = primaryVideoBitrate ?? formatBitRateBps
|
|
};
|
|
}
|
|
|
|
private static MediaStreamInfo? TryParseStream(JsonElement s)
|
|
{
|
|
if (!s.TryGetProperty("codec_type", out var ct) || ct.ValueKind != JsonValueKind.String)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var streamDuration = GetStreamDuration(s);
|
|
var t = (ct.GetString() ?? string.Empty).ToLowerInvariant();
|
|
if (t == "video")
|
|
{
|
|
return new MediaStreamInfo
|
|
{
|
|
Index = GetInt(s, "index", -1),
|
|
Kind = MediaStreamKind.Video,
|
|
CodecName = GetStr(s, "codec_name", "?") ?? "?",
|
|
Profile = GetStr(s, "profile", null),
|
|
Encoder = GetTagExact(s, "encoder"),
|
|
Language = GetTagLang(s),
|
|
Title = GetTagTitle(s),
|
|
IsDefault = GetDisposition(s, "default", false),
|
|
Width = (int?)GetIntNullable(s, "width"),
|
|
Height = (int?)GetIntNullable(s, "height"),
|
|
AverageFrameRate = ParseFpsByKey(s, "avg_frame_rate"),
|
|
FrameRate = ParseFps(s),
|
|
PixelFormat = GetStr(s, "pix_fmt", null),
|
|
ColorSpace = GetStr(s, "color_space", null),
|
|
ColorPrimaries = GetStr(s, "color_primaries", null),
|
|
ColorTransfer = GetStr(s, "color_transfer", null),
|
|
BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"),
|
|
DurationSeconds = streamDuration
|
|
};
|
|
}
|
|
|
|
if (t == "audio")
|
|
{
|
|
return new MediaStreamInfo
|
|
{
|
|
Index = GetInt(s, "index", -1),
|
|
Kind = MediaStreamKind.Audio,
|
|
CodecName = GetStr(s, "codec_name", "?") ?? "?",
|
|
Profile = GetStr(s, "profile", null),
|
|
Encoder = GetTagExact(s, "encoder"),
|
|
Language = GetTagLang(s),
|
|
Title = GetTagTitle(s),
|
|
IsDefault = GetDisposition(s, "default", false),
|
|
BitRateBps = ParseLong(s, "bit_rate") ?? ParseLong(s, "max_bit_rate"),
|
|
Channels = (int?)GetIntNullable(s, "channels"),
|
|
SampleRateHz = (int?)GetIntNullable(s, "sample_rate"),
|
|
DurationSeconds = streamDuration
|
|
};
|
|
}
|
|
|
|
if (t == "subtitle")
|
|
{
|
|
var isForcedDisposition = GetDisposition(s, "forced", false);
|
|
return new MediaStreamInfo
|
|
{
|
|
Index = GetInt(s, "index", -1),
|
|
Kind = MediaStreamKind.Subtitle,
|
|
CodecName = GetStr(s, "codec_name", "?") ?? "?",
|
|
Profile = GetStr(s, "profile", null),
|
|
Encoder = GetTagExact(s, "encoder"),
|
|
Language = GetTagLang(s),
|
|
Title = GetTagTitle(s),
|
|
IsDefault = GetDisposition(s, "default", false),
|
|
IsForcedByDisposition = isForcedDisposition,
|
|
IsForced = isForcedDisposition,
|
|
ForcedDetectionReason = isForcedDisposition ? "disposition" : null,
|
|
SubtitleFormat = GetStr(s, "codec_name", null),
|
|
FileNameTag = GetTagExact(s, "filename"),
|
|
DurationSeconds = streamDuration
|
|
};
|
|
}
|
|
|
|
if (t == "attachment")
|
|
{
|
|
return new MediaStreamInfo
|
|
{
|
|
Index = GetInt(s, "index", -1),
|
|
Kind = MediaStreamKind.Attachment,
|
|
CodecName = "attachment",
|
|
DurationSeconds = streamDuration,
|
|
AttachmentDeclaredFileName = GetTagExact(s, "filename"),
|
|
AttachmentDeclaredMimeType = GetTagExact(s, "mimetype"),
|
|
};
|
|
}
|
|
|
|
if (t == "data")
|
|
{
|
|
return new MediaStreamInfo
|
|
{
|
|
Index = GetInt(s, "index", -1),
|
|
Kind = MediaStreamKind.Data,
|
|
CodecName = GetStr(s, "codec_name", "data") ?? "data",
|
|
DurationSeconds = streamDuration
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static double? GetStreamDuration(JsonElement s)
|
|
{
|
|
if (!s.TryGetProperty("duration", out var d))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return ParseDuration(d);
|
|
}
|
|
|
|
private static int GetInt(JsonElement s, string name, int def)
|
|
{
|
|
if (s.TryGetProperty(name, out var n))
|
|
{
|
|
if (n.ValueKind == JsonValueKind.Number && n.TryGetInt32(out var v))
|
|
{
|
|
return v;
|
|
}
|
|
|
|
if (n.ValueKind == JsonValueKind.String && int.TryParse(n.GetString(), out var s2))
|
|
{
|
|
return s2;
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
private static int? GetIntNullable(JsonElement s, string name)
|
|
{
|
|
if (s.TryGetProperty(name, out var n) && n.ValueKind == JsonValueKind.Number)
|
|
{
|
|
return n.GetInt32();
|
|
}
|
|
|
|
if (s.TryGetProperty(name, out var s2) && s2.ValueKind == JsonValueKind.String && int.TryParse(s2.GetString(), out var p))
|
|
{
|
|
return p;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? GetStr(JsonElement s, string name, string? def)
|
|
{
|
|
if (s.TryGetProperty(name, out var n) && n.ValueKind == JsonValueKind.String)
|
|
{
|
|
return n.GetString() ?? def;
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
private static long? ParseLong(JsonElement s, string name)
|
|
{
|
|
if (!s.TryGetProperty(name, out var p))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (p.ValueKind == JsonValueKind.String && long.TryParse(p.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var a))
|
|
{
|
|
return a;
|
|
}
|
|
|
|
if (p.ValueKind == JsonValueKind.Number)
|
|
{
|
|
if (p.TryGetInt64(out var l))
|
|
{
|
|
return l;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? GetTagLang(JsonElement s)
|
|
{
|
|
if (s.TryGetProperty("tags", out var t) && t.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (t.TryGetProperty("language", out var l) && l.ValueKind == JsonValueKind.String)
|
|
{
|
|
return l.GetString();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? GetTagTitle(JsonElement s)
|
|
{
|
|
if (s.TryGetProperty("tags", out var t) && t.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (t.TryGetProperty("title", out var l) && l.ValueKind == JsonValueKind.String)
|
|
{
|
|
return l.GetString();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static string? GetTagExact(JsonElement stream, string tagKey)
|
|
{
|
|
if (!stream.TryGetProperty("tags", out var tags) || tags.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!tags.TryGetProperty(tagKey, out var val) || val.ValueKind != JsonValueKind.String)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return val.GetString();
|
|
}
|
|
|
|
private static bool GetDisposition(JsonElement s, string d, bool def)
|
|
{
|
|
if (s.TryGetProperty("disposition", out var dis) && dis.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (dis.TryGetProperty(d, out var v))
|
|
{
|
|
if (v.ValueKind == JsonValueKind.Number)
|
|
{
|
|
return v.GetInt32() != 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
return def;
|
|
}
|
|
|
|
private static double? ParseFps(JsonElement s)
|
|
{
|
|
return ParseFpsByKey(s, "r_frame_rate");
|
|
}
|
|
|
|
private static double? ParseFpsByKey(JsonElement s, string key)
|
|
{
|
|
if (!s.TryGetProperty(key, out var f) || f.ValueKind != JsonValueKind.String)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return ParseFpsString(f.GetString() ?? string.Empty);
|
|
}
|
|
|
|
private static double? ParseFpsString(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s) || s == "0/0")
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var p = s.IndexOf('/');
|
|
if (p > 0)
|
|
{
|
|
if (int.TryParse(s.AsSpan(0, p), out var a) && int.TryParse(s.AsSpan(p + 1), out var b) && b != 0)
|
|
{
|
|
return a / (double)b;
|
|
}
|
|
}
|
|
|
|
if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
|
|
{
|
|
return d;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static double? ParseDuration(JsonElement d)
|
|
{
|
|
if (d.ValueKind == JsonValueKind.Number)
|
|
{
|
|
return d.GetDouble();
|
|
}
|
|
|
|
if (d.ValueKind == JsonValueKind.String &&
|
|
double.TryParse(d.GetString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var v))
|
|
{
|
|
return v;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|