Replay
- Hand the replay file to nginx with X-Accel-Redirect instead of streaming it
through the backend. A replay is tens of gigabytes, so Python was copying the
whole file and holding a worker for the entire transfer: measured 8 MB in
8.46s (~1 MB/s). nginx serves it from disk with sendfile at ~400 MB/s (1 GB in
2.68s) and the backend process does not appear in CPU usage during the transfer.
Authentication and the path-traversal check still run in Python; only the bytes
moved. The internal location is what keeps the path from being reachable
directly and bypassing that check.
- nginx reloads and the media directory is aliased internally.
Settings
- GET /settings/transcode-profiles returned the bare mapping while the settings
page reads `data.profiles`, so it always saw undefined, fell back to an empty
object, and every save posted `{"profiles": {}}` — which the PUT rejects as
"at least one profile". Saving settings could never succeed. The response now
matches the shape PUT accepts, verified by round-tripping it.
Auth
- Derive the admin token instead of storing it. The token is
sha256(password + ":token"), but it was kept in a module-level set, which only
works with one process: with uvicorn --workers N, logging in on one worker left
the others rejecting the token and the admin appeared logged out at random.
Viewer
- Show the real session id in the stats panel; the previous code assigned it
from a local variable that was never populated, so it always read "不可用".
- Send the first heartbeat before the first command poll. Both fired together on
mount, so the poll reached the backend before the session existed and 404'd on
every page load.
Housekeeping
- Ignore backend/nodes.json; the admin node manager writes it at runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
from pydantic_settings import BaseSettings
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
|
|
APP_DIR = Path(__file__).resolve().parent
|
|
DEFAULT_DATA_DIR = APP_DIR / "data"
|
|
DEFAULT_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
database_url: str = f"sqlite+aiosqlite:///{(DEFAULT_DATA_DIR / 'streamcore.db').as_posix()}"
|
|
admin_password: str = "NewVision_20061010"
|
|
|
|
media_origin_host: str = "127.0.0.1"
|
|
media_origin_rtmp_port: int = 1935
|
|
media_origin_api_port: int = 9997
|
|
media_origin_hls_port: int = 8888
|
|
# MediaMTX is supervised by the backend: started at boot if the origin is not
|
|
# already answering, and restarted if it dies. Set LS_MEDIA_ORIGIN_AUTOSTART=false
|
|
# to manage it externally (e.g. systemd, or an edge node's own launcher).
|
|
media_origin_autostart: bool = True
|
|
# "mediamtx" resolves via PATH; the repo-local copies are the fallback so a
|
|
# checkout works without a system-wide install. On Windows the binary sits in
|
|
# mediamtx\, on Linux/macOS it is usually a bare `mediamtx` on PATH.
|
|
media_origin_binary: str = "mediamtx"
|
|
media_origin_config: str = str(APP_DIR.parent / "mediamtx" / "mediamtx.yml")
|
|
replay_media_dir: str = str(APP_DIR.parent / "media")
|
|
hls_runtime_dir: str = str(APP_DIR.parent / "runtime" / "hls")
|
|
hls_segment_seconds: int = 4
|
|
|
|
edge_nodes: list[dict] = [
|
|
{
|
|
"id": "default",
|
|
"name": "Origin (Default)",
|
|
"host": "127.0.0.1",
|
|
"hls_port": 8888,
|
|
"api_port": 9997,
|
|
"weight": 100,
|
|
"enabled": True,
|
|
"subnet": "0.0.0.0/0",
|
|
"is_origin": True,
|
|
},
|
|
]
|
|
|
|
load_balance_strategy: Literal["round-robin", "weighted", "least-conn", "subnet"] = "subnet"
|
|
|
|
qualities: list[dict] = [
|
|
{"label": "1080p", "width": 1920, "height": 1080, "bitrate": "5000k"},
|
|
{"label": "720p", "width": 1280, "height": 720, "bitrate": "2500k"},
|
|
{"label": "480p", "width": 854, "height": 480, "bitrate": "1000k"},
|
|
{"label": "360p", "width": 640, "height": 360, "bitrate": "600k"},
|
|
]
|
|
|
|
# Extra rendition that is published alongside the configured ones: the
|
|
# publisher's own video+audio stream remuxed into HLS without re-encoding.
|
|
# It costs almost no CPU (stream copy) and gives viewers a lossless
|
|
# "原画" (source) option even when every configured rendition downsizes.
|
|
source_rendition_label: str = "原画"
|
|
source_rendition_enabled: bool = True
|
|
# On-disk/URL name for that rendition. It IS the HLS path segment, and MediaMTX
|
|
# rejects any path containing non-alphanumeric characters, so the Chinese
|
|
# display label above cannot be used here — a CI would 500 trying to serve it.
|
|
source_rendition_slug: str = "source"
|
|
|
|
transcode_profiles: dict[str, list[dict]] = {
|
|
"standard": qualities,
|
|
"bandwidth-saver": [
|
|
{"label": "720p", "width": 1280, "height": 720, "bitrate": "1800k"},
|
|
{"label": "480p", "width": 854, "height": 480, "bitrate": "800k"},
|
|
{"label": "360p", "width": 640, "height": 360, "bitrate": "450k"},
|
|
],
|
|
}
|
|
|
|
multicast_enabled: bool = True
|
|
multicast_address_base: str = "239.0.0.1"
|
|
multicast_port_base: int = 5000
|
|
|
|
brand_name: str = "柳高新视角媒体平台"
|
|
brand_tagline: str = "New Vision Media Platform"
|
|
brand_logo_url: str = "https://yhlstatic.cn-nb1.rains3.com/Image_1790648201679_296.png"
|
|
|
|
model_config = {"env_prefix": "LS_"}
|
|
|
|
|
|
settings = Settings()
|