From c082a92dcf9436c767e0d9001436f0fa88d81880 Mon Sep 17 00:00:00 2001 From: yibo Date: Thu, 20 Aug 2026 19:56:25 +0800 Subject: [PATCH] h3-console: 下载视频加 Content-Disposition,文件名不再清一色 file.mp4 --- CHANGELOG.md | 19 +++++++++++++++++++ h3-console/server.py | 10 +++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a451eb1..4896610 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # 变更记录 +## 2026-08-20(夜,又续六)— 下载文件名统一叫 file.mp4 的问题 + +**背景**:用户发现下载多个任务的视频,文件名都叫 `file.mp4`——存到同一个下载目录会互相 +覆盖。查了 `/v1/videos/{jid}/file` 这个下载接口:两条返回路径(正常经 api.py 转发的 +`StreamingResponse`,和后端忘了任务时读磁盘兜底的 `FileResponse`)都没设 +`Content-Disposition` 头,浏览器只能从 URL 最后一段猜文件名——URL 是 `.../file`(没有 +扩展名),配合 `Content-Type: video/mp4` 猜出来就是清一色 `file.mp4`,不是产物本身有问题, +纯粹是没告诉浏览器该存成什么名字。 + +**结果**:两条路径都加上 `Content-Disposition: attachment; filename="h3-{任务id}.mp4"`—— +用 console 自己的任务 id(12 位十六进制,任务间互不相同)拼文件名,不需要额外维护一份 +命名方案。改的是 `server.py`(有状态的后端逻辑,需要重启),重启前确认 `/v1/backends` +四张卡都空闲(`queued=0,running=0`)不会打断任务,8080(局域网+调度器)和 8081 +(`/console` 子路径)两个实例都重启验证过:`curl -sI` 直接 GET 一条已完成任务的文件, +返回头里 `content-disposition: attachment; filename="h3-63c91f4b6bc7.mp4"`,下载下来是 +合法的 9MB mp4(`ISO Media, MP4 Base Media v1`)。 + +--- + ## 2026-08-20(夜,又续五)— 832×480 高分辨率 5s/10s/15s 实测 + 揪出一个真实并发 bug **背景**:用户想知道高分辨率(832×480,线上标准档)配 fl2va 首尾帧条件下,5s/10s/15s 三档能不能跑。 diff --git a/h3-console/server.py b/h3-console/server.py index 681a4ad..e4a2647 100644 --- a/h3-console/server.py +++ b/h3-console/server.py @@ -293,6 +293,12 @@ async def download_video(jid: str, request: Request): # gateway 的响应里存了 `backend`(真实拥有这条任务的那个 GPU 后端),直接绕过 gateway # 这层转发、把 Range 头透传给 api.py,把它的状态码/头原样带回去,问题在这一层就解决, # 不用碰 gateway.py/api.py 一行代码。 + # 下载/播放到本地的文件名——不带这个的话浏览器只能从 URL 最后一段猜,我们的 URL 都是 + # 以 "/file" 结尾(没有扩展名),猜出来清一色叫 "file.mp4",多个任务的视频存到同一个 + # 下载目录会互相覆盖。用 console 自己的任务 id 拼一个每条任务都不同的文件名。 + download_filename = f"h3-{jid}.mp4" + content_disposition = f'attachment; filename="{download_filename}"' + upstream_url = f"{row['backend']}/v1/videos/{row['gateway_id']}/file" headers = {} if "range" in request.headers: @@ -313,7 +319,8 @@ async def download_video(jid: str, request: Request): cached = json.loads(row["last_known_json"]) if row["last_known_json"] else {} file_path = cached.get("file") if file_path and os.path.exists(file_path): - return FileResponse(file_path, media_type="video/mp4") + return FileResponse(file_path, media_type="video/mp4", + headers={"Content-Disposition": content_disposition}) raise HTTPException(404, "后端进程重启后忘了这条任务的记录,磁盘上也没找到对应文件") async def stream(): @@ -323,6 +330,7 @@ async def download_video(jid: str, request: Request): passthrough = {k: v for k, v in upstream.headers.items() if k.lower() in ("content-length", "content-range", "accept-ranges", "content-type")} + passthrough["content-disposition"] = content_disposition return StreamingResponse(stream(), status_code=upstream.status_code, headers=passthrough) -- libgit2 0.21.4