feat: expand livestream management platform

This commit is contained in:
admin committed 2026-08-19 22:24:01 +08:00
1 parent 383c066d86
commit ed62d42a63
12 files changed
+223 -24

No files matched your search

+50
View File
@@ -23,6 +23,20 @@ from models.audit import ControlCommand, ControlSession, ViewerSession
router = APIRouter(prefix="/api/streams", tags=["streams"])
class RenditionInput(BaseModel):
label: str
enabled: bool = True
width: int
height: int
fps: int = 30
codec: str = "libx264"
bitrate: str = "2500k"
crf: int = 23
preset: str = "veryfast"
audio_bitrate: str = "128k"
keyframe_seconds: int = 2
class StreamCreate(BaseModel):
title: str
description: Optional[str] = None
@@ -31,6 +45,7 @@ class StreamCreate(BaseModel):
replay_file: Optional[str] = None
chapters: list[dict] = Field(default_factory=list)
transcode_profile: str = "standard"
renditions: Optional[list[RenditionInput]] = None
homepage_visible: bool = True
remote_control_enabled: bool = False
@@ -43,6 +58,7 @@ class StreamUpdate(BaseModel):
replay_file: Optional[str] = None
chapters: Optional[list[dict]] = None
transcode_profile: Optional[str] = None
renditions: Optional[list[RenditionInput]] = None
homepage_visible: Optional[bool] = None
remote_control_enabled: Optional[bool] = None
@@ -115,6 +131,35 @@ def _normalize_chapters(chapters: list[dict]) -> list:
return sorted(normalized, key=lambda chapter: chapter["time"])
def _normalize_renditions(renditions: list[dict]) -> list:
codecs = {"libx264", "libx265"}
presets = {"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow"}
normalized = []
labels = set()
for rendition in renditions:
label = str(rendition.get("label", "")).strip()
if not label or not label.replace("-", "").replace("_", "").isalnum() or label in labels:
raise HTTPException(status_code=422, detail="清晰度标签无效或重复")
labels.add(label)
if not rendition.get("enabled", True):
continue
width, height = int(rendition["width"]), int(rendition["height"])
fps, crf = int(rendition.get("fps", 30)), int(rendition.get("crf", 23))
keyframe_seconds = int(rendition.get("keyframe_seconds", 2))
codec, preset = rendition.get("codec", "libx264"), rendition.get("preset", "veryfast")
bitrate, audio_bitrate = str(rendition.get("bitrate", "")), str(rendition.get("audio_bitrate", ""))
if not (160 <= width <= 7680 and 90 <= height <= 4320 and 1 <= fps <= 120 and 0 <= crf <= 51 and 1 <= keyframe_seconds <= 10):
raise HTTPException(status_code=422, detail="清晰度参数超出允许范围")
if codec not in codecs or preset not in presets:
raise HTTPException(status_code=422, detail="编码器或预设不受支持")
if not bitrate[:-1].isdigit() or bitrate[-1:] not in {"k", "M"} or not audio_bitrate[:-1].isdigit() or audio_bitrate[-1:] not in {"k", "M"}:
raise HTTPException(status_code=422, detail="码率格式必须类似 2500k 或 5M")
normalized.append({"label": label, "enabled": True, "width": width, "height": height, "fps": fps, "codec": codec, "bitrate": bitrate, "crf": crf, "preset": preset, "audio_bitrate": audio_bitrate, "keyframe_seconds": keyframe_seconds})
if not normalized:
raise HTTPException(status_code=422, detail="至少启用一个清晰度")
return normalized
def _serialize(s, include_private: bool = True):
data = {
"id": s.id, "title": s.title, "description": s.description,
@@ -126,6 +171,7 @@ def _serialize(s, include_private: bool = True):
"replay_file": s.replay_file,
"chapters": json.loads(s.chapters_json or "[]"),
"transcode_profile": s.transcode_profile,
"renditions": json.loads(getattr(s, "renditions_json", None)) if getattr(s, "renditions_json", None) else None,
"homepage_visible": s.homepage_visible,
"remote_control_enabled": s.remote_control_enabled,
"created_at": str(s.created_at),
@@ -272,6 +318,7 @@ async def create_stream(body: StreamCreate, db: AsyncSession = Depends(get_db),
_media_path(body.replay_file)
if body.transcode_profile not in settings.transcode_profiles:
raise HTTPException(status_code=422, detail="转码配置不存在")
renditions = _normalize_renditions([item.model_dump() for item in body.renditions]) if body.renditions is not None else None
svc = StreamService(db)
try:
stream = await svc.create_stream(
@@ -282,6 +329,7 @@ async def create_stream(body: StreamCreate, db: AsyncSession = Depends(get_db),
replay_file=body.replay_file,
chapters=body.chapters,
transcode_profile=body.transcode_profile,
renditions_json=json.dumps(renditions, ensure_ascii=False) if renditions is not None else None,
homepage_visible=body.homepage_visible,
remote_control_enabled=body.remote_control_enabled,
)
@@ -323,6 +371,8 @@ async def update_stream(stream_id: int, body: StreamUpdate, db: AsyncSession = D
_media_path(updates["replay_file"])
if "transcode_profile" in updates and updates["transcode_profile"] not in settings.transcode_profiles:
raise HTTPException(status_code=422, detail="转码配置不存在")
if "renditions" in updates:
updates["renditions_json"] = json.dumps(_normalize_renditions(updates.pop("renditions")), ensure_ascii=False)
if "chapters" in updates:
if not updates.get("replay_file") and not existing.replay_file:
raise HTTPException(status_code=409, detail="请先选择回放文件再编辑章节")