diff --git a/clients/manifest.go b/clients/manifest.go index 05d1d31c..ee430b96 100644 --- a/clients/manifest.go +++ b/clients/manifest.go @@ -6,11 +6,8 @@ import ( "fmt" "io" "net/url" - "os" "path" - "path/filepath" "sort" - "strconv" "strings" "sync" "time" @@ -280,15 +277,6 @@ func GenerateAndUploadManifests(sourceManifest m3u8.MediaPlaylist, targetOSURL s } } - if isClip { - _, totalSegs := video.GetTotalDurationAndSegments(renditionPlaylist) - // Only add DISCONTINUITY tag if more than one segment exists in clipped playlist - if totalSegs > 1 { - renditionPlaylist.Segments[1].Discontinuity = true - renditionPlaylist.Segments[totalSegs-1].Discontinuity = true - } - } - // Write #EXT-X-ENDLIST renditionPlaylist.Close() @@ -357,13 +345,6 @@ func ClipInputManifest(requestID, sourceURL, clipTargetUrl string, startTimeUnix return nil, fmt.Errorf("error clipping: failed to download original manifest: %w", err) } - // Generate the absolute path URLS for segmens from the manifest's relative path - // TODO: optimize later and only get absolute path URLs for the start/end segments - sourceSegmentURLs, err := GetSourceSegmentURLs(sourceURL, origManifest) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to get segment urls: %w", err) - } - // Convert start/end time specified in UNIX time (milliseconds) to seconds wrt the first segment startTime, endTime, err := video.ConvertUnixMillisToSeconds(requestID, origManifest.Segments[0], startTimeUnixMillis, endTimeUnixMillis) if err != nil { @@ -371,110 +352,11 @@ func ClipInputManifest(requestID, sourceURL, clipTargetUrl string, startTimeUnix } // Find the segments at the clipping start/end timestamp boundaries - segs, clipsegs, err := video.ClipManifest(requestID, &origManifest, startTime, endTime) + segs, _, err := video.ClipManifest(requestID, &origManifest, startTime, endTime) if err != nil { return nil, fmt.Errorf("error clipping: failed to get start/end segments: %w", err) } - // Only the first and last segments should be clipped. - // And segs can be a single segment (if start/end times fall within the same segment) - // or it can span several segments startng from start-time and spanning to end-time - var segsToClip []*m3u8.MediaSegment - if len(segs) == 1 { - segsToClip = []*m3u8.MediaSegment{segs[0]} - } else { - segsToClip = []*m3u8.MediaSegment{segs[0], segs[len(segs)-1]} - } - // Create temp local storage dir to hold all clipping related files to upload later - clipStorageDir, err := os.MkdirTemp(os.TempDir(), "clip_stage_") - if err != nil { - return nil, fmt.Errorf("error clipping: failed to create temp clipping storage dir: %w", err) - } - defer os.RemoveAll(clipStorageDir) // nolint:errcheck - - // Download start/end segments and clip - for i, v := range segsToClip { - // Create temp local file to store the segments: - clipSegmentFileName := filepath.Join(clipStorageDir, requestID+"_"+strconv.FormatUint(v.SeqId, 10)+".ts") - defer os.Remove(clipSegmentFileName) - clipSegmentFile, err := os.Create(clipSegmentFileName) - if err != nil { - return nil, err - } - defer clipSegmentFile.Close() - - // Download the segment from OS and write to the temp local file - segmentURL := sourceSegmentURLs[v.SeqId].URL - dStorage := NewDStorageDownload() - err = backoff.Retry(func() error { - rc, err := GetFile(context.Background(), requestID, segmentURL.String(), dStorage) - if err != nil { - return fmt.Errorf("error clipping: failed to download segment %d: %w", v.SeqId, err) - } - defer rc.Close() - - // Write the segment data to the temp local file - _, err = io.Copy(clipSegmentFile, rc) - if err != nil { - return fmt.Errorf("error clipping: failed to write segment %d: %w", v.SeqId, err) - } - return nil - }, DownloadRetryBackoff()) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to download or write segments to local temp storage: %w", err) - } - - // Locally clip (i.e re-encode + clip) those relevant segments at the specified start/end timestamps - clippedSegmentFileName := filepath.Join(clipStorageDir, requestID+"_"+strconv.FormatUint(v.SeqId, 10)+"_clip.ts") - if len(segs) == 1 { - // If start/end times fall within same segment, then clip just that single segment - duration := endTime - startTime - err = video.ClipSegment(requestID, clipSegmentFileName, clippedSegmentFileName, clipsegs[0].ClipOffsetSecs, clipsegs[0].ClipOffsetSecs+duration) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to clip segment %d: %w", v.SeqId, err) - } - } else { - // If start/end times fall within different segments, then clip segment from start-time to end of segment - // or clip from beginning of segment to end-time. - if i == 0 { - err = video.ClipSegment(requestID, clipSegmentFileName, clippedSegmentFileName, clipsegs[0].ClipOffsetSecs, -1) - } else { - err = video.ClipSegment(requestID, clipSegmentFileName, clippedSegmentFileName, -1, clipsegs[1].ClipOffsetSecs) - } - if err != nil { - return nil, fmt.Errorf("error clipping: failed to clip segment %d: %w", v.SeqId, err) - } - } - - // Upload clipped segment to OS - clippedSegmentFile, err := os.Open(clippedSegmentFileName) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to open clipped segment %d: %w", v.SeqId, err) - } - defer clippedSegmentFile.Close() // nolint:errcheck - - clippedSegmentOSFilename := "clip_" + strconv.FormatUint(v.SeqId, 10) + ".ts" - err = UploadToOSURL(clipTargetUrl, clippedSegmentOSFilename, clippedSegmentFile, MaxCopyFileDuration) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to upload clipped segment %d: %w", v.SeqId, err) - } - - // Get duration of clipped segment(s) to use in the clipped manifest - p := video.Probe{} - clipSegProbe, err := p.ProbeFile(requestID, clippedSegmentFileName) - if err != nil { - return nil, fmt.Errorf("error clipping: failed to probe file: %w", err) - } - vidTrack, err := clipSegProbe.GetTrack(video.TrackTypeVideo) - if err != nil { - return nil, fmt.Errorf("error clipping: unknown duration of clipped segment: %w", err) - } - // Overwrite segs with new uri/duration. Note that these are pointers - // so the start/end segments in original segs slice are directly modified - v.Duration = vidTrack.DurationSec - v.URI = clippedSegmentOSFilename - } - // Generate the new clipped manifest clippedPlaylist, err := CreateClippedPlaylist(origManifest, segs) if err != nil { @@ -514,24 +396,17 @@ func CreateClippedPlaylist(origManifest m3u8.MediaPlaylist, segs []*m3u8.MediaSe return nil, fmt.Errorf("error clipping: failed to create clipped media playlist: %w", err) } var t time.Time - for i, s := range segs { + for _, s := range segs { if s == nil { break } - // TODO/HACK: Currently all segments between the start/end segments will always + // TODO/HACK: Currently all segments will always // be in the same place from root folder. Find a smarter way to handle this later. - if i != 0 && i != (len(segs)-1) { - s.URI = "../" + s.URI - } + s.URI = "../" + s.URI // Remove PROGRAM-DATE-TIME tag from all segments so that player doesn't // run into seek issues or display incorrect times on playhead s.ProgramDateTime = t - // Add a DISCONTINUITY tag to let hls players know about different encoding between - // segments. But don't do this if there's a single segment in the clipped manifest - if i-1 == 0 || (totalSegs > 2 && i == totalSegs-1) { - s.Discontinuity = true - } // Add segment to clipped manifest err := clippedPlaylist.AppendSegment(s) diff --git a/clients/manifest_test.go b/clients/manifest_test.go index c06e54cf..ae88a3b7 100644 --- a/clients/manifest_test.go +++ b/clients/manifest_test.go @@ -256,13 +256,11 @@ func TestCompliantClippedManifest(t *testing.T) { #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:15 #EXTINF:10.000,blah0 -source/0.ts -#EXT-X-DISCONTINUITY +../source/0.ts #EXTINF:15.000,blah1 ../source/1.ts -#EXT-X-DISCONTINUITY #EXTINF:10.000,blah2 -source/2.ts +../source/2.ts #EXT-X-ENDLIST `