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 ..runtime import runtime
|
||||||
from ..services import go2rtc
|
from ..services import go2rtc
|
||||||
from ..transcode import (encode_args, norm_profile, norm_bitrate, norm_preset,
|
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"])
|
router = APIRouter(prefix="/api/cameras", tags=["cameras"])
|
||||||
|
|
||||||
@@ -86,6 +86,8 @@ async def camera_live_transcoded(
|
|||||||
preset: str = Query(None),
|
preset: str = Query(None),
|
||||||
keyint: str = Query(None),
|
keyint: str = Query(None),
|
||||||
codec: str = Query(None),
|
codec: str = Query(None),
|
||||||
|
rc: str = Query(None),
|
||||||
|
qp: str = Query(None),
|
||||||
stream: str = Query("sub"),
|
stream: str = Query("sub"),
|
||||||
):
|
):
|
||||||
"""Live с перекодированием в H.264 (fragmented mp4) под профиль/битрейт клиента.
|
"""Live с перекодированием в H.264 (fragmented mp4) под профиль/битрейт клиента.
|
||||||
@@ -103,7 +105,8 @@ async def camera_live_transcoded(
|
|||||||
"-fflags", "nobuffer", "-flags", "low_delay",
|
"-fflags", "nobuffer", "-flags", "low_delay",
|
||||||
"-rtsp_transport", "tcp", "-i", src,
|
"-rtsp_transport", "tcp", "-i", src,
|
||||||
*encode_args(norm_codec(codec), norm_profile(profile), norm_bitrate(bitrate),
|
*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",
|
"-an",
|
||||||
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
||||||
"-f", "mp4", "pipe:1",
|
"-f", "mp4", "pipe:1",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ from fastapi.responses import FileResponse, StreamingResponse
|
|||||||
|
|
||||||
from ..runtime import runtime
|
from ..runtime import runtime
|
||||||
from ..transcode import (H264_PROFILES, BITRATES, encode_args, norm_profile,
|
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"])
|
router = APIRouter(prefix="/api/recordings", tags=["recordings"])
|
||||||
|
|
||||||
@@ -53,6 +54,8 @@ async def recording_transcoded(
|
|||||||
preset: str = Query(None),
|
preset: str = Query(None),
|
||||||
keyint: str = Query(None),
|
keyint: str = Query(None),
|
||||||
codec: str = Query(None),
|
codec: str = Query(None),
|
||||||
|
rc: str = Query(None),
|
||||||
|
qp: str = Query(None),
|
||||||
start: float = Query(0.0),
|
start: float = Query(0.0),
|
||||||
):
|
):
|
||||||
"""Перекодирование H.265→H.264 на лету (fragmented mp4) — для браузеров без HEVC.
|
"""Перекодирование 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 []),
|
*(["-ss", f"{start:.3f}"] if start and start > 0 else []),
|
||||||
"-i", rec.path,
|
"-i", rec.path,
|
||||||
*encode_args(norm_codec(codec), norm_profile(profile), norm_bitrate(bitrate),
|
*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",
|
"-an",
|
||||||
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
"-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
||||||
"-f", "mp4", "pipe:1",
|
"-f", "mp4", "pipe:1",
|
||||||
|
|||||||
+63
-22
@@ -1,17 +1,25 @@
|
|||||||
"""Общие параметры перекодирования H.264 (профиль, битрейт, пресет, GOP)."""
|
"""Общие параметры перекодирования H.264/VP9: профиль, битрейт, пресет, GOP, режим RC."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
H264_PROFILES = ("baseline", "main", "high")
|
H264_PROFILES = ("baseline", "main", "high")
|
||||||
BITRATES = ("500k", "1000k", "2000k", "4000k", "8000k", "10000k", "16000k", "20000k", "24000k")
|
BITRATES = ("500k", "1000k", "2000k", "4000k", "8000k", "10000k", "16000k", "20000k", "24000k")
|
||||||
PRESETS = ("ultrafast", "superfast")
|
PRESETS = ("ultrafast", "superfast", "veryfast", "faster", "fast", "medium")
|
||||||
KEYINTS = (1, 2, 5) # интервал ключевых кадров, секунды
|
KEYINTS = (1, 2, 5) # интервал ключевых кадров, секунды
|
||||||
CODECS = ("h264", "vp9")
|
CODECS = ("h264", "vp9")
|
||||||
|
RC_MODES = ("vbr", "cbr", "qp") # контроль битрейта: переменный / постоянный / постоянный квантизатор
|
||||||
|
|
||||||
DEFAULT_PROFILE = "main"
|
DEFAULT_PROFILE = "main"
|
||||||
DEFAULT_BITRATE = "2000k"
|
DEFAULT_BITRATE = "2000k"
|
||||||
DEFAULT_PRESET = "superfast"
|
DEFAULT_PRESET = "superfast"
|
||||||
DEFAULT_KEYINT = 2
|
DEFAULT_KEYINT = 2
|
||||||
DEFAULT_CODEC = "h264"
|
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:
|
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
|
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:
|
def norm_keyint(k) -> int:
|
||||||
try:
|
try:
|
||||||
k = int(k)
|
k = int(k)
|
||||||
@@ -38,51 +50,80 @@ def norm_keyint(k) -> int:
|
|||||||
return k if k in KEYINTS else DEFAULT_KEYINT
|
return k if k in KEYINTS else DEFAULT_KEYINT
|
||||||
|
|
||||||
|
|
||||||
def x264_args(profile: str, bitrate: str, low_latency: bool = False,
|
def norm_qp(q) -> int:
|
||||||
preset: str | None = None, keyint_sec=None) -> list[str]:
|
try:
|
||||||
"""Аргументы libx264: профиль, битрейт, пресет и интервал ключевых кадров.
|
q = int(q)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return DEFAULT_QP
|
||||||
|
return max(QP_MIN, min(QP_MAX, q))
|
||||||
|
|
||||||
Ключевые кадры расставляются по времени (через -force_key_frames), поэтому
|
|
||||||
интервал в секундах соблюдается независимо от fps камеры."""
|
def _kbps(bitrate: str) -> int:
|
||||||
bufsize = f"{int(bitrate[:-1]) * 2}k"
|
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)
|
preset = norm_preset(preset)
|
||||||
sec = norm_keyint(keyint_sec)
|
sec = norm_keyint(keyint_sec)
|
||||||
|
rc = norm_rc(rc)
|
||||||
args = [
|
args = [
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
"-preset", preset,
|
"-preset", preset,
|
||||||
"-profile:v", profile,
|
"-profile:v", profile,
|
||||||
"-b:v", bitrate,
|
|
||||||
"-maxrate", bitrate,
|
|
||||||
"-bufsize", bufsize,
|
|
||||||
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
||||||
"-pix_fmt", "yuv420p",
|
"-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:
|
if low_latency:
|
||||||
args += ["-tune", "zerolatency"]
|
args += ["-tune", "zerolatency"]
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def vp9_args(bitrate: str, low_latency: bool = False,
|
def vp9_args(bitrate: str, low_latency: bool = False,
|
||||||
preset: str | None = None, keyint_sec=None) -> list[str]:
|
preset: str | None = None, keyint_sec=None,
|
||||||
"""Аргументы кодировщика libvpx-VP9 (профиль H.264 неприменим)."""
|
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)
|
sec = norm_keyint(keyint_sec)
|
||||||
# быстрый пресет → выше cpu-used (быстрее, ниже качество); realtime для live
|
rc = norm_rc(rc)
|
||||||
cpu = "8" if norm_preset(preset) == "ultrafast" else "6"
|
args = [
|
||||||
return [
|
|
||||||
"-c:v", "libvpx-vp9",
|
"-c:v", "libvpx-vp9",
|
||||||
"-b:v", bitrate,
|
|
||||||
"-maxrate", bitrate,
|
|
||||||
"-deadline", "realtime",
|
"-deadline", "realtime",
|
||||||
"-cpu-used", cpu,
|
"-cpu-used", _VP9_CPU.get(norm_preset(preset), "6"),
|
||||||
"-row-mt", "1",
|
"-row-mt", "1",
|
||||||
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
"-force_key_frames", f"expr:gte(t,n_forced*{sec})",
|
||||||
"-pix_fmt", "yuv420p",
|
"-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,
|
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)."""
|
"""Аргументы видеокодировщика под выбранный кодек (h264 | vp9)."""
|
||||||
if norm_codec(codec) == "vp9":
|
if norm_codec(codec) == "vp9":
|
||||||
return vp9_args(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)
|
return x264_args(profile, bitrate, low_latency, preset, keyint_sec, rc, qp)
|
||||||
|
|||||||
+15
-6
@@ -12,7 +12,7 @@ function post(path) { return fetch(path, { method: "POST" }); }
|
|||||||
// ── клиентские настройки (этот браузер), localStorage ───────────
|
// ── клиентские настройки (этот браузер), localStorage ───────────
|
||||||
const PREFS_KEY = "corevision.prefs";
|
const PREFS_KEY = "corevision.prefs";
|
||||||
// mode: "player" — свой плеер (клиентский WASM-декод H.265), "transcode" — серверный транскод
|
// mode: "player" — свой плеер (клиентский WASM-декод H.265), "transcode" — серверный транскод
|
||||||
const PROFILE_DEFAULT = { mode: "player", codec: "h264", profile: "main", bitrate: "2000k", preset: "superfast", keyint: 2 };
|
const PROFILE_DEFAULT = { mode: "player", codec: "h264", profile: "main", bitrate: "2000k", preset: "superfast", keyint: 2, rc: "vbr", qp: 23 };
|
||||||
const PROF_KEY = { "1": "prof1", "2": "prof2", "a": "profA" }; // 1 поток / 2 поток / архив
|
const PROF_KEY = { "1": "prof1", "2": "prof2", "a": "profA" }; // 1 поток / 2 поток / архив
|
||||||
// общие пресеты перекодирования — выставляют все параметры профиля разом
|
// общие пресеты перекодирования — выставляют все параметры профиля разом
|
||||||
const TR_PRESETS = {
|
const TR_PRESETS = {
|
||||||
@@ -65,7 +65,7 @@ function setProfilePref(which, patch) {
|
|||||||
function transcodeQueryFor(which) { // строка запроса под профиль
|
function transcodeQueryFor(which) { // строка запроса под профиль
|
||||||
const e = encodeURIComponent, pr = transcodeProfile(which);
|
const e = encodeURIComponent, pr = transcodeProfile(which);
|
||||||
return `profile=${e(pr.profile)}&bitrate=${e(pr.bitrate)}&preset=${e(pr.preset)}` +
|
return `profile=${e(pr.profile)}&bitrate=${e(pr.bitrate)}&preset=${e(pr.preset)}` +
|
||||||
`&keyint=${e(pr.keyint)}&codec=${e(pr.codec)}`;
|
`&keyint=${e(pr.keyint)}&codec=${e(pr.codec)}&rc=${e(pr.rc)}&qp=${e(pr.qp)}`;
|
||||||
}
|
}
|
||||||
function profileMode(which) { // "native" | "player" | "webcodecs" | "transcode"
|
function profileMode(which) { // "native" | "player" | "webcodecs" | "transcode"
|
||||||
const m = transcodeProfile(which).mode;
|
const m = transcodeProfile(which).mode;
|
||||||
@@ -1461,9 +1461,11 @@ const Settings = {
|
|||||||
presets +
|
presets +
|
||||||
`<label>Кодек${sel("codec", [["h264", "H.264"], ["vp9", "VP9"]])}</label>` +
|
`<label>Кодек${sel("codec", [["h264", "H.264"], ["vp9", "VP9"]])}</label>` +
|
||||||
`<label>Профиль H.264${sel("profile", [["baseline", "baseline"], ["main", "main"], ["high", "high"]])}</label>` +
|
`<label>Профиль H.264${sel("profile", [["baseline", "baseline"], ["main", "main"], ["high", "high"]])}</label>` +
|
||||||
|
`<label>Контроль битрейта${sel("rc", [["vbr", "VBR (переменный)"], ["cbr", "CBR (постоянный)"], ["qp", "QP (постоянный)"]])}</label>` +
|
||||||
`<label>Битрейт${sel("bitrate", [["500k", "0,5 Мбит/с"], ["1000k", "1 Мбит/с"], ["2000k", "2 Мбит/с"], ["4000k", "4 Мбит/с"], ["8000k", "8 Мбит/с"], ["10000k", "10 Мбит/с"], ["16000k", "16 Мбит/с"], ["20000k", "20 Мбит/с"], ["24000k", "24 Мбит/с"]])}</label>` +
|
`<label>Битрейт${sel("bitrate", [["500k", "0,5 Мбит/с"], ["1000k", "1 Мбит/с"], ["2000k", "2 Мбит/с"], ["4000k", "4 Мбит/с"], ["8000k", "8 Мбит/с"], ["10000k", "10 Мбит/с"], ["16000k", "16 Мбит/с"], ["20000k", "20 Мбит/с"], ["24000k", "24 Мбит/с"]])}</label>` +
|
||||||
|
`<label>QP (квантизатор)<input type="number" data-f="qp" min="0" max="51" value="${pr.qp}" style="width:72px"></label>` +
|
||||||
`<label>Интервал ключевых кадров${sel("keyint", [[1, "1 секунда"], [2, "2 секунды"], [5, "5 секунд"]])}</label>` +
|
`<label>Интервал ключевых кадров${sel("keyint", [[1, "1 секунда"], [2, "2 секунды"], [5, "5 секунд"]])}</label>` +
|
||||||
`<label>Пресет (Preset)${sel("preset", [["ultrafast", "Ultrafast"], ["superfast", "Superfast"]])}</label>`
|
`<label>Пресет (Preset)${sel("preset", [["ultrafast", "Ultrafast"], ["superfast", "Superfast"], ["veryfast", "Veryfast"], ["faster", "Faster"], ["fast", "Fast"], ["medium", "Medium"]])}</label>`
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1492,6 +1494,12 @@ const Settings = {
|
|||||||
const prof = row.querySelector('[data-f="profile"]');
|
const prof = row.querySelector('[data-f="profile"]');
|
||||||
const syncCodec = () => { prof.disabled = codec.value === "vp9"; };
|
const syncCodec = () => { prof.disabled = codec.value === "vp9"; };
|
||||||
syncCodec();
|
syncCodec();
|
||||||
|
// режим контроля битрейта: VBR/CBR → поле «Битрейт», QP → поле «QP»
|
||||||
|
const rcSel = row.querySelector('[data-f="rc"]');
|
||||||
|
const brLabel = row.querySelector('[data-f="bitrate"]').closest("label");
|
||||||
|
const qpLabel = row.querySelector('[data-f="qp"]').closest("label");
|
||||||
|
const syncRc = () => { const isQp = rcSel.value === "qp"; qpLabel.hidden = !isQp; brLabel.hidden = isQp; };
|
||||||
|
syncRc();
|
||||||
// подсветить пресет, совпадающий с текущими значениями
|
// подсветить пресет, совпадающий с текущими значениями
|
||||||
const highlight = () => {
|
const highlight = () => {
|
||||||
const cur = { codec: codec.value, profile: prof.value, bitrate: row.querySelector('[data-f="bitrate"]').value,
|
const cur = { codec: codec.value, profile: prof.value, bitrate: row.querySelector('[data-f="bitrate"]').value,
|
||||||
@@ -1505,12 +1513,12 @@ const Settings = {
|
|||||||
const p = TR_PRESETS[btn.dataset.preset];
|
const p = TR_PRESETS[btn.dataset.preset];
|
||||||
const put = (f, v) => { const s = row.querySelector(`[data-f="${f}"]`); if (s) s.value = String(v); };
|
const put = (f, v) => { const s = row.querySelector(`[data-f="${f}"]`); if (s) s.value = String(v); };
|
||||||
put("codec", p.codec); put("profile", p.profile); put("bitrate", p.bitrate);
|
put("codec", p.codec); put("profile", p.profile); put("bitrate", p.bitrate);
|
||||||
put("keyint", p.keyint); put("preset", p.preset);
|
put("keyint", p.keyint); put("preset", p.preset); put("rc", "vbr");
|
||||||
syncCodec(); highlight();
|
syncCodec(); syncRc(); highlight();
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
row.querySelectorAll("select[data-f]").forEach((s) => {
|
row.querySelectorAll("select[data-f]").forEach((s) => {
|
||||||
s.addEventListener("change", () => { syncCodec(); highlight(); });
|
s.addEventListener("change", () => { syncCodec(); syncRc(); highlight(); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("prefs-save").onclick = () => {
|
document.getElementById("prefs-save").onclick = () => {
|
||||||
@@ -1524,6 +1532,7 @@ const Settings = {
|
|||||||
mode: sel ? sel.value : "player",
|
mode: sel ? sel.value : "player",
|
||||||
codec: get("codec"), profile: get("profile"), bitrate: get("bitrate"),
|
codec: get("codec"), profile: get("profile"), bitrate: get("bitrate"),
|
||||||
preset: get("preset"), keyint: +get("keyint"),
|
preset: get("preset"), keyint: +get("keyint"),
|
||||||
|
rc: get("rc"), qp: +get("qp"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
saved.textContent = "Сохранено в этом браузере ✓";
|
saved.textContent = "Сохранено в этом браузере ✓";
|
||||||
|
|||||||
Reference in New Issue
Block a user