Files
ds-galo/backend/internal/video/ffmpeg.go
T
iu d38ede0201 feat(video): add video upload with remux/transcode playback
Add video as a media kind in photos: streaming upload with per-kind
limits, ffprobe metadata, poster frame reusing the image variant
pipeline, and a playback object (remux when web-compatible, else
h264/yuv420p/aac transcode). Video jobs run on a separate pool with a
long lease. Frontend gains a video viewer, grid badge/duration and
video upload.
2026-10-11 20:37:53 +03:00

71 lines
2.1 KiB
Go

package video
import (
"bytes"
"context"
"fmt"
"os/exec"
"strconv"
"strings"
)
// RemuxArgs builds the ffmpeg arguments for a lossless copy into a faststart
// mp4. Streams are limited to the first video and optional first audio track;
// subtitles and extra tracks are dropped so players do not stumble on them.
func RemuxArgs(in, out string) []string {
return []string{
"-hide_banner", "-nostdin", "-y",
"-i", in,
"-map", "0:v:0", "-map", "0:a:0?",
"-c", "copy",
"-movflags", "+faststart",
out,
}
}
// TranscodeArgs builds the ffmpeg arguments for a browser-safe h264/aac mp4.
// The video is scaled to fit maxLongSide without upscaling and forced to
// even dimensions; ffmpeg applies display rotation during re-encoding.
func TranscodeArgs(in, out string, maxLongSide int) []string {
scale := fmt.Sprintf(
"scale=w='min(%d,iw)':h='min(%d,ih)':force_original_aspect_ratio=decrease:force_divisible_by=2",
maxLongSide, maxLongSide)
return []string{
"-hide_banner", "-nostdin", "-y",
"-i", in,
"-map", "0:v:0", "-map", "0:a:0?",
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
"-pix_fmt", "yuv420p", "-vf", scale,
"-c:a", "aac", "-b:a", "128k",
"-movflags", "+faststart",
out,
}
}
// PosterArgs builds the ffmpeg arguments to extract a single poster frame at
// atSecond, already scaled so downstream image pipelines decode a bounded frame.
func PosterArgs(in, out string, atSecond int) []string {
scale := fmt.Sprintf("scale=w='min(%d,iw)':h=-2", MaxLongSide)
return []string{
"-hide_banner", "-nostdin", "-y",
"-ss", strconv.Itoa(atSecond),
"-i", in,
"-frames:v", "1",
"-vf", scale,
"-f", "image2",
out,
}
}
// Run executes ffmpeg with the given arguments and returns an error carrying
// the captured stderr on failure.
func Run(ctx context.Context, ffmpegPath string, args ...string) error {
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
var errb bytes.Buffer
cmd.Stderr = &errb
if err := cmd.Run(); err != nil {
return fmt.Errorf("ffmpeg %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(errb.String()))
}
return nil
}