From f4f425de424def0899c6a04ad82bec835904982b Mon Sep 17 00:00:00 2001 From: cryptocommuniums-afk Date: Sun, 15 Mar 2026 02:16:32 +0800 Subject: [PATCH] Show upload size during media finalization --- client/src/pages/Recorder.tsx | 83 +++++++++++++++++++++++++++++------ docs/FEATURES.md | 1 + docs/verified-features.md | 5 ++- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/client/src/pages/Recorder.tsx b/client/src/pages/Recorder.tsx index c96585f..d9a401e 100644 --- a/client/src/pages/Recorder.tsx +++ b/client/src/pages/Recorder.tsx @@ -123,6 +123,25 @@ function getArchiveProgress(session: MediaSession | null) { } } +function getArchivePhaseLabel(mode: RecorderMode, session: MediaSession | null, taskProgress?: number | null) { + if (mode === "finalizing" && !taskProgress) { + return "正在提交归档任务"; + } + if (session?.archiveStatus === "completed") { + return "回放已生成"; + } + if (session?.archiveStatus === "failed") { + return "归档失败"; + } + if (session?.archiveStatus === "processing") { + return "正在生成回放"; + } + if (session?.archiveStatus === "queued") { + return "正在合并分段"; + } + return "等待归档"; +} + function formatFileSize(bytes: number) { if (!bytes) return "0 MB"; return `${(bytes / 1024 / 1024).toFixed(bytes > 20 * 1024 * 1024 ? 1 : 2)} MB`; @@ -174,6 +193,7 @@ export default function Recorder() { const [isOnline, setIsOnline] = useState(() => navigator.onLine); const [reconnectAttempts, setReconnectAttempts] = useState(0); const [queuedSegments, setQueuedSegments] = useState(0); + const [queuedBytes, setQueuedBytes] = useState(0); const [uploadedSegments, setUploadedSegments] = useState(0); const [uploadBytes, setUploadBytes] = useState(0); const [cameraError, setCameraError] = useState(""); @@ -187,8 +207,10 @@ export default function Recorder() { const mobile = useMemo(() => isMobileDevice(), []); const mimeType = useMemo(() => pickRecorderMimeType(), []); const currentPlaybackUrl = mediaSession?.playback.mp4Url || mediaSession?.playback.webmUrl || ""; - const archiveProgress = getArchiveProgress(mediaSession); const archiveTaskQuery = useBackgroundTask(archiveTaskId); + const archiveProgress = archiveTaskQuery.data?.progress ?? getArchiveProgress(mediaSession); + const archivePhaseLabel = getArchivePhaseLabel(mode, mediaSession, archiveTaskQuery.data?.progress); + const totalUploadBytes = uploadBytes + queuedBytes; const syncSessionState = useCallback((session: MediaSession | null) => { currentSessionRef.current = session; @@ -198,6 +220,11 @@ export default function Recorder() { setUploadBytes(session?.uploadedBytes ?? 0); }, []); + const syncQueuedUploadState = useCallback(() => { + setQueuedSegments(pendingUploadsRef.current.length); + setQueuedBytes(pendingUploadsRef.current.reduce((total, item) => total + item.blob.size, 0)); + }, []); + useEffect(() => { modeRef.current = mode; }, [mode]); @@ -328,7 +355,7 @@ export default function Recorder() { const processUploadQueue = useCallback(async () => { if (uploadInFlightRef.current || pendingUploadsRef.current.length === 0 || !currentSessionRef.current?.id || !navigator.onLine) { - setQueuedSegments(pendingUploadsRef.current.length); + syncQueuedUploadState(); return; } @@ -343,7 +370,7 @@ export default function Recorder() { nextSegment.blob ); pendingUploadsRef.current.shift(); - setQueuedSegments(pendingUploadsRef.current.length); + syncQueuedUploadState(); syncSessionState(response.session); } } catch (error: any) { @@ -351,7 +378,7 @@ export default function Recorder() { } finally { uploadInFlightRef.current = false; } - }, [syncSessionState]); + }, [syncQueuedUploadState, syncSessionState]); const enqueueSegment = useCallback(async (blob: Blob, durationForSegmentMs: number) => { if (!blob.size) return; @@ -360,9 +387,9 @@ export default function Recorder() { durationMs: Math.max(1, durationForSegmentMs), blob, }); - setQueuedSegments(pendingUploadsRef.current.length); + syncQueuedUploadState(); await processUploadQueue(); - }, [processUploadQueue]); + }, [processUploadQueue, syncQueuedUploadState]); const flushPendingSegments = useCallback(async () => { while (pendingUploadsRef.current.length > 0 || uploadInFlightRef.current) { @@ -588,6 +615,7 @@ export default function Recorder() { setUploadedSegments(0); setUploadBytes(0); setQueuedSegments(0); + setQueuedBytes(0); setReconnectAttempts(0); setArchiveTaskId(null); segmentSequenceRef.current = 0; @@ -664,6 +692,7 @@ export default function Recorder() { setMarkers([]); setDurationMs(0); setQueuedSegments(0); + setQueuedBytes(0); setUploadedSegments(0); setUploadBytes(0); setReconnectAttempts(0); @@ -976,8 +1005,8 @@ export default function Recorder() {
{uploadedSegments}
-
缓存队列
-
{queuedSegments}
+
已传体积
+
{formatFileSize(uploadBytes)}
@@ -989,6 +1018,8 @@ export default function Recorder() { 后台归档处理中 {archiveTaskQuery.data?.message || "录制文件正在后台整理、转码并登记到视频库。"} + 当前已上传 {formatFileSize(uploadBytes)} + {queuedBytes > 0 ? `,待上传 ${formatFileSize(queuedBytes)}` : ""}。 你可以离开当前页面,完成后任务中心会提示结果。 @@ -1144,12 +1175,20 @@ export default function Recorder() {
- 上传总量 + 已上传文件 {formatFileSize(uploadBytes)}
- 缓存分段 - {queuedSegments} + 待上传缓存 + {queuedSegments} 段 · {formatFileSize(queuedBytes)} +
+
+ 录制源文件累计 + {formatFileSize(totalUploadBytes)} +
+
+ 总片段数 + {uploadedSegments + queuedSegments}
服务端状态 @@ -1160,16 +1199,32 @@ export default function Recorder() { {(mode === "finalizing" || mode === "archived" || mediaSession?.archiveStatus === "failed") && (
- 归档进度 + {archivePhaseLabel} {archiveProgress}%
+
+
+
已上传
+
{formatFileSize(uploadBytes)}
+
+
+
待上传
+
{formatFileSize(queuedBytes)}
+
+
+
片段总数
+
{uploadedSegments + queuedSegments} 段
+
+

- {mediaSession?.archiveStatus === "completed" + {archiveTaskQuery.data?.message + ? `${archiveTaskQuery.data.message},当前已上传 ${formatFileSize(uploadBytes)}。` + : mediaSession?.archiveStatus === "completed" ? "归档完成,已生成可回放文件并同步到视频库。" : mediaSession?.archiveStatus === "failed" ? mediaSession.lastError || "归档失败,请检查媒体服务日志。" - : "Worker 正在合并分段并生成归档文件。"} + : `Worker 正在合并分段并生成归档文件,当前已上传 ${formatFileSize(uploadBytes)}。`}

)} diff --git a/docs/FEATURES.md b/docs/FEATURES.md index a67bfbd..99b3132 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -39,6 +39,7 @@ - 手动标记:录制中支持手动插入剪辑点 - 自动重连:摄像头 track 断开时自动尝试恢复 - 归档回放:worker 合并片段并生成 WebM,FFmpeg 可用时额外生成 MP4 +- 归档状态可视化:录制页在“合并分段 / 生成回放”阶段显示任务进度、已上传体积、待上传体积和片段总数 - 视频库登记:归档完成后由 app worker 自动写回现有视频库 - 上传稳定性:媒体分段上传遇到 `502/503/504` 会自动重试 diff --git a/docs/verified-features.md b/docs/verified-features.md index 64e42c5..81fc8b3 100644 --- a/docs/verified-features.md +++ b/docs/verified-features.md @@ -1,11 +1,11 @@ # Verified Features -本文档记录当前已经通过自动化验证或构建验证的项目。更新时间:2026-03-15 02:09 CST。 +本文档记录当前已经通过自动化验证或构建验证的项目。更新时间:2026-03-15 02:15 CST。 ## 最新完整验证记录 - 通过命令:`pnpm verify` -- 验证时间:2026-03-15 02:09 - 02:10 CST +- 验证时间:2026-03-15 02:15 CST - 结果摘要:`pnpm check` 通过,`pnpm test` 通过(95/95),`pnpm test:go` 通过,`pnpm build` 通过,`pnpm test:e2e` 通过(7/7) - 数据库状态:已执行 `set -a && source .env && set +a && pnpm exec drizzle-kit migrate`,`0007_grounded_live_ops` 已成功应用 @@ -82,6 +82,7 @@ | 实时分析 | 摄像头启动入口渲染 | 通过 | | 实时分析打分 | 启动分析后出现实时评分结果 | 通过 | | 在线录制 | 启动摄像头、开始录制、手动标记、结束归档 | 通过 | +| 在线录制归档进度展示 | 录制页显示归档进度、已上传体积、待上传体积与片段总数 | 通过 | | 录制焦点视图 | 移动端最大化焦点视图与主操作按钮渲染 | 通过 | | 录制结果入库 | 归档完成后视频库可见录制结果 | 通过 |