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:
@@ -31,7 +31,9 @@ const photoColumns = `p.id, COALESCE(p.album_id, ''), p.uploader_id,
|
||||
COALESCE(o_t.storage_key, ''),
|
||||
COALESCE(o_m.storage_key, ''),
|
||||
COALESCE(o_p.storage_key, ''),
|
||||
COALESCE(o_pb.storage_key, ''),
|
||||
p.original_filename, COALESCE(o_o.size, 0), COALESCE(o_o.width, 0), COALESCE(o_o.height, 0), COALESCE(o_o.mime_type, ''),
|
||||
p.kind, COALESCE(p.duration_ms, 0),
|
||||
p.taken_at, p.latitude, p.longitude, COALESCE(p.caption, ''), p.created_at,
|
||||
u.id, u.name, COALESCE(u.avatar_url, ''),
|
||||
(SELECT count(*) FROM likes l WHERE l.photo_id = p.id),
|
||||
@@ -50,14 +52,17 @@ const photoJoins = `
|
||||
LEFT JOIN photo_objects po_m ON po_m.photo_id = p.id AND po_m.role = 'medium'
|
||||
LEFT JOIN objects o_m ON o_m.id = po_m.object_id
|
||||
LEFT JOIN photo_objects po_p ON po_p.photo_id = p.id AND po_p.role = 'preview'
|
||||
LEFT JOIN objects o_p ON o_p.id = po_p.object_id`
|
||||
LEFT JOIN objects o_p ON o_p.id = po_p.object_id
|
||||
LEFT JOIN photo_objects po_pb ON po_pb.photo_id = p.id AND po_pb.role = 'playback'
|
||||
LEFT JOIN objects o_pb ON o_pb.id = po_pb.object_id`
|
||||
|
||||
func scanPhoto(row pgx.Row) (*models.Photo, error) {
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
err := row.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey, &p.PlaybackKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.Kind, &p.DurationMS,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
&p.LikesCount, &p.CommentsCount, &p.LikedByMe, &exifText, &p.DeletedAt, &tagsText)
|
||||
@@ -114,10 +119,11 @@ func (s *Store) CreateWithOriginal(ctx context.Context, p *models.Photo, obj *Ob
|
||||
return err
|
||||
}
|
||||
if err := tx.QueryRow(ctx,
|
||||
`INSERT INTO photos (id, album_id, uploader_id, original_filename, taken_at, latitude, longitude, caption, exif)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
|
||||
`INSERT INTO photos (id, album_id, uploader_id, original_filename, taken_at, latitude, longitude, caption, exif, kind, duration_ms)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)
|
||||
RETURNING created_at`,
|
||||
p.ID, p.AlbumID, p.UploaderID, p.OriginalFilename, p.TakenAt, p.Latitude, p.Longitude, p.Caption, string(exifJSON),
|
||||
mediaKind(p.Kind), p.DurationMS,
|
||||
).Scan(&p.CreatedAt); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,7 +133,8 @@ func (s *Store) CreateWithOriginal(ctx context.Context, p *models.Photo, obj *Ob
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
`INSERT INTO variant_jobs (photo_id) VALUES ($1) ON CONFLICT (photo_id) DO NOTHING`, p.ID); err != nil {
|
||||
`INSERT INTO variant_jobs (photo_id, kind) VALUES ($1, $2) ON CONFLICT (photo_id) DO NOTHING`,
|
||||
p.ID, mediaKind(p.Kind)); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
@@ -189,8 +196,9 @@ func (s *Store) List(ctx context.Context, in ListParams) ([]models.Photo, error)
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey, &p.PlaybackKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.Kind, &p.DurationMS,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
&p.LikesCount, &p.CommentsCount, &p.LikedByMe, &exifText, &p.DeletedAt, &tagsText); err != nil {
|
||||
@@ -248,9 +256,10 @@ func (s *Store) Clone(ctx context.Context, src *models.Photo, albumID, uploaderI
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
if _, err := tx.Exec(ctx,
|
||||
`INSERT INTO photos (id, album_id, uploader_id, original_filename, taken_at, latitude, longitude, caption, exif)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`,
|
||||
id, albumID, uploaderID, src.OriginalFilename, src.TakenAt, src.Latitude, src.Longitude, src.Caption, string(exifJSON)); err != nil {
|
||||
`INSERT INTO photos (id, album_id, uploader_id, original_filename, taken_at, latitude, longitude, caption, exif, kind, duration_ms)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11)`,
|
||||
id, albumID, uploaderID, src.OriginalFilename, src.TakenAt, src.Latitude, src.Longitude, src.Caption, string(exifJSON),
|
||||
mediaKind(src.Kind), src.DurationMS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx,
|
||||
@@ -302,8 +311,9 @@ func (s *Store) ListTrash(ctx context.Context, userID string) ([]models.Photo, e
|
||||
var p models.Photo
|
||||
var exifText, tagsText string
|
||||
if err := rows.Scan(&p.ID, &p.AlbumID, &p.UploaderID, &p.StorageKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey,
|
||||
&p.ThumbKey, &p.MediumKey, &p.PreviewKey, &p.PlaybackKey,
|
||||
&p.OriginalFilename, &p.Size, &p.Width, &p.Height, &p.MimeType,
|
||||
&p.Kind, &p.DurationMS,
|
||||
&p.TakenAt, &p.Latitude, &p.Longitude, &p.Caption, &p.CreatedAt,
|
||||
&p.Uploader.ID, &p.Uploader.Name, &p.Uploader.AvatarURL,
|
||||
&p.LikesCount, &p.CommentsCount, &p.LikedByMe, &exifText, &p.DeletedAt, &p.AlbumTitle, &tagsText); err != nil {
|
||||
@@ -446,6 +456,23 @@ func (s *Store) UpdateFilename(ctx context.Context, id, filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMediaMeta records probed video facts: duration on the photo and frame
|
||||
// dimensions on the original object.
|
||||
func (s *Store) SetMediaMeta(ctx context.Context, photoID, objectID string, durationMS int64, width, height int) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if _, err := tx.Exec(ctx, `UPDATE photos SET duration_ms = $2 WHERE id = $1`, photoID, durationMS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE objects SET width = $2, height = $3 WHERE id = $1`, objectID, width, height); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
type backfillRow struct {
|
||||
ID string
|
||||
StorageKey string
|
||||
|
||||
Reference in New Issue
Block a user