diff --git a/ffmpeg.go b/ffmpeg.go index d35823af..a67a34bc 100644 --- a/ffmpeg.go +++ b/ffmpeg.go @@ -11,6 +11,7 @@ import ( type FilterComplexBuilder struct { filterComplex *strings.Builder style *StyleOptions + framerate int termWidth int termHeight int prevStageName string @@ -50,6 +51,7 @@ func NewVideoFilterBuilder(videoOpts *VideoOptions) *FilterComplexBuilder { termHeight: termHeight, termWidth: termWidth, style: videoOpts.Style, + framerate: videoOpts.Framerate, prevStageName: "padded", } } @@ -104,6 +106,18 @@ func calcTermDimensions(style StyleOptions) (int, int) { return width, height } +// fps pins a secondary stream to the target framerate. The window bar and +// margin fill sources (lavfi color, looped images) all default to 25fps, and +// they are the *main* input of the overlays below, so without this the whole +// render is forced to 25fps regardless of `Set Framerate`. +// Returns an empty string for screenshots, which have no framerate. +func (fb *FilterComplexBuilder) fps() string { + if fb.framerate <= 0 { + return "" + } + return fmt.Sprintf(",fps=%d", fb.framerate) +} + // WithWindowBar adds window bar options to ffmepg filter_complex. func (fb *FilterComplexBuilder) WithWindowBar(barStream int) *FilterComplexBuilder { if fb.style.WindowBar != "" { @@ -111,10 +125,11 @@ func (fb *FilterComplexBuilder) WithWindowBar(barStream int) *FilterComplexBuild _, _ = fmt.Fprintf( fb.filterComplex, ` - [%d]loop=-1[loopbar]; + [%d]loop=-1%s[loopbar]; [loopbar][%s]overlay=0:%d[withbar] `, barStream, + fb.fps(), fb.prevStageName, fb.style.WindowBarSize, ) @@ -154,12 +169,13 @@ func (fb *FilterComplexBuilder) WithMarginFill(marginStream int) *FilterComplexB _, _ = fmt.Fprintf( fb.filterComplex, ` - [%d]scale=%d:%d[bg]; + [%d]scale=%d:%d%s[bg]; [bg][%s]overlay=(W-w)/2:(H-h)/2:shortest=1[withbg] `, marginStream, fb.style.Width, fb.style.Height, + fb.fps(), fb.prevStageName, ) fb.prevStageName = "withbg" diff --git a/ffmpeg_test.go b/ffmpeg_test.go new file mode 100644 index 00000000..5a2acc63 --- /dev/null +++ b/ffmpeg_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "strings" + "testing" +) + +// The margin fill and window bar sources default to 25fps and are the *main* +// input of their overlay, so they must be pinned to the target framerate or +// they silently override `Set Framerate`. +func TestSecondaryStreamsUseFramerate(t *testing.T) { + opts := DefaultVideoOptions() + opts.Framerate = 60 + opts.Style = DefaultStyleOptions() + opts.Style.WindowBar = "Colorful" + opts.Style.MarginFill = "#000000" + + got := strings.Join(NewVideoFilterBuilder(&opts). + WithWindowBar(2). + WithMarginFill(3). + Build(), " ") + + for _, want := range []string{"loop=-1,fps=60[loopbar]", ",fps=60[bg]"} { + if !strings.Contains(got, want) { + t.Errorf("filter_complex missing %q\ngot: %s", want, got) + } + } +} + +// Screenshots have no framerate; an fps filter there would be `fps=0`. +func TestScreenshotOmitsFramerate(t *testing.T) { + style := DefaultStyleOptions() + style.WindowBar = "Colorful" + + got := strings.Join(NewScreenshotFilterComplexBuilder(style). + WithWindowBar(2). + WithMarginFill(3). + Build(), " ") + + if strings.Contains(got, "fps=") { + t.Errorf("screenshot filter should not set fps, got: %s", got) + } +}