Compare commits
2 Commits
4e0f724661
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 18cfc9b16a | |||
| 8b914aae8a |
200
apps/server/api/rtsp.py
Normal file
200
apps/server/api/rtsp.py
Normal 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"]
|
||||
@@ -1,60 +1,12 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<el-container style="height: 100vh">
|
||||
<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>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</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>
|
||||
@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 {
|
||||
font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #0F172A;
|
||||
background: #020617;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background: #0F172A;
|
||||
padding: 20px;
|
||||
background: #020617;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -97,9 +49,28 @@ body {
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 5px;
|
||||
transition: background 200ms ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
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>
|
||||
Reference in New Issue
Block a user