Skip to content

Commit

Permalink
clip: add map arguments to ffmpeg clip commands
Browse files Browse the repository at this point in the history
* The clip segments that are re-encoded will end up with the video track
  first and audio track second wheras the track ordering is reversed for
the source recorded segments. If these segments are transmuxed into a
single ts/mp4 file, the resulting file will stop playing after the first
segment's worth of video. To resolve this, this commit adds the 'map'
args to the ffmpeg command to ensure the audio track is always placed
first and video track second similar to the recorded segments.

* The ffmpeg call is also being switched to a cmd.Run call since the
  go-ffmpeg library doesn't allow us to have multiple '-map' args in the
command line (one instance gets dropped resulting in an incorrect
ffmpeg cmd).
  • Loading branch information
emranemran committed Oct 3, 2023
1 parent 70dd5bd commit 2eeb00c
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 47 deletions.
7 changes: 3 additions & 4 deletions clients/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ func SortTranscodedStats(transcodedStats []*video.RenditionStats) {
}

func ClipInputManifest(requestID, sourceURL, clipTargetUrl string, startTimeUnixMillis, endTimeUnixMillis int64) (clippedManifestUrl *url.URL, err error) {

// Get the source manifest that will be clipped
origManifest, err := DownloadRenditionManifest(requestID, sourceURL)
if err != nil {
Expand Down Expand Up @@ -286,17 +285,17 @@ func ClipInputManifest(requestID, sourceURL, clipTargetUrl string, startTimeUnix
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
err = video.ClipSegment(clipSegmentFileName, clippedSegmentFileName, startTime, endTime)
err = video.ClipSegment(requestID, clipSegmentFileName, clippedSegmentFileName, startTime, endTime)
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(clipSegmentFileName, clippedSegmentFileName, clipsegs[0].ClipOffsetSecs, -1)
err = video.ClipSegment(requestID, clipSegmentFileName, clippedSegmentFileName, clipsegs[0].ClipOffsetSecs, -1)
} else {
err = video.ClipSegment(clipSegmentFileName, clippedSegmentFileName, -1, clipsegs[1].ClipOffsetSecs)
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)
Expand Down
100 changes: 57 additions & 43 deletions video/clip.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package video

import (
"bytes"
"context"
"fmt"
"github.com/grafov/m3u8"
"github.com/livepeer/catalyst-api/log"
ffmpeg "github.com/u2takey/ffmpeg-go"
"os/exec"
"time"
)

Expand Down Expand Up @@ -85,7 +87,7 @@ func ConvertUnixMillisToSeconds(requestID string, firstSegment *m3u8.MediaSegmen
startTimeSeconds := float64(startTimeUnixMillis-firstSegUnixMillis) / 1000.0
endTimeSeconds := float64(endTimeUnixMillis-firstSegUnixMillis) / 1000.0

log.Log(requestID, "Clipping timestamps",
log.Log(requestID, "clipping timestamps",
"start-PROGRAM-DATE-TIME-UTC", firstSegProgramDateTimeUTC,
"UNIX-time-milliseconds", firstSegUnixMillis,
"start-time-unix-milliseconds", startTimeUnixMillis,
Expand Down Expand Up @@ -135,7 +137,7 @@ func ClipManifest(requestID string, manifest *m3u8.MediaPlaylist, startTime, end
return nil, []ClipSegmentInfo{}, fmt.Errorf("error clipping: no relevant segments found in the specified time range")
}

log.Log(requestID, "Clipping segments", "from", firstSegmentToClip, "to", lastSegmentToClip)
log.Log(requestID, "clipping segments", "from", firstSegmentToClip, "to", lastSegmentToClip)

// If the clip start/end times fall within the same segment, then
// save only the single segment's info
Expand All @@ -153,57 +155,69 @@ func ClipManifest(requestID string, manifest *m3u8.MediaPlaylist, startTime, end
// This allows for frame-accurate clipping.
// Currently using a combination of these settings for the encode step:
//
// "c:v": "libx264": Specifies H.264 video codec.
// "g": "48": Inserts keyframe every 48 frames (GOP size).
// "keyint_min": "48": Minimum keyframe interval.
// "sc_threshold": 50: Detects scene changes with threshold 50.
// "bf": "0": Disables B-frames for bidirectional prediction.
// "c:a": "aac": re-encode audio and clip.
func ClipSegment(tsInputFile, tsOutputFile string, startTime, endTime float64) error {
var args ffmpeg.KwArgs
// "c:v": "libx264": Specifies H.264 video codec.
// "g": "48": Inserts keyframe every 48 frames (GOP size).
// "keyint_min": "48": Minimum keyframe interval.
// "sc_threshold": 50: Detects scene changes with threshold 50.
// "bf": "0": Disables B-frames for bidirectional prediction.
// "c:a": "aac": re-encode audio and clip.
// "map 0:a map 0:v": so that audio track is always first which matches recording segments
func ClipSegment(requestID, tsInputFile, tsOutputFile string, startTime, endTime float64) error {

var baseArgs []string
mapArgs := []string{"-map", "0:a", "-map", "0:v"}

// append input file
baseArgs = append(baseArgs,
"-i", tsInputFile)

// append args that will apply re-encoding
baseArgs = append(baseArgs,
"-bf", "0",
"-c:a", "aac",
"-c:v", "libx264",
"-g", "48",
"-keyint_min", "48",
"-sc_threshold", "50")

if endTime < 0 {
// Clip from specified start time to the end of
// the segment (when clipping starting segment)
// Clip from specified start time to the end of the segment
// (when clipping starting segment)
start := formatTime(startTime)
args = ffmpeg.KwArgs{"bf": "0",
"c:a": "aac",
"c:v": "libx264",
"g": "48",
"keyint_min": "48",
"sc_threshold": 50,
"ss": start}
baseArgs = append(baseArgs, "-ss", start)
} else if startTime < 0 {
// Clip from beginning of segment to specified end time
// without re-encoding (when clipping ending segment)
// (when clipping ending segment)
end := formatTime(endTime)
args = ffmpeg.KwArgs{"bf": "0",
"c:a": "aac",
"c:v": "libx264",
"g": "48",
"keyint_min": "48",
"sc_threshold": 50,
"ss": "00:00:00.000",
"to": end}
baseArgs = append(baseArgs, "-ss", "00:00:00.000", "-to", end)
} else {
// Clip from specified start/end times (when
// start/end falls within same segment)
// Clip from specified start/end times
// (when start/end falls within same segment)
start := formatTime(startTime)
end := formatTime(endTime)
args = ffmpeg.KwArgs{"bf": "0",
"c:a": "aac",
"c:v": "libx264",
"g": "48",
"keyint_min": "48",
"sc_threshold": 50,
"ss": start,
"to": end}
baseArgs = append(baseArgs, "-ss", start, "-to", end)
}

err := ffmpeg.Input(tsInputFile).
Output(tsOutputFile, args).
OverWriteOutput().ErrorToStdOut().Run()
// append map parameters so that audio track is always first and video track second
baseArgs = append(baseArgs, mapArgs...)

// append output file
baseArgs = append(baseArgs, tsOutputFile, "-y")

timeout, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
cmd := exec.CommandContext(timeout, "ffmpeg", baseArgs...)

log.Log(requestID, "clipping", "compiled-command", fmt.Sprintf("ffmpeg %s", baseArgs))

var outputBuf bytes.Buffer
var stdErr bytes.Buffer
cmd.Stdout = &outputBuf
cmd.Stderr = &stdErr
err := cmd.Run()
if err != nil {
return fmt.Errorf("failed to clip segments from %s: %w", tsInputFile, err)
return fmt.Errorf("failed to clip segments from %s [%s] [%s]: %w", tsInputFile, outputBuf.String(), stdErr.String(), err)
}

return nil
}

0 comments on commit 2eeb00c

Please sign in to comment.