Files
wehub-resource-sync 95f0813436
build / build (push) Failing after 0s
nightly / nightly (push) Failing after 0s
build / snapshot (push) Failing after 0s
lint / lint (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 13:02:37 +08:00

169 lines
4.3 KiB
Go

// Package vhs video.go spawns the ffmpeg process to convert the frames,
// collected by go-rod's screenshots into the input folder, to a GIF, WebM,
// MP4.
//
// MakeGIF takes several options to modify the behaviour of the ffmpeg process,
// which can be configured through the Set command.
//
// Set MaxColors 256
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
textFrameFormat = "frame-text-%05d.png"
cursorFrameFormat = "frame-cursor-%05d.png"
)
const (
mp4 = ".mp4"
webm = ".webm"
gif = ".gif"
)
// randomDir returns a random temporary directory to be used for storing frames
// from screenshots of the terminal.
func randomDir() string {
tmp, err := os.MkdirTemp(os.TempDir(), "vhs")
if err != nil {
log.Printf("Error creating temporary directory: %v", err)
}
return tmp
}
// VideoOutputs is a mapping from file type to file path for all video outputs
// of VHS.
type VideoOutputs struct {
GIF string
WebM string
MP4 string
Frames string
}
// VideoOptions is the set of options for converting frames to a GIF.
type VideoOptions struct {
Framerate int
PlaybackSpeed float64
Input string
MaxColors int
Output VideoOutputs
StartingFrame int
Style *StyleOptions
}
const (
defaultFramerate = 50
defaultStartingFrame = 1
)
// DefaultVideoOptions is the set of default options for converting frames
// to a GIF, which are used if they are not overridden.
func DefaultVideoOptions() VideoOptions {
return VideoOptions{
Framerate: defaultFramerate,
Input: randomDir(),
MaxColors: defaultMaxColors,
Output: VideoOutputs{GIF: "", WebM: "", MP4: "", Frames: ""},
PlaybackSpeed: defaultPlaybackSpeed,
StartingFrame: defaultStartingFrame,
}
}
func marginFillIsColor(marginFill string) bool {
return strings.HasPrefix(marginFill, "#")
}
// makeMedia takes a list of images (as frames) and converts them to a GIF/WebM/MP4.
func makeMedia(opts VideoOptions, targetFile string) *exec.Cmd {
if targetFile == "" {
return nil
}
log.Println(GrayStyle.Render("Creating " + targetFile + "..."))
ensureDir(targetFile)
//nolint:gosec
return exec.Command(
"ffmpeg",
buildFFopts(opts, targetFile)...,
)
}
// ensureDir ensures that the file path of the output can be created by
// creating all the necessary nested folders.
func ensureDir(output string) {
err := os.MkdirAll(filepath.Dir(output), 0o750)
if err != nil {
fmt.Println(ErrorStyle.Render("Unable to create output directory: "), output)
}
}
// buildFFopts assembles an ffmpeg command from some VideoOptions.
func buildFFopts(opts VideoOptions, targetFile string) []string {
var args []string //nolint:prealloc
streamCounter := 2
streamBuilder := NewStreamBuilder(streamCounter, opts.Input, opts.Style)
// Input frame options, used no matter what
// Stream 0: text frames
// Stream 1: cursor frames
streamBuilder.args = append(streamBuilder.args,
"-y",
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, textFrameFormat),
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, cursorFrameFormat),
)
streamBuilder = streamBuilder.
WithMargin().
WithBar().
WithCorner()
filterBuilder := NewVideoFilterBuilder(&opts).
WithWindowBar(streamBuilder.barStream).
WithBorderRadius(streamBuilder.cornerStream).
WithMarginFill(streamBuilder.marginStream)
// Format-specific options
switch filepath.Ext(targetFile) {
case gif:
filterBuilder = filterBuilder.WithGIF()
case webm:
streamBuilder = streamBuilder.WithWebm()
case mp4:
streamBuilder = streamBuilder.WithMP4()
}
args = append(args, streamBuilder.Build()...)
args = append(args, filterBuilder.Build()...)
args = append(args, targetFile)
return args
}
// MakeGIF takes a list of images (as frames) and converts them to a GIF.
func MakeGIF(opts VideoOptions) *exec.Cmd {
return makeMedia(opts, opts.Output.GIF)
}
// MakeWebM takes a list of images (as frames) and converts them to a WebM.
func MakeWebM(opts VideoOptions) *exec.Cmd {
return makeMedia(opts, opts.Output.WebM)
}
// MakeMP4 takes a list of images (as frames) and converts them to an MP4.
func MakeMP4(opts VideoOptions) *exec.Cmd {
return makeMedia(opts, opts.Output.MP4)
}