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:
@@ -842,6 +842,34 @@ async def trigger_backfill_phashes(current_user: User = Depends(get_current_user
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
@router.post("/maintenance/backfill-video-cache")
|
||||
async def trigger_backfill_video_cache(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Pre-transcode every active video in the library so /playback hits
|
||||
the cache on first user click instead of paying the encode cost
|
||||
inline. Idempotent — pretranscode_video skips photos whose cache is
|
||||
already populated and current. CPU-bound; runs on the low-priority
|
||||
queue so it doesn't fight thumbnails or other user-facing tasks."""
|
||||
from app.tasks.video import pretranscode_video
|
||||
result = await db.execute(
|
||||
select(Photo.id, Photo.filepath).where(
|
||||
Photo.media_type == 'video',
|
||||
Photo.is_discarded.is_(False),
|
||||
)
|
||||
)
|
||||
rows = result.all()
|
||||
queued = 0
|
||||
for photo_id, filepath in rows:
|
||||
try:
|
||||
pretranscode_video.delay(photo_id, filepath)
|
||||
queued += 1
|
||||
except Exception as e:
|
||||
logger.warning(f"failed to queue pretranscode for {photo_id}: {e}")
|
||||
return {"status": "queued", "count": queued}
|
||||
|
||||
|
||||
@router.post("/maintenance/start-watcher")
|
||||
async def start_file_watcher(current_user: User = Depends(get_current_user)):
|
||||
"""Start the filesystem watcher. Uses a Redis lock so only one
|
||||
|
||||
Reference in New Issue
Block a user