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.
71 lines
2.1 KiB
Go
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
|
|
}
|