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.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user