IDK man, changes I guess

This commit is contained in:
2026-11-06 17:57:01 +00:00
parent f6ac9c2f34
commit 8e9bc500fa
15 changed files with 785 additions and 61 deletions

View File

@@ -41,14 +41,20 @@ type ExportRequest struct {
// animated GIF (also uses Height and FPS)
Dither string `json:"dither"` // one of gifDithers
Loop bool `json:"loop"`
// captions burned into the picture (re-encode and GIF only)
Captions map[string]string `json:"captions"` // source id -> .srt path
CaptionSize string `json:"captionSize"` // key of captionSizes
CaptionStyle string `json:"captionStyle"` // key of captionStyles
}
// gifDithers are the paletteuse dither modes offered for GIF export.
var gifDithers = []string{"sierra2_4a", "bayer", "none"}
type clipJob struct {
m *Media
in, out float64
m *Media
in, out float64
cues []Cue // captions to burn in, on the source's timeline
captions string // .srt file with the clip's cues, timed from In (see writeClipCaptions)
}
func (c clipJob) dur() float64 { return c.out - c.in }
@@ -109,6 +115,10 @@ func (a *App) handleExport(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "unknown export mode")
return
}
if err := a.attachCaptions(&req, clips); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
outputs := []string{out}
if !req.Join {
@@ -334,6 +344,14 @@ func (a *App) runExport(ctx context.Context, c *JobCtl, req *ExportRequest, clip
}
}
captionDir, err := writeClipCaptions(clips)
if captionDir != "" {
defer os.RemoveAll(captionDir)
}
if err != nil {
return fmt.Errorf("could not prepare the captions: %w", err)
}
switch {
case copyJoin:
out := outputs[0]
@@ -527,6 +545,7 @@ func encodeArgs(clips []clipJob, req *ExportRequest, ext string) ([]string, erro
}
anyAudio = anyAudio && !req.NoAudio
f.CaptionSize, f.CaptionStyle = req.CaptionSize, req.CaptionStyle
vc, _ := codecByName(req.VCodec)
var venc []string
venc, f.PixFmt = videoEncoderArgs(vc, req.Quality, req.Speed)
@@ -567,6 +586,7 @@ func gifArgs(clips []clipJob, req *ExportRequest, palette string) (paletteRun, g
return nil, nil, err
}
f.PixFmt = "bgra" // what palettegen and paletteuse take
f.CaptionSize, f.CaptionStyle = req.CaptionSize, req.CaptionStyle
inputs, graph, n := joinGraph(clips, f)
paletteRun = append(ffmpegBaseArgs(), inputs...)
@@ -593,6 +613,8 @@ type outFormat struct {
PixFmt string
SampleRate int
Layout string
// how burned-in captions look (keys of captionSizes and captionStyles)
CaptionSize, CaptionStyle string
}
// frameFormat picks the output frame size and rate: the first video clip's
@@ -625,7 +647,8 @@ func frameFormat(clips []clipJob, height int, fps string) (outFormat, error) {
// joinGraph returns the input arguments (one seeked input per clip, plus
// generated black video or silence for clips that lack it), how many inputs
// that is, and a filter graph that converts every clip to f, letterboxing as
// needed, and joins them with the concat filter into [outv] (and [outa]).
// needed and drawing its captions, and joins them with the concat filter into
// [outv] (and [outa]).
func joinGraph(clips []clipJob, f outFormat) (args []string, graph string, inputs int) {
var filters []string
var concatIn strings.Builder
@@ -637,17 +660,24 @@ func joinGraph(clips []clipJob, f outFormat) (args []string, graph string, input
src := inputs
inputs++
// Captions are drawn on the output-sized frame, so they get the same
// size in every clip. After fps the frames are timed from the In point
// on the output frame grid, which is how writeClipCaptions times them.
captions := ""
if cl.captions != "" {
captions = "," + captionFilter(cl.captions, f)
}
if v := cl.m.Video; v != nil {
chain := fmt.Sprintf("[%d:%d]", src, v.Index)
if !v.SquarePixels {
chain += "scale='trunc(iw*sar/2)*2':ih,setsar=1,"
}
chain += fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,fps=%s,format=%s[v%d]",
f.W, f.H, f.W, f.H, f.FPS, f.PixFmt, i)
chain += fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease:force_divisible_by=2,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black,setsar=1,fps=%s%s,format=%s[v%d]",
f.W, f.H, f.W, f.H, f.FPS, captions, f.PixFmt, i)
filters = append(filters, chain)
} else {
args = append(args, "-f", "lavfi", "-t", d, "-i", fmt.Sprintf("color=c=black:s=%dx%d:r=%s", f.W, f.H, f.FPS))
filters = append(filters, fmt.Sprintf("[%d:v]setsar=1,format=%s[v%d]", inputs, f.PixFmt, i))
filters = append(filters, fmt.Sprintf("[%d:v]setsar=1%s,format=%s[v%d]", inputs, captions, f.PixFmt, i))
inputs++
}
fmt.Fprintf(&concatIn, "[v%d]", i)