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
+6 -3
View File
@@ -22,6 +22,7 @@ const (
RoleThumb = "thumb"
RoleMedium = "medium"
RolePreview = "preview"
RolePlayback = "playback"
)
var (
@@ -285,8 +286,9 @@ func (s *Store) ClaimStalePending(ctx context.Context, olderThan time.Duration,
// ClaimVariantJobs leases due variant jobs and returns their photo ids. The
// lease pushes next_attempt_at forward so the same job is not picked up twice
// while it is being processed.
func (s *Store) ClaimVariantJobs(ctx context.Context, limit int, lease time.Duration) ([]string, error) {
// while it is being processed. A non-empty kind restricts claims to that media
// kind so long video work cannot starve image jobs.
func (s *Store) ClaimVariantJobs(ctx context.Context, kind string, limit int, lease time.Duration) ([]string, error) {
if limit <= 0 {
limit = 10
}
@@ -300,12 +302,13 @@ func (s *Store) ClaimVariantJobs(ctx context.Context, limit int, lease time.Dura
`SELECT vj.photo_id FROM variant_jobs vj
JOIN photos p ON p.id = vj.photo_id
WHERE vj.next_attempt_at <= now()
AND ($2 = '' OR vj.kind = $2)
ORDER BY (vj.attempts > 0) ASC,
CASE WHEN vj.attempts = 0 THEN p.created_at END DESC NULLS LAST,
vj.next_attempt_at ASC,
vj.photo_id DESC
LIMIT $1
FOR UPDATE OF vj SKIP LOCKED`, limit)
FOR UPDATE OF vj SKIP LOCKED`, limit, kind)
if err != nil {
return nil, err
}