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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,153 @@
|
||||
# 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
|
||||
|
||||
1. Миграции (паритет): `photos + kind + duration_ms` (default `image`),
|
||||
`photo_objects` role CHECK + `playback`, `variant_jobs + kind`.
|
||||
2. Выкладка приложения с ffmpeg/ffprobe на хосте; существующие изображения не
|
||||
перепроцессируются (только `kind=image`).
|
||||
3. Откат: колонки аддитивны; для отката ролей удаляются привязки `playback`, затем
|
||||
восстанавливается прежнее ограничение; видео-записи остаются как `kind=video` и
|
||||
могут быть удалены вручную.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Точный эвристический кадр постера (фиксированные `-ss 1` vs поиск нечёрного кадра).
|
||||
- Стоит ли шарить `original` как `playback` для уже-идеальных файлов вместо remux
|
||||
(экономия диска против единообразия URL).
|
||||
@@ -0,0 +1,57 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Альбом задуман как общее пространство для медиа семьи или команды, но сегодня принимает
|
||||
только изображения: загрузка отклоняет всё, что не `image/*`, генерация вариантов и
|
||||
вьюер работают исключительно с картинками. Видео с телефонов и камер остаётся вне
|
||||
системы — теряется в мессенджерах и разрывает контекст альбома. Пользователю нужно
|
||||
загружать видео в те же альбомы и гарантированно воспроизводить его в браузере.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Загрузка видео в альбом наравне с фото: стриминг multipart, отдельный лимит 2 ГБ
|
||||
(картинки остаются 100 МБ).
|
||||
- Сущность фото становится медиа-записью: `kind` (`image` | `video`), `duration_ms`,
|
||||
`playback_url`; набор ролей объекта пополняется ролью `playback`.
|
||||
- Асинхронная подготовка видео в фоне: извлечение метаданных (ffprobe), постер-кадр
|
||||
(переиспользует существующую генерацию WebP-вариантов thumb/medium/preview) и
|
||||
воспроизводимый mp4 — remux без перекодирования для web-совместимых источников,
|
||||
иначе транскод в h264/yuv420p/aac с `faststart`.
|
||||
- Очередь заданий различает вид задачи и отделяет длительные (транскод) задания от
|
||||
коротких (картинки): длительный лизинг, таймаут и отдельный малый пул, чтобы
|
||||
транскод не вытеснял генерацию вариантов изображений.
|
||||
- Внешняя зависимость `ffmpeg`/`ffprobe` (без Docker — бинарник на хосте): путь
|
||||
конфигурируется, доступность отражается в health, при отсутствии видео-функции
|
||||
отключаются.
|
||||
- UI: плеер `<video>` во вьюере, постер и признак видео с длительностью в сетке,
|
||||
приём видео в загрузчике.
|
||||
- **BREAKING**: изменяются схема `photos` и ограничение ролей `photo_objects` на обоих
|
||||
бэкендах метаданных (PostgreSQL и SQLite), с пересборкой таблицы в SQLite.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `videos`: обработка и воспроизведение видео в альбоме — приём файла, извлечение
|
||||
метаданных, постер-кадр, remux/транскод в web-формат, контракт воспроизведения и
|
||||
готовности.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `photos`: сущность фото становится медиа-записью (`kind`, `duration_ms`,
|
||||
`playback_url`); расширяется допустимый ввод и лимиты размера.
|
||||
- `media-objects`: набор ролей объекта включает `playback`; очередь заданий различает
|
||||
виды и поддерживает длительные задания с длительным лизингом.
|
||||
- `ui-workspace`: вьюер, сетка и загрузчик получают поддержку видео.
|
||||
- `dev-seed-data`: набор seed-данных включает видео-фикстуры.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Backend**: `photos` (service, handlers, pipeline, worker, objects, keys), новые
|
||||
миграции PostgreSQL и SQLite, `storage` (стриминговое чтение больших объектов),
|
||||
`app` (роуты/health), `config` (VIDEO-*, FFMPEG_PATH, лимиты).
|
||||
- **Frontend**: `types.ts`, `Viewer.svelte`, `PhotoGrid.svelte`, `Uploader.svelte`.
|
||||
- **Зависимости**: `ffmpeg`/`ffprobe` на хосте; целевой remux/транскод в mp4 h264/aac.
|
||||
- **Без изменений**: `albums`, `share-links`, `social`, `realtime` — работают через
|
||||
роль `playback` и постер-картинку без правок поведения.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Создание альбомов и фотографий
|
||||
Скрипт SHALL создавать заданное параметрами число альбомов и фотографий в каждом от имени
|
||||
аутентифицированного владельца через `POST /api/albums` и
|
||||
`POST /api/albums/:id/photos`. Названия альбомов SHALL содержать метку прогона. При
|
||||
включённом флаге видео скрипт SHALL дополнительно загружать видео-фикстуры.
|
||||
|
||||
#### Scenario: Наполнение альбомами и фото
|
||||
- **WHEN** скрипт запущен с числом альбомов и фотографий на альбом
|
||||
- **THEN** создаются указанные альбомы и фотографии, а имена альбомов помечены меткой прогона
|
||||
|
||||
#### Scenario: Воспроизводимость
|
||||
- **WHEN** задан числовой seed
|
||||
- **THEN** структура и параметры генерируемых данных детерминированы для этого seed
|
||||
|
||||
#### Scenario: Флаг видео
|
||||
- **WHEN** скрипт запущен с включённым флагом видео
|
||||
- **THEN** помимо изображений часть альбомов получает видео-фикстуры
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Наполнение видео
|
||||
При включённом флаге видео скрипт SHALL загружать видео-фикстуру через общий эндпоинт
|
||||
загрузки; если генерация видео недоступна, скрипт SHALL пропускать видео и продолжать без
|
||||
ошибки.
|
||||
|
||||
#### Scenario: Видео включено
|
||||
- **WHEN** включён флаг видео
|
||||
- **THEN** в часть альбомов загружаются видео-фикстуры, различимые по метке прогона
|
||||
|
||||
#### Scenario: Генерация недоступна
|
||||
- **WHEN** видео-инструменты недоступны, а флаг видео включён
|
||||
- **THEN** скрипт пропускает видео и продолжает наполнение
|
||||
@@ -0,0 +1,51 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Журнал объектов
|
||||
|
||||
Каждый физический объект SHALL иметь ровно одну запись в слое метаданных с ключом,
|
||||
MIME-типом, размером, габаритами и состоянием; фото SHALL ссылаться на объекты по
|
||||
ролям original, thumb, medium, preview и playback.
|
||||
|
||||
#### Scenario: Привязка ролей
|
||||
|
||||
- **WHEN** фото загружено и обработано
|
||||
- **THEN** фото ссылается на объект-оригинал и, когда они готовы, на объекты thumb, medium и preview
|
||||
|
||||
#### Scenario: Оригинал без вариантов
|
||||
|
||||
- **WHEN** варианты ещё не сгенерированы
|
||||
- **THEN** фото доступно с оригиналом, а thumb, medium и preview отсутствуют
|
||||
|
||||
#### Scenario: Привязка роли воспроизведения
|
||||
|
||||
- **WHEN** видео обработано
|
||||
- **THEN** медиа ссылается на объект playback наряду с оригиналом и постером
|
||||
|
||||
### Requirement: Генерация вариантов
|
||||
Генерация вариантов SHALL выполняться через надёжную очередь заданий с
|
||||
однократным выполнением и повтором при сбое; готовый результат SHALL
|
||||
транслироваться подписчикам альбома. Очередь SHALL поддерживать настраиваемую
|
||||
пропускную способность, SHALL обрабатывать более свежие фото раньше остальных и
|
||||
SHALL различать короткие задания изображений и длительные задания видео.
|
||||
|
||||
#### Scenario: Повтор после рестарта
|
||||
- **WHEN** процесс перезапускается с незавершёнными заданиями
|
||||
- **THEN** генерация вариантов продолжается
|
||||
|
||||
#### Scenario: Событие после генерации
|
||||
- **WHEN** варианты фото готовы
|
||||
- **THEN** подписчикам альбома публикуется событие обновления этого фото
|
||||
|
||||
#### Scenario: Приоритет свежих фото
|
||||
- **WHEN** в очереди есть готовые к обработке задания разного возраста
|
||||
- **THEN** система обрабатывает более свежие фото раньше
|
||||
|
||||
#### Scenario: Настраиваемая пропускная способность
|
||||
- **WHEN** задан уровень параллелизма генерации
|
||||
- **THEN** система обрабатывает варианты с этим уровнем параллелизма
|
||||
|
||||
#### Scenario: Длительное задание видео
|
||||
- **WHEN** выполняется длительная подготовка видео
|
||||
- **THEN** задание удерживается на весь срок выполнения и не вытесняет генерацию вариантов изображений
|
||||
@@ -0,0 +1,58 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Загрузка фото
|
||||
Пользователь с правом загрузки SHALL иметь возможность загружать медиа (изображения и
|
||||
видео) по одному и массово; система SHALL принимать ввод `image/*` и `video/*`.
|
||||
|
||||
#### Scenario: Успешная загрузка
|
||||
- **WHEN** участник с правом загрузки отправляет файл изображения
|
||||
- **THEN** система сохраняет оригинал и создаёт запись фото с метаданными
|
||||
|
||||
#### Scenario: Успешная загрузка видео
|
||||
- **WHEN** участник с правом загрузки отправляет видеофайл
|
||||
- **THEN** система сохраняет оригинал и создаёт медиа-запись с признаком видео
|
||||
|
||||
#### Scenario: Загрузка без права
|
||||
- **WHEN** наблюдатель пытается загрузить фото
|
||||
- **THEN** система отклоняет запрос
|
||||
|
||||
#### Scenario: Неподдерживаемый файл
|
||||
- **WHEN** загружается файл, не являющийся изображением или видео
|
||||
- **THEN** система отклоняет загрузку
|
||||
|
||||
### Requirement: Ограничение размера загрузки
|
||||
Система SHALL ограничивать размер загружаемых файлов отдельно для изображений и видео и
|
||||
SHALL отвечать понятной ошибкой при превышении лимита, а не ошибкой разбора формы.
|
||||
|
||||
#### Scenario: Превышение лимита
|
||||
- **WHEN** загружаемый файл превышает максимальный размер
|
||||
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
|
||||
|
||||
#### Scenario: Превышение лимита видео
|
||||
- **WHEN** загружаемое видео превышает лимит видео
|
||||
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
|
||||
|
||||
#### Scenario: Файл в пределах лимита
|
||||
- **WHEN** файл не превышает лимит
|
||||
- **THEN** система принимает multipart-загрузку и обрабатывает фото
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Медиа-запись фото
|
||||
Данные фото SHALL включать вид медиа (`image` | `video`), длительность для видео и адрес
|
||||
воспроизведения, когда видео-вариант готов; для изображений адрес воспроизведения
|
||||
отсутствует.
|
||||
|
||||
#### Scenario: Запись изображения
|
||||
- **WHEN** клиент получает данные изображения
|
||||
- **THEN** вид медиа — изображение, длительность и адрес воспроизведения отсутствуют
|
||||
|
||||
#### Scenario: Запись видео без готового варианта
|
||||
- **WHEN** клиент получает данные видео, а вариант ещё не готов
|
||||
- **THEN** вид медиа — видео, длительность присутствует, адрес воспроизведения отсутствует
|
||||
|
||||
#### Scenario: Запись видео с готовым вариантом
|
||||
- **WHEN** видео-вариант готов
|
||||
- **THEN** данные медиа содержат адрес воспроизведения
|
||||
@@ -0,0 +1,60 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Загрузка фотографий из рабочего стола
|
||||
Загрузка SHALL запускаться действием в тулбаре; перетаскивание файлов над альбомом SHALL
|
||||
показывать оверлей и загружать их; пустой альбом SHALL показывать подсказку-зону загрузки.
|
||||
Загрузка SHALL быть доступна только ролям с правом загрузки; принимать SHALL изображения и
|
||||
видео.
|
||||
|
||||
#### Scenario: Перетаскивание
|
||||
- **WHEN** пользователь с правом загрузки перетаскивает файлы над альбомом
|
||||
- **THEN** показывается оверлей загрузки, и файлы загружаются
|
||||
|
||||
#### Scenario: Пустой альбом
|
||||
- **WHEN** пользователь открывает альбом без фото и имеет право загрузки
|
||||
- **THEN** в центре показана зона-подсказка загрузки
|
||||
|
||||
#### Scenario: Загрузка видео
|
||||
- **WHEN** пользователь с правом загрузки выбирает или перетаскивает видеофайл
|
||||
- **THEN** видео загружается в альбом наравне с изображениями
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Воспроизведение видео в просмотре
|
||||
Видео в режиме просмотра SHALL воспроизводиться встроенным плеером с нативными контролами и
|
||||
постером до готовности варианта; зум, панорамирование и режим панорамы SHALL к видео не
|
||||
применяться, а действия скачивания, переноса, копирования и удаления SHALL оставаться доступными.
|
||||
|
||||
#### Scenario: Плеер
|
||||
- **WHEN** пользователь открывает видео в режиме просмотра
|
||||
- **THEN** показывается встроенный плеер, а до готовности варианта — постер
|
||||
|
||||
#### Scenario: Перемотка
|
||||
- **WHEN** пользователь перематывает воспроизведение
|
||||
- **THEN** плеер показывает запрошенный момент видео
|
||||
|
||||
#### Scenario: Готовность на лету
|
||||
- **WHEN** вариант видео становится готов, пока просмотр открыт
|
||||
- **THEN** плеер становится доступен без перезагрузки страницы
|
||||
|
||||
#### Scenario: Действия над видео
|
||||
- **WHEN** открыто видео в режиме просмотра
|
||||
- **THEN** доступны скачивание оригинала, перенос, копирование и удаление по правам
|
||||
|
||||
### Requirement: Плитка видео в сетке
|
||||
Плитка видео в сетке SHALL показывать постер, признак видео и длительность; открытие плитки
|
||||
SHALL открывать режим просмотра.
|
||||
|
||||
#### Scenario: Постер и признак
|
||||
- **WHEN** видео показано в сетке
|
||||
- **THEN** плитка показывает постер, значок воспроизведения и длительность
|
||||
|
||||
#### Scenario: Постер не готов
|
||||
- **WHEN** постер ещё не готов
|
||||
- **THEN** плитка показывает плейсхолдер и сетка отображается корректно
|
||||
|
||||
#### Scenario: Открытие из сетки
|
||||
- **WHEN** пользователь открывает плитку видео
|
||||
- **THEN** открывается режим просмотра видео
|
||||
@@ -0,0 +1,111 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Поддержка видео в альбомах: приём файла, фоновая подготовка web-совместимого варианта,
|
||||
постер-кадр и воспроизведение в браузере.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Приём видео
|
||||
Участник с правом загрузки SHALL загружать видео в альбом; система SHALL принимать ввод
|
||||
`video/*` и SHALL отклонять файлы сверх лимита видео понятной ошибкой о превышении размера.
|
||||
|
||||
#### Scenario: Успешная загрузка видео
|
||||
- **WHEN** участник с правом загрузки отправляет видеофайл в пределах лимита
|
||||
- **THEN** система сохраняет оригинал и создаёт медиа-запись с признаком видео
|
||||
|
||||
#### Scenario: Превышение лимита видео
|
||||
- **WHEN** загружается файл больше лимита видео
|
||||
- **THEN** система отклоняет загрузку с явной ошибкой о превышении размера
|
||||
|
||||
#### Scenario: Не медиафайл
|
||||
- **WHEN** загружается файл, не являющийся изображением или видео
|
||||
- **THEN** система отклоняет загрузку
|
||||
|
||||
### Requirement: Метаданные видео
|
||||
При обработке система SHALL извлекать длительность, размеры кадра и поворот и SHALL
|
||||
возвращать длительность в данных медиа.
|
||||
|
||||
#### Scenario: Длительность доступна
|
||||
- **WHEN** видео успешно обработано
|
||||
- **THEN** данные медиа содержат длительность видео
|
||||
|
||||
#### Scenario: Метаданные недоступны
|
||||
- **WHEN** из видео не удаётся извлечь метаданные
|
||||
- **THEN** запись сохраняется, а длительность пуста
|
||||
|
||||
### Requirement: Постер видео
|
||||
Система SHALL извлекать кадр-постер и SHALL генерировать из него варианты thumb, medium и
|
||||
preview тем же способом, что для изображений, чтобы сетка и обложка отображали видео постером.
|
||||
|
||||
#### Scenario: Генерация постера
|
||||
- **WHEN** видео обработано
|
||||
- **THEN** система создаёт варианты thumb, medium и preview из кадра-постера
|
||||
|
||||
#### Scenario: Обложка из видео
|
||||
- **WHEN** видео становится обложкой альбома
|
||||
- **THEN** альбом отображает постер видео
|
||||
|
||||
### Requirement: Гарантированное воспроизведение
|
||||
Система SHALL готовить воспроизводимый вариант видео: SHALL выполнять remux без
|
||||
перекодирования, когда источник web-совместим, и SHALL транскодировать в h264/yuv420p/aac
|
||||
с faststart иначе; система SHALL NOT увеличивать разрешение и SHALL ограничивать длинную
|
||||
сторону 1080p.
|
||||
|
||||
#### Scenario: Совместимый источник
|
||||
- **WHEN** источник — h264/yuv420p с совместимым профилем, звуком aac или без звука, в подходящем контейнере и в пределах разрешения
|
||||
- **THEN** система подготавливает вариант remux без перекодирования
|
||||
|
||||
#### Scenario: Несовместимый источник
|
||||
- **WHEN** источник не является web-совместимым (например, hevc, 10-bit или yuv444)
|
||||
- **THEN** система транскодирует видео в h264/yuv420p/aac
|
||||
|
||||
#### Scenario: Разрешение выше предела
|
||||
- **WHEN** длинная сторона источника превышает 1080p
|
||||
- **THEN** система масштабирует видео до 1080p, не увеличивая меньший источник
|
||||
|
||||
### Requirement: Готовность и воспроизведение видео
|
||||
Данные медиа SHALL содержать адрес воспроизводимого варианта, когда он готов; клиент SHALL
|
||||
воспроизводить его с перемоткой; до готовности система SHALL отдавать постер и признак
|
||||
обработки, а обновление о готовности SHALL транслироваться подписчикам.
|
||||
|
||||
#### Scenario: Вариант не готов
|
||||
- **WHEN** видео ещё обрабатывается
|
||||
- **THEN** данные медиа содержат постер и не содержат адрес воспроизведения
|
||||
|
||||
#### Scenario: Вариант готов
|
||||
- **WHEN** обработка завершена
|
||||
- **THEN** данные медиа содержат адрес воспроизведения, а подписчики получают обновление
|
||||
|
||||
#### Scenario: Перемотка
|
||||
- **WHEN** клиент перематывает воспроизведение
|
||||
- **THEN** система отдаёт запрошенный диапазон байт
|
||||
|
||||
### Requirement: Фоновая подготовка видео
|
||||
Задание подготовки видео SHALL выполняться в фоне с длительным лизингом, таймаутом и
|
||||
повторами при сбое и SHALL NOT вытеснять генерацию вариантов изображений.
|
||||
|
||||
#### Scenario: Повтор при сбое
|
||||
- **WHEN** подготовка видео завершается ошибкой
|
||||
- **THEN** задание повторяется с задержкой до достижения предела попыток
|
||||
|
||||
#### Scenario: Изоляция от картинок
|
||||
- **WHEN** длительное видео обрабатывается
|
||||
- **THEN** генерация вариантов изображений продолжает выполняться
|
||||
|
||||
### Requirement: Оригинал видео
|
||||
Система SHALL сохранять исходный видеофайл и SHALL использовать его для скачивания
|
||||
независимо от подготовленного варианта.
|
||||
|
||||
#### Scenario: Скачивание оригинала
|
||||
- **WHEN** участник скачивает видео
|
||||
- **THEN** система возвращает адрес исходного файла
|
||||
|
||||
### Requirement: Отсутствие видео-инструментов
|
||||
При отсутствии `ffmpeg`/`ffprobe` система SHALL отключать приём видео и SHALL отражать
|
||||
недоступность видео-обработки в состоянии здоровья.
|
||||
|
||||
#### Scenario: Инструменты отсутствуют
|
||||
- **WHEN** видео-инструменты недоступны
|
||||
- **THEN** приём видео отклоняется, а health сообщает о недоступности видео
|
||||
@@ -0,0 +1,49 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Схема и миграции (паритет PostgreSQL и SQLite)
|
||||
|
||||
- [x] 1.1 Добавить миграции PostgreSQL: `photos.kind` (default `image`) и `duration_ms`, роль `playback` в `photo_objects`, `variant_jobs.kind`; проверить `make migrate` и `go test ./internal/db/...`.
|
||||
- [x] 1.2 Добавить парные миграции SQLite (пересборка `photo_objects` с ролью `playback`); проверить миграционные тесты SQLite (`go test ./internal/db/...`).
|
||||
- [x] 1.3 Дополнить контрактные тесты паритета бэкендов (роль `playback`, колонки `kind`) и прогнать `go test ./internal/repo/...`.
|
||||
|
||||
## 2. Конфигурация и видео-инструменты
|
||||
|
||||
- [x] 2.1 Ввести `IMAGE_MAX_UPLOAD_SIZE`, `VIDEO_MAX_UPLOAD_SIZE`, `FFMPEG_PATH`, `FFPROBE_PATH`, `VIDEO_TRANSCODE_CONCURRENCY`, `VIDEO_TEMP_DIR` в `config` и `.env.example`; покрыть `go test ./internal/config/...`.
|
||||
- [x] 2.2 Определять доступность `ffmpeg`/`ffprobe` на старте, выставлять `VIDEO_ENABLED` и отражать статус видео в `/api/health`; проверить `go test ./internal/app/...` (health).
|
||||
|
||||
## 3. Модель медиа и потоковое хранилище
|
||||
|
||||
- [x] 3.1 Добавить `Photo.Kind`, `Photo.DurationMS`, `Photo.PlaybackURL` и заполнение playback-URL в `attachURLs`; проверить юнит-тестом привязки URL.
|
||||
- [x] 3.2 Добавить `Open(ctx, key) (io.ReadCloser, error)` в `storage.Backend` для fs и s3; проверить `go test ./internal/storage/...`.
|
||||
|
||||
## 4. Потоковая загрузка и лимиты
|
||||
|
||||
- [x] 4.1 Перевести загрузку на стриминг во временный файл с расчётом размера и checksum и лимитом по виду файла; проверить `go test ./internal/photos/...`.
|
||||
- [x] 4.2 Расширить приём на `video/*` (запись с `kind=video`, длительность до обработки), сохранив отказ для прочих типов; проверить тестами загрузки image/video/неподдерживаемого файла.
|
||||
|
||||
## 5. Обработка видео в воркере
|
||||
|
||||
- [x] 5.1 Реализовать разбор `ffprobe` и матрицу совместимости; юнит-тесты на h264/yuv420p, yuv444, hevc, mkv, vp9 и rotation.
|
||||
- [x] 5.2 Реализовать построение команд remux/транскод и извлечение постера (`-ss 1`, fallback `-ss 0`) с передачей в `buildVariants`; проверить тестом с подставным ffmpeg (пропуск без ffmpeg).
|
||||
- [x] 5.3 Ввести `variant_jobs.kind`, длинный лизинг/таймаут и отдельный пул видео; проверить `go test ./internal/photos/...` (worker), включая изоляцию длительного задания от генерации картинок.
|
||||
|
||||
## 6. API-контракт и frontend
|
||||
|
||||
- [x] 6.1 Добавить `kind`, `duration_ms`, `playback_url` в `types.ts`; проверить `npm run check`.
|
||||
- [x] 6.2 Добавить в `Viewer.svelte` ветку `<video controls poster>` без перехвата клавиш зума/панорамы; проверить `npm run check` и `npm run build`.
|
||||
- [x] 6.3 Добавить в `PhotoGrid.svelte` постер, значок воспроизведения и длительность, а в `Uploader.svelte` — приём `video/*` с лимитом по виду; проверить `npm run check`/`npm run build`.
|
||||
|
||||
## 7. Dev-наполнение и документация
|
||||
|
||||
- [x] 7.1 Добавить флаг видео в seed-скрипт и Make-цель (`VIDEO=1`) с пропуском при отсутствии инструментов; проверить `make testdata DRY=1 VIDEO=1`.
|
||||
- [x] 7.2 Обновить `README.md` и `.env.example`: зависимость ffmpeg/ffprobe, лимиты и видео-переменные; проверить запуск по инструкции на чистом окружении.
|
||||
|
||||
## 8. Интеграционные проверки
|
||||
|
||||
- [x] 8.1 E2E: загрузка видео приводит к постеру и `playback_url` на SQLite (пропуск без ffmpeg); `go test ./internal/e2e/...`.
|
||||
- [x] 8.2 Проверить воспроизведение видео по share-ссылке и неизменность сборки мусора и корзины для видео (ручной прогон и профильные тесты).
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Заархивировать изменение после удовлетворения требований ревью проекта.
|
||||
- Проверить результат архива.
|
||||
Reference in New Issue
Block a user