@@ -2,6 +2,7 @@ package pipeline
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "net/url"
78 "os"
@@ -23,6 +24,10 @@ import (
2324
2425const LocalSourceFilePattern = "sourcevideo*.mp4"
2526
27+ // ErrKeyframe indicates that a probed segment did not start with a keyframe and
28+ // requires re-segmenting with different parameters.
29+ var ErrKeyframe = errors .New ("keyframe error" )
30+
2631type ffmpeg struct {
2732 // The base of where to output source segments to
2833 SourceOutputURL * url.URL
@@ -45,7 +50,24 @@ func (f *ffmpeg) Name() string {
4550}
4651
4752func (f * ffmpeg ) HandleStartUploadJob (job * JobInfo ) (* HandlerOutput , error ) {
53+ // First attempt: try cheap "copy" based segmenting.
54+ out , err := f .handleStartUploadJob (job , false )
55+ if err != nil && errors .Is (err , ErrKeyframe ) {
56+ // If we hit a keyframe error when probing source segments, re-run the
57+ // whole pipeline from the top but with a more expensive segmentation
58+ // mode that re-encodes and forces keyframes.
59+ log .Log (job .RequestID , "keyframe error while probing source segments, retrying with re-encoding segmentation" )
60+ return f .handleStartUploadJob (job , true )
61+ }
62+ return out , err
63+ }
64+
65+ // handleStartUploadJob contains the core logic of the ffmpeg pipeline. The
66+ // reencodeSegmentation flag controls whether we use a cheap "copy" based
67+ // segmenting pass or a more expensive re-encoding pass that forces keyframes.
68+ func (f * ffmpeg ) handleStartUploadJob (job * JobInfo , reencodeSegmentation bool ) (* HandlerOutput , error ) {
4869 log .Log (job .RequestID , "Handling job via FFMPEG/Livepeer pipeline" )
70+ job .ReencodeSegmentation = reencodeSegmentation
4971
5072 sourceOutputURL := f .SourceOutputURL .JoinPath (job .RequestID )
5173 segmentingTargetURL := sourceOutputURL .JoinPath (config .SEGMENTING_SUBDIR , config .SEGMENTING_TARGET_MANIFEST )
@@ -58,7 +80,7 @@ func (f *ffmpeg) HandleStartUploadJob(job *JobInfo) (*HandlerOutput, error) {
5880 var localSourceTmp string
5981 if job .InputFileInfo .Format != "hls" {
6082 var err error
61- localSourceTmp , err = copyFileToLocalTmpAndSegment (job )
83+ localSourceTmp , err = copyFileToLocalTmpAndSegment (job , reencodeSegmentation )
6284 if err != nil {
6385 return nil , err
6486 }
@@ -287,15 +309,15 @@ func (f *ffmpeg) probeSourceSegments(job *JobInfo, sourceSegments []*m3u8.MediaS
287309 return nil
288310 }
289311 segCount := len (sourceSegments )
290- if segCount < 4 {
312+ if segCount < 6 {
291313 for _ , segment := range sourceSegments {
292314 if err := f .probeSourceSegment (job .RequestID , segment , job .SegmentingTargetURL ); err != nil {
293315 return err
294316 }
295317 }
296318 return nil
297319 }
298- segmentsToCheck := []int {0 , 1 , segCount - 2 , segCount - 1 }
320+ segmentsToCheck := []int {0 , 1 , 2 , 3 , segCount - 2 , segCount - 1 }
299321 for _ , i := range segmentsToCheck {
300322 if err := f .probeSourceSegment (job .RequestID , sourceSegments [i ], job .SegmentingTargetURL ); err != nil {
301323 return err
@@ -313,6 +335,22 @@ func (f *ffmpeg) probeSourceSegment(requestID string, seg *m3u8.MediaSegment, so
313335 if err != nil {
314336 return fmt .Errorf ("failed to create signed url for %s: %w" , u .Redacted (), err )
315337 }
338+
339+ // check that the segment starts with a keyframe
340+ if err := backoff .Retry (func () error {
341+ output , err := f .probe .CheckFirstFrame (probeURL )
342+ if err != nil {
343+ return fmt .Errorf ("failed to check segment starts with keyframe: %w" , err )
344+ }
345+ // ffprobe should print I for i-frame
346+ if ! strings .HasPrefix (output , "I" ) || strings .Contains (output , "non-existing PPS" ) {
347+ return fmt .Errorf ("segment does not start with keyframe: %w" , ErrKeyframe )
348+ }
349+ return nil
350+ }, retries (6 )); err != nil {
351+ return err
352+ }
353+
316354 if err := backoff .Retry (func () error {
317355 _ , err = f .probe .ProbeFile (requestID , probeURL )
318356 if err != nil {
@@ -335,7 +373,7 @@ func (f *ffmpeg) probeSourceSegment(requestID string, seg *m3u8.MediaSegment, so
335373 return nil
336374}
337375
338- func copyFileToLocalTmpAndSegment (job * JobInfo ) (string , error ) {
376+ func copyFileToLocalTmpAndSegment (job * JobInfo , reencodeSegmentation bool ) (string , error ) {
339377 // Create a temporary local file to write to
340378 localSourceFile , err := os .CreateTemp (os .TempDir (), LocalSourceFilePattern )
341379 if err != nil {
@@ -358,7 +396,7 @@ func copyFileToLocalTmpAndSegment(job *JobInfo) (string, error) {
358396 }
359397
360398 // Begin Segmenting
361- log .Log (job .RequestID , "Beginning segmenting via FFMPEG/Livepeer pipeline" )
399+ log .Log (job .RequestID , "Beginning segmenting via FFMPEG/Livepeer pipeline" , "reencode" , reencodeSegmentation )
362400 job .ReportProgress (clients .TranscodeStatusPreparing , 0.5 )
363401
364402 // FFMPEG fails when presented with a raw IP + Path type URL, so we prepend "http://" to it
@@ -368,7 +406,7 @@ func copyFileToLocalTmpAndSegment(job *JobInfo) (string, error) {
368406 }
369407
370408 destinationURL := fmt .Sprintf ("%s/api/ffmpeg/%s/index.m3u8" , internalAddress , job .StreamName )
371- if err := video .Segment (localSourceFile .Name (), destinationURL , job .TargetSegmentSizeSecs ); err != nil {
409+ if err := video .Segment (localSourceFile .Name (), destinationURL , job .TargetSegmentSizeSecs , reencodeSegmentation ); err != nil {
372410 return "" , err
373411 }
374412
0 commit comments