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,95 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCanRemux(t *testing.T) {
|
||||
base := func() *Info {
|
||||
return &Info{
|
||||
VideoCodec: "h264",
|
||||
PixelFormat: "yuv420p",
|
||||
Profile: "High",
|
||||
AudioCodec: "aac",
|
||||
Container: "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
mut func(*Info)
|
||||
cap int
|
||||
want bool
|
||||
}{
|
||||
{"web compatible", func(*Info) {}, MaxLongSide, true},
|
||||
{"no audio", func(i *Info) { i.AudioCodec = "" }, MaxLongSide, true},
|
||||
{"empty profile accepted", func(i *Info) { i.Profile = "" }, MaxLongSide, true},
|
||||
{"hevc", func(i *Info) { i.VideoCodec = "hevc" }, MaxLongSide, false},
|
||||
{"vp9", func(i *Info) { i.VideoCodec = "vp9" }, MaxLongSide, false},
|
||||
{"yuv444p", func(i *Info) { i.PixelFormat = "yuv444p" }, MaxLongSide, false},
|
||||
{"ten bit profile", func(i *Info) { i.Profile = "High 10" }, MaxLongSide, false},
|
||||
{"mp3 audio", func(i *Info) { i.AudioCodec = "mp3" }, MaxLongSide, false},
|
||||
{"mpegts container", func(i *Info) { i.Container = "mpegts" }, MaxLongSide, false},
|
||||
{"mkv h264 remuxable", func(i *Info) { i.Container = "matroska,webm" }, MaxLongSide, true},
|
||||
{"over cap", func(i *Info) { i.Width = 3840; i.Height = 2160 }, MaxLongSide, false},
|
||||
{"cap disabled", func(i *Info) { i.Width = 3840; i.Height = 2160 }, 0, true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
i := base()
|
||||
tc.mut(i)
|
||||
if got := i.CanRemux(tc.cap); got != tc.want {
|
||||
t.Fatalf("CanRemux = %v, want %v (%+v)", got, tc.want, i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeFakeTool(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "ffprobe")
|
||||
script := "#!/bin/sh\ncat <<'JSON'\n" + body + "\nJSON\n"
|
||||
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||
t.Fatalf("write fake tool: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestProbeParsesOutput(t *testing.T) {
|
||||
probe := writeFakeTool(t, `{"streams":[{"codec_type":"video","codec_name":"h264","profile":"High","pix_fmt":"yuv420p","width":1920,"height":1080,"side_data_list":[{"rotation":-90}]},{"codec_type":"audio","codec_name":"aac"}],"format":{"format_name":"mov,mp4,m4a,3gp,3g2,mj2","duration":"12.345678"}}`)
|
||||
info, err := Probe(context.Background(), probe, "input.mp4")
|
||||
if err != nil {
|
||||
t.Fatalf("probe: %v", err)
|
||||
}
|
||||
if info.VideoCodec != "h264" || info.PixelFormat != "yuv420p" || info.AudioCodec != "aac" {
|
||||
t.Fatalf("codecs = %+v", info)
|
||||
}
|
||||
if info.Width != 1920 || info.Height != 1080 || info.LongSide() != 1920 {
|
||||
t.Fatalf("dimensions = %+v", info)
|
||||
}
|
||||
if info.Rotation != 270 {
|
||||
t.Fatalf("rotation = %d, want 270", info.Rotation)
|
||||
}
|
||||
if info.DurationMS != 12346 {
|
||||
t.Fatalf("duration = %d, want 12346", info.DurationMS)
|
||||
}
|
||||
if !info.CanRemux(MaxLongSide) {
|
||||
t.Fatalf("expected remuxable, got %+v", info)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeFailsOnNonZeroExit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "ffprobe")
|
||||
if err := os.WriteFile(path, []byte("#!/bin/sh\necho boom >&2\nexit 1\n"), 0o755); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
if _, err := Probe(context.Background(), path, "x"); err == nil {
|
||||
t.Fatal("expected probe error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user