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
+18
View File
@@ -44,6 +44,9 @@ type Backend interface {
Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) error
Delete(ctx context.Context, keys ...string) error
Get(ctx context.Context, key string) ([]byte, error)
// Open returns a streaming reader over a blob, for processing objects too
// large to buffer in memory (for example video). The caller closes it.
Open(ctx context.Context, key string) (io.ReadCloser, error)
// URL returns a time-limited signed app URL the client can use to fetch the blob.
URL(ctx context.Context, key string) (string, error)
// ServeObject validates the signed URL and writes the blob, including
@@ -191,6 +194,21 @@ func (c *S3) Get(ctx context.Context, key string) ([]byte, error) {
return data, nil
}
// Open streams a blob from S3 for processing without buffering it fully.
func (c *S3) Open(ctx context.Context, key string) (io.ReadCloser, error) {
out, err := c.s3.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(c.bucket),
Key: aws.String(key),
})
if err != nil {
if isNoSuchKey(err) {
return nil, ErrObjectNotFound
}
return nil, fmt.Errorf("open object %s: %w", key, err)
}
return out.Body, nil
}
// URL returns a signed app URL. The blob itself is streamed by ServeObject, so
// the S3 endpoint is never exposed to the client.
func (c *S3) URL(_ context.Context, key string) (string, error) {