feat: expand livestream management platform
This commit is contained in:
1 parent
383c066d86
commit
ed62d42a63
12 files changed
+223
-24
No files matched your search
@@ -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="请先选择回放文件再编辑章节")
|
||||
|
||||
Reference in new issue
Block a user