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


Former-commit-id: 35ca34c0b0
This commit is contained in:
crypton 2021-02-06 16:50:12 -08:00
parent cbb6c5a055
commit 83c9478007
3 changed files with 53 additions and 10 deletions

View file

@ -23,7 +23,23 @@ public async Task Audio_FromStream_Duration()
var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); var streamAnalysis = await FFProbe.AnalyseAsync(inputStream);
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); 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] [TestMethod]
public void Probe_Success() public void Probe_Success()
{ {

3
FFMpegCore/Assembly.cs Normal file
View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("FFMpegCore.Test")]

View file

@ -7,7 +7,7 @@ namespace FFMpegCore
{ {
internal class MediaAnalysis : IMediaAnalysis 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) internal MediaAnalysis(string path, FFProbeAnalysis analysis)
{ {
@ -23,7 +23,7 @@ private MediaFormat ParseFormat(Format analysisFormat)
{ {
return new MediaFormat return new MediaFormat
{ {
Duration = TimeSpan.Parse(analysisFormat.Duration ?? "0"), Duration = ParseDuration(analysisFormat.Duration),
FormatName = analysisFormat.FormatName, FormatName = analysisFormat.FormatName,
FormatLongName = analysisFormat.FormatLongName, FormatLongName = analysisFormat.FormatLongName,
StreamCount = analysisFormat.NbStreams, 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) if (!string.IsNullOrEmpty(duration))
? TimeSpan.Parse(ffProbeStream.Duration) {
: TimeSpan.Parse(TrimTimeSpan(ffProbeStream.GetDuration()) ?? "0"); 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 ParseDuration(ffProbeStream.Duration);
return durationMatch.Success ? durationMatch.Groups[1].Value : null;
} }
private AudioStream ParseAudioStream(FFProbeStream stream) private AudioStream ParseAudioStream(FFProbeStream stream)