From e3265e40f05c17562012c3d2f98fba3411cbd1dc Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Tue, 28 Jul 2026 16:19:27 +0800 Subject: [PATCH] =?UTF-8?q?modify:=20aiofile=20=E6=96=87=E4=BB=B6=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aibox_sophon_adapter/routes.py | 59 +++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/aibox_sophon_adapter/routes.py b/aibox_sophon_adapter/routes.py index 37322fa..e6c6c49 100644 --- a/aibox_sophon_adapter/routes.py +++ b/aibox_sophon_adapter/routes.py @@ -10,11 +10,14 @@ AI Box Sophon Adapter — 自定义 FastAPI 路由 import os import json import uuid +import hashlib import logging +import aiofiles + from fastapi import APIRouter, UploadFile, File, Form, Request, Body, HTTPException +from fastapi.responses import FileResponse from pydantic import ValidationError -from aiofile import AIOFile from .models.video_models import VideoUploadResponse, VideoUploadResult from .models.alarm_models import ( @@ -55,6 +58,8 @@ async def video_upload( """ # 生成唯一 VideoId video_id = uuid.uuid4().hex + # 获取设备声明的文件大小(若 Starlette 不支持则记 -1) + declared_size = getattr(Video, 'size', -1) or -1 try: # 复用网关存储路径约定: data/uploads/// @@ -64,18 +69,24 @@ async def video_upload( dest = os.path.join(upload_root, object_key) os.makedirs(os.path.dirname(dest), exist_ok=True) - # 流式写入文件(与网关 /api/v1/storage/upload 一致) - async with AIOFile(dest, "wb") as afp: + # 流式写入文件,同时计算 MD5 和总字节数 + total_bytes = 0 + md5_hash = hashlib.md5() + async with aiofiles.open(dest, "wb") as afp: while chunk := await Video.read(1024 * 1024): await afp.write(chunk) + total_bytes += len(chunk) + md5_hash.update(chunk) file_size = os.path.getsize(dest) + file_md5 = md5_hash.hexdigest() logger.info( f"[VideoUpload] 视频已保存: VideoId={video_id}, " f"BoardId={BoardId}, BoardIp={BoardIp}, " f"TaskSession={TaskSession}, File={Video.filename}, " - f"Size={file_size} bytes, Path={object_key}" + f"declared={declared_size}, received={total_bytes}, disk={file_size}, " + f"md5={file_md5}, Path={object_key}" ) return VideoUploadResponse( @@ -86,7 +97,7 @@ async def video_upload( except Exception as e: logger.error( f"[VideoUpload] 上传失败: BoardId={BoardId}, " - f"BoardIp={BoardIp}, Error={e}" + f"BoardIp={BoardIp}, Error={type(e).__name__}: {e}" ) return VideoUploadResponse( Result=VideoUploadResult(Code=-1, Desc=str(e)), @@ -94,6 +105,44 @@ async def video_upload( ) +@router.get( + "/video/{video_id}", + summary="视频下载/播放", + description="根据 VideoId 下载或流式播放告警关联视频", +) +async def video_download( + request: Request, + video_id: str, +) -> FileResponse: + """ + 视频播放端点 + + 根据 video_upload 返回的 VideoId 查找并返回视频文件。 + 支持浏览器内嵌播放(Content-Disposition: inline)。 + """ + upload_root = request.app.state.upload_root + worker_type = "aibox_sophon_adapter" + # 在 uploads 目录下查找匹配的视频文件 + video_dir = os.path.join(upload_root, worker_type, video_id) + if not os.path.isdir(video_dir): + raise HTTPException(status_code=404, detail=f"视频不存在: {video_id}") + + # 取目录下第一个文件作为视频(通常只有一个) + files = os.listdir(video_dir) + if not files: + raise HTTPException(status_code=404, detail=f"视频目录为空: {video_id}") + + video_path = os.path.join(video_dir, files[0]) + if not os.path.isfile(video_path): + raise HTTPException(status_code=404, detail=f"视频文件不存在: {files[0]}") + + return FileResponse( + video_path, + media_type="video/mp4", + headers={"Content-Disposition": "inline"}, + ) + + @router.post( "/alarm_report", summary="告警上报接口",