Compare commits

...

32 Commits

Author SHA1 Message Date
CI Bot 005f500ee1 fix(cover): short MediaKit polling timeout in API cover endpoint (E1/E2)
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 50s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Successful in 1m36s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m51s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m3s
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 57s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m32s
E1/E2 fallback calls used default 30 polling attempts * 2s = 60s each,
causing the cover API to hang ~2 minutes when MediaKit is slow/unreachable.
Set max_poll_attempts=5, poll_interval=2, max_retries=0 for API-side
fallback so it fails fast (~10s) instead of blocking the request.

Worker-side extraction keeps default timeout (background task, safe).
2026-08-23 00:03:52 +08:00
xiaoxia a11178eb13 fix(cover): extract cover from source asset when no backend preview exists (#1459)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m20s
AI Code Review / AI Code Review (pull_request) Failing after 4m50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m53s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Integration Tests (push) Successful in 3m29s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m29s
CI/CD Pipeline / Unit Tests (push) Successful in 14m27s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m42s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-22 23:15:33 +08:00
xiaoxia 4a6d84921d fix(cover): add plan.config.cover_candidates fallback in cover generation endpoint (#1458)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 53s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m24s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m29s
AI Code Review / AI Code Review (pull_request) Successful in 2m32s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m56s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m12s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 14m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m32s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-22 21:51:01 +08:00
xiaoxia 98cd806d29 style: 封面预览框缩小至max-width 180px居中
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m30s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m48s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
AI Code Review / AI Code Review (pull_request) Failing after 2m54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m31s
CI/CD Pipeline / Integration Tests (push) Successful in 5m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
CI/CD Pipeline / Unit Tests (push) Successful in 17m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m48s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
2026-08-22 20:34:52 +08:00
xiaoxia 6a57431f68 fix: 配音素材时长/大小显示为0 — fallback读取metadata字段
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 59s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m54s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m39s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m22s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m23s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m0s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Successful in 14m39s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m17s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
2026-08-22 18:41:18 +08:00
xiaoxia d19fa951ee fix: 标题/不换行+字体映射表修复
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m0s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m26s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m59s
AI Code Review / AI Code Review (pull_request) Failing after 6m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m12s
CI/CD Pipeline / Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m44s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m37s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-22 18:23:46 +08:00
xiaoxia e928068efd refactor: v3调度器——所有状态存入ref彻底消除RAF闭包陷阱+片段切换先播下一段再切可见性
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m29s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m33s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m11s
AI Code Review / AI Code Review (pull_request) Successful in 6m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m50s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m5s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Integration Tests (push) Successful in 4m18s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m7s
CI/CD Pipeline / Unit Tests (push) Successful in 13m49s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
2026-08-22 15:35:37 +08:00
xiaoxia 18371dc438 fix: 修复RAF循环穿透导致指数级增长+音频同步重置卡顿
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m54s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m3s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m6s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Successful in 5m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Unit Tests (push) Successful in 13m23s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
useSegmentScheduler.ts:
- 新增 tickRef 保存最新 tick 引用,所有 RAF 调度通过 () => tickRef.current()
- 片段边界切换分支加 return,阻止旧闭包穿透到末尾重复调度 RAF
  (这是网页越来越卡的根因:每次片段切换 RAF 链数量翻倍)
- play() 和重播分支的 RAF 同样改用 tickRef

FrontendPreviewPlayer.tsx:
- 片段切换音频同步 useEffect 移除 currentTime 依赖
  原依赖每200ms变化导致 audio.currentTime 被反复重置,音频卡顿
2026-08-22 13:38:02 +08:00
xiaoxia 4e9c9a2eda fix: 禁用useCanvasPlayer后台解码+恢复所有video预加载
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m15s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m43s
AI Code Review / AI Code Review (pull_request) Successful in 7m52s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m9s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Integration Tests (push) Successful in 5m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m4s
CI/CD Pipeline / Unit Tests (push) Successful in 15m32s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m19s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
- useCanvasPlayer 新增 enabled 参数,enabled=false 时 hasSupport=false
  且 init useEffect 直接 return,彻底不执行 WebCodecs 初始化和后台解码
- FrontendPreviewPlayer 传入 enabled=(useWebCodecs && !forceVideoFallback),
  useWebCodecs=false 时 useCanvasPlayer 完全不启动,消除网页卡顿元凶
- 恢复所有 video 元素 preload=auto + src,撤掉条件预加载逻辑
  (之前条件预加载导致 switchToSegment 时频繁动态设 src,影响播放和进度)
2026-08-22 02:37:46 +08:00
xiaoxia b8a349d5e0 refactor: 默认使用原生video播放,彻底解决主线程解码卡顿
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m29s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 50s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m25s
AI Code Review / AI Code Review (pull_request) Successful in 8m47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 6m45s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m53s
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
FrontendPreviewPlayer.tsx:
- useWebCodecs 默认 false,走浏览器原生硬件解码(独立线程)
- 删除 Canvas ResizeObserver useEffect(默认路径不需要)
- video 元素只预加载当前+下一片段,其余 preload=none+不设src,减少并发连接
- 片段指示器统一显示片段序号,不再显示Canvas/降级提示
- isWebCodecsSupported 不再导入,清理 TS6133

useSegmentScheduler.ts:
- tick 进度更新 200ms 节流(5fps),避免 60fps setCurrentTime 触发整树 re-render
- 重播/seek 时重置节流 ref
- switchToSegment 兜底:若 video 无 src 则动态设置并 load(),兼容 seek 到未预加载片段

useCanvasPlayer.ts 不删,保留作为兜底
2026-08-22 01:53:43 +08:00
xiaoxia 9c02e69ede fix: 第13轮 — 背压阈值20→64+移除idleTicks超时+等待10ms→5ms
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m46s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Failing after 5m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m51s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Unit Tests (push) Successful in 14m38s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m7s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m3s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m11s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
- 背压阈值从20提到64,给硬件解码器足够流水线深度
- 等待间隔10ms→5ms,减少轮询开销
- 删除idleTicks 3秒超时,帧队列容量(180)才是真正背压
- 保留帧输出等待(500ms)和完成日志
2026-08-22 01:09:59 +08:00
xiaoxia ece1f25f71 fix: 第12轮 — 背压超时保护+帧输出等待+feedFrames阈值
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m11s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m24s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m49s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m32s
AI Code Review / AI Code Review (pull_request) Failing after 5m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
- 背压等待加 idleTicks 计数,超3秒自动break防止死循环
- 解码完成后等待帧输出(最多500ms),确保队列有帧
- 加解码完成日志便于排查
- renderFrame 中 feedFrames 调用加 size<80 阈值保护
2026-08-22 00:57:38 +08:00
xiaoxia 2885ede533 fix: 第11轮 — 解码流水线批量提交+背压+移除每帧log
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m26s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m36s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m43s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m3s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m28s
AI Code Review / AI Code Review (pull_request) Successful in 9m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 13m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m50s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
- decodeSegmentBatch: 移除每帧 await+3s超时,改为批量提交 decode()
- 添加 decodeQueueSize>20 背压,防止解码器内部队列积压
- 批量提交后 16ms 等待让解码器输出帧
- feedFrames 缓冲阈值 100→80,减少后台解码抢占渲染
- 删除 drawTitle 每帧 console.log(JSON.stringify 开销导致抖动)
2026-08-22 00:15:27 +08:00
xiaoxia ac31c88c96 fix: 修复 TS2367 类型收窄错误 — decoder.state 显式断言为 string
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m4s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m3s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m9s
AI Code Review / AI Code Review (pull_request) Failing after 8m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m24s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m42s
CI/CD Pipeline / Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m11s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-21 23:43:15 +08:00
xiaoxia 585a3b6887 fix: 第10轮方案A — 滑动窗口+持久解码器+二分查找
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m6s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m40s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m31s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 7m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 7m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m17s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
架构级重构,从根本解决抖动/卡顿/黑屏/重播失效/关键帧错误:

- FrameQueue 默认容量 200,getCurrentFrame 改二分查找 O(log n)
- 每个片段持有持久化 VideoDecoder,连续喂 sample 不报关键帧错误
- segmentSampleCursorRef 记录游标,支持断点续解码
- feedFrames 后台补充:队列低于 100 帧自动续解,保持 ≥6s 缓冲
- init 只解前 2 个片段各 60 帧,播放中后台补充
- 重播/seek:关闭旧解码器→清空队列→重置游标→重新解码
- isFeedingRef 防重入
2026-08-21 23:31:44 +08:00
xiaoxia 412aa83b32 fix: 第9轮最终简化方案 — init全量解码+播放时只渲染
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m17s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m33s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m2s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
根因:之前 init 只解 60 帧,播放中续解码导致关键帧错误、
重复帧、队列混乱、抖动。

新方案:
- init 阶段一次性解码全部 894 帧(队列 1024 足够容纳)
- 播放时只做 drawImage,不触发任何解码
- 重播直接复用已有帧,不重新解码
- seek 直接定位,不清帧队列
- 删除 decodeAroundPosition 动态解码函数
2026-08-21 22:16:22 +08:00
xiaoxia c6eeca75fb fix: 删除 decodeSegment 的 startFrame 参数,修复关键帧错误
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 50s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m20s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 3m34s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m3s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Unit Tests (push) Successful in 14m50s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m51s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m49s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m22s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
WebCodecs 解码器要求第一个 chunk 必须是关键帧。续解码从 startFrame=60
开始时,samplesCollected[60] 不是关键帧 → 报错 "A key frame is required"。

修复:
- 删除 startFrame 参数,sample 循环永远从 0 开始
- decodeAroundPosition 不再传 startFrame
- totalDecoded 计算不再使用 startFrame 偏移
2026-08-21 21:25:19 +08:00
xiaoxia df4de37f73 fix: MAX_INIT_FRAMES 500 → 60,减少 init 阶段解码帧数降低播放抖动
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m27s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m59s
AI Code Review / AI Code Review (pull_request) Failing after 5m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m4s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m24s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
init 阶段解码 894 帧导致 getCurrentFrame 遍历过多帧,播放抖动。
改为每片段只解 60 帧(≈2s),播放时由 decodeAroundPosition 按需续解。
2026-08-21 21:11:08 +08:00
xiaoxia 88c07ef142 fix: 视频 fallback 路径片段切换卡顿 — 预加载策略升级
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m46s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m9s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m26s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 14m29s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
根因:UseWebCodecs=false 时走原生 video fallback,preload 策略为
当前片段 auto / 下一片段 metadata / 其余 none,导致切换时下一片段
需现场从网络缓冲,卡顿约 10 秒。

修复:
1. 所有 video 元素 preload="auto",让浏览器提前缓冲全部片段
2. tick 循环中距出点 ≤3s 时主动 seek 下一片段触发缓冲
3. switchToSegment 超时从 10s 降到 3s(预加载正常时无需久等)
2026-08-21 20:55:36 +08:00
xiaoxia 0996ba45ec fix: 续解码架构 + 帧队列扩容 1024 + 重播/seek/destroy 同步帧计数
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m39s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m10s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m20s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m15s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Unit Tests (push) Successful in 13m46s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m57s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m2s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m6s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
2026-08-21 20:30:45 +08:00
xiaoxia a34639975c fix: 增大 init 解码上限 + 重播不清帧队列 + sample 循环 generation 检查
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 57s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m7s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m21s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m46s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Failing after 3m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m9s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m39s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m50s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Unit Tests (push) Successful in 14m33s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m4s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m52s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m10s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-21 19:58:40 +08:00
xiaoxia 4b68c9be9c fix: 给 decoder.decode() 加 3s 超时保护,防止解码卡死
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m42s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Unit Tests (push) Successful in 13m51s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m49s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m10s
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
CI/CD Pipeline / Integration Tests (push) Successful in 5m50s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-21 19:09:39 +08:00
xiaoxia fbe0a6e826 fix: 视频播放结束后重播时重置 currentTime 到起点
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m16s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m42s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m12s
AI Code Review / AI Code Review (pull_request) Failing after 3m28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m21s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 15m21s
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m50s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m2s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m44s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-21 17:18:35 +08:00
xiaoxia 7f2861e98e fix: 移除 decoder.flush() 调用,修复 Chromium 下 flush 永久挂起导致解码卡死
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m57s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m43s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m33s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m59s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Unit Tests (push) Successful in 14m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m44s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m13s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m47s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
2026-08-21 16:57:20 +08:00
xiaoxia b1aa866252 fix: 修复 flush 超时 catch 块类型错误导致 CI 构建失败
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m11s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-21 16:53:53 +08:00
xiaoxia f484de79c4 ci: trigger web build 1787302071
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Failing after 2m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m41s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 4m30s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-21 16:47:51 +08:00
xiaoxia 9263db56db ci: retry build
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m53s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m24s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 4m4s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m24s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m46s
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
2026-08-21 16:41:53 +08:00
xiaoxia ebe43f163b chore: DIAG_v2 诊断日志 + flush 5s 超时保护
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m33s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m39s
AI Code Review / AI Code Review (pull_request) Failing after 2m20s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m11s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 4m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-21 16:30:59 +08:00
xiaoxia 3e38395f46 chore: 添加 [DIAG] 诊断日志定位 decode loop 卡死点
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m34s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m30s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 41s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m58s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 22m3s
CI/CD Pipeline / Unit Tests (push) Failing after 22m9s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 22m10s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 7m39s
CI/CD Pipeline / Integration Tests (push) Successful in 7m41s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 10s
2026-08-21 15:43:16 +08:00
xiaoxia 9cb20c38ae fix: Strict Mode 双执行导致 decode loop 提前退出
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m3s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m28s
CI/CD Pipeline / Unit Tests (push) Successful in 15m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
React 18 Strict Mode 下 effect 执行两次(run → cleanup → run),
destroy() 和 init() 都调用 decodeGenerationRef.current++ 导致
generation 被多增两次,decode loop 内 generation check 误判为
seek/destroy 事件而提前退出。

修复:
- destroy() 移除 decodeGenerationRef.current++
- init() 移除 decodeGenerationRef.current++
- seek()/play() 保留 generation 递增不变
- decoder.state === 'closed' 时 return 而非 break,让第二轮
  init 重新创建 decoder 正常解码
2026-08-21 15:27:42 +08:00
xiaoxia d784af7f67 fix: 重置 isDestroyedRef 修复 effect 生命周期导致解码永久跳过
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m44s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m58s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m38s
CI/CD Pipeline / Unit Tests (push) Successful in 13m55s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 9m17s
CI/CD Pipeline / CI Gate (push) Has been skipped
useEffect cleanup 调用 destroy() 将 isDestroyedRef 设为 true,
但新的 init 周期不重置它,导致 loadSegment/decodeSegment 立即 return,
isReady 永远为 false → canPlay 为 false → '暂无可播放素材'

修复:
- init 开始时重置 isDestroyedRef = false + 递增 decodeGeneration
- 同步重置 hasDecodeError/errorMessage/isReady 状态
- decode 循环内每次迭代后增加 cancelled 检查
- 增加诊断日志(decode loop 前后 + cancelled 分支)
2026-08-21 15:01:17 +08:00
xiaoxia 1b6ecc39f0 fix: WebCodecs 解码失败时自动 fallback 到原生 video 播放
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m21s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m37s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m52s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 13m30s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m7s
CI/CD Pipeline / CI Gate (push) Has been skipped
- useCanvasPlayer 新增 hasDecodeError/errorMessage 状态和 onError 回调
- decodeSegment catch 块不再静默失败,更新状态并通知上层
- VideoDecoder error 回调同步报告错误状态
- 连续 3 次解码失败时报告 hasDecodeError
- FrontendPreviewPlayer 检测 hasDecodeError 后自动切换 video fallback
- 解码失败时显示具体错误信息而非静默'暂无可播放素材'
- 新增 normalizeCodecString 函数规范化 codec 字符串
- configure 调用增加 codec 字符码调试日志
2026-08-21 14:43:12 +08:00
13 changed files with 1166 additions and 588 deletions
+169 -39
View File
@@ -31,8 +31,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Generation"])
# ── Schemas ──────────────────────────────────────────────────────────────
@@ -65,6 +63,44 @@ class GenerateCoverResponse(BaseModel):
# ── Route ────────────────────────────────────────────────────────────────
def _persist_cover_frame(frame_url: str, plan_id: str) -> str:
"""下载 MediaKit 返回的临时帧图并转存到 OSS covers/ 路径。"""
import tempfile
import uuid
from pathlib import Path
tmp_path: str | None = None
try:
import httpx
resp = httpx.get(frame_url, timeout=30, follow_redirects=True)
resp.raise_for_status()
if not resp.content:
return frame_url
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
tmp.write(resp.content)
tmp_path = tmp.name
from packages.shared.storage import get_shared_storage_service
storage = get_shared_storage_service()
cover_key = f"covers/{plan_id}/cover_{uuid.uuid4().hex[:8]}.jpg"
storage.upload_file(
file_or_path=tmp_path,
storage_key=cover_key,
content_type="image/jpeg",
)
public_url = storage.get_url(cover_key)
return public_url or frame_url
except Exception:
logger.warning("封面帧转存失败,返回原始 URL: plan_id=%s", plan_id, exc_info=True)
return frame_url
finally:
if tmp_path:
Path(tmp_path).unlink(missing_ok=True)
@router.post("/generate-cover", response_model=GenerateCoverResponse)
def generate_cover(
body: GenerateCoverRequest,
@@ -198,44 +234,31 @@ def generate_cover(
exc_info=True,
)
# 仍然找不到才报 400
if not rendered_storage_key:
logger.error("[封面生成] ❌ 找不到预览视频: plan_id=%s", plan_id)
raise HTTPException(
status_code=400,
detail="请先生成预览视频,再生成封面",
)
# 回写到 plan.config
plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key})
# 使用裸 URLrendered/* 已配置公开读)
# 使用裸 URLrendered/* 已配置公开读);找不到渲染视频时不立即报错,
# 因为步骤 E 可以直接从源素材抽帧(历史数据或 Worker 抽帧失败时的兜底)
primary_video_url = None
try:
if rendered_storage_key.startswith("http"):
primary_video_url = rendered_storage_key
else:
from packages.shared.storage import get_shared_storage_service
if rendered_storage_key:
plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key})
try:
if rendered_storage_key.startswith("http"):
primary_video_url = rendered_storage_key
else:
from packages.shared.storage import get_shared_storage_service
storage_svc = get_shared_storage_service()
primary_video_url = storage_svc.get_url(rendered_storage_key)
# 防御性规范化:合并路径中的双斜杠(// -> /),但保留协议头的 ://
# 历史数据中 project_id 为空时会产生 projects//tasks/ 路径,
# MediaKit 的 HTTP 客户端会规范化 URL 导致 404
if primary_video_url:
import re as _re
storage_svc = get_shared_storage_service()
primary_video_url = storage_svc.get_url(rendered_storage_key)
if primary_video_url:
import re as _re
primary_video_url = _re.sub(r"(?<!:)//", "/", primary_video_url)
logger.info(
"获取预览视频URL用于封面生成: plan_id=%s url=%s",
plan_id,
primary_video_url[:80] if primary_video_url else "",
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"获取预览视频URL失败: {e}",
) from e
primary_video_url = _re.sub(r"(?<!:)//", "/", primary_video_url)
logger.info(
"获取预览视频URL用于封面生成: plan_id=%s url=%s",
plan_id,
primary_video_url[:80] if primary_video_url else "",
)
except Exception as e:
logger.warning("获取预览视频URL失败: plan_id=%s err=%s", plan_id, e)
primary_video_url = None
# 统一封面管道:优先从 GenerationTask.cover_url 读取渲染后视频抽帧的封面
# 多步查找 cover_url,和查找视频 URL 一样的 fallback 逻辑
@@ -310,6 +333,113 @@ def generate_cover(
exc_info=True,
)
# 步骤 D:从 plan.config.cover_candidates 读取(Worker 渲染时写入)
if not cover_url_from_task:
_candidates = (plan.config or {}).get("cover_candidates") or []
if isinstance(_candidates, list) and _candidates:
_first = _candidates[0]
if isinstance(_first, dict):
cover_url_from_task = _first.get("image_url") or _first.get("url") or ""
if cover_url_from_task:
logger.info(
"[封面生成] 统一管道封面(步骤D-cover_candidates): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
# 步骤 E1:如果有已渲染的预览视频 URL 但 cover_url 未持久化(历史数据),
# 直接从渲染视频抽帧
if not cover_url_from_task and primary_video_url:
try:
from packages.shared.mediakit_client import get_mediakit_client
mk_client = get_mediakit_client()
if mk_client.is_available:
logger.info(
"[封面生成] 步骤E1-从渲染视频抽帧: plan_id=%s url=%s",
plan_id,
primary_video_url[:80],
)
snapshots = mk_client.extract_frames(
video_url=primary_video_url,
strategy="SpecifiedFrames",
max_frames=1,
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
if raw:
cover_url_from_task = _persist_cover_frame(raw, plan_id)
logger.info(
"[封面生成] 统一管道封面(步骤E1-rendered-video): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
except Exception:
logger.warning(
"[封面生成] 步骤E1从渲染视频抽帧失败: plan_id=%s",
plan_id,
exc_info=True,
)
# 步骤 E2:当 A/B/C/D/E1 均未命中(如历史预览任务无 cover_url)时,
# 直接从用户选择的第一个视频素材中抽取封面帧作为兜底。API 请求内短超时,不阻塞。
if not cover_url_from_task and body.asset_ids:
from packages.adapters.sqlalchemy_impl.asset_repository import (
SQLAlchemyAssetRepository,
)
from packages.shared.mediakit_client import get_mediakit_client
from packages.shared.storage import get_shared_storage_service
asset_repo = SQLAlchemyAssetRepository(db)
storage_svc = get_shared_storage_service()
mk_client = get_mediakit_client()
if mk_client.is_available:
for aid in body.asset_ids:
try:
asset = asset_repo.get(aid)
if not asset or asset.file_type != "video":
continue
sk = asset.storage_key or ""
if not sk:
continue
src_url = sk if sk.startswith("http") else storage_svc.get_url(sk)
if not src_url:
continue
logger.info(
"[封面生成] 步骤E-从素材抽帧: plan_id=%s asset_id=%s url=%s",
plan_id,
aid,
src_url[:80],
)
snapshots = mk_client.extract_frames(
video_url=src_url,
strategy="SpecifiedFrames",
max_frames=1,
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
if raw:
cover_url_from_task = _persist_cover_frame(raw, plan_id)
logger.info(
"[封面生成] 统一管道封面(步骤E-source-asset): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
break
except Exception:
logger.warning(
"[封面生成] 步骤E从素材抽帧失败: plan_id=%s asset_id=%s",
plan_id,
aid,
exc_info=True,
)
if cover_url_from_task:
# 标题已在预览视频渲染时烧录(ASS字幕),封面帧自然包含标题
cover_data = {
@@ -325,13 +455,13 @@ def generate_cover(
return GenerateCoverResponse(plan_id=plan_id, cover=cover_data)
logger.warning(
"[封面生成] 统一管道未找到 cover_url: plan_id=%s",
"[封面生成] 统一管道未找到 cover_url (A/B/C/D均未命中): plan_id=%s",
plan_id,
)
# ai_frame/ai_regenerate 类型必须从渲染管道获取,不再回退到 AI 服务
raise HTTPException(
status_code=400,
detail="封面尚未生成,请先重新生成预览视频以触发封面自动提取",
detail="封面生成失败:未找到可抽帧的视频素材,请确认已上传视频素材后重试",
)
from packages.shared.ai_service import run_generate_cover
+1 -4
View File
@@ -231,10 +231,7 @@ test.describe("Core generation flow", () => {
(response) => {
const url = response.url()
const path = new URL(url).pathname
return (
response.request().method() === "POST" &&
path.endsWith("/generation/tasks")
)
return response.request().method() === "POST" && path.endsWith("/generation/tasks")
},
{ timeout: 30_000 },
)
@@ -17,7 +17,7 @@ import {
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
import { useCanvasPlayer, isWebCodecsSupported } from "../hooks/useCanvasPlayer"
import { useCanvasPlayer } from "../hooks/useCanvasPlayer"
interface FrontendPreviewPlayerProps {
assets: AssetItem[]
@@ -82,7 +82,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
titleSettings,
}) => {
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
const useWebCodecs = isWebCodecsSupported()
// 默认走原生 video 播放(浏览器硬件解码,独立线程,不阻塞 UI)
// WebCodecs 仅在明确需要时启用(保留代码作为兜底)
const useWebCodecs = false
// ── 两条路径共用同一个 canvas ref(fallback 路径不使用) ──
const canvasRef = useRef<HTMLCanvasElement>(null)
@@ -112,12 +114,30 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
[segments],
)
// WebCodecs 解码失败后强制走 video fallback
const [forceVideoFallback, setForceVideoFallback] = useState(false)
const handleCanvasError = useCallback((err: Error) => {
console.error("[FrontendPreviewPlayer] Canvas decode Error, switching to video fallback:", err)
setForceVideoFallback(true)
}, [])
const { state: canvasState, controls: canvasControls } = useCanvasPlayer(
canvasRef,
canvasSegments,
useWebCodecs ? canvasTitle : undefined,
useWebCodecs && !forceVideoFallback ? canvasSegments : [],
useWebCodecs && !forceVideoFallback ? canvasTitle : undefined,
handleCanvasError,
useWebCodecs && !forceVideoFallback,
)
// WebCodecs 报告解码失败时自动切换到 video fallback
useEffect(() => {
if (canvasState.hasDecodeError && !forceVideoFallback) {
console.warn("[FrontendPreviewPlayer] hasDecodeError detected, forcing video fallback")
setForceVideoFallback(true)
}
}, [canvasState.hasDecodeError, forceVideoFallback])
// ── Video 播放器(fallback 路径) ──
const {
isPlaying: videoIsPlaying,
@@ -130,12 +150,13 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
videoRefs,
} = useSegmentScheduler(segments)
// 选择哪条路径的状态
const isPlaying = useWebCodecs ? canvasState.isPlaying : videoIsPlaying
const currentTime = useWebCodecs ? canvasState.currentTime : videoCurrentTime
const totalDuration = useWebCodecs ? canvasState.duration : videoTotalDuration
const canPlay = useWebCodecs ? canvasState.isReady : videoCanPlay
const isBuffering = useWebCodecs ? canvasState.isBuffering : false
// 选择哪条路径的状态(WebCodecs 解码失败时强制走 video fallback
const effectiveUseWebCodecs = useWebCodecs && !forceVideoFallback
const isPlaying = effectiveUseWebCodecs ? canvasState.isPlaying : videoIsPlaying
const currentTime = effectiveUseWebCodecs ? canvasState.currentTime : videoCurrentTime
const totalDuration = effectiveUseWebCodecs ? canvasState.duration : videoTotalDuration
const canPlay = effectiveUseWebCodecs ? canvasState.isReady : videoCanPlay
const isBuffering = effectiveUseWebCodecs ? canvasState.isBuffering : false
// ── 配音音频同步 ──
const audioRef = useRef<HTMLAudioElement | null>(null)
@@ -172,16 +193,18 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
}, [isPlaying, currentTime])
// 片段切换时同步音频(仅 fallback 路径需要)
const segmentSyncKey = useWebCodecs ? -1 : videoCurrentSegIdx
const segmentSyncKey = effectiveUseWebCodecs ? -1 : videoCurrentSegIdx
useEffect(() => {
const audio = audioRef.current
if (!audio || !audio.src || !isPlaying) return
audio.currentTime = currentTime
}, [segmentSyncKey, isPlaying, currentTime])
// 注意:不要把 currentTime 放进依赖数组,否则每200ms会重置音频位置导致卡顿
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentSyncKey, isPlaying])
const handleSeekTo = useCallback(
(time: number) => {
if (useWebCodecs) {
if (effectiveUseWebCodecs) {
canvasControls.seek(time)
} else {
videoSeekTo(time)
@@ -191,11 +214,11 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
audio.currentTime = time
}
},
[useWebCodecs, canvasControls, videoSeekTo],
[effectiveUseWebCodecs, canvasControls, videoSeekTo],
)
const handleTogglePlay = useCallback(() => {
if (useWebCodecs) {
if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) {
canvasControls.pause()
} else {
@@ -204,7 +227,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
} else {
videoTogglePlayPause()
}
}, [useWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
}, [effectiveUseWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
// ── 进度条拖拽 ──
const [isDragging, setIsDragging] = useState(false)
@@ -247,32 +270,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
// ── Canvas ResizeObserver ──
// ── Canvas 容器 ref(保留声明,WebCodecs 兜底路径仍引用) ──
const canvasContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!useWebCodecs || !canPlay) return
const container = canvasContainerRef.current
const canvas = canvasRef.current
if (!container || !canvas) return
// 立即设置一次 canvas 像素分辨率,避免默认 300×150 导致首帧变形
const initRect = container.getBoundingClientRect()
if (initRect.width > 0 && initRect.height > 0) {
const dpr = window.devicePixelRatio || 1
canvas.width = initRect.width * dpr
canvas.height = initRect.height * dpr
}
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
canvas.width = width * window.devicePixelRatio
canvas.height = height * window.devicePixelRatio
}
}
})
ro.observe(container)
return () => ro.disconnect()
}, [useWebCodecs, canPlay])
// ── 未就绪 ──
if (!ready || !assets.length) {
@@ -298,6 +297,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
// ── 无播放片段 ──
if (!canPlay) {
const showDecodeError = forceVideoFallback && canvasState.hasDecodeError
return (
<div
className="xx-preview-empty"
@@ -316,6 +316,19 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
<LoadingOutlined style={{ fontSize: 48, color: "#fff", marginBottom: 12 }} spin />
<p style={{ color: "rgba(255,255,255,0.8)" }}>...</p>
</>
) : showDecodeError ? (
<>
<PlayCircleOutlined style={{ fontSize: 48, color: "#ef4444", marginBottom: 12 }} />
<p className="xx-preview-empty-title" style={{ color: "rgba(255,255,255,0.9)" }}>
</p>
<p
className="xx-preview-empty-desc"
style={{ color: "rgba(255,255,255,0.6)", maxWidth: 300, textAlign: "center" }}
>
{canvasState.errorMessage || "当前浏览器不支持该视频编码格式,请刷新重试"}
</p>
</>
) : (
<>
<PlayCircleOutlined
@@ -332,7 +345,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
return (
<>
{/* ── Canvas 渲染层(WebCodecs 路径) ── */}
{useWebCodecs && (
{effectiveUseWebCodecs && (
<div
ref={canvasContainerRef}
style={{
@@ -353,8 +366,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
</div>
)}
{/* ── Video 渲染层(fallback 路径 ── */}
{!useWebCodecs &&
{/* ── Video 渲染层(默认路径,浏览器原生硬件解码 ── */}
{!effectiveUseWebCodecs &&
segments.map((seg, i) => (
<video
key={seg.assetId}
@@ -362,9 +375,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
ref={(el) => {
videoRefs.current[i] = el
}}
preload={
i === videoCurrentSegIdx ? "auto" : i === videoCurrentSegIdx + 1 ? "metadata" : "none"
}
preload="auto"
src={seg.videoUrl}
style={{
position: "absolute",
@@ -423,7 +434,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
zIndex: 10,
}}
>
{useWebCodecs ? "Canvas" : `片段 ${videoCurrentSegIdx + 1}/${segments.length}`}
{`片段 ${videoCurrentSegIdx + 1}/${segments.length}`}
</div>
{/* 控制条 */}
@@ -16,6 +16,7 @@ import { LoadingOutlined } from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { TitleSettings } from "../types"
import { getFontFamily } from "../constants"
import FrontendPreviewPlayer from "./FrontendPreviewPlayer"
interface PreviewVideoPanelProps {
@@ -87,7 +88,7 @@ function buildTitleStyle(settings: TitleSettings, containerHeight: number): Reac
: (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * 400 // fallback
const base: React.CSSProperties = {
fontFamily: settings.font || "思源黑体",
fontFamily: getFontFamily(settings.font),
fontSize: `${fontSizePx}px`,
color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400,
@@ -176,7 +177,12 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
position: "absolute",
}}
>
{displayTitle}
{displayTitle.split("/").map((part, i) => (
<span key={i}>
{i > 0 && <br />}
{part}
</span>
))}
</div>
</div>
)
@@ -17,6 +17,16 @@ interface Step5VoiceSelectProps {
}
/** 格式化时长 mm:ss */
/** 获取素材实际时长(优先顶层 durationfallback 到 metadata.duration */
const getDuration = (item: AssetItem): number => {
return item.duration ?? (item.metadata?.duration as number) ?? 0
}
/** 获取素材实际文件大小 */
const getFileSize = (item: AssetItem): number => {
return item.file_size ?? (item.metadata?.file_size as number) ?? 0
}
const formatDuration = (seconds?: number): string => {
if (!seconds || seconds <= 0) return "00:00"
const m = Math.floor(seconds / 60)
@@ -89,7 +99,7 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
// 如果启用了时长校验,且配音时长不足
if (totalVideoDuration > 0) {
const material = materials.find((m) => m.id === id)
if (material && (material.duration || 0) < totalVideoDuration) {
if (material && getDuration(material) < totalVideoDuration) {
setPendingVoiceId(id)
setDurationWarningOpen(true)
return
@@ -271,25 +281,24 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
}}
>
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
{formatDuration(item.duration)}
{totalVideoDuration > 0 &&
(Number(item.duration) || 0) < Number(totalVideoDuration) && (
<span
style={{
color: "#ff4d4f",
fontSize: 11,
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
gap: 2,
}}
>
<WarningOutlined />
</span>
)}
{formatDuration(getDuration(item))}
{totalVideoDuration > 0 && getDuration(item) < Number(totalVideoDuration) && (
<span
style={{
color: "#ff4d4f",
fontSize: 11,
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
gap: 2,
}}
>
<WarningOutlined />
</span>
)}
</span>
<span>{formatFileSize(item.file_size)}</span>
<span>{formatFileSize(getFileSize(item))}</span>
</div>
</div>
)
@@ -318,7 +327,9 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
return (
<p>
<strong>{pendingMaterial ? formatDuration(pendingMaterial.duration) : "--"}</strong>
<strong>
{pendingMaterial ? formatDuration(getDuration(pendingMaterial)) : "--"}
</strong>
<strong>{formatDuration(totalVideoDuration)}</strong>
@@ -2,6 +2,7 @@
* 标题预设样式网格
*/
import React from "react"
import { getFontFamily } from "../../constants"
interface TitlePresetItem {
key: string
@@ -35,7 +36,7 @@ const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({
>
<span
className="xx-title-preset-preview-text"
style={{ ...p.previewStyle, ...(fontFamily ? { fontFamily } : {}) }}
style={{ ...p.previewStyle, fontFamily: getFontFamily(fontFamily || "思源黑体") }}
>
</span>
+15
View File
@@ -56,6 +56,21 @@ export const FONT_OPTIONS = [
"华康俪金黑",
]
/* ── 标题字体 CSS font-family 映射(中文显示名 → 浏览器可识别的字体栈) ── */
export const FONT_FAMILY_MAP: Record<string, string> = {
: '"Source Han Sans SC", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif',
: '"Source Han Serif SC", "Noto Serif SC", "Songti SC", "SimSun", serif',
: '"PingFang SC", -apple-system, "Helvetica Neue", sans-serif',
PingFang: '"PingFang SC", -apple-system, "Helvetica Neue", sans-serif',
: '"Microsoft YaHei", "PingFang SC", sans-serif',
: '"KaiTi", "STKaiti", "DFKai-SB", serif',
: '"华康俪金黑", "DFLiJinHei-W8", "Source Han Sans SC", "Microsoft YaHei", sans-serif',
}
export function getFontFamily(font: string): string {
return FONT_FAMILY_MAP[font] || FONT_FAMILY_MAP["思源黑体"]
}
/* ── 标题样式预设 ── */
export const TITLE_PRESETS = [
{
+2
View File
@@ -2309,6 +2309,8 @@
.xx-cover-preview-box {
position: relative;
aspect-ratio: 9 / 16;
max-width: 180px;
margin: 0 auto;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
overflow: hidden;
@@ -9,8 +9,21 @@ import { createFile } from "mp4box"
import type { Movie, Sample } from "mp4box"
// ── 常量 ──
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
const MAX_INIT_FRAMES = 60
/**
* 规范化 mp4box 提取的 codec 字符串为 WebCodecs 兼容格式
* mp4box 返回的 codec 可能包含 mp4box 特有后缀(如 avc1.640028),
* WebCodecs 要求标准 ISO BMFF codec string(如 avc1.640028
* 大部分情况下格式一致,但需要做防御性处理
*/
function normalizeCodecString(codec: string): string {
// mp4box 有时返回带空格的 codec,去掉
const trimmed = codec.trim()
// HEVC: mp4box 可能返回 hev1.1.6.L93.B0 或 hvc1.1.6.L93.B0
// WebCodecs 接受 hev1.x.x 或 hvc1.x.x,两者都可
// H.264: mp4box 返回 avc1.640028WebCodecs 也接受
return trimmed
}
// ── MP4 Box 解析辅助函数 ──
@@ -96,7 +109,7 @@ class FrameQueue {
private frames: FrameEntry[] = []
private maxSize: number
constructor(maxSize = 5) {
constructor(maxSize = 200) {
this.maxSize = maxSize
}
@@ -108,24 +121,36 @@ class FrameQueue {
this.frames.push(entry)
}
/** 获取当前时间戳应显示的帧 */
/** 获取当前时间戳应显示的帧(二分查找,O(log n) */
getCurrentFrame(timestamp: number): VideoFrame | null {
let best: FrameEntry | null = null
let bestIdx = -1
for (let i = 0; i < this.frames.length; i++) {
const f = this.frames[i]
if (f.pts <= timestamp + 0.01) {
best = f
bestIdx = i
if (this.frames.length === 0) return null
const target = timestamp + 0.01
// 找到最后一个 pts <= target 的帧(右边界)
let lo = 0,
hi = this.frames.length - 1,
bestIdx = -1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (this.frames[mid].pts <= target) {
bestIdx = mid
lo = mid + 1
} else {
hi = mid - 1
}
}
if (bestIdx < 0) return null
// 关闭并移除 bestIdx 之前的所有已播放帧
for (let i = 0; i < bestIdx; i++) {
this.frames[i].frame.close()
}
if (bestIdx >= 0) {
this.frames = this.frames.slice(bestIdx)
}
return best?.frame ?? null
this.frames.splice(0, bestIdx)
// 此时 bestIdx 对应帧已在索引 0
return this.frames[0]?.frame ?? null
}
clear() {
@@ -172,6 +197,10 @@ export interface CanvasPlayerState {
duration: number
isReady: boolean
isBuffering: boolean
/** WebCodecs 解码失败时为 true,调用方应 fallback 到原生 video 播放 */
hasDecodeError: boolean
/** 解码错误信息(用于 UI 展示) */
errorMessage: string
}
export interface CanvasPlayerControls {
@@ -209,21 +238,29 @@ export function useCanvasPlayer(
stroke?: boolean
shadow?: boolean
},
onError?: (error: Error) => void,
enabled: boolean = true,
) {
const [state, setState] = useState<CanvasPlayerState>({
hasSupport: isWebCodecsSupported(),
hasSupport: enabled && isWebCodecsSupported(),
isPlaying: false,
currentTime: 0,
duration: 0,
isReady: false,
isBuffering: false,
hasDecodeError: false,
errorMessage: "",
})
// ── 内部引用 ──
const decoderRef = useRef<VideoDecoder | null>(null)
const frameQueueRef = useRef(new FrameQueue(600))
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */
const decodedSegmentsRef = useRef(new Set<number>())
const frameQueueRef = useRef(new FrameQueue(200))
/** 每个片段持久化解码器,避免每次新建导致关键帧错误 */
const segmentDecodersRef = useRef(new Map<number, VideoDecoder>())
/** 每个片段已送入解码器的 sample 游标(用于续解码) */
const segmentSampleCursorRef = useRef(new Map<number, number>())
/** 后台补充解码是否正在运行(防重入) */
const isFeedingRef = useRef(false)
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
const decodeGenerationRef = useRef(0)
const rafRef = useRef<number>(0)
@@ -234,6 +271,8 @@ export function useCanvasPlayer(
const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 })
const isDestroyedRef = useRef(false)
const lastProgressUpdateRef = useRef<number>(0)
const onErrorRef = useRef(onError)
onErrorRef.current = onError
// 计算总时长
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
@@ -367,7 +406,7 @@ export function useCanvasPlayer(
globalEndTime: globalStart + (segment.endTime - segment.startTime),
trackId: videoTrack.id ?? 1,
timescale: videoTrack.timescale ?? 90000,
codec: videoTrack.codec ?? "avc1.42E01E",
codec: normalizeCodecString(videoTrack.codec ?? "avc1.42E01E"),
videoWidth: videoTrack.track_width || 1280,
videoHeight: videoTrack.track_height || 720,
description,
@@ -451,87 +490,91 @@ export function useCanvasPlayer(
[segments, extractCodecDescription],
)
// ── 初始化 VideoDecoder 并解码指定片段 ──
const decodeSegment = useCallback(
async (_buffer: ArrayBuffer, meta: SegmentMeta, maxFrames?: number): Promise<void> => {
if (isDestroyedRef.current) return
// ── 解码片段的一批帧(使用持久化解码器,支持从断点续解码) ──
/**
* @param segIdx 片段索引
* @param maxFrames 本次最多解码多少帧
*/
const decodeSegmentBatch = useCallback(
async (segIdx: number, maxFrames: number = 60): Promise<number> => {
if (isDestroyedRef.current) return 0
const metas = segmentMetaRef.current
const meta = metas[segIdx]
if (!meta) return 0
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) return 0
let decoderReady = false
const gen = decodeGenerationRef.current
let decoder = segmentDecodersRef.current.get(segIdx)
let cursor = segmentSampleCursorRef.current.get(segIdx) ?? 0
const samples = meta.samples
// 配置解码器(每个片段可能需要不同的 codec/分辨率
const decoder = new VideoDecoder({
output: (frame: VideoFrame) => {
// 从第一帧获取实际尺寸
if (videoDimRef.current.width === 0 || videoDimRef.current.height === 0) {
videoDimRef.current = { width: frame.codedWidth, height: frame.codedHeight }
console.log(
`[useCanvasPlayer] Actual frame size: ${frame.codedWidth}x${frame.codedHeight}`,
)
}
const localTime = frame.timestamp / 1_000_000
const globalTime = localTime + meta.globalStartTime
frameQueueRef.current.push({
frame,
pts: globalTime,
duration: (frame.duration ?? 0) / 1_000_000,
})
},
error: (e: DOMException) => {
console.error("[useCanvasPlayer] Decoder error:", e)
},
})
console.log("[useCanvasPlayer] configure:", {
codec: meta.codec,
description: meta.description,
descriptionByteLength: meta.description?.byteLength,
videoWidth: meta.videoWidth,
videoHeight: meta.videoHeight,
})
try {
await decoder.configure({
codec: meta.codec,
...(meta.description ? { description: meta.description } : {}),
// 如果还没有解码器,新建一个(从关键帧开始,不会报 key frame 错误
if (!decoder || decoder.state === "closed") {
decoder = new VideoDecoder({
output: (frame: VideoFrame) => {
if (videoDimRef.current.width === 0 || videoDimRef.current.height === 0) {
videoDimRef.current = { width: frame.codedWidth, height: frame.codedHeight }
}
const localTime = frame.timestamp / 1_000_000
const globalTime = localTime + meta.globalStartTime
frameQueueRef.current.push({
frame,
pts: globalTime,
duration: (frame.duration ?? 0) / 1_000_000,
})
},
error: (e: DOMException) => {
console.error(`[useCanvasPlayer] Segment ${segIdx} decoder error:`, e)
// 重置该片段的解码器和游标,允许重试
segmentDecodersRef.current.delete(segIdx)
segmentSampleCursorRef.current.set(segIdx, 0)
},
})
decoderRef.current = decoder
decoderReady = true
// 标记缓冲结束,让 UI 开始渲染
try {
await decoder.configure({
codec: meta.codec,
...(meta.description ? { description: meta.description } : {}),
})
} catch (err) {
console.error(`[useCanvasPlayer] Segment ${segIdx} configure failed:`, err)
const error = err instanceof Error ? err : new Error(String(err))
setState((s) => ({
...s,
isBuffering: false,
hasDecodeError: true,
errorMessage: `视频解码失败: ${error.message || "不支持的编解码器"}`,
}))
onErrorRef.current?.(error)
return 0
}
segmentDecodersRef.current.set(segIdx, decoder)
cursor = 0
// 标记缓冲结束
setState((s) => ({ ...s, isBuffering: false }))
} catch (err) {
console.error("[useCanvasPlayer] Decoder configure failed for segment:", err)
return
}
if (!decoderReady) return
if (decoder.state !== "configured") return 0
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片
const samplesCollected = meta.samples
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: ${samplesCollected.length} samples to decode`,
)
if (samplesCollected.length === 0) {
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId)
return
}
// 送入解码器
let decodedCount = 0
let skippedCount = 0
let decodeErrors = 0
for (const sample of samplesCollected) {
if (!sample.data || isDestroyedRef.current) {
skippedCount++
// 从 cursor 继续喂 sample(流水线批量提交,不 await 单个 decode
let decoded = 0
let si = cursor
while (si < samples.length && decoded < maxFrames) {
if (decodeGenerationRef.current !== gen || isDestroyedRef.current) break
if ((decoder.state as string) === "closed") break
// 帧队列快满时停止提交(这才是真正的背压)
if (frameQueueRef.current.size >= 180) break
// 解码器内部队列积压过多时短暂让出线程(阈值64,给硬件足够流水线深度)
if (decoder.decodeQueueSize > 64) {
await new Promise((r) => setTimeout(r, 5))
continue
}
if (decoder.state === "closed") break
// 初始化阶段限制解码帧数,避免帧缓冲溢出
if (maxFrames && decodedCount >= maxFrames) {
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: init decode limited to ${maxFrames} frames`,
)
break
}
const sample = samples[si]
si++
if (!sample.data) continue
const chunk = new EncodedVideoChunk({
type: sample.is_sync ? "key" : "delta",
@@ -541,85 +584,73 @@ export function useCanvasPlayer(
})
try {
await decoder.decode(chunk) // 修复:await 捕获异步错误
decodedCount++
decoder.decode(chunk)
decoded++
} catch (e) {
decodeErrors++
console.warn(`[useCanvasPlayer] Decode chunk error (${decodeErrors}):`, e)
// 连续 3 次解码失败,放弃当前片段
if (decodeErrors >= 3) {
console.error("[useCanvasPlayer] Too many decode errors, aborting segment")
break
}
console.warn(`[useCanvasPlayer] Segment ${segIdx} decode error:`, e)
break
}
}
cursor = si
segmentSampleCursorRef.current.set(segIdx, cursor)
// 等待解码器输出帧(最多500ms)
if (decoded > 0 && (decoder.state as string) === "configured") {
let waited = 0
while (frameQueueRef.current.size < Math.min(decoded, 10) && waited < 500) {
await new Promise((r) => setTimeout(r, 20))
waited += 20
if (isDestroyedRef.current) break
}
}
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: decoded ${decodedCount}, skipped ${skippedCount}, errors ${decodeErrors}, decoder.state=${decoder.state}`,
`[useCanvasPlayer] Segment ${segIdx} decoded ${decoded} frames, queue size: ${frameQueueRef.current.size}`,
)
// flush 仅在解码器状态正常时执行
if (decoder.state === "configured") {
try {
await decoder.flush()
console.log(`[useCanvasPlayer] Segment ${meta.assetId}: flush complete`)
} catch (e) {
console.warn("[useCanvasPlayer] Decoder flush error:", e)
}
}
return decoded
},
[],
)
// ── 后台持续补充帧 ──
/**
* 按需解码当前播放位置 ±1 个片段。
* 在渲染循环中定期调用,避免一次性解码所有片段导致环形缓冲区溢出丢帧。
* 使用"先标记再解码"模式防止并发重复解码,失败时移除标记允许重试。
* 根据当前播放时间,确保队列中有足够缓冲
* 播放循环每 200ms 调用一次
*/
const decodeAroundPosition = useCallback(
const feedFrames = useCallback(
async (currentTime: number) => {
const metas = segmentMetaRef.current
if (!metas || metas.length === 0) return
if (isFeedingRef.current) return
isFeedingRef.current = true
try {
const metas = segmentMetaRef.current
if (!metas || metas.length === 0) return
// 记录当前代数,seek 后代数变化则中止
const gen = decodeGenerationRef.current
// 队列帧数充足时不解码(目标:保持 >= 80 帧缓冲)
if (frameQueueRef.current.size >= 80) return
let targetIdx = -1
let acc = 0
for (let i = 0; i < metas.length; i++) {
const dur = metas[i].globalEndTime - metas[i].globalStartTime
if (currentTime < acc + dur) {
targetIdx = i
break
// 找到当前播放的片段
let targetIdx = 0
let acc = 0
for (let i = 0; i < metas.length; i++) {
const dur = metas[i].globalEndTime - metas[i].globalStartTime
if (currentTime < acc + dur) {
targetIdx = i
break
}
acc += dur
}
acc += dur
}
if (targetIdx === -1) targetIdx = metas.length - 1
for (
let i = Math.max(0, targetIdx - 1);
i <= Math.min(metas.length - 1, targetIdx + 1);
i++
) {
// seek 已作废当前解码任务
if (decodeGenerationRef.current !== gen) return
if (decodedSegmentsRef.current.has(i)) continue
const meta = metas[i]
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) continue
// 先标记为解码中,防止下一帧渲染时重复发起解码
decodedSegmentsRef.current.add(i)
try {
await decodeSegment(buffer, meta, 300)
} catch (e) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef.current.delete(i)
console.warn(`[useCanvasPlayer] 按需解码片段 ${i} 失败:`, e)
// 依次补充:当前片段 → 下一个片段 → 再下一个
for (let offset = 0; offset <= 2; offset++) {
const idx = targetIdx + offset
if (idx >= metas.length) break
if (frameQueueRef.current.size >= 180) break
await decodeSegmentBatch(idx, 60)
}
// await 后再次检查代数,seek 期间不更新标记
if (decodeGenerationRef.current !== gen) return
} finally {
isFeedingRef.current = false
}
},
[decodeSegment],
[decodeSegmentBatch],
)
// ── 标题绘制 ──
@@ -636,7 +667,6 @@ export function useCanvasPlayer(
// 按 "/" 分割为多行("/" 作为手动换行符)
const lines = title.text.split(/[//⁄∕]/)
console.log("[drawTitle] 原始标题:", JSON.stringify(title.text), "分割后:", lines)
const lineHeight = fontSize * 1.3
const totalHeight = lines.length * lineHeight
@@ -748,8 +778,10 @@ export function useCanvasPlayer(
}
return s
})
// 按需解码当前 ±1 片段
decodeAroundPosition(currentTime)
// 后台补充帧:队列不足时自动续解码
if (frameQueueRef.current.size < 80) {
void feedFrames(currentTime)
}
}
if (currentTime >= totalDuration) {
@@ -758,29 +790,46 @@ export function useCanvasPlayer(
}
rafRef.current = requestAnimationFrame(renderFrame)
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition])
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, feedFrames])
// ── 播放控制 ──
const play = useCallback(async () => {
if (!state.hasSupport || isDestroyedRef.current) return
// 重播场景:currentTime 已回到起点但 decodedSegmentsRef 仍有旧标记
// 此时 FrameQueue 中旧帧已被淘汰,需清空标记让 decodeAroundPosition 重新解码
if (state.currentTime <= 0.1 && decodedSegmentsRef.current.size > 0) {
// 重播:必须关闭旧解码器、清空队列、重置游标,从头重新解码
if (state.currentTime >= totalDuration - 0.1 || state.currentTime <= 0.1) {
decodeGenerationRef.current++
decodedSegmentsRef.current.clear()
// 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null
// 关闭所有持久化解码器
for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear()
playStartOffsetRef.current = 0
setState((s) => ({ ...s, currentTime: 0 }))
// 重新初始化解码
const metas = segmentMetaRef.current
const initialDecodeCount = Math.min(metas.length, 2)
for (let i = 0; i < initialDecodeCount; i++) {
await decodeSegmentBatch(i, 60)
}
}
setState((s) => ({ ...s, isPlaying: true }))
playStartRef.current = performance.now()
playStartOffsetRef.current = state.currentTime
if (state.currentTime < 0.1) {
playStartOffsetRef.current = 0
} else {
playStartOffsetRef.current = state.currentTime
}
lastProgressUpdateRef.current = 0
rafRef.current = requestAnimationFrame(renderFrame)
// 立即触发一次按需解码,不等渲染循环 200ms 节流
decodeAroundPosition(state.currentTime)
}, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition])
}, [state.hasSupport, state.currentTime, totalDuration, renderFrame, decodeSegmentBatch])
const pause = useCallback(() => {
setState((s) => ({ ...s, isPlaying: false }))
@@ -790,33 +839,61 @@ export function useCanvasPlayer(
const seek = useCallback(
async (time: number) => {
const clampedTime = Math.max(0, Math.min(time, totalDuration))
decodeGenerationRef.current++
// 关闭所有解码器、清空队列、重置游标
for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear()
setState((s) => ({ ...s, currentTime: clampedTime }))
playStartOffsetRef.current = clampedTime
playStartRef.current = performance.now()
// seek 时递增解码代数,作废正在进行的异步解码
decodeGenerationRef.current++
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记
frameQueueRef.current.clear()
decodedSegmentsRef.current.clear()
await decodeAroundPosition(clampedTime)
// 找到 seek 目标片段,从该片段开始解码
const metas = segmentMetaRef.current
let targetIdx = 0,
acc = 0
for (let i = 0; i < metas.length; i++) {
const dur = metas[i].globalEndTime - metas[i].globalStartTime
if (clampedTime < acc + dur) {
targetIdx = i
break
}
acc += dur
}
await decodeSegmentBatch(targetIdx, 60)
await decodeSegmentBatch(Math.min(targetIdx + 1, metas.length - 1), 60)
},
[totalDuration, decodeAroundPosition],
[totalDuration, decodeSegmentBatch],
)
const destroy = useCallback(() => {
isDestroyedRef.current = true
cancelAnimationFrame(rafRef.current)
// 关闭所有持久化解码器
for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
if (decoderRef.current && decoderRef.current.state !== "closed") {
decoderRef.current.close()
}
// 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧)
decodeGenerationRef.current++
frameQueueRef.current.clear()
segmentDataRef.current.clear()
segmentMetaRef.current = []
decodedSegmentsRef.current.clear()
}, [])
// ── 预加载下一个片段的数据 ──
@@ -833,6 +910,7 @@ export function useCanvasPlayer(
// ── 初始化:加载并解码所有片段 ──
useEffect(() => {
if (!enabled) return
if (!state.hasSupport || segments.length === 0) {
console.log("[useCanvasPlayer] Skip init:", {
hasSupport: state.hasSupport,
@@ -842,10 +920,24 @@ export function useCanvasPlayer(
}
let cancelled = false
console.log("[useCanvasPlayer] Init start, segments:", segments.length)
const init = async () => {
setState((s) => ({ ...s, isBuffering: true }))
// ✅ 关键修复:重置销毁标记,允许新的 init 周期正常工作
// destroy() 在 useEffect cleanup 中被调用,将 isDestroyedRef 设为 true
// 如果不重置,后续的 loadSegment / decodeSegment 会立即 return
isDestroyedRef.current = false
// ✅ Strict Mode 修复:init 不再递增 generation
// seek() 和 play() 仍保留 generation 递增用于中止异步解码
// 重置错误状态,避免上一轮的解码错误影响新的 init 周期
setState((s) => ({
...s,
isBuffering: true,
hasDecodeError: false,
errorMessage: "",
isReady: false,
}))
console.log("[useCanvasPlayer] Init start v2_DIAG, segments:", segments.length)
// 1. 加载所有片段数据
for (const seg of segments) {
@@ -887,32 +979,44 @@ export function useCanvasPlayer(
segmentMetaRef.current = metas
// 3. 按需解码:初始只解码3 个片段,后续通过 decodeAroundPosition 动态加载
// 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏
decodedSegmentsRef.current.clear()
const initGen = decodeGenerationRef.current
const initialDecodeCount = Math.min(metas.length, 3)
for (let i = 0; i < initialDecodeCount; i++) {
if (cancelled) break
// seek 或 destroy 已作废当前初始化
if (decodeGenerationRef.current !== initGen) break
const meta = metas[i]
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) continue
// 先标记为解码中,防止重复解码
decodedSegmentsRef.current.add(i)
// 3. 初始化解码:关闭旧解码器,2 个片段各解 60 帧
// 后续由 feedFrames 后台补充
for (const d of segmentDecodersRef.current.values()) {
try {
await decodeSegment(buffer, meta, MAX_INIT_FRAMES)
} catch (e) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef.current.delete(i)
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear()
const initGen = decodeGenerationRef.current
const initialDecodeCount = Math.min(metas.length, 2)
console.log(
`[useCanvasPlayer] Starting init decode: ${initialDecodeCount} segments, metas: ${metas.length}`,
)
for (let i = 0; i < initialDecodeCount; i++) {
if (cancelled) break
if (decodeGenerationRef.current !== initGen) break
console.log(`[DIAG_v2] Init decode segment ${i}...`)
try {
await decodeSegmentBatch(i, 60)
console.log(`[DIAG_v2] Init decode segment ${i} done`)
} catch (e) {
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
}
if (cancelled) break
}
console.log(`[useCanvasPlayer] Init decode finished, cancelled:`, cancelled)
if (!cancelled) {
console.log("[useCanvasPlayer] Init complete, isReady = true")
console.log("[useCanvasPlayer] Init complete, isReady = true, duration:", totalDuration)
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
} else {
console.warn("[useCanvasPlayer] Init was cancelled before completion")
}
}
@@ -1,55 +1,37 @@
/**
* 素材片段调度器 Hook(多 video 元素方案 v2
* 每个片段对应一个独立 <video> 元素,全部预加载,通过 display 切换实现无缝播放
* 替代单 video + 切 src 方案,消除片段切换延迟
* 素材片段调度器 Hook(多 video 元素方案 v3
*
* v3 修复:
* - 所有动态状态存入 ref,tick 为稳定函数,彻底消除 RAF 闭包陷阱
* - 片段切换时先启动下一个 video 再切可见性,消除冻屏间隔
* - 进度更新 200ms 节流
*/
import { useState, useRef, useCallback, useEffect, useMemo } from "react"
/** 单个播放片段 */
export interface PlaybackSegment {
/** 素材 ID */
assetId: string
/** 素材视频 URL */
videoUrl: string
/** 片段在素材中的入点(秒) */
startTime: number
/** 片段在素材中的出点(秒) */
endTime: number
/** 片段在时间线中的顺序 */
order: number
}
/** 调度器返回 */
export interface SegmentSchedulerState {
/** 是否正在播放 */
isPlaying: boolean
/** 当前播放的全局时间(秒) */
currentTime: number
/** 总时长(秒) */
totalDuration: number
/** 当前片段索引 */
currentSegmentIndex: number
/** 当前片段的本地播放时间 */
segmentLocalTime: number
/** 是否已播完 */
isEnded: boolean
/** 是否可以播放(至少有 1 个片段) */
canPlay: boolean
/** 播放 */
play: () => void
/** 暂停 */
pause: () => void
/** 切换播放/暂停 */
togglePlayPause: () => void
/** 跳转到全局时间 */
seekTo: (time: number) => void
/** 每个片段对应的 video 元素 ref 数组 */
videoRefs: React.MutableRefObject<(HTMLVideoElement | null)[]>
}
/**
* 根据全局时间定位对应的片段和本地时间
*/
function findSegmentAtTime(
segments: PlaybackSegment[],
globalTime: number,
@@ -66,9 +48,6 @@ function findSegmentAtTime(
return { index: segments.length - 1, localTime: segments[segments.length - 1].endTime }
}
/**
* 计算每个片段的全局起始时间
*/
function buildTimeline(segments: PlaybackSegment[]): number[] {
const starts: number[] = []
let acc = 0
@@ -79,224 +58,242 @@ function buildTimeline(segments: PlaybackSegment[]): number[] {
return starts
}
/**
* useSegmentScheduler — 多 video 元素版素材片段调度器
*
* 核心改变:
* - 每个片段对应一个独立 <video> 元素(由组件渲染,ref 传入)
* - 所有 video 在挂载时即设置 src + preload="auto",浏览器自动预加载
* - 切换片段仅改 currentSegmentIndex + display,无需重新 load
* - 实现无缝切换,无加载延迟
*/
export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedulerState {
/** 每个片段对应的 video 元素 ref(由组件 JSX 渲染并绑定) */
const videoRefs = useRef<(HTMLVideoElement | null)[]>([])
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0)
const [isEnded, setIsEnded] = useState(false)
const rafRef = useRef<number>(0)
const rafRef = useRef(0)
const isSeekingRef = useRef(false)
const lastTimeUpdateRef = useRef(0)
// 计算时间线
const timelineStarts = useMemo(() => buildTimeline(segments), [segments])
const totalDuration = useMemo(
// 所有动态值存入 ref,tick 始终读取最新值,不依赖闭包
const segIdxRef = useRef(0)
const segmentsRef = useRef(segments)
const timelineStartsData = useMemo(() => buildTimeline(segments), [segments])
const totalDurationData = useMemo(
() => segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0),
[segments],
)
const timelineStartsRef = useRef(timelineStartsData)
const totalDurationRef = useRef(totalDurationData)
const isPlayingRef = useRef(false)
segmentsRef.current = segments
timelineStartsRef.current = timelineStartsData
totalDurationRef.current = totalDurationData
const canPlay = segments.length > 0
// 当前片段信息
const currentSegment = segments[currentSegmentIndex] || null
const segmentLocalTime = currentSegment
? currentTime - (timelineStarts[currentSegmentIndex] || 0) + currentSegment.startTime
: 0
useEffect(() => {
segIdxRef.current = currentSegmentIndex
}, [currentSegmentIndex])
useEffect(() => {
isPlayingRef.current = isPlaying
}, [isPlaying])
const waitForReady = useCallback((video: HTMLVideoElement, timeout = 3000): Promise<void> => {
if (video.readyState >= 3) return Promise.resolve()
return new Promise((resolve) => {
const onCanPlay = () => {
video.removeEventListener("canplay", onCanPlay)
clearTimeout(timer)
resolve()
}
const timer = setTimeout(() => {
video.removeEventListener("canplay", onCanPlay)
resolve()
}, timeout)
video.addEventListener("canplay", onCanPlay)
})
}, [])
/**
* 切换到指定片段
* 不改变 src(video 已在 JSX 中设置),仅 seek + 等待可播
*/
const switchToSegment = useCallback(
(index: number, seekToLocalTime?: number): Promise<void> => {
return new Promise((resolve) => {
// 暂停当前视频
const prevVideo = videoRefs.current[currentSegmentIndex]
if (prevVideo) prevVideo.pause()
async (index: number, seekToLocalTime?: number) => {
const segs = segmentsRef.current
const video = videoRefs.current[index]
if (!video || index >= segs.length) return
const video = videoRefs.current[index]
if (!video || index >= segments.length) {
resolve()
return
}
const seg = segs[index]
const localTime = seekToLocalTime ?? seg.startTime
const oldIdx = segIdxRef.current
const oldVideo = videoRefs.current[oldIdx]
const seg = segments[index]
const localTime = seekToLocalTime ?? seg.startTime
if (oldVideo && oldVideo !== video) oldVideo.pause()
// 设置播放位置
if (!video.src && seg.videoUrl) {
video.src = seg.videoUrl
video.load()
}
if (Math.abs(video.currentTime - localTime) > 0.05) {
video.currentTime = localTime
}
// 如果已有足够帧数据,直接 resolve
if (video.readyState >= 2) {
setCurrentSegmentIndex(index)
resolve()
return
}
segIdxRef.current = index
setCurrentSegmentIndex(index)
// 等待 canplay 事件
const onCanPlay = () => {
video.removeEventListener("canplay", onCanPlay)
clearTimeout(timeoutId)
setCurrentSegmentIndex(index)
resolve()
}
// 10 秒超时保护
const timeoutId = setTimeout(() => {
video.removeEventListener("canplay", onCanPlay)
console.warn(
`[useSegmentScheduler] 片段 ${index} 预加载超时 (10s), readyState=${video.readyState}`,
)
setCurrentSegmentIndex(index)
resolve()
}, 10000)
video.addEventListener("canplay", onCanPlay)
})
await waitForReady(video)
},
[segments, currentSegmentIndex],
[waitForReady],
)
/** 播放循环 — 检测片段边界并切换 */
// 稳定的 tick 函数,空依赖,所有值从 ref 读取
const tick = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex]
const segs = segmentsRef.current
const idx = segIdxRef.current
const video = videoRefs.current[idx]
if (!video || isSeekingRef.current) {
rafRef.current = requestAnimationFrame(tick)
return
}
const seg = segments[currentSegmentIndex]
const seg = segs[idx]
if (!seg) return
// 检查是否到达出点(容差 0.15s
if (video.currentTime >= seg.endTime - 0.15) {
video.pause()
const nextIndex = currentSegmentIndex + 1
if (nextIndex < segments.length) {
switchToSegment(nextIndex).then(() => {
setIsPlaying(true)
rafRef.current = requestAnimationFrame(tick)
const nextVideo = videoRefs.current[nextIndex]
if (nextVideo) {
const canPlay = () => {
nextVideo
.play()
.catch((e) =>
console.warn("[useSegmentScheduler] auto-play next segment failed:", e),
)
}
if (nextVideo.readyState >= 3) {
canPlay()
} else {
const timeout = setTimeout(canPlay, 300)
nextVideo.addEventListener(
"canplay",
() => {
clearTimeout(timeout)
canPlay()
},
{ once: true },
)
}
// 预加载下一个片段
const nextIndex = idx + 1
if (nextIndex < segs.length) {
const nextVideo = videoRefs.current[nextIndex]
if (nextVideo) {
const timeToEnd = seg.endTime - video.currentTime
if (timeToEnd <= 2 && nextVideo.readyState < 3) {
const nextSeg = segs[nextIndex]
if (Math.abs(nextVideo.currentTime - nextSeg.startTime) > 0.5) {
nextVideo.currentTime = nextSeg.startTime
}
})
}
}
}
// 检测片段边界
if (video.currentTime >= seg.endTime - 0.1) {
if (nextIndex < segs.length) {
const nextVideo = videoRefs.current[nextIndex]
const nextSeg = segs[nextIndex]
const accumulatedTime =
(timelineStarts[currentSegmentIndex] || 0) + (seg.endTime - seg.startTime)
(timelineStartsRef.current[idx] || 0) + (seg.endTime - seg.startTime)
if (nextVideo) {
if (Math.abs(nextVideo.currentTime - nextSeg.startTime) > 0.1) {
nextVideo.currentTime = nextSeg.startTime
}
// 先启动下一个视频(muted,可安全同时播放)
nextVideo
.play()
.catch((e) => console.warn("[useSegmentScheduler] next segment play failed:", e))
}
// 立即切换可见性
segIdxRef.current = nextIndex
setCurrentSegmentIndex(nextIndex)
setCurrentTime(accumulatedTime)
lastTimeUpdateRef.current = 0
setIsPlaying(true)
// 下一帧暂停旧视频(让新视频先渲染,避免冻屏)
const oldVideo = video
requestAnimationFrame(() => {
oldVideo.pause()
})
rafRef.current = requestAnimationFrame(tick)
return
} else {
video.pause()
setIsPlaying(false)
setIsEnded(true)
setCurrentTime(totalDuration)
setCurrentTime(totalDurationRef.current)
return
}
} else {
const globalTime =
(timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime)
setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration)))
}
const globalTime = (timelineStartsRef.current[idx] || 0) + (video.currentTime - seg.startTime)
const now = performance.now()
if (now - lastTimeUpdateRef.current >= 200) {
lastTimeUpdateRef.current = now
setCurrentTime(Math.max(0, Math.min(globalTime, totalDurationRef.current)))
}
rafRef.current = requestAnimationFrame(tick)
}, [segments, currentSegmentIndex, timelineStarts, totalDuration, switchToSegment])
}, [])
/** 播放 */
const play = useCallback(async () => {
if (!canPlay) return
setIsEnded(false)
// 确保第一段可播放
const firstVideo = videoRefs.current[0]
if (firstVideo && currentSegmentIndex === 0 && firstVideo.readyState < 2) {
await switchToSegment(0)
}
const video = videoRefs.current[currentSegmentIndex]
const idx = segIdxRef.current
const video = videoRefs.current[idx]
if (!video) return
try {
const playPromise = video.play()
if (playPromise !== undefined) {
await playPromise
if (idx === 0 && video.readyState < 2) {
if (!video.src && segmentsRef.current[0]?.videoUrl) {
video.src = segmentsRef.current[0].videoUrl
video.load()
}
await waitForReady(video)
}
try {
await video.play()
setIsPlaying(true)
cancelAnimationFrame(rafRef.current)
rafRef.current = requestAnimationFrame(tick)
} catch (err) {
console.warn("[useSegmentScheduler] 播放失败:", err)
}
}, [canPlay, switchToSegment, tick, currentSegmentIndex])
}, [canPlay, waitForReady, tick])
/** 暂停 */
const pause = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex]
const video = videoRefs.current[segIdxRef.current]
if (video) video.pause()
setIsPlaying(false)
cancelAnimationFrame(rafRef.current)
}, [currentSegmentIndex])
}, [])
/** 切换播放/暂停 */
const togglePlayPause = useCallback(() => {
if (isPlaying) {
if (isPlayingRef.current) {
pause()
} else {
if (isEnded) {
// 播放结束后再次播放,从头开始
setIsEnded(false)
switchToSegment(0, segments[0]?.startTime).then(() => {
const video = videoRefs.current[0]
if (video) {
video.play().catch((e) => console.warn("[useSegmentScheduler] restart play failed:", e))
setIsPlaying(true)
setCurrentTime(0)
rafRef.current = requestAnimationFrame(tick)
}
})
lastTimeUpdateRef.current = 0
const firstVideo = videoRefs.current[0]
if (firstVideo) {
videoRefs.current.forEach((v, i) => {
if (v && i !== 0) v.pause()
})
firstVideo.currentTime = segmentsRef.current[0]?.startTime || 0
segIdxRef.current = 0
setCurrentSegmentIndex(0)
setCurrentTime(0)
firstVideo
.play()
.then(() => {
setIsPlaying(true)
cancelAnimationFrame(rafRef.current)
rafRef.current = requestAnimationFrame(tick)
})
.catch((e) => console.warn("[useSegmentScheduler] restart failed:", e))
}
} else {
play()
}
}
}, [isPlaying, isEnded, pause, play, switchToSegment, segments, tick])
}, [isEnded, pause, play, tick])
/** 跳转到指定全局时间 */
const seekTo = useCallback(
async (time: number) => {
if (!canPlay) return
const clampedTime = Math.max(0, Math.min(time, totalDuration))
const { index, localTime } = findSegmentAtTime(segments, clampedTime)
const clampedTime = Math.max(0, Math.min(time, totalDurationRef.current))
const { index, localTime } = findSegmentAtTime(segmentsRef.current, clampedTime)
isSeekingRef.current = true
cancelAnimationFrame(rafRef.current)
if (index !== currentSegmentIndex) {
if (index !== segIdxRef.current) {
await switchToSegment(index, localTime)
} else {
const video = videoRefs.current[index]
@@ -305,48 +302,54 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
setCurrentTime(clampedTime)
setIsEnded(false)
lastTimeUpdateRef.current = 0
if (isPlayingRef.current) {
const video = videoRefs.current[index]
if (video) {
video.play().catch(() => {})
}
rafRef.current = requestAnimationFrame(tick)
}
setTimeout(() => {
isSeekingRef.current = false
}, 200)
},
[canPlay, totalDuration, segments, currentSegmentIndex, switchToSegment],
[canPlay, switchToSegment, tick],
)
// 确保 videoRefs 数组长度与 segments 一致 + 强制预加载
useEffect(() => {
videoRefs.current = videoRefs.current.slice(0, segments.length)
while (videoRefs.current.length < segments.length) {
videoRefs.current.push(null)
}
// 强制预加载:所有 video 元素挂载后,调用 load() 确保浏览器真正开始加载数据
videoRefs.current.forEach((video) => {
if (video) {
video.load()
}
})
}, [segments])
// 组件卸载时清理
useEffect(() => {
return () => {
cancelAnimationFrame(rafRef.current)
}
}, [])
// 片段列表变化时重置
useEffect(() => {
cancelAnimationFrame(rafRef.current)
segIdxRef.current = 0
setIsPlaying(false)
setCurrentTime(0)
setCurrentSegmentIndex(0)
setIsEnded(false)
}, [segments])
const currentSegment = segments[currentSegmentIndex] || null
const segmentLocalTime = currentSegment
? currentTime - (timelineStartsRef.current[currentSegmentIndex] || 0) + currentSegment.startTime
: 0
return {
isPlaying,
currentTime,
totalDuration,
totalDuration: totalDurationData,
currentSegmentIndex,
segmentLocalTime,
isEnded,
+294 -1
View File
@@ -182,6 +182,7 @@ class TestUnifiedCoverPipelineEndpoint:
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch("packages.shared.storage.get_shared_storage_service") as mock_storage_getter,
patch("packages.shared.mediakit_client.get_mediakit_client") as mock_mk_getter,
):
mock_repo = MagicMock()
mock_repo.get.return_value = mock_task
@@ -194,6 +195,13 @@ class TestUnifiedCoverPipelineEndpoint:
mock_storage_svc.get_url.return_value = "https://oss.example.com/rendered/plan-2/video.mp4"
mock_storage_getter.return_value = mock_storage_svc
# MediaKit 抽帧也返回 None,模拟最终失败
mock_mk = MagicMock()
mock_mk.is_available = True
mock_mk.extract_frames.return_value = None
mock_mk_getter.return_value = mock_mk
# body 不传 asset_ids,步骤 E2 不会进入
from app.api.routes.generation_cover import generate_cover
with pytest.raises(HTTPException) as exc_info:
@@ -207,7 +215,6 @@ class TestUnifiedCoverPipelineEndpoint:
)
assert exc_info.value.status_code == 400
assert "封面尚未生成" in exc_info.value.detail
def test_cover_url_found_via_source_edit_plan(self):
"""步骤B:通过 source_edit_plan_id 找到预览任务的 cover_url。"""
@@ -318,6 +325,157 @@ class TestUnifiedCoverPipelineEndpoint:
template_id="template-y",
)
def test_cover_url_found_via_cover_candidates_image_url(self):
"""步骤Dplan.config.cover_candidates 有 image_url 时,直接使用第一个候选封面。"""
from unittest.mock import MagicMock, patch
from app.api.routes.generation_cover import GenerateCoverRequest
mock_plan = MagicMock()
# 步骤A/B/C 都找不到,进入步骤D
mock_plan.config = {
"rendered_storage_key": "rendered/plan-z/video.mp4", # 必须有预览视频才能通过前置检查
"cover_candidates": [
{"image_url": "https://oss.example.com/candidates/cover-1.jpg", "score": 0.95},
{"image_url": "https://oss.example.com/candidates/cover-2.jpg", "score": 0.80},
],
}
mock_plan_svc = MagicMock()
mock_plan_svc.get_plan_or_raise.return_value = mock_plan
mock_template_svc = MagicMock()
mock_db = MagicMock()
body = GenerateCoverRequest(cover_type="ai_frame")
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch("app.api.routes.generation_cover.normalize_plan_config") as mock_normalize,
):
mock_repo = MagicMock()
mock_repo.get.return_value = None
mock_repo.list_by_source_edit_plan.return_value = []
mock_repo.list_latest_completed_preview.return_value = []
mock_repo_cls.return_value = mock_repo
mock_normalize.return_value = {
"cover": {"type": "ai_frame", "image_url": "https://oss.example.com/candidates/cover-1.jpg"}
}
from app.api.routes.generation_cover import generate_cover
result = generate_cover(
body=body,
template_id="template-z",
plan_id="plan-z",
services=(mock_template_svc, mock_plan_svc),
db=mock_db,
current_user=MagicMock(),
)
# 步骤D从 cover_candidates 第一个元素的 image_url 提取封面
assert result.cover["image_url"] == "https://oss.example.com/candidates/cover-1.jpg"
# 验证 plan.config 被更新(至少调用一次:rendered_storage_key + cover
assert mock_plan_svc.update_plan_config.call_count >= 1
def test_cover_url_found_via_cover_candidates_url_key(self):
"""步骤Dcover_candidates 用 url 键(非 image_url)时,也能正确提取。"""
from unittest.mock import MagicMock, patch
from app.api.routes.generation_cover import GenerateCoverRequest
mock_plan = MagicMock()
mock_plan.config = {
"rendered_storage_key": "rendered/plan-w/video.mp4", # 必须有预览视频才能通过前置检查
"cover_candidates": [
{"url": "https://oss.example.com/candidates/alt-cover.jpg"},
],
}
mock_plan_svc = MagicMock()
mock_plan_svc.get_plan_or_raise.return_value = mock_plan
mock_template_svc = MagicMock()
mock_db = MagicMock()
body = GenerateCoverRequest(cover_type="ai_frame")
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch("app.api.routes.generation_cover.normalize_plan_config") as mock_normalize,
):
mock_repo = MagicMock()
mock_repo.get.return_value = None
mock_repo.list_by_source_edit_plan.return_value = []
mock_repo.list_latest_completed_preview.return_value = []
mock_repo_cls.return_value = mock_repo
mock_normalize.return_value = {
"cover": {"type": "ai_frame", "image_url": "https://oss.example.com/candidates/alt-cover.jpg"}
}
from app.api.routes.generation_cover import generate_cover
result = generate_cover(
body=body,
template_id="template-w",
plan_id="plan-w",
services=(mock_template_svc, mock_plan_svc),
db=mock_db,
current_user=MagicMock(),
)
# 步骤D fallback 到 url 键
assert result.cover["image_url"] == "https://oss.example.com/candidates/alt-cover.jpg"
def test_cover_candidates_skips_non_dict_first_element(self):
"""步骤Dcover_candidates 第一个元素不是 dict 时,安全跳过不崩溃。"""
from unittest.mock import MagicMock, patch
from app.api.routes.generation_cover import GenerateCoverRequest
from fastapi import HTTPException
mock_plan = MagicMock()
mock_plan.config = {
"rendered_storage_key": "rendered/plan-skip/video.mp4", # 必须有预览视频才能通过前置检查
"cover_candidates": ["not-a-dict", 42, None],
}
mock_plan_svc = MagicMock()
mock_plan_svc.get_plan_or_raise.return_value = mock_plan
mock_template_svc = MagicMock()
mock_db = MagicMock()
body = GenerateCoverRequest(cover_type="ai_frame")
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch("packages.shared.storage.get_shared_storage_service") as mock_storage_getter,
):
mock_repo = MagicMock()
mock_repo.get.return_value = None
mock_repo.list_by_source_edit_plan.return_value = []
mock_repo.list_latest_completed_preview.return_value = []
mock_repo_cls.return_value = mock_repo
# storage fallback 也找不到封面
mock_storage_svc = MagicMock()
mock_storage_svc.get_url.return_value = ""
mock_storage_getter.return_value = mock_storage_svc
from app.api.routes.generation_cover import generate_cover
# 所有步骤都失败,应返回 400
with pytest.raises(HTTPException) as exc_info:
generate_cover(
body=body,
template_id="template-skip",
plan_id="plan-skip",
services=(mock_template_svc, mock_plan_svc),
db=mock_db,
current_user=MagicMock(),
)
assert exc_info.value.status_code == 400
class TestSourceEditPlanFallback:
"""测试步骤 2.5:通过 source_edit_plan_id 查找预览视频兜底逻辑。"""
@@ -687,3 +845,138 @@ class TestUploadCoverType:
assert result.cover["image_url"] == "https://oss.example.com/uploaded/my-cover.png"
# 验证没有调用任何预览视频查找逻辑
# (normalize_plan_config 是唯一被调用的外部函数)
def test_cover_extracted_from_source_asset_when_no_preview(self):
"""步骤E2:无后端渲染产物时,直接从用户选择的视频素材抽帧。"""
from unittest.mock import MagicMock, patch
from app.api.routes.generation_cover import GenerateCoverRequest
mock_plan = MagicMock()
mock_plan.config = {} # 无 rendered_storage_key,无 generation_task_id
mock_plan_svc = MagicMock()
mock_plan_svc.get_plan_or_raise.return_value = mock_plan
mock_template_svc = MagicMock()
mock_db = MagicMock()
# 模拟视频素材
mock_asset = MagicMock()
mock_asset.file_type = "video"
mock_asset.storage_key = "uploads/source-clip.mp4"
mock_asset_repo = MagicMock()
mock_asset_repo.get.return_value = mock_asset
mock_mk = MagicMock()
mock_mk.is_available = True
mock_mk.extract_frames.return_value = [{"image_url": "https://mediakit.internal/frame-abc.jpg"}]
mock_storage = MagicMock()
mock_storage.get_url.return_value = "https://oss.example.com/uploads/source-clip.mp4"
body = GenerateCoverRequest(
cover_type="ai_frame",
asset_ids=["asset-video-1"],
)
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch(
"packages.adapters.sqlalchemy_impl.asset_repository.SQLAlchemyAssetRepository",
return_value=mock_asset_repo,
),
patch("packages.shared.mediakit_client.get_mediakit_client", return_value=mock_mk),
patch("packages.shared.storage.get_shared_storage_service", return_value=mock_storage),
patch(
"app.api.routes.generation_cover._persist_cover_frame",
return_value="https://oss.example.com/covers/final.jpg",
),
patch("app.api.routes.generation_cover.normalize_plan_config") as mock_normalize,
):
mock_repo = MagicMock()
mock_repo.get.return_value = None
mock_repo.list_by_source_edit_plan.return_value = []
mock_repo.list_latest_completed_preview.return_value = []
mock_repo_cls.return_value = mock_repo
mock_normalize.return_value = {
"cover": {"type": "ai_frame", "image_url": "https://oss.example.com/covers/final.jpg"}
}
from app.api.routes.generation_cover import generate_cover
result = generate_cover(
body=body,
template_id="tpl-source",
plan_id="plan-source",
services=(mock_template_svc, mock_plan_svc),
db=mock_db,
current_user=MagicMock(),
)
assert result.cover["image_url"] == "https://oss.example.com/covers/final.jpg"
mock_mk.extract_frames.assert_called_once()
# 确保用的是源素材 URL
call_kwargs = mock_mk.extract_frames.call_args.kwargs
assert "source-clip.mp4" in call_kwargs["video_url"]
def test_step_e_skips_non_video_assets(self):
"""步骤E2:asset_ids 里只有图片素材时,不调用 MediaKit 并返回 400。"""
from unittest.mock import MagicMock, patch
from app.api.routes.generation_cover import GenerateCoverRequest
from fastapi import HTTPException
mock_plan = MagicMock()
mock_plan.config = {}
mock_plan_svc = MagicMock()
mock_plan_svc.get_plan_or_raise.return_value = mock_plan
mock_template_svc = MagicMock()
mock_db = MagicMock()
mock_image_asset = MagicMock()
mock_image_asset.file_type = "image"
mock_image_asset.storage_key = "uploads/photo.png"
mock_asset_repo = MagicMock()
mock_asset_repo.get.return_value = mock_image_asset
mock_mk = MagicMock()
mock_mk.is_available = True
body = GenerateCoverRequest(
cover_type="ai_frame",
asset_ids=["asset-img-1"],
)
with (
patch("app.api.routes.generation_cover.SQLAlchemyGenerationTaskRepository") as mock_repo_cls,
patch(
"packages.adapters.sqlalchemy_impl.asset_repository.SQLAlchemyAssetRepository",
return_value=mock_asset_repo,
),
patch("packages.shared.mediakit_client.get_mediakit_client", return_value=mock_mk),
patch("packages.shared.storage.get_shared_storage_service") as mock_storage_getter,
):
mock_repo = MagicMock()
mock_repo.get.return_value = None
mock_repo.list_by_source_edit_plan.return_value = []
mock_repo.list_latest_completed_preview.return_value = []
mock_repo_cls.return_value = mock_repo
mock_storage_getter.return_value = MagicMock()
from app.api.routes.generation_cover import generate_cover
with pytest.raises(HTTPException) as exc_info:
generate_cover(
body=body,
template_id="tpl-img",
plan_id="plan-img",
services=(mock_template_svc, mock_plan_svc),
db=mock_db,
current_user=MagicMock(),
)
assert exc_info.value.status_code == 400
mock_mk.extract_frames.assert_not_called()
+24 -5
View File
@@ -13,6 +13,18 @@ os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
def _patch_session_local(mock_session):
"""Patch worker_app.db.SessionLocal robustly even when other tests
have pre-registered a MagicMock for worker_app.db in sys.modules.
Uses patch.dict to inject a clean module so that
'from worker_app.db import SessionLocal' resolves correctly."""
from types import ModuleType
_fresh_db = ModuleType("worker_app.db")
_fresh_db.SessionLocal = lambda *a, **kw: mock_session
return patch.dict(sys.modules, {"worker_app.db": _fresh_db})
class TestLoadTemplateSegmentDurations:
"""_load_template_segment_durations 单元测试 (covers lines 198-226)."""
@@ -40,8 +52,7 @@ class TestLoadTemplateSegmentDurations:
mock_session = MagicMock()
mock_session.query.return_value = mock_query
# Patch at the source module since it's imported inside the function
with patch("worker_app.db.SessionLocal", return_value=mock_session):
with _patch_session_local(mock_session):
result = _load_template_segment_durations("tpl_123")
assert result == [5.0, 8.0, 3.0]
@@ -63,16 +74,24 @@ class TestLoadTemplateSegmentDurations:
mock_session = MagicMock()
mock_session.query.return_value = mock_query
with patch("worker_app.db.SessionLocal", return_value=mock_session):
with _patch_session_local(mock_session):
result = _load_template_segment_durations("tpl_456")
assert result == [5.0]
def test_db_error_returns_empty(self):
"""数据库异常返回空列表,不抛出。"""
from types import ModuleType
from worker_app.tasks.generation import _load_template_segment_durations
with patch("worker_app.db.SessionLocal", side_effect=Exception("DB down")):
_err_db = ModuleType("worker_app.db")
def _raise(*a, **kw):
raise Exception("DB down")
_err_db.SessionLocal = _raise
with patch.dict(sys.modules, {"worker_app.db": _err_db}):
result = _load_template_segment_durations("tpl_789")
assert result == []
@@ -86,7 +105,7 @@ class TestLoadTemplateSegmentDurations:
mock_session = MagicMock()
mock_session.query.return_value = mock_query
with patch("worker_app.db.SessionLocal", return_value=mock_session):
with _patch_session_local(mock_session):
result = _load_template_segment_durations("tpl_empty")
assert result == []
+78 -92
View File
@@ -1,7 +1,6 @@
"""Unit tests for apps/api/app/api/routes/health.py
覆盖 _check_database() 和 _check_migrations() 中 psycopg3 连接逻辑。
确保增量覆盖率 ≥ 60%(目标覆盖 lines 52, 127)。
"""
from unittest.mock import AsyncMock, MagicMock, patch
@@ -9,63 +8,70 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
def _make_cursor(fetchone_result=None):
"""Create a mock cursor with context manager support."""
cur = MagicMock()
cur.__enter__ = MagicMock(return_value=cur)
cur.__exit__ = MagicMock(return_value=False)
if fetchone_result is not None:
cur.fetchone.return_value = fetchone_result
return cur
def _make_conn(cursor_result=None):
conn = MagicMock()
conn.cursor.return_value = cursor_result or _make_cursor()
return conn
@pytest.mark.asyncio
class TestCheckDatabase:
"""Tests for _check_database() health check function."""
@patch("apps.api.app.api.routes.health.settings")
@patch("apps.api.app.api.routes.health.psycopg.connect")
async def test_check_database_success(self, mock_connect, mock_settings):
"""PostgreSQL 连接成功时返回 healthy。"""
async def test_check_database_success(self):
mock_cur = _make_cursor(fetchone_result=(1,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
# Mock connection and cursor
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
mock_cursor.__exit__ = MagicMock(return_value=False)
mock_cursor.fetchone.return_value = (1,)
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value = mock_conn
from apps.api.app.api.routes import health
from apps.api.app.api.routes.health import _check_database
result = await _check_database()
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_database()
assert result["status"] == "healthy"
assert result["type"] == "postgresql"
assert result["message"] == "Database connection successful"
mock_connect.assert_called_once_with(
mock_psycopg.connect.assert_called_once_with(
"postgresql+psycopg://test:test@localhost/test", connect_timeout=3
)
mock_cursor.execute.assert_called_once_with("SELECT 1")
mock_cur.execute.assert_called_once_with("SELECT 1")
mock_conn.close.assert_called_once()
@patch("apps.api.app.api.routes.health.settings")
@patch("apps.api.app.api.routes.health.psycopg.connect")
async def test_check_database_connection_failure(self, mock_connect, mock_settings):
"""PostgreSQL 连接失败时返回 unhealthy。"""
async def test_check_database_connection_failure(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
mock_connect.side_effect = Exception("connection refused")
from apps.api.app.api.routes.health import _check_database
from apps.api.app.api.routes import health
result = await _check_database()
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.side_effect = Exception("connection refused")
result = await health._check_database()
assert result["status"] == "unhealthy"
assert result["type"] == "postgresql"
assert "connection refused" in result["message"]
@patch("apps.api.app.api.routes.health.settings")
async def test_check_database_in_memory(self, mock_settings):
"""使用内存数据库时跳过 PostgreSQL 检查。"""
async def test_check_database_in_memory(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = True
from apps.api.app.api.routes.health import _check_database
from apps.api.app.api.routes import health
result = await _check_database()
with patch.object(health, "settings", mock_settings):
result = await health._check_database()
assert result["status"] == "healthy"
assert result["type"] == "in_memory"
@@ -73,80 +79,66 @@ class TestCheckDatabase:
@pytest.mark.asyncio
class TestCheckMigrations:
"""Tests for _check_migrations() health check function."""
@patch("apps.api.app.api.routes.health.settings")
@patch("apps.api.app.api.routes.health.psycopg.connect")
async def test_check_migrations_success(self, mock_connect, mock_settings):
"""所有迁移表存在时返回 healthy。"""
async def test_check_migrations_success(self):
mock_cur = _make_cursor(fetchone_result=(5,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
mock_cursor.__exit__ = MagicMock(return_value=False)
mock_cursor.fetchone.return_value = (5,) # 5 tables found
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value = mock_conn
from apps.api.app.api.routes import health
from apps.api.app.api.routes.health import _check_migrations
result = await _check_migrations()
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_migrations()
assert result["status"] == "healthy"
assert result["message"] == "Database migrations applied"
mock_connect.assert_called_once_with(
mock_psycopg.connect.assert_called_once_with(
"postgresql+psycopg://test:test@localhost/test", connect_timeout=3
)
mock_conn.close.assert_called_once()
@patch("apps.api.app.api.routes.health.settings")
@patch("apps.api.app.api.routes.health.psycopg.connect")
async def test_check_migrations_missing_tables(self, mock_connect, mock_settings):
"""迁移表不完整时返回 unhealthy。"""
async def test_check_migrations_missing_tables(self):
mock_cur = _make_cursor(fetchone_result=(2,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
mock_cursor.__exit__ = MagicMock(return_value=False)
mock_cursor.fetchone.return_value = (2,) # Only 2 of 5 tables
mock_conn.cursor.return_value = mock_cursor
mock_connect.return_value = mock_conn
from apps.api.app.api.routes import health
from apps.api.app.api.routes.health import _check_migrations
result = await _check_migrations()
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_migrations()
assert result["status"] == "unhealthy"
assert "Missing tables" in result["message"]
assert "2/5" in result["message"]
@patch("apps.api.app.api.routes.health.settings")
@patch("apps.api.app.api.routes.health.psycopg.connect")
async def test_check_migrations_connection_failure(self, mock_connect, mock_settings):
"""数据库连接失败时返回 unhealthy。"""
async def test_check_migrations_connection_failure(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
mock_connect.side_effect = Exception("connection refused")
from apps.api.app.api.routes.health import _check_migrations
from apps.api.app.api.routes import health
result = await _check_migrations()
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.side_effect = Exception("connection refused")
result = await health._check_migrations()
assert result["status"] == "unhealthy"
assert "Migration check failed" in result["message"]
@patch("apps.api.app.api.routes.health.settings")
async def test_check_migrations_in_memory(self, mock_settings):
"""使用内存数据库时跳过迁移检查。"""
async def test_check_migrations_in_memory(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = True
from apps.api.app.api.routes.health import _check_migrations
from apps.api.app.api.routes import health
result = await _check_migrations()
with patch.object(health, "settings", mock_settings):
result = await health._check_migrations()
assert result["status"] == "healthy"
assert "no migrations needed" in result["message"]
@@ -154,33 +146,27 @@ class TestCheckMigrations:
@pytest.mark.asyncio
class TestStartupCheck:
"""Tests for startup_check() endpoint."""
@patch("apps.api.app.api.routes.health._check_migrations")
@patch("apps.api.app.api.routes.health._check_database")
async def test_startup_all_healthy(self, mock_db, mock_mig):
"""所有检查通过时返回 started。"""
mock_db.return_value = {"status": "healthy"}
mock_mig.return_value = {"status": "healthy"}
async def test_startup_all_healthy(self):
from apps.api.app.api.routes import health
from apps.api.app.api.routes.health import startup_check
result = await startup_check()
with patch.object(health, "_check_migrations", new_callable=AsyncMock) as mock_mig, patch.object(health, "_check_database", new_callable=AsyncMock) as mock_db:
mock_db.return_value = {"status": "healthy"}
mock_mig.return_value = {"status": "healthy"}
result = await health.startup_check()
assert result["status"] == "started"
@patch("apps.api.app.api.routes.health._check_migrations")
@patch("apps.api.app.api.routes.health._check_database")
async def test_startup_db_unhealthy(self, mock_db, mock_mig):
"""数据库不健康时返回 starting + 503。"""
mock_db.return_value = {"status": "unhealthy", "message": "fail"}
mock_mig.return_value = {"status": "healthy"}
async def test_startup_db_unhealthy(self):
import json
from apps.api.app.api.routes.health import startup_check
from apps.api.app.api.routes import health
result = await startup_check()
with patch.object(health, "_check_migrations", new_callable=AsyncMock) as mock_mig, patch.object(health, "_check_database", new_callable=AsyncMock) as mock_db:
mock_db.return_value = {"status": "unhealthy", "message": "fail"}
mock_mig.return_value = {"status": "healthy"}
result = await health.startup_check()
assert result.status_code == 503
import json
body = json.loads(result.body)
assert body["status"] == "starting"