MUMBLE "couldn't create video" -- моя регрессия из 576baa3/753d21c. В логах у части загрузок есть Stream mapping и "Press [q]", но нет "Output #0": ffmpeg не доработал, его снял дедлайн. Две причины: - "-threads 1" душил многопоточное декодирование vp9. 12-секундное 640x360 в 2 секунды одним потоком не влезает. - checkGeometry вызывал ffprobe ВНУТРИ того же 2-секундного контекста, что и кодек, отбирая у него время. - убран -threads 1, -max_alloc оставлен (на скорость не влияет); - probe получил свой бюджет ProbeDeadline=1s, ffmpeg -- полные ProcessingDeadline=2.5s. Сумма с GetDuration укладывается в 7с контекста handleCreate с запасом; - семафор 16 -> 48: под флудом 16 слотов создавали очередь, и чекер выпадал по таймауту ещё до обработки. Бомбы теперь отсекаются дёшево, слоты освобождаются быстро. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
2.4 KiB
Go
95 lines
2.4 KiB
Go
package video
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"github.com/disintegration/imaging"
|
||
"image"
|
||
"image/png"
|
||
"io"
|
||
"os"
|
||
"os/exec"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
var (
|
||
// handleCreate даёт 7с на всё: probe + ffmpeg + GetDuration для субтитров.
|
||
// 1.0 + 2.5 + 2.5 = 6.0, секунда про запас.
|
||
ProcessingDeadline = 2500 * time.Millisecond
|
||
// Header-only ffprobe; cheap, но не должен отъедать бюджет у кодека.
|
||
ProbeDeadline = 1 * time.Second
|
||
semaphore = make(chan struct{}, 48)
|
||
)
|
||
|
||
func generatePreview(ctx context.Context, inp string, out string) error {
|
||
// -max_alloc caps a single ffmpeg allocation: a 1080p frame needs ~8 MB, a
|
||
// 16383x16383 bomb needs ~1 GB. Second line of defence behind checkGeometry,
|
||
// and it works even when the probe told us nothing.
|
||
cmd := exec.CommandContext(ctx, "ffmpeg", "-y",
|
||
"-max_alloc", "134217728", "-i", inp, "-vframes", "1", out)
|
||
s := strings.Builder{}
|
||
cmd.Stdout = &s
|
||
cmd.Stderr = &s
|
||
err := cmd.Run()
|
||
if err != nil {
|
||
fmt.Println(s.String())
|
||
}
|
||
return err
|
||
}
|
||
|
||
func GeneratePreview(ctx context.Context, inp string, out string) error {
|
||
select {
|
||
case semaphore <- struct{}{}:
|
||
defer func() { <-semaphore }()
|
||
|
||
// Probe gets its OWN budget. Sharing ProcessingDeadline with ffmpeg
|
||
// starved the encode and killed legit uploads ("cannot create video").
|
||
pctx, pcancel := context.WithTimeout(ctx, ProbeDeadline)
|
||
err := checkGeometry(pctx, inp)
|
||
pcancel()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
fctx, fcancel := context.WithTimeout(ctx, ProcessingDeadline)
|
||
defer fcancel()
|
||
return generatePreview(fctx, inp, out)
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
}
|
||
}
|
||
|
||
func Blur(inp string, out string) error {
|
||
f, err := os.Open(inp)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer f.Close()
|
||
|
||
// Same guard on the decode side: imaging.Blur keeps several copies of the
|
||
// bitmap alive, so an oversized preview is an OOM all by itself.
|
||
cfg, _, err := image.DecodeConfig(f)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if cfg.Width*cfg.Height > MaxPixels {
|
||
return fmt.Errorf("preview %dx%d is too large to blur", cfg.Width, cfg.Height)
|
||
}
|
||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||
return err
|
||
}
|
||
|
||
img, _, err := image.Decode(f)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
outimg := imaging.Blur(img, 5)
|
||
outfile, err := os.Create(out)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer outfile.Close()
|
||
return png.Encode(outfile, outimg)
|
||
}
|