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:
2026-06-01 16:38:04 +05:00
parent 5828873910
commit c876a46e61
5 changed files with 90 additions and 33 deletions
+15 -6
View File
@@ -12,7 +12,7 @@ function post(path) { return fetch(path, { method: "POST" }); }
// ── клиентские настройки (этот браузер), localStorage ───────────
const PREFS_KEY = "corevision.prefs";
// 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 TR_PRESETS = {
@@ -65,7 +65,7 @@ function setProfilePref(which, patch) {
function transcodeQueryFor(which) { // строка запроса под профиль
const e = encodeURIComponent, pr = transcodeProfile(which);
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"
const m = transcodeProfile(which).mode;
@@ -1461,9 +1461,11 @@ const Settings = {
presets +
`<label>Кодек${sel("codec", [["h264", "H.264"], ["vp9", "VP9"]])}</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>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>Пресет (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 syncCodec = () => { prof.disabled = codec.value === "vp9"; };
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 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 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("keyint", p.keyint); put("preset", p.preset);
syncCodec(); highlight();
put("keyint", p.keyint); put("preset", p.preset); put("rc", "vbr");
syncCodec(); syncRc(); highlight();
};
});
row.querySelectorAll("select[data-f]").forEach((s) => {
s.addEventListener("change", () => { syncCodec(); highlight(); });
s.addEventListener("change", () => { syncCodec(); syncRc(); highlight(); });
});
});
document.getElementById("prefs-save").onclick = () => {
@@ -1524,6 +1532,7 @@ const Settings = {
mode: sel ? sel.value : "player",
codec: get("codec"), profile: get("profile"), bitrate: get("bitrate"),
preset: get("preset"), keyint: +get("keyint"),
rc: get("rc"), qp: +get("qp"),
});
});
saved.textContent = "Сохранено в этом браузере ✓";