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:
iu
2026-10-11 20:37:53 +03:00
parent d059d042f0
commit d38ede0201
52 changed files with 2717 additions and 82 deletions
+125
View File
@@ -0,0 +1,125 @@
package photos
import (
"bytes"
"context"
"io"
"strings"
"testing"
"galo/internal/models"
"galo/internal/storage"
)
// fakeBackend satisfies storage.Backend by embedding the interface; only the
// methods exercised by the code under test are implemented.
type fakeBackend struct {
storage.Backend
}
func (fakeBackend) URL(_ context.Context, key string) (string, error) {
return "signed:" + key, nil
}
func (fakeBackend) Open(_ context.Context, _ string) (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte("fake-video-bytes"))), nil
}
func (fakeBackend) Put(context.Context, string, io.Reader, int64, string) error { return nil }
func TestAttachURLsPlayback(t *testing.T) {
svc := &Service{storage: fakeBackend{}}
ctx := context.Background()
p := &models.Photo{
StorageKey: "objects/a/original.mp4",
ThumbKey: "objects/a/thumb.webp",
PreviewKey: "objects/a/preview.webp",
PlaybackKey: "objects/a/playback.mp4",
}
if err := svc.attachURLs(ctx, p); err != nil {
t.Fatalf("attachURLs: %v", err)
}
if p.PlaybackURL != "signed:objects/a/playback.mp4" {
t.Fatalf("playback url = %q", p.PlaybackURL)
}
if p.ThumbURL != "signed:objects/a/thumb.webp" {
t.Fatalf("thumb url = %q", p.ThumbURL)
}
// An image without a playback binding exposes no playback URL.
img := &models.Photo{StorageKey: "objects/b/original.jpg", ThumbKey: "objects/b/thumb.webp"}
if err := svc.attachURLs(ctx, img); err != nil {
t.Fatalf("attachURLs image: %v", err)
}
if img.PlaybackURL != "" {
t.Fatalf("image playback url = %q, want empty", img.PlaybackURL)
}
}
func TestMediaKind(t *testing.T) {
if got := mediaKind(""); got != KindImage {
t.Fatalf("empty kind = %q, want image", got)
}
if got := mediaKind(KindVideo); got != KindVideo {
t.Fatalf("video kind = %q", got)
}
if got := mediaKind("bogus"); got != KindImage {
t.Fatalf("bogus kind = %q, want image", got)
}
}
func TestResolveMedia(t *testing.T) {
pngHead := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a}
cases := []struct {
name string
declared string
filename string
head []byte
wantKind string
wantMimePref string
}{
{"image header", "image/png", "x.png", nil, KindImage, "image/png"},
{"video header", "video/mp4", "clip.mp4", nil, KindVideo, "video/mp4"},
{"video by extension", "", "clip.mp4", nil, KindVideo, "video/mp4"},
{"image by extension", "", "x.jpg", nil, KindImage, "image/jpeg"},
{"sniff png", "", "", pngHead, KindImage, "image/png"},
{"generic mime by extension", "application/octet-stream", "movie.mkv", nil, KindVideo, ""},
{"unsupported", "text/plain", "notes.txt", nil, "", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
kind, mime := resolveMedia(tc.declared, tc.filename, tc.head)
if kind != tc.wantKind {
t.Fatalf("kind = %q, want %q", kind, tc.wantKind)
}
if tc.wantMimePref != "" && mime != tc.wantMimePref {
t.Fatalf("mime = %q, want %q", mime, tc.wantMimePref)
}
if kind != "" && !strings.HasPrefix(mime, kind+"/") {
t.Fatalf("mime %q does not match kind %q", mime, kind)
}
})
}
}
func TestUploadRejectsUnsupportedAndDisabledVideo(t *testing.T) {
ctx := context.Background()
svc := &Service{albums: stubAlbums{}, imageMax: 1 << 20, videoMax: 1 << 30, videoEnabled: false}
_, err := svc.Upload(ctx, UploadInput{
AlbumID: "a", UserID: "u", Filename: "notes.txt", MimeType: "text/plain",
Reader: bytes.NewReader([]byte("hello")),
})
if err == nil || !strings.Contains(err.Error(), "image or video") {
t.Fatalf("unsupported err = %v, want invalid_file", err)
}
_, err = svc.Upload(ctx, UploadInput{
AlbumID: "a", UserID: "u", Filename: "clip.mp4", MimeType: "video/mp4",
Reader: bytes.NewReader([]byte("....")),
})
if err == nil || !strings.Contains(err.Error(), "not available") {
t.Fatalf("disabled video err = %v, want video_disabled", err)
}
}