-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
clipping: add methods to enable clip HLS manifests
ClipManifest takes a manifest and the start/end clip times and returns MediaSegments that correspond to those times.
- Loading branch information
1 parent
5662003
commit 93c93ca
Showing
2 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package video | ||
|
||
import ( | ||
"fmt" | ||
"github.com/grafov/m3u8" | ||
"github.com/livepeer/catalyst-api/log" | ||
) | ||
|
||
func getTotalDurationAndSegments(manifest *m3u8.MediaPlaylist) (float64, uint64) { | ||
if manifest == nil { | ||
return 0.0, 0 | ||
} | ||
var totalDuration float64 | ||
var segmentCount uint64 | ||
for _, segment := range manifest.Segments { | ||
if segment == nil { | ||
break | ||
} | ||
totalDuration += segment.Duration | ||
segmentCount++ | ||
} | ||
return totalDuration, segmentCount | ||
} | ||
|
||
// Finds the segment in an HLS manifest that contains the timestamp (aka playhead) specified. | ||
func getRelevantSegment(allSegments []*m3u8.MediaSegment, playHeadTime float64, startingIdx uint64) (uint64, error) { | ||
playHeadDiff := 0.0 | ||
|
||
for _, segment := range allSegments[startingIdx:] { | ||
// Break if we reach the end of the MediaSegment slice that contains all segments in the manifest | ||
if segment == nil { | ||
break | ||
} | ||
// Skip any segments where duration is 0s | ||
if segment.Duration <= 0.0 { | ||
return 0, fmt.Errorf("error clipping: found 0s duration segments") | ||
} | ||
// Check if the playhead is within the current segment and skip to | ||
// the next segment if it's not. Also update the play head by referencing | ||
// the starting time of the next segment. | ||
playHeadDiff = playHeadTime - segment.Duration | ||
if playHeadDiff > 0.0 { | ||
playHeadTime = playHeadDiff | ||
continue | ||
} | ||
// If we reach here, then we've found the relevant segment that falls within the playHeadTime | ||
return segment.SeqId, nil | ||
} | ||
return 0, fmt.Errorf("error clipping: did not find a segment that falls within %v seconds", playHeadTime) | ||
} | ||
|
||
// Function to find relevant segments that span from the clipping start and end times | ||
func ClipManifest(requestID string, manifest *m3u8.MediaPlaylist, startTime, endTime float64) ([]*m3u8.MediaSegment, error) { | ||
var startSegIdx, endSegIdx uint64 | ||
var err error | ||
|
||
manifestDuration, manifestSegments := getTotalDurationAndSegments(manifest) | ||
|
||
// Find the segment index that correlates with the specified startTime | ||
// but error out it exceeds the manifest's duration. | ||
if startTime > manifestDuration { | ||
return nil, fmt.Errorf("error clipping: start time specified exceeds duration of manifest") | ||
} else { | ||
startSegIdx, err = getRelevantSegment(manifest.Segments, startTime, 0) | ||
if err != nil { | ||
return nil, fmt.Errorf("error clipping: failed to get a starting index") | ||
} | ||
} | ||
|
||
// Find the segment index that correlates with the specified endTime. | ||
if endTime > manifestDuration { | ||
endSegIdx = manifestSegments - 1 | ||
} else { | ||
endSegIdx, err = getRelevantSegment(manifest.Segments, endTime, startSegIdx) | ||
if err != nil { | ||
return nil, fmt.Errorf("error clipping: failed to get an ending index") | ||
} | ||
} | ||
|
||
// Generate a slice of all segments that overlap with startTime to endTime | ||
relevantSegments := manifest.Segments[startSegIdx : endSegIdx+1] | ||
totalRelSegs := len(relevantSegments) | ||
if totalRelSegs == 0 { | ||
return nil, fmt.Errorf("error clipping: no relevant segments found in the specified time range") | ||
} | ||
|
||
log.Log(requestID, "Clipping segments", "from", startSegIdx, "to", endSegIdx) | ||
|
||
return relevantSegments, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package video | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/grafov/m3u8" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
const manifestA = `#EXTM3U | ||
#EXT-X-VERSION:3 | ||
#EXT-X-PLAYLIST-TYPE:VOD | ||
#EXT-X-TARGETDURATION:5 | ||
#EXT-X-MEDIA-SEQUENCE:0 | ||
#EXTINF:10.4160000000, | ||
0.ts | ||
#EXTINF:5.3340000000, | ||
5000.ts | ||
#EXT-X-ENDLIST` | ||
|
||
const manifestB = `#EXTM3U | ||
#EXT-X-VERSION:3 | ||
#EXT-X-PLAYLIST-TYPE:EVENT | ||
#EXT-X-TARGETDURATION:5 | ||
#EXT-X-MEDIA-SEQUENCE:0 | ||
#EXT-X-PROGRAM-DATE-TIME:2023-06-06T00:27:38.157Z | ||
#EXTINF:5.780, | ||
0.ts | ||
#EXT-X-PROGRAM-DATE-TIME:2023-06-06T00:27:43.937Z | ||
#EXTINF:6.000, | ||
1.ts | ||
#EXT-X-PROGRAM-DATE-TIME:2023-06-06T00:27:49.937Z | ||
#EXTINF:6.000, | ||
2.ts | ||
#EXT-X-PROGRAM-DATE-TIME:2023-06-06T00:27:55.937Z | ||
#EXTINF:1.000, | ||
3.ts | ||
#EXT-X-ENDLIST` | ||
|
||
const manifestC = `#EXTM3U | ||
#EXT-X-VERSION:3 | ||
#EXT-X-PLAYLIST-TYPE:EVENT | ||
#EXT-X-TARGETDURATION:5 | ||
#EXT-X-MEDIA-SEQUENCE:0 | ||
0.ts | ||
1.ts | ||
2.ts | ||
3.ts | ||
#EXT-X-ENDLIST` | ||
|
||
func TestClippingFailsWhenInvalidManifestIsUsed(t *testing.T) { | ||
|
||
sourceManifestC, _, err := m3u8.DecodeFrom(strings.NewReader(manifestC), true) | ||
require.NoError(t, err) | ||
plC := sourceManifestC.(*m3u8.MediaPlaylist) | ||
|
||
_, err = ClipManifest("1234", plC, 1, 5) | ||
require.ErrorContains(t, err, "error clipping") | ||
} | ||
|
||
func TestClippingSucceedsWhenValidManifestIsUsed(t *testing.T) { | ||
sourceManifestA, _, err := m3u8.DecodeFrom(strings.NewReader(manifestA), true) | ||
require.NoError(t, err) | ||
plA := sourceManifestA.(*m3u8.MediaPlaylist) | ||
|
||
// start/end falls in same segment: ensure only 0.ts is returned | ||
segs, err := ClipManifest("1234", plA, 1, 5) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(0), uint64(segs[0].SeqId)) | ||
require.Equal(t, 1, len(segs)) | ||
|
||
// start/end falls in different segments: ensure only 0.ts and 1.ts is returned | ||
segs, err = ClipManifest("1234", plA, 1, 10.5) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(0), uint64(segs[0].SeqId)) | ||
require.Equal(t, uint64(1), uint64(segs[1].SeqId)) | ||
require.Equal(t, 2, len(segs)) | ||
|
||
// start/end with millisecond precision: ensure 0.ts and 1.ts is returned | ||
segs, err = ClipManifest("1234", plA, 10.416, 10.5) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(0), uint64(segs[0].SeqId)) | ||
require.Equal(t, uint64(1), uint64(segs[1].SeqId)) | ||
require.Equal(t, 2, len(segs)) | ||
|
||
sourceManifestB, _, err := m3u8.DecodeFrom(strings.NewReader(manifestB), true) | ||
require.NoError(t, err) | ||
plB := sourceManifestB.(*m3u8.MediaPlaylist) | ||
|
||
// start/end spans the full duration of playlist: ensure 0.ts and 3.ts is returned | ||
segs, err = ClipManifest("1234", plB, 0, 18.78) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(0), uint64(segs[0].SeqId)) | ||
require.Equal(t, uint64(3), uint64(segs[3].SeqId)) | ||
require.Equal(t, 4, len(segs)) | ||
|
||
// start exceeds the duration of playlist: ensure no segments are returned | ||
segs, err = ClipManifest("1234", plB, 30, 20.78) | ||
require.ErrorContains(t, err, "start time specified exceeds duration of manifest") | ||
|
||
// end exceeds the duration of playlist: ensure only 0.ts is returned | ||
segs, err = ClipManifest("1234", plB, 0, 20.78) | ||
require.NoError(t, err) | ||
require.Equal(t, uint64(0), uint64(segs[0].SeqId)) | ||
require.Equal(t, uint64(3), uint64(segs[3].SeqId)) | ||
require.Equal(t, 4, len(segs)) | ||
} |