feat(playback): pre-transcode HEVC videos in background; veryfast preset
Long videos blocked /playback for the entire encode duration. The fix is to populate the cache before the user clicks, not when they click. Changes: - Extract ffprobe + ffmpeg helpers to services/video.py so the request handler and the background task share one sync implementation. The endpoint wraps calls in asyncio.to_thread; celery just calls them. - New tasks/video.py with pretranscode_video. Idempotent: skips when the cache is already current and skips passthrough-safe sources (h264 in mp4/m4v/webm). 30-min task time limit so the long-tail files (3GP archive, multi-minute 1080p clips) still complete. - scan_folder now dispatches pretranscode_video alongside generate_thumbnails / extract_metadata for any new video row. - POST /library/maintenance/backfill-video-cache enqueues every active video so the existing library catches up. - libx264 preset bumped from fast to veryfast. ~2x throughput on this CPU-only box, output a few % larger but well within disk budget. - /playback simplifies to: cache check, passthrough if h264 in web-safe container, else sync transcode (still there as fallback for races against the queued task). Once the backfill task drains, /playback should be near-instant for every video. Any video added afterwards is pre-transcoded at scan time, so the user keeps that property going forward.
This commit is contained in:
@@ -20,6 +20,7 @@ from app.database import AsyncSessionLocal
|
||||
from app.models import Photo, Folder, SourceRoot
|
||||
from app.config import settings
|
||||
from app.tasks.thumbs import generate_thumbnails
|
||||
from app.tasks.video import pretranscode_video
|
||||
from app.services.metadata import extract_metadata
|
||||
from app.services.date_guess import has_date_warning
|
||||
|
||||
@@ -195,6 +196,7 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta
|
||||
# Defer task dispatch until AFTER commit so workers don't
|
||||
# query for rows that aren't visible to other sessions yet.
|
||||
pending_dispatch: list[str] = []
|
||||
pending_video_pretranscode: list[tuple[str, str]] = []
|
||||
|
||||
for filename in batch:
|
||||
filepath = os.path.join(root, filename)
|
||||
@@ -316,6 +318,13 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta
|
||||
# below; otherwise the worker can race the writer
|
||||
# and see "Photo not found".
|
||||
pending_dispatch.append(photo.id)
|
||||
if photo.media_type == 'video':
|
||||
# Pre-transcode HEVC and other non-web-safe
|
||||
# videos at scan time so the user doesn't
|
||||
# pay the encode cost on first <video> click.
|
||||
pending_video_pretranscode.append(
|
||||
(photo.id, filepath)
|
||||
)
|
||||
|
||||
processed_files += 1
|
||||
progress_set(REDIS_KEY_PROCESSED, processed_files)
|
||||
@@ -344,6 +353,8 @@ async def _scan_folder_async(folder_path: str, source_root_id: Optional[str], ta
|
||||
for photo_id in pending_dispatch:
|
||||
generate_thumbnails.delay(photo_id)
|
||||
extract_metadata.delay(photo_id)
|
||||
for vid_photo_id, vid_path in pending_video_pretranscode:
|
||||
pretranscode_video.delay(vid_photo_id, vid_path)
|
||||
|
||||
# Update folder scan timestamp
|
||||
folder.last_scanned = datetime.utcnow()
|
||||
|
||||
Reference in New Issue
Block a user