transcode: add CBR/VBR/QP rate control + more x264 presets
Player/transcoding settings gain encoder controls: - Rate-control mode VBR / CBR / QP (constant quantizer). x264: VBR uses -b:v/-maxrate/-bufsize, CBR adds -x264-params nal-hrd=cbr (min=max=b:v), QP uses -qp N (no bitrate). VP9 maps QP -> -crf N -b:v 0, CBR -> min=max=b:v. - Preset list expanded to ultrafast/superfast/veryfast/faster/fast/medium (was only ultrafast/superfast); for VP9 the preset maps to cpu-used. - New QP field in the UI, shown when rate control = QP, otherwise the bitrate field is shown. Both /play.mp4 (archive) and /live.mp4 accept rc + qp query params. All three modes verified producing valid output on the live restream. VERSION -> 0.0.119. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ from ..config import Camera, save_config
|
||||
from ..runtime import runtime
|
||||
from ..services import go2rtc
|
||||
from ..transcode import (encode_args, norm_profile, norm_bitrate, norm_preset,
|
||||
norm_keyint, norm_codec)
|
||||
norm_keyint, norm_codec, norm_rc, norm_qp)
|
||||
|
||||
router = APIRouter(prefix="/api/cameras", tags=["cameras"])
|
||||
|
||||
@@ -86,6 +86,8 @@ async def camera_live_transcoded(
|
||||
preset: str = Query(None),
|
||||
keyint: str = Query(None),
|
||||
codec: str = Query(None),
|
||||
rc: str = Query(None),
|
||||
qp: str = Query(None),
|
||||
stream: str = Query("sub"),
|
||||
):
|
||||
"""Live с перекодированием в H.264 (fragmented mp4) под профиль/битрейт клиента.
|
||||
@@ -103,7 +105,8 @@ async def camera_live_transcoded(
|
||||
"-fflags", "nobuffer", "-flags", "low_delay",
|
||||
"-rtsp_transport", "tcp", "-i", src,
|
||||
*encode_args(norm_codec(codec), norm_profile(profile), norm_bitrate(bitrate),
|
||||
low_latency=True, preset=norm_preset(preset), keyint_sec=norm_keyint(keyint)),
|
||||
low_latency=True, preset=norm_preset(preset), keyint_sec=norm_keyint(keyint),
|
||||
rc=norm_rc(rc), qp=norm_qp(qp)),
|
||||
"-an",
|
||||
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
||||
"-f", "mp4", "pipe:1",
|
||||
|
||||
@@ -9,7 +9,8 @@ from fastapi.responses import FileResponse, StreamingResponse
|
||||
|
||||
from ..runtime import runtime
|
||||
from ..transcode import (H264_PROFILES, BITRATES, encode_args, norm_profile,
|
||||
norm_bitrate, norm_preset, norm_keyint, norm_codec)
|
||||
norm_bitrate, norm_preset, norm_keyint, norm_codec,
|
||||
norm_rc, norm_qp)
|
||||
|
||||
router = APIRouter(prefix="/api/recordings", tags=["recordings"])
|
||||
|
||||
@@ -53,6 +54,8 @@ async def recording_transcoded(
|
||||
preset: str = Query(None),
|
||||
keyint: str = Query(None),
|
||||
codec: str = Query(None),
|
||||
rc: str = Query(None),
|
||||
qp: str = Query(None),
|
||||
start: float = Query(0.0),
|
||||
):
|
||||
"""Перекодирование H.265→H.264 на лету (fragmented mp4) — для браузеров без HEVC.
|
||||
@@ -71,7 +74,8 @@ async def recording_transcoded(
|
||||
*(["-ss", f"{start:.3f}"] if start and start > 0 else []),
|
||||
"-i", rec.path,
|
||||
*encode_args(norm_codec(codec), norm_profile(profile), norm_bitrate(bitrate),
|
||||
preset=norm_preset(preset), keyint_sec=norm_keyint(keyint)),
|
||||
preset=norm_preset(preset), keyint_sec=norm_keyint(keyint),
|
||||
rc=norm_rc(rc), qp=norm_qp(qp)),
|
||||
"-an",
|
||||
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
||||
"-f", "mp4", "pipe:1",
|
||||
|
||||
+63
-22
@@ -1,17 +1,25 @@
|
||||
"""Общие параметры перекодирования H.264 (профиль, битрейт, пресет, GOP)."""
|
||||
"""Общие параметры перекодирования H.264/VP9: профиль, битрейт, пресет, GOP, режим RC."""
|
||||
from __future__ import annotations
|
||||
|
||||
H264_PROFILES = ("baseline", "main", "high")
|
||||
BITRATES = ("500k", "1000k", "2000k", "4000k", "8000k", "10000k", "16000k", "20000k", "24000k")
|
||||
PRESETS = ("ultrafast", "superfast")
|
||||
PRESETS = ("ultrafast", "superfast", "veryfast", "faster", "fast", "medium")
|
||||
KEYINTS = (1, 2, 5) # интервал ключевых кадров, секунды
|
||||
CODECS = ("h264", "vp9")
|
||||
RC_MODES = ("vbr", "cbr", "qp") # контроль битрейта: переменный / постоянный / постоянный квантизатор
|
||||
|
||||
DEFAULT_PROFILE = "main"
|
||||
DEFAULT_BITRATE = "2000k"
|
||||
DEFAULT_PRESET = "superfast"
|
||||
DEFAULT_KEYINT = 2
|
||||
DEFAULT_CODEC = "h264"
|
||||
DEFAULT_RC = "vbr"
|
||||
DEFAULT_QP = 23
|
||||
QP_MIN, QP_MAX = 0, 51
|
||||
|
||||
# пресет x264 → cpu-used VP9 (больше число = быстрее кодирование, ниже качество)
|
||||
_VP9_CPU = {"ultrafast": "8", "superfast": "8", "veryfast": "7",
|
||||
"faster": "6", "fast": "5", "medium": "4"}
|
||||
|
||||
|
||||
def norm_profile(p: str | None) -> str:
|
||||
@@ -30,6 +38,10 @@ def norm_preset(p: str | None) -> str:
|
||||
return p if p in PRESETS else DEFAULT_PRESET
|
||||
|
||||
|
||||
def norm_rc(m: str | None) -> str:
|
||||
return m if m in RC_MODES else DEFAULT_RC
|
||||
|
||||
|
||||
def norm_keyint(k) -> int:
|
||||
try:
|
||||
k = int(k)
|
||||
@@ -38,51 +50,80 @@ def norm_keyint(k) -> int:
|
||||
return k if k in KEYINTS else DEFAULT_KEYINT
|
||||
|
||||
|
||||
def x264_args(profile: str, bitrate: str, low_latency: bool = False,
|
||||
preset: str | None = None, keyint_sec=None) -> list[str]:
|
||||
"""Аргументы libx264: профиль, битрейт, пресет и интервал ключевых кадров.
|
||||
def norm_qp(q) -> int:
|
||||
try:
|
||||
q = int(q)
|
||||
except (TypeError, ValueError):
|
||||
return DEFAULT_QP
|
||||
return max(QP_MIN, min(QP_MAX, q))
|
||||
|
||||
Ключевые кадры расставляются по времени (через -force_key_frames), поэтому
|
||||
интервал в секундах соблюдается независимо от fps камеры."""
|
||||
bufsize = f"{int(bitrate[:-1]) * 2}k"
|
||||
|
||||
def _kbps(bitrate: str) -> int:
|
||||
return int(bitrate[:-1])
|
||||
|
||||
|
||||
def x264_args(profile: str, bitrate: str, low_latency: bool = False,
|
||||
preset: str | None = None, keyint_sec=None,
|
||||
rc: str = DEFAULT_RC, qp=DEFAULT_QP) -> list[str]:
|
||||
"""Аргументы libx264: профиль, пресет, GOP и режим контроля битрейта.
|
||||
|
||||
rc: vbr — целевой битрейт с потолком (-b:v/-maxrate); cbr — постоянный
|
||||
битрейт (nal-hrd=cbr, min=max=b:v); qp — постоянный квантизатор (-qp, без
|
||||
битрейта). Ключевые кадры расставляются по времени (-force_key_frames)."""
|
||||
preset = norm_preset(preset)
|
||||
sec = norm_keyint(keyint_sec)
|
||||
rc = norm_rc(rc)
|
||||
args = [
|
||||
"-c:v", "libx264",
|
||||
"-preset", preset,
|
||||
"-profile:v", profile,
|
||||
"-b:v", bitrate,
|
||||
"-maxrate", bitrate,
|
||||
"-bufsize", bufsize,
|
||||
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
||||
"-pix_fmt", "yuv420p",
|
||||
]
|
||||
if rc == "qp":
|
||||
args += ["-qp", str(norm_qp(qp))]
|
||||
elif rc == "cbr":
|
||||
kb = _kbps(bitrate)
|
||||
args += ["-b:v", bitrate, "-minrate", bitrate, "-maxrate", bitrate,
|
||||
"-bufsize", f"{kb}k", "-x264-params", "nal-hrd=cbr"]
|
||||
else: # vbr
|
||||
kb = _kbps(bitrate)
|
||||
args += ["-b:v", bitrate, "-maxrate", bitrate, "-bufsize", f"{kb * 2}k"]
|
||||
if low_latency:
|
||||
args += ["-tune", "zerolatency"]
|
||||
return args
|
||||
|
||||
|
||||
def vp9_args(bitrate: str, low_latency: bool = False,
|
||||
preset: str | None = None, keyint_sec=None) -> list[str]:
|
||||
"""Аргументы кодировщика libvpx-VP9 (профиль H.264 неприменим)."""
|
||||
preset: str | None = None, keyint_sec=None,
|
||||
rc: str = DEFAULT_RC, qp=DEFAULT_QP) -> list[str]:
|
||||
"""Аргументы libvpx-VP9 (профиль H.264 неприменим).
|
||||
|
||||
rc: vbr/cbr — по битрейту; qp — постоянное качество (-crf N -b:v 0, у VP9
|
||||
нет прямого -qp). cpu-used маппится из пресета. realtime для live."""
|
||||
sec = norm_keyint(keyint_sec)
|
||||
# быстрый пресет → выше cpu-used (быстрее, ниже качество); realtime для live
|
||||
cpu = "8" if norm_preset(preset) == "ultrafast" else "6"
|
||||
return [
|
||||
rc = norm_rc(rc)
|
||||
args = [
|
||||
"-c:v", "libvpx-vp9",
|
||||
"-b:v", bitrate,
|
||||
"-maxrate", bitrate,
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", cpu,
|
||||
"-cpu-used", _VP9_CPU.get(norm_preset(preset), "6"),
|
||||
"-row-mt", "1",
|
||||
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
||||
"-pix_fmt", "yuv420p",
|
||||
]
|
||||
if rc == "qp":
|
||||
args += ["-crf", str(norm_qp(qp)), "-b:v", "0"]
|
||||
elif rc == "cbr":
|
||||
args += ["-b:v", bitrate, "-minrate", bitrate, "-maxrate", bitrate]
|
||||
else: # vbr
|
||||
args += ["-b:v", bitrate, "-maxrate", bitrate]
|
||||
return args
|
||||
|
||||
|
||||
def encode_args(codec: str, profile: str, bitrate: str, low_latency: bool = False,
|
||||
preset: str | None = None, keyint_sec=None) -> list[str]:
|
||||
preset: str | None = None, keyint_sec=None,
|
||||
rc: str = DEFAULT_RC, qp=DEFAULT_QP) -> list[str]:
|
||||
"""Аргументы видеокодировщика под выбранный кодек (h264 | vp9)."""
|
||||
if norm_codec(codec) == "vp9":
|
||||
return vp9_args(bitrate, low_latency, preset, keyint_sec)
|
||||
return x264_args(profile, bitrate, low_latency, preset, keyint_sec)
|
||||
return vp9_args(bitrate, low_latency, preset, keyint_sec, rc, qp)
|
||||
return x264_args(profile, bitrate, low_latency, preset, keyint_sec, rc, qp)
|
||||
|
||||
Reference in New Issue
Block a user