package main import ( "fmt" "os" "path/filepath" "strings" ) // FilterComplexBuilder generates -filter_complex option of ffmepg. type FilterComplexBuilder struct { filterComplex *strings.Builder style *StyleOptions termWidth int termHeight int prevStageName string } // NewVideoFilterBuilder returns instance of FilterComplexBuilder with video config. func NewVideoFilterBuilder(videoOpts *VideoOptions) *FilterComplexBuilder { filterCode := strings.Builder{} termWidth, termHeight := calcTermDimensions(*videoOpts.Style) _, _ = fmt.Fprintf(&filterCode, ` [0][1]overlay[merged]; [merged]scale=%d:%d:force_original_aspect_ratio=1[scaled]; [scaled]fps=%d,setpts=PTS/%f[speed]; [speed]pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s[padded]; [padded]fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s[padded] `, termWidth-double(videoOpts.Style.Padding), termHeight-double(videoOpts.Style.Padding), videoOpts.Framerate, videoOpts.PlaybackSpeed, termWidth, termHeight, videoOpts.Style.BackgroundColor, videoOpts.Style.Padding, videoOpts.Style.Padding, videoOpts.Style.Padding, videoOpts.Style.Padding, videoOpts.Style.BackgroundColor, ) return &FilterComplexBuilder{ filterComplex: &filterCode, termHeight: termHeight, termWidth: termWidth, style: videoOpts.Style, prevStageName: "padded", } } // NewScreenshotFilterComplexBuilder returns instance of FilterComplexBuilder with screenshot config. func NewScreenshotFilterComplexBuilder(style *StyleOptions) *FilterComplexBuilder { filterCode := strings.Builder{} termWidth, termHeight := calcTermDimensions(*style) _, _ = fmt.Fprintf(&filterCode, ` [0][1]overlay[merged]; [merged]scale=%d:%d:force_original_aspect_ratio=1[scaled]; [scaled]pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s[padded]; [padded]fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s[padded] `, termWidth-double(style.Padding), termHeight-double(style.Padding), termWidth, termHeight, style.BackgroundColor, style.Padding, style.Padding, style.Padding, style.Padding, style.BackgroundColor, ) return &FilterComplexBuilder{ filterComplex: &filterCode, termHeight: termHeight, termWidth: termWidth, style: style, prevStageName: "padded", } } // calcTermDimensions computes terminal dimensions. // It returns width and height values. func calcTermDimensions(style StyleOptions) (int, int) { width := style.Width height := style.Height if style.MarginFill != "" { width = width - double(style.Margin) height = height - double(style.Margin) } if style.WindowBar != "" { height = height - style.WindowBarSize } return width, height } // WithWindowBar adds window bar options to ffmepg filter_complex. func (fb *FilterComplexBuilder) WithWindowBar(barStream int) *FilterComplexBuilder { if fb.style.WindowBar != "" { fb.filterComplex.WriteString(";") _, _ = fmt.Fprintf( fb.filterComplex, ` [%d]loop=-1[loopbar]; [loopbar][%s]overlay=0:%d[withbar] `, barStream, fb.prevStageName, fb.style.WindowBarSize, ) fb.prevStageName = "withbar" } return fb } // WithBorderRadius adds border radius options to ffmepg filter_complex. func (fb *FilterComplexBuilder) WithBorderRadius(cornerMarkStream int) *FilterComplexBuilder { if fb.style.BorderRadius != 0 { fb.filterComplex.WriteString(";") _, _ = fmt.Fprintf( fb.filterComplex, ` [%d]loop=-1[loopmask]; [%s][loopmask]alphamerge[rounded] `, cornerMarkStream, fb.prevStageName, ) fb.prevStageName = "rounded" } return fb } // WithMarginFill adds margin options to ffmepg filter_complex. func (fb *FilterComplexBuilder) WithMarginFill(marginStream int) *FilterComplexBuilder { // Overlay terminal on margin if fb.style.MarginFill != "" { // ffmpeg will complain if the final filter ends with a semicolon, // so we add one BEFORE we start adding filters. fb.filterComplex.WriteString(";") _, _ = fmt.Fprintf( fb.filterComplex, ` [%d]scale=%d:%d[bg]; [bg][%s]overlay=(W-w)/2:(H-h)/2:shortest=1[withbg] `, marginStream, fb.style.Width, fb.style.Height, fb.prevStageName, ) fb.prevStageName = "withbg" } return fb } // WithGIF adds gif options to ffmepg filter_complex. func (fb *FilterComplexBuilder) WithGIF() *FilterComplexBuilder { fb.filterComplex.WriteString(";") _, _ = fmt.Fprintf( fb.filterComplex, ` [%s]split[plt_a][plt_b]; [plt_a]palettegen=max_colors=256[plt]; [plt_b][plt]paletteuse[palette]`, fb.prevStageName, ) fb.prevStageName = "palette" return fb } // Build returns filter_complex used in ffmepg. func (fb *FilterComplexBuilder) Build() []string { return []string{ "-filter_complex", fb.filterComplex.String(), "-map", "[" + fb.prevStageName + "]", } } // StreamBuilder generates streams used by ffmepg. type StreamBuilder struct { args []string counter int style *StyleOptions termWidth int termHeight int input string barStream int cornerStream int marginStream int } // NewStreamBuilder returns instance of StreamBuilder. func NewStreamBuilder(streamCounter int, input string, style *StyleOptions) *StreamBuilder { termWidth, termHeight := calcTermDimensions(*style) return &StreamBuilder{ counter: streamCounter, args: []string{}, style: style, termWidth: termWidth, termHeight: termHeight, input: input, } } // WithMargin adds margin stream. func (sb *StreamBuilder) WithMargin() *StreamBuilder { if sb.style.MarginFill != "" { if marginFillIsColor(sb.style.MarginFill) { // Create plain color stream sb.args = append(sb.args, "-f", "lavfi", "-i", fmt.Sprintf( "color=%s:s=%dx%d", sb.style.MarginFill, sb.style.Width, sb.style.Height, ), ) } else { // Check for existence first. _, err := os.Stat(sb.style.MarginFill) if err != nil { fmt.Println(ErrorStyle.Render("Unable to read margin file: "), sb.style.MarginFill) } // Add image stream sb.args = append(sb.args, "-loop", "1", "-i", sb.style.MarginFill, ) } sb.marginStream = sb.counter sb.counter++ } return sb } // WithBar adds bar stream. func (sb *StreamBuilder) WithBar() *StreamBuilder { barPath := filepath.Join(sb.input, "bar.png") if sb.style.WindowBar != "" { MakeWindowBar(sb.termWidth, sb.termHeight, *sb.style, barPath) sb.args = append(sb.args, "-i", barPath, ) sb.barStream = sb.counter sb.counter++ } return sb } // WithCorner adds corner stream. func (sb *StreamBuilder) WithCorner() *StreamBuilder { maskPath := filepath.Join(sb.input, "mask.png") if sb.style.BorderRadius != 0 { if sb.style.WindowBar != "" { MakeBorderRadiusMask(sb.termWidth, sb.termHeight+sb.style.WindowBarSize, sb.style.BorderRadius, maskPath) } else { MakeBorderRadiusMask(sb.termWidth, sb.termHeight, sb.style.BorderRadius, maskPath) } sb.args = append(sb.args, "-i", maskPath, ) sb.cornerStream = sb.counter sb.counter++ } return sb } // WithMP4 adds mp4 stream with required config. func (sb *StreamBuilder) WithMP4() *StreamBuilder { sb.args = append(sb.args, "-vcodec", "libx264", "-pix_fmt", "yuv420p", "-an", "-crf", "20", ) return sb } // WithWebm adds webm stream with required config. func (sb *StreamBuilder) WithWebm() *StreamBuilder { sb.args = append(sb.args, "-pix_fmt", "yuv420p", "-an", "-crf", "30", "-b:v", "0", ) return sb } // Build returns streams for using with ffmepg. func (sb *StreamBuilder) Build() []string { return sb.args }