Compare commits

...

2 Commits

Author SHA1 Message Date
18cfc9b16a feat(server):补充RESTful 接口 2026-06-12 15:00:24 +08:00
8b914aae8a refactor(web): 重构App.vue为纯路由出口 2026-06-12 14:27:37 +08:00
2 changed files with 225 additions and 54 deletions

200
apps/server/api/rtsp.py Normal file
View File

@@ -0,0 +1,200 @@
"""RTSP 流管理 API (MVP-2 / D13-D14)
提供 RTSP 流的增删改查、启停控制、状态监控等 RESTful 接口。
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from core.settings import get_settings
from services.stream_manager import StreamManager
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/rtsp", tags=["RTSP 流管理"])
# 全局 StreamManager 实例 (由 main.py 初始化)
_stream_manager: Optional[StreamManager] = None
def init_stream_manager(model_service: Any = None) -> StreamManager:
"""初始化全局 StreamManager 实例。"""
global _stream_manager
settings = get_settings()
_stream_manager = StreamManager(
model_service=model_service,
buffer_capacity=settings.rtsp.buffer_capacity,
max_streams=settings.rtsp.max_streams,
detect_interval=settings.rtsp.detect_interval,
)
return _stream_manager
def get_stream_manager() -> StreamManager:
"""获取全局 StreamManager 实例。"""
if _stream_manager is None:
raise RuntimeError("StreamManager 未初始化,请先调用 init_stream_manager()")
return _stream_manager
# ---------------------------------------------------------------------------
# 请求/响应模型
# ---------------------------------------------------------------------------
class AddStreamRequest(BaseModel):
"""添加 RTSP 流请求。"""
stream_id: str = Field(..., min_length=1, max_length=64, description="流标识")
rtsp_url: str = Field(..., description="RTSP 流地址")
model_id: str = Field(default="fire_detection", description="检测模型ID")
confidence: float = Field(default=0.5, ge=0.0, le=1.0, description="置信度阈值")
iou: float = Field(default=0.45, ge=0.0, le=1.0, description="IOU阈值")
frame_skip: int = Field(default=0, ge=0, description="帧采样间隔")
class UpdateStreamConfigRequest(BaseModel):
"""更新流配置请求。"""
model_id: Optional[str] = None
confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)
iou: Optional[float] = Field(default=None, ge=0.0, le=1.0)
frame_skip: Optional[int] = Field(default=None, ge=0)
class StreamOperationResponse(BaseModel):
"""通用操作响应。"""
success: bool
message: str
# ---------------------------------------------------------------------------
# API 路由
# ---------------------------------------------------------------------------
@router.post("/streams", response_model=StreamOperationResponse)
async def add_stream(req: AddStreamRequest):
"""添加一路 RTSP 流。"""
from services.rtsp_service import StreamConfig
config = StreamConfig(
stream_id=req.stream_id,
rtsp_url=req.rtsp_url,
model_id=req.model_id,
confidence=req.confidence,
iou=req.iou,
frame_skip=req.frame_skip,
)
manager = get_stream_manager()
result = await manager.add_stream(req.stream_id, req.rtsp_url, config=config)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return StreamOperationResponse(**result)
@router.delete("/streams/{stream_id}", response_model=StreamOperationResponse)
async def remove_stream(stream_id: str):
"""移除一路 RTSP 流。"""
manager = get_stream_manager()
result = await manager.remove_stream(stream_id)
if not result["success"]:
raise HTTPException(status_code=404, detail=result["message"])
return StreamOperationResponse(**result)
@router.post("/streams/{stream_id}/start", response_model=StreamOperationResponse)
async def start_stream(stream_id: str):
"""启动一路 RTSP 流。"""
manager = get_stream_manager()
result = await manager.start_stream(stream_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return StreamOperationResponse(**result)
@router.post("/streams/{stream_id}/stop", response_model=StreamOperationResponse)
async def stop_stream(stream_id: str):
"""停止单路 RTSP 流。"""
manager = get_stream_manager()
result = await manager.stop_stream(stream_id)
if not result["success"]:
raise HTTPException(status_code=404, detail=result["message"])
return StreamOperationResponse(**result)
@router.put("/streams/{stream_id}/config", response_model=StreamOperationResponse)
async def update_stream_config(stream_id: str, req: UpdateStreamConfigRequest):
"""更新流的检测配置。"""
manager = get_stream_manager()
result = await manager.update_stream_config(
stream_id,
model_id=req.model_id,
confidence=req.confidence,
iou=req.iou,
frame_skip=req.frame_skip,
)
if not result["success"]:
raise HTTPException(status_code=404, detail=result["message"])
return StreamOperationResponse(**result)
@router.get("/streams/{stream_id}")
async def get_stream_info(stream_id: str):
"""获取单路流状态信息。"""
manager = get_stream_manager()
info = manager.get_stream_info(stream_id)
if info is None:
raise HTTPException(status_code=404, detail=f"{stream_id} 不存在")
return info
@router.get("/streams")
async def list_streams():
"""获取所有流状态信息。"""
manager = get_stream_manager()
return {"streams": manager.get_all_streams_info()}
@router.get("/health")
async def rtsp_health():
"""RTSP 管理器健康检查。"""
manager = get_stream_manager()
return manager.get_health()
@router.post("/start-all", response_model=StreamOperationResponse)
async def start_all_streams():
"""启动所有已添加的流。"""
manager = get_stream_manager()
await manager.start_all()
return StreamOperationResponse(success=True, message="所有流已启动")
@router.post("/stop-all", response_model=StreamOperationResponse)
async def stop_all_streams():
"""停止所有流。"""
manager = get_stream_manager()
await manager.stop_all()
return StreamOperationResponse(success=True, message="所有流已停止")
__all__ = ["router", "init_stream_manager", "get_stream_manager"]

View File

@@ -1,60 +1,12 @@
<template> <template>
<div id="app"> <div id="app">
<el-container style="height: 100vh"> <router-view />
<el-header class="header">
<div class="header-content">
<h1 class="title">🎯 视频模型检测平台</h1>
<div class="header-info">
<el-tag type="success">运行中</el-tag>
</div>
</div>
</el-header>
<el-container>
<router-view />
</el-container>
</el-container>
</div> </div>
</template> </template>
<script setup> <script setup>
</script> </script>
<style scoped>
.header {
background: linear-gradient(135deg, #0F172A 0%, #1E293B 100%);
color: #F8FAFC;
padding: 0 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
}
.title {
font-size: 24px;
margin: 0;
font-weight: 600;
font-family: 'Fira Code', monospace;
letter-spacing: -0.5px;
background: linear-gradient(135deg, #22C55E 0%, #16A34A 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-info {
display: flex;
gap: 12px;
align-items: center;
}
</style>
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap');
@@ -66,18 +18,18 @@
body { body {
font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #0F172A; background: #020617;
color: #F8FAFC; color: #F8FAFC;
} }
#app { #app {
width: 100%; width: 100%;
height: 100%; height: 100vh;
} }
.el-main { .el-main {
background: #0F172A; background: #020617;
padding: 20px; padding: 0;
} }
::selection { ::selection {
@@ -97,9 +49,28 @@ body {
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #334155; background: #334155;
border-radius: 5px; border-radius: 5px;
transition: background 200ms ease;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #475569; background: #475569;
} }
/* 全局 cursor pointer */
button,
[role="button"],
.el-button,
.el-menu-item,
.el-tag,
.el-pagination li {
cursor: pointer;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
</style> </style>