FFMpegCore/FFMpegCore/FFProbe/MediaAnalysis.cs
2024-01-30 00:52:25 +02:00

254 lines
11 KiB
C#

using System.Text.RegularExpressions;
using FFMpegCore.Builders.MetaData;
namespace FFMpegCore
{
internal class MediaAnalysis : IMediaAnalysis
{
internal MediaAnalysis(FFProbeAnalysis analysis)
{
Format = ParseFormat(analysis.Format);
Chapters = analysis.Chapters.Select(c => ParseChapter(c)).ToList();
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList();
ErrorData = analysis.ErrorData;
}
private MediaFormat ParseFormat(Format analysisFormat)
{
return new MediaFormat
{
Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration),
StartTime = MediaAnalysisUtils.ParseDuration(analysisFormat.StartTime),
FormatName = analysisFormat.FormatName,
FormatLongName = analysisFormat.FormatLongName,
StreamCount = analysisFormat.NbStreams,
ProbeScore = analysisFormat.ProbeScore,
BitRate = long.Parse(analysisFormat.BitRate ?? "0"),
Tags = analysisFormat.Tags.ToCaseInsensitive(),
};
}
private ChapterData ParseChapter(Chapter analysisChapter)
{
var title = analysisChapter.Tags.FirstOrDefault(t => t.Key == "title").Value;
var start = MediaAnalysisUtils.ParseDuration(analysisChapter.StartTime);
var end = MediaAnalysisUtils.ParseDuration(analysisChapter.EndTime);
return new ChapterData(title, start, end);
}
public TimeSpan Duration => new[]
{
Format.Duration,
PrimaryVideoStream?.Duration ?? TimeSpan.Zero,
PrimaryAudioStream?.Duration ?? TimeSpan.Zero
}.Max();
public MediaFormat Format { get; }
public List<ChapterData> Chapters { get; }
public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault();
public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault();
public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault();
public List<VideoStream> VideoStreams { get; }
public List<AudioStream> AudioStreams { get; }
public List<SubtitleStream> SubtitleStreams { get; }
public IReadOnlyList<string> ErrorData { get; }
private int? GetBitDepth(FFProbeStream stream)
{
var bitDepth = int.TryParse(stream.BitsPerRawSample, out var bprs) ? bprs :
stream.BitsPerSample;
return bitDepth == 0 ? null : bitDepth;
}
private VideoStream ParseVideoStream(FFProbeStream stream)
{
return new VideoStream
{
Index = stream.Index,
AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')),
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
CodecTag = stream.CodecTag,
CodecTagString = stream.CodecTagString,
DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'),
SampleAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.SampleAspectRatio, ':'),
Duration = MediaAnalysisUtils.ParseDuration(stream.Duration),
StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime),
FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')),
Height = stream.Height ?? 0,
Width = stream.Width ?? 0,
Profile = stream.Profile,
PixelFormat = stream.PixelFormat,
ColorRange = stream.ColorRange,
ColorSpace = stream.ColorSpace,
ColorTransfer = stream.ColorTransfer,
ColorPrimaries = stream.ColorPrimaries,
Rotation = MediaAnalysisUtils.ParseRotation(stream),
Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags.ToCaseInsensitive(),
BitDepth = GetBitDepth(stream),
};
}
private AudioStream ParseAudioStream(FFProbeStream stream)
{
return new AudioStream
{
Index = stream.Index,
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
CodecTag = stream.CodecTag,
CodecTagString = stream.CodecTagString,
Channels = stream.Channels ?? default,
ChannelLayout = stream.ChannelLayout,
Duration = MediaAnalysisUtils.ParseDuration(stream.Duration),
StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime),
SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default,
Profile = stream.Profile,
Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags.ToCaseInsensitive(),
BitDepth = GetBitDepth(stream),
};
}
private SubtitleStream ParseSubtitleStream(FFProbeStream stream)
{
return new SubtitleStream
{
Index = stream.Index,
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
CodecName = stream.CodecName,
CodecLongName = stream.CodecLongName,
Duration = MediaAnalysisUtils.ParseDuration(stream.Duration),
StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime),
Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags.ToCaseInsensitive(),
};
}
}
public static class MediaAnalysisUtils
{
private static readonly Regex DurationRegex = new(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled);
internal static Dictionary<string, string> ToCaseInsensitive(this Dictionary<string, string>? dictionary)
{
return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary<string, string>();
}
public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
public static (int, int) ParseRatioInt(string input, char separator)
{
if (string.IsNullOrEmpty(input))
{
return (0, 0);
}
var ratio = input.Split(separator);
return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1]));
}
public static (double, double) ParseRatioDouble(string input, char separator)
{
if (string.IsNullOrEmpty(input))
{
return (0, 0);
}
var ratio = input.Split(separator);
return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0);
}
public static double ParseDoubleInvariant(string line) =>
double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
public static int ParseIntInvariant(string line) =>
int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
public static long ParseLongInvariant(string line) =>
long.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
public static TimeSpan ParseDuration(string duration)
{
if (!string.IsNullOrEmpty(duration))
{
var match = DurationRegex.Match(duration);
if (match.Success)
{
// ffmpeg may provide < 3-digit number of milliseconds (omitting trailing zeros), which won't simply parse correctly
// e.g. 00:12:02.11 -> 12 minutes 2 seconds and 110 milliseconds
var millisecondsPart = match.Groups[4].Value;
if (millisecondsPart.Length < 3)
{
millisecondsPart = millisecondsPart.PadRight(3, '0');
}
var hours = int.Parse(match.Groups[1].Value);
var minutes = int.Parse(match.Groups[2].Value);
var seconds = int.Parse(match.Groups[3].Value);
var milliseconds = int.Parse(millisecondsPart);
return new TimeSpan(0, hours, minutes, seconds, milliseconds);
}
else
{
return TimeSpan.Zero;
}
}
else
{
return TimeSpan.Zero;
}
}
public static int ParseRotation(FFProbeStream fFProbeStream)
{
var displayMatrixSideData = fFProbeStream.SideData?.Find(item => item.TryGetValue("side_data_type", out var rawSideDataType) && rawSideDataType.ToString() == "Display Matrix");
if (displayMatrixSideData?.TryGetValue("rotation", out var rawRotation) ?? false)
{
return (int)float.Parse(rawRotation.ToString());
}
else
{
return (int)float.Parse(fFProbeStream.GetRotate() ?? "0");
}
}
public static Dictionary<string, bool>? FormatDisposition(Dictionary<string, int>? disposition)
{
if (disposition == null)
{
return null;
}
var result = new Dictionary<string, bool>(disposition.Count, StringComparer.Ordinal);
foreach (var pair in disposition)
{
result.Add(pair.Key, ToBool(pair.Value));
}
static bool ToBool(int value) => value switch
{
0 => false,
1 => true,
_ => throw new ArgumentOutOfRangeException(nameof(value),
$"Not expected disposition state value: {value}")
};
return result;
}
}
}