From 4b89de8478f0769c822a8b98c2c152e3c52977b7 Mon Sep 17 00:00:00 2001 From: emranemran Date: Thu, 7 Sep 2023 10:54:56 -0700 Subject: [PATCH] input_copy: "manually" parse duration of HLS streams when ffprobe fails For some HLS streams, the duration cannot be detected correctly and ffprobe reports "N/A" as duration. This was evident in long running livestreams that were 100s of hours in length. In such cases, we parse the manifest file using the m3u library to manually determine the duration by adding up segment lengths as reported by the EXTINF tags. --- clients/input_copy.go | 21 +++++++++++++++++++++ video/clip.go | 4 ++-- video/clip_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/clients/input_copy.go b/clients/input_copy.go index 2137ac718..932455da5 100644 --- a/clients/input_copy.go +++ b/clients/input_copy.go @@ -72,6 +72,18 @@ func (s *InputCopy) CopyInputToS3(requestID string, inputFile, osTransferURL *ur log.Log(requestID, "probe succeeded", "source", inputFile.String(), "dest", osTransferURL.String()) videoTrack, err := inputFileProbe.GetTrack(video.TrackTypeVideo) hasVideoTrack := err == nil + // verify the duration of the video track and don't process if we can't determine duration + if hasVideoTrack && videoTrack.DurationSec == 0 { + duration := 0.0 + if IsHLSInput(inputFile) { + duration = getVideoTrackDuration(requestID, signedURL) + } + if duration == 0.0 { + log.Log(requestID, "input file duration is 0 or cannot be determined") + } else { + videoTrack.DurationSec = duration + } + } if hasVideoTrack { log.Log(requestID, "probed video track:", "container", inputFileProbe.Format, "codec", videoTrack.Codec, "bitrate", videoTrack.Bitrate, "duration", videoTrack.DurationSec, "w", videoTrack.Width, "h", videoTrack.Height, "pix-format", videoTrack.PixelFormat, "FPS", videoTrack.FPS) } @@ -88,6 +100,15 @@ func (s *InputCopy) CopyInputToS3(requestID string, inputFile, osTransferURL *ur return inputFileProbe, signedURL, nil } +func getVideoTrackDuration(requestID, manifestUrl string) float64 { + manifest, err := DownloadRenditionManifest(requestID, manifestUrl) + if err != nil { + return 0 + } + manifestDuration, _ := video.GetTotalDurationAndSegments(&manifest) + return manifestDuration +} + func getSignedURL(osTransferURL *url.URL) (string, error) { // check if plain https is accessible, if not then the bucket must be private and we need to generate a signed url // in most cases signed urls work fine as input but in the edge case where we have to fall back to mediaconvert diff --git a/video/clip.go b/video/clip.go index 94f0714f4..f0aeb5cc1 100644 --- a/video/clip.go +++ b/video/clip.go @@ -15,7 +15,7 @@ func formatTime(seconds float64) string { return timeObj.Format("15:04:05") } -func getTotalDurationAndSegments(manifest *m3u8.MediaPlaylist) (float64, uint64) { +func GetTotalDurationAndSegments(manifest *m3u8.MediaPlaylist) (float64, uint64) { if manifest == nil { return 0.0, 0 } @@ -60,7 +60,7 @@ func ClipManifest(requestID string, manifest *m3u8.MediaPlaylist, startTime, end var startSegIdx, endSegIdx uint64 var err error - manifestDuration, manifestSegments := getTotalDurationAndSegments(manifest) + manifestDuration, manifestSegments := GetTotalDurationAndSegments(manifest) // Find the segment index that correlates with the specified startTime // but error out it exceeds the manifest's duration. diff --git a/video/clip_test.go b/video/clip_test.go index ab3a8bc84..ba51ae0a4 100644 --- a/video/clip_test.go +++ b/video/clip_test.go @@ -49,6 +49,45 @@ const manifestC = `#EXTM3U 3.ts #EXT-X-ENDLIST` +// an example of a manifest that ffprobe fails on when +// trying to determine the duration. +const manifestD = `#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-PLAYLIST-TYPE:EVENT +#EXT-X-TARGETDURATION:11.0000000000 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PROGRAM-DATE-TIME:2023-08-28T10:17:20.948Z +#EXTINF:10.634, +source/0.ts +#EXT-X-PROGRAM-DATE-TIME:2023-08-28T10:17:31.582Z +#EXTINF:10.000, +source/1.ts +#EXT-X-PROGRAM-DATE-TIME:2023-08-28T10:17:41.582Z +#EXTINF:10.000, +source/63744.ts +#EXT-X-PROGRAM-DATE-TIME:2023-09-05T00:13:30.682Z +#EXTINF:10.000, +source/63745.ts +` + +func TestManifestDurationCalculation(t *testing.T) { + sourceManifestB, _, err := m3u8.DecodeFrom(strings.NewReader(manifestB), true) + require.NoError(t, err) + plB := sourceManifestB.(*m3u8.MediaPlaylist) + + dur, segs := GetTotalDurationAndSegments(plB) + require.Equal(t, 18.78, dur) + require.Equal(t, uint64(4), segs) + + sourceManifestD, _, err := m3u8.DecodeFrom(strings.NewReader(manifestD), true) + require.NoError(t, err) + plD := sourceManifestD.(*m3u8.MediaPlaylist) + + dur, segs = GetTotalDurationAndSegments(plD) + require.Equal(t, 40.634, dur) + require.Equal(t, uint64(4), segs) +} + func TestClippingFailsWhenInvalidManifestIsUsed(t *testing.T) { sourceManifestC, _, err := m3u8.DecodeFrom(strings.NewReader(manifestC), true)