Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clipping: search for first and last segments to clip #777

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions video/clip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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
}

Check warning on line 12 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L11-L12

Added lines #L11 - L12 were not covered by tests

var totalDuration float64
allSegments := manifest.GetAllSegments()
for _, segment := range allSegments {
totalDuration += segment.Duration
}
return totalDuration, uint64(len(allSegments))
}

// 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

Check warning on line 29 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L29

Added line #L29 was not covered by tests
}
// Skip any segments where duration is 0s
if segment.Duration <= 0.0 {
return 0, fmt.Errorf("error clipping: found 0s duration segments")
}

Check warning on line 34 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L33-L34

Added lines #L33 - L34 were not covered by tests
// 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)

Check warning on line 46 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L46

Added line #L46 was not covered by tests
}

// 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")
}

Check warning on line 64 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L63-L64

Added lines #L63 - L64 were not covered by tests
}

// 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")
}

Check warning on line 74 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}

// Generate a slice of all segments that overlap with startTime to endTime
relevantSegments := manifest.Segments[startSegIdx : endSegIdx+1]
emranemran marked this conversation as resolved.
Show resolved Hide resolved
totalRelSegs := len(relevantSegments)
if totalRelSegs == 0 {
return nil, fmt.Errorf("error clipping: no relevant segments found in the specified time range")
}

Check warning on line 82 in video/clip.go

View check run for this annotation

Codecov / codecov/patch

video/clip.go#L81-L82

Added lines #L81 - L82 were not covered by tests

log.Log(requestID, "Clipping segments", "from", startSegIdx, "to", endSegIdx)

return relevantSegments, nil
}
114 changes: 114 additions & 0 deletions video/clip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
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)
length := len(segs)
require.NoError(t, err)
require.Equal(t, uint64(0), uint64(segs[0].SeqId))
require.Equal(t, 1, length)

// start/end falls in different segments: ensure only 0.ts and 1.ts is returned
segs, err = ClipManifest("1234", plA, 1, 10.5)
length = len(segs)
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, length)

// start/end with millisecond precision: ensure 0.ts and 1.ts is returned
segs, err = ClipManifest("1234", plA, 10.416, 10.5)
length = len(segs)
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, length)

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)
length = len(segs)
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, length)

// 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")
require.Equal(t, segs, []*m3u8.MediaSegment(nil))

// end exceeds the duration of playlist: ensure only 0.ts is returned
segs, err = ClipManifest("1234", plB, 0, 20.78)
length = len(segs)
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, length)
}
Loading