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.
10 KiB
Design
Context
См. proposal.md — Why. Текущее состояние, которое формирует подход:
- Загрузка читает multipart целиком в память (
photos/handlers.go:71) и держит единый лимит 100 МБ (handlers.go:16); генерация вариантов — чистый Go (imaging/webp) вphotos/pipeline.go, роли объекта —original|thumb|medium|preview(migrations/000008,sqlite/000001). - Очередь
variant_jobs— одно задание на фото, лизингinterval*4(worker.go:47), параллелизм — общий (WORKER_CONCURRENCY). - Отдача объектов уже поддерживает
Rangeи тип содержимого (storage.go:233), значит плеер получит перемотку без изменений. - Ограничения проекта: без Docker (ffmpeg — бинарник на хосте), один Go-бинарник, паритет метаданных PostgreSQL и SQLite, непрозрачные ключи и журнал объектов.
Goals / Non-Goals
Goals:
- Видео становится медиа-записью в той же сущности
photosс видомkind, длительностью и адресом воспроизведения. - Гарантированное воспроизведение в браузере: remux для web-совместимых источников, транскод для остальных; постер переиспользует существующую генерацию изображений.
- Длительная обработка не вытесняет генерацию вариантов изображений.
- Паритет обоих бэкендов метаданных и обоих бэкендов хранилища.
Non-Goals:
- Адаптивный стриминг (HLS/DASH) и несколько rendition'ов.
- Обрезка, монтаж, субтитры, несколько звуковых дорожек в playback.
- Чанковая/возобновляемая загрузка и фоновая догрузка больших файлов.
Decisions
1. Модель: расширяем photos, а не новая сущность
Добавляем photos.kind (image|video, default image) и photos.duration_ms.
Причина: журнал photo_objects, альбомы, права, корзина, share, перенос/копия и
social не знают о пикселях. Альтернатива — отдельная videos — дублирует всю эту
логику; обобщение в media — широкий рефактор схемы и UI без выгоды для MVP.
2. Новая роль объекта playback; постер — обычные image-варианты
original = загруженный файл (скачивание, fallback)
playback = remux/транскод mp4 (источник <video>)
thumb/medium/preview = WebP-постер (buildVariants, как для фото)
Постер — это изображение, поэтому buildVariants (pipeline.go:124) переиспользуется
без изменений, а сетка/обложка/CoverPicker работают как раньше. Изменяется только
ограничение ролей на обоих бэкендах: PG — DROP/ADD CONSTRAINT (как 000011),
SQLite — пересборка photo_objects (как sqlite/000003).
3. Загрузка: стриминг во временный файл
io.ReadAll не масштабируется на 2 ГБ. Загрузка SHALL стримиться во временный файл,
из него считаются размер и checksum, затем Put в хранилище. Лимит — по виду файла
(IMAGE_MAX_UPLOAD_SIZE=100 МБ, VIDEO_MAX_UPLOAD_SIZE=2 ГБ); для fs и s3
используется один путь.
4. Хранилище: потоковое чтение для обработки
Backend.Get возвращает []byte — для входа ffmpeg нужен поток. Добавляем
Open(ctx, key) (io.ReadCloser, error) в storage.Backend (fs: os.Open; s3:
GetObject.Body). Воркер спуливает оригинал во временный файл (seekable вход), затем
пишет транскод в отдельный временный файл (нужен для +faststart) и Put с известным
размером.
5. Обработка: ffprobe решает remux или транскод
ffprobe -v error -print_format json -show_streams -show_format in
-> container, vcodec, acodec, pix_fmt, profile, rotate, w/h, duration
совместимо (mp4|mov|mkv, h264, yuv420p, профиль Baseline/Main/High,
aac или без звука, длинная сторона <= 1920):
ffmpeg -map 0:v:0 -map 0:a:0? -c copy -movflags +faststart out.mp4
иначе:
ffmpeg -map 0:v:0 -map 0:a:0? -c:v libx264 -preset veryfast -crf 23
-pix_fmt yuv420p -vf "scale='min(1920,iw)':-2" -c:a aac -b:a 128k
-movflags +faststart out.mp4
постер: -ss 1 -frames:v 1 (fallback -ss 0) -> buildVariants
Альтернатива — всегда транскодировать: проще, но жжёт CPU на уже-хороших файлах.
Remux сохраняет rotation-метаданные; транскод применяет AutoOrient/rotate.
6. Очередь: вид задания и отдельный пул
variant_jobs получает kind (image|video). Видео-задание выполняется длительно:
длинный лизинг и таймаут (минуты), отдельный малый пул
(VIDEO_TRANSCODE_CONCURRENCY=1–2), чтобы длинный транскод не занимал слоты картинок.
Приоритет свежих и повтор с backoff сохраняются; перезапуск восстанавливает очередь из
состояния метаданных.
7. Контракт API и готовность
Photo получает kind, duration_ms, playback_url. Готовность выводится из наличия
роли playback: до готовности playback_url пуст, клиент показывает постер; по
завершении воркер публикует photo.updated (существующий worker.go:194). Явное поле
статуса не вводим — состояния выводятся из данных, как у текущих вариантов.
8. Наличие видео-инструментов
На старте проверяются ffmpeg/ffprobe (FFMPEG_PATH/FFPROBE_PATH). При отсутствии
VIDEO_ENABLED эффективно false: приём видео отклоняется, /api/health сообщает
недоступность видео, изображения продолжают работать. Падения на старте нет.
9. Frontend
types.ts:kind,duration_ms,playback_url.Uploader.svelte:acceptвключаетvideo/*, лимит по виду.Viewer.svelte: ветка<video controls poster={posterSrc} src={playback_url}>; зум, панорама и пробел/клавиши зума не перехватываются у нативного плеера.PhotoGrid.svelte: плитка видео показывает постер, значок воспроизведения и длительность.
Risks / Trade-offs
- [ffmpeg отсутствует на хосте] →
VIDEO_ENABLED=false, health-предупреждение, приём видео отклоняется; изображения не страдают. - [CPU thrash от транскода] → малый отдельный пул,
preset veryfast, cap 1080p, remux для совместимых файлов. - [Повторный захват длинного задания] → длинный лизинг и таймаут; по завершении — идемпотентная привязка результата.
- [Временный диск под 2 ГБ] → спул с
defer-очисткой; проверка свободного места и понятная ошибка; временный каталог конфигурируем. - [Экзотические контейнеры/поворот] → матрица совместимости покрывается тестами (hevc, yuv444, mkv, vp9, rotation).
- [Миграция ролей в SQLite] → пересборка таблицы с переносом привязок; контрактные тесты PostgreSQL и SQLite подтверждают паритет.
- [Время подготовки варианта] → часть видео готова не сразу; UI показывает постер и признак обработки, обновление приходит по SSE.
Migration Plan
- Миграции (паритет):
photos + kind + duration_ms(defaultimage),photo_objectsrole CHECK +playback,variant_jobs + kind. - Выкладка приложения с ffmpeg/ffprobe на хосте; существующие изображения не
перепроцессируются (только
kind=image). - Откат: колонки аддитивны; для отката ролей удаляются привязки
playback, затем восстанавливается прежнее ограничение; видео-записи остаются какkind=videoи могут быть удалены вручную.
Open Questions
- Точный эвристический кадр постера (фиксированные
-ss 1vs поиск нечёрного кадра). - Стоит ли шарить
originalкакplaybackдля уже-идеальных файлов вместо remux (экономия диска против единообразия URL).