From 35ca34c0b00a9453a26010a41a41e127d66b0413 Mon Sep 17 00:00:00 2001 From: crypton Date: Sat, 6 Feb 2021 16:50:12 -0800 Subject: [PATCH] ffprobe duration parsing - on large recordings (e.g. radio transmissions), ffprobe might return number of hours which is too large for TimeSpan.Parse (exception: The TimeSpan string '149:07:50.911750' could not be parsed because at least one of the numeric components is out of range or contains too many digits.) - use regex groups to extract components (hours/minutes/seconds/millis) then parse/create new timespan from that - NOTICE: this will discard microseconds provided by ffprobe, not sure if this is significant - ffprobe has inconsitencies with how it represents millisecond component. Sometimes it may return just `82` for 820 milliseconds, so padding with 0s is required on the left. Likewise, sometimes it might return microseconds past milliseconds (first 3 significant figures); this is currently discarded - Added InternalsVisibleTo to help with unit testing *just* the duration parsing function --- FFMpegCore.Test/FFProbeTests.cs | 18 ++++++++++++- FFMpegCore/Assembly.cs | 3 +++ FFMpegCore/FFProbe/MediaAnalysis.cs | 42 ++++++++++++++++++++++------- 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 FFMpegCore/Assembly.cs diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 21d9d34..8dc675b 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -23,7 +23,23 @@ public async Task Audio_FromStream_Duration() var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); } - + + [TestMethod] + public void MediaAnalysis_ParseDuration() + { + var durationHHMMSS = new FFProbeStream { Duration = "05:12:59.177" }; + var longDuration = new FFProbeStream { Duration = "149:07:50.911750" }; + var shortDuration = new FFProbeStream { Duration = "00:00:00.83" }; + + var testdurationHHMMSS = MediaAnalysis.ParseDuration(durationHHMMSS); + var testlongDuration = MediaAnalysis.ParseDuration(longDuration); + var testshortDuration = MediaAnalysis.ParseDuration(shortDuration); + + Assert.IsTrue(testdurationHHMMSS.Days == 0 && testdurationHHMMSS.Hours == 5 && testdurationHHMMSS.Minutes == 12 && testdurationHHMMSS.Seconds == 59 && testdurationHHMMSS.Milliseconds == 177); + Assert.IsTrue(testlongDuration.Days == 6 && testlongDuration.Hours == 5 && testlongDuration.Minutes == 7 && testlongDuration.Seconds == 50 && testlongDuration.Milliseconds == 911); + Assert.IsTrue(testdurationHHMMSS.Days == 0 && testshortDuration.Hours == 0 && testshortDuration.Minutes == 0 && testshortDuration.Seconds == 0 && testshortDuration.Milliseconds == 830); + } + [TestMethod] public void Probe_Success() { diff --git a/FFMpegCore/Assembly.cs b/FFMpegCore/Assembly.cs new file mode 100644 index 0000000..0117671 --- /dev/null +++ b/FFMpegCore/Assembly.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("FFMpegCore.Test")] \ No newline at end of file diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 5a43aa2..011a8db 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -7,7 +7,7 @@ namespace FFMpegCore { internal class MediaAnalysis : IMediaAnalysis { - private static readonly Regex DurationRegex = new Regex("^(\\d{1,2}:\\d{1,2}:\\d{1,2}(.\\d{1,7})?)", RegexOptions.Compiled); + private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); internal MediaAnalysis(string path, FFProbeAnalysis analysis) { @@ -23,7 +23,7 @@ private MediaFormat ParseFormat(Format analysisFormat) { return new MediaFormat { - Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"), + Duration = ParseDuration(analysisFormat.Duration), FormatName = analysisFormat.FormatName, FormatLongName = analysisFormat.FormatLongName, StreamCount = analysisFormat.NbStreams, @@ -74,17 +74,41 @@ private VideoStream ParseVideoStream(FFProbeStream stream) }; } - private static TimeSpan ParseDuration(FFProbeStream ffProbeStream) + internal static TimeSpan ParseDuration(string duration) { - return !string.IsNullOrEmpty(ffProbeStream.Duration) - ? TimeSpan.Parse(ffProbeStream.Duration) - : TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0"); + 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; + } } - private static string? TrimTimeSpan(string? durationTag) + internal static TimeSpan ParseDuration(FFProbeStream ffProbeStream) { - var durationMatch = DurationRegex.Match(durationTag ?? ""); - return durationMatch.Success ? durationMatch.Groups[1].Value : null; + return ParseDuration(ffProbeStream.Duration); } private AudioStream ParseAudioStream(FFProbeStream stream)