video_platform.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. from __future__ import annotations
  2. from datetime import datetime
  3. from fastapi import APIRouter, Depends, Query, Request, Response
  4. from fastapi.responses import FileResponse, StreamingResponse
  5. from sqlalchemy.orm import Session
  6. from app.db.session import get_db
  7. from app.schemas.common import Page
  8. from app.schemas.video import (
  9. VideoAnalysisEventRead,
  10. VideoChannelCreate,
  11. VideoChannelRead,
  12. VideoChannelUpdate,
  13. VideoEventUpdate,
  14. VideoModuleConfig,
  15. VideoModuleSummaryRead,
  16. VideoPlatformOverview,
  17. VideoRecordRead,
  18. VideoSnapshotRead,
  19. )
  20. from app.services import video_platform_service as video_service
  21. router = APIRouter(prefix="/video-platform", tags=["工业视频平台"])
  22. @router.get("/overview", response_model=VideoPlatformOverview, summary="视频平台总览")
  23. def overview(
  24. mine_id: str = Query(...),
  25. db: Session = Depends(get_db),
  26. ):
  27. return video_service.platform_overview(db, mine_id)
  28. @router.get("/modules", response_model=list[VideoModuleConfig], summary="模块页签与检索项配置")
  29. def modules(
  30. mine_id: str = Query(...),
  31. db: Session = Depends(get_db),
  32. ):
  33. return video_service.module_configs(db, mine_id)
  34. @router.get(
  35. "/modules/{module}/summary",
  36. response_model=VideoModuleSummaryRead,
  37. summary="模块状态总览与统计卡片",
  38. )
  39. def module_summary(
  40. module: str,
  41. mine_id: str = Query(...),
  42. db: Session = Depends(get_db),
  43. ):
  44. return video_service.module_summary(db, mine_id, module)
  45. # ---------------------------------------------------------------------------
  46. # 视频通道(通道管理、设备树、分屏预览)
  47. # ---------------------------------------------------------------------------
  48. @router.get("/channels", response_model=list[VideoChannelRead], summary="视频通道列表")
  49. def list_channels(
  50. mine_id: str = Query(...),
  51. module: str | None = Query(None, description="door/local_fan/main_fan"),
  52. status: str | None = Query(None, description="online/offline/fault"),
  53. keyword: str | None = Query(None, description="通道名称、安装地点或编码"),
  54. limit: int = Query(200, ge=1, le=500),
  55. db: Session = Depends(get_db),
  56. ):
  57. return video_service.list_channels(
  58. db, mine_id, module=module, status=status, keyword=keyword, limit=limit
  59. )
  60. @router.post(
  61. "/channels",
  62. response_model=VideoChannelRead,
  63. status_code=201,
  64. summary="新增视频通道",
  65. )
  66. def create_channel(payload: VideoChannelCreate, db: Session = Depends(get_db)):
  67. return video_service.create_channel(db, payload)
  68. @router.get(
  69. "/channels/{channel_id}",
  70. response_model=VideoChannelRead,
  71. summary="视频通道详情",
  72. )
  73. def channel_detail(channel_id: str, db: Session = Depends(get_db)):
  74. return video_service.get_channel(db, channel_id)
  75. @router.patch(
  76. "/channels/{channel_id}",
  77. response_model=VideoChannelRead,
  78. summary="更新视频通道",
  79. )
  80. def update_channel(
  81. channel_id: str,
  82. payload: VideoChannelUpdate,
  83. db: Session = Depends(get_db),
  84. ):
  85. return video_service.update_channel(db, channel_id, payload)
  86. @router.delete(
  87. "/channels/{channel_id}",
  88. status_code=204,
  89. summary="删除视频通道",
  90. )
  91. def delete_channel(channel_id: str, db: Session = Depends(get_db)) -> Response:
  92. video_service.delete_channel(db, channel_id)
  93. return Response(status_code=204)
  94. @router.get(
  95. "/channels/{channel_id}/stream",
  96. summary="通道视频流(读取数据库中的磁盘路径,支持 Range)",
  97. )
  98. def channel_stream(
  99. channel_id: str,
  100. request: Request,
  101. db: Session = Depends(get_db),
  102. ):
  103. path, content_type = video_service.resolve_channel_media(channel_id, db)
  104. file_size = path.stat().st_size
  105. range_header = request.headers.get("range")
  106. start, end = video_service.parse_range_header(range_header, file_size)
  107. headers = {
  108. "Accept-Ranges": "bytes",
  109. "Content-Length": str(max(end - start + 1, 0)),
  110. "Cache-Control": "no-store",
  111. }
  112. if range_header:
  113. headers["Content-Range"] = f"bytes {start}-{end}/{file_size}"
  114. return StreamingResponse(
  115. video_service.iter_file_range(path, start, end),
  116. status_code=206,
  117. media_type=content_type,
  118. headers=headers,
  119. )
  120. return StreamingResponse(
  121. video_service.iter_file_range(path, start, end),
  122. status_code=200,
  123. media_type=content_type,
  124. headers=headers,
  125. )
  126. @router.get(
  127. "/channels/{channel_id}/snapshot",
  128. summary="通道最近识别抓拍图(抓图按钮)",
  129. )
  130. def channel_snapshot(channel_id: str, db: Session = Depends(get_db)):
  131. path, content_type = video_service.resolve_channel_latest_snapshot(channel_id, db)
  132. return FileResponse(path, media_type=content_type, filename=path.name)
  133. # ---------------------------------------------------------------------------
  134. # 识别抓拍
  135. # ---------------------------------------------------------------------------
  136. @router.get(
  137. "/snapshots",
  138. response_model=Page[VideoSnapshotRead],
  139. summary="识别抓拍图像列表(时间、类型、设备检索)",
  140. )
  141. def list_snapshots(
  142. mine_id: str = Query(...),
  143. module: str | None = Query(None),
  144. snapshot_type: str | None = Query(None, description="vehicle_passage/person_passage 等"),
  145. event_type: str | None = Query(None, description="告警类型或操作类型名称"),
  146. channel_id: str | None = Query(None),
  147. start: datetime | None = Query(None, description="抓拍时间起"),
  148. end: datetime | None = Query(None, description="抓拍时间止"),
  149. keyword: str | None = Query(None, description="标题、车牌、人员或类型关键字"),
  150. page: int = Query(1, ge=1),
  151. page_size: int = Query(24, ge=1, le=200),
  152. db: Session = Depends(get_db),
  153. ):
  154. items, total = video_service.list_snapshots(
  155. db,
  156. mine_id,
  157. module=module,
  158. snapshot_type=snapshot_type,
  159. event_type=event_type,
  160. channel_id=channel_id,
  161. start=start,
  162. end=end,
  163. keyword=keyword,
  164. page=page,
  165. page_size=page_size,
  166. )
  167. return Page(items=items, total=total, page=page, page_size=page_size)
  168. @router.get(
  169. "/snapshots/{snapshot_id}",
  170. response_model=VideoSnapshotRead,
  171. summary="识别抓拍详情",
  172. )
  173. def snapshot_detail(snapshot_id: str, db: Session = Depends(get_db)):
  174. return video_service.get_snapshot(db, snapshot_id)
  175. @router.get("/snapshots/{snapshot_id}/image", summary="识别抓拍原图")
  176. def snapshot_image(snapshot_id: str, db: Session = Depends(get_db)):
  177. path, content_type = video_service.resolve_snapshot_media(snapshot_id, db)
  178. return FileResponse(path, media_type=content_type, filename=path.name)
  179. @router.get("/snapshots/{snapshot_id}/thumbnail", summary="识别抓拍缩略图")
  180. def snapshot_thumbnail(snapshot_id: str, db: Session = Depends(get_db)):
  181. path, content_type = video_service.resolve_snapshot_thumbnail(snapshot_id, db)
  182. return FileResponse(path, media_type=content_type, filename=path.name)
  183. # ---------------------------------------------------------------------------
  184. # 视频分析事件
  185. # ---------------------------------------------------------------------------
  186. @router.get(
  187. "/events",
  188. response_model=list[VideoAnalysisEventRead],
  189. summary="视频分析事件与实时告警",
  190. )
  191. def list_events(
  192. mine_id: str = Query(...),
  193. module: str | None = Query(None),
  194. level: str | None = Query(None, description="danger/warning/info"),
  195. status: str | None = Query(None, description="pending/processing/handled/ignored"),
  196. event_type: str | None = Query(None),
  197. start: datetime | None = Query(None),
  198. end: datetime | None = Query(None),
  199. limit: int = Query(100, ge=1, le=1000),
  200. db: Session = Depends(get_db),
  201. ):
  202. return video_service.list_events(
  203. db,
  204. mine_id,
  205. module=module,
  206. level=level,
  207. status=status,
  208. event_type=event_type,
  209. start=start,
  210. end=end,
  211. limit=limit,
  212. )
  213. @router.patch(
  214. "/events/{event_id}",
  215. response_model=VideoAnalysisEventRead,
  216. summary="更新事件处理状态",
  217. )
  218. def update_event(
  219. event_id: str,
  220. payload: VideoEventUpdate,
  221. db: Session = Depends(get_db),
  222. ):
  223. return video_service.update_event(db, event_id, payload)
  224. # ---------------------------------------------------------------------------
  225. # 通行与操作记录
  226. # ---------------------------------------------------------------------------
  227. @router.get(
  228. "/records",
  229. response_model=Page[VideoRecordRead],
  230. summary="通行与操作记录列表",
  231. )
  232. def list_records(
  233. mine_id: str = Query(...),
  234. module: str | None = Query(None),
  235. record_type: str | None = Query(
  236. None,
  237. description=(
  238. "vehicle_passage/person_passage/door_unclosed/person_track/"
  239. "area_stay/person_warn/person_detect/device_operation/device_warn"
  240. ),
  241. ),
  242. channel_id: str | None = Query(None),
  243. start: datetime | None = Query(None),
  244. end: datetime | None = Query(None),
  245. keyword: str | None = Query(None),
  246. page: int = Query(1, ge=1),
  247. page_size: int = Query(50, ge=1, le=200),
  248. db: Session = Depends(get_db),
  249. ):
  250. items, total = video_service.list_records(
  251. db,
  252. mine_id,
  253. module=module,
  254. record_type=record_type,
  255. channel_id=channel_id,
  256. start=start,
  257. end=end,
  258. keyword=keyword,
  259. page=page,
  260. page_size=page_size,
  261. )
  262. return Page(items=items, total=total, page=page, page_size=page_size)