Плеер и перекодирование — настройки этого браузера
- Для каждого потока выберите способ воспроизведения: как есть —
- поток отдаётся без обработки (ffmpeg -c copy), браузер декодирует HEVC сам
- (нужна аппаратная поддержка); встроенный плеер — декодирует H.265
- в браузере софтом (WASM hevc.js, в Web Worker), тянет и основной поток
- (3200×1800, включая тайлы), и субпоток, без нагрузки на сервер (декод
- нагружает CPU клиента); WebCodecs (железо) — аппаратный декод H.265 на GPU клиента,
- тянет основной поток 3200×1800 при 0% CPU сервера (рекомендуется для
- основного потока); перекодировать — сервер транскодирует в H.264 /
- VP9 (нагрузка на CPU сервера, для слабых клиентов).
+ Для каждого потока выберите способ воспроизведения: встроенный плеер —
+ декодирует поток прямо в браузере без нагрузки на сервер (ffmpeg -c copy):
+ H.265/H.265+ — софтом (WASM hevc.js в Web Worker), H.264/H.264+ — средствами
+ браузера; тянет и основной поток (3200×1800, включая тайлы), и субпоток
+ (декод нагружает CPU клиента); перекодировать — сервер транскодирует
+ в H.264 / VP9 (нагрузка на CPU сервера, для слабых клиентов).
1 поток (основной)
-
-
@@ -178,9 +173,7 @@
2 поток (суб)
-
-
@@ -189,7 +182,6 @@
Архив
-
diff --git a/frontend/static/app.js b/frontend/static/app.js
index 04f6923..db723af 100644
--- a/frontend/static/app.js
+++ b/frontend/static/app.js
@@ -69,9 +69,9 @@ function transcodeQueryFor(which) { // строка запрос
return `profile=${e(pr.profile)}&bitrate=${e(pr.bitrate)}&preset=${e(pr.preset)}` +
`&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;
- return (m === "transcode" || m === "native" || m === "webcodecs") ? m : "player";
+function profileMode(which) { // "player" | "transcode"
+ // режимы "native" (как есть) и "webcodecs" (железо) упразднены → встроенный плеер
+ return transcodeProfile(which).mode === "transcode" ? "transcode" : "player";
}
// раскладки: маппинг камер по ячейкам, на размерность (этот браузер)
function getLayout(dim) {
@@ -198,19 +198,6 @@ function connectStatusWS(onCameras) {
ws.onclose = () => setTimeout(() => connectStatusWS(onCameras), 3000);
}
-// ── go2rtc плеер ────────────────────────────────────────────────
-let _rtcLoaded = null;
-function loadRtcComponent(go2rtcUrl) {
- if (_rtcLoaded) return _rtcLoaded;
- _rtcLoaded = new Promise((resolve, reject) => {
- const s = document.createElement("script");
- s.src = go2rtcUrl.replace(/\/$/, "") + "/video-rtc.js";
- s.onload = resolve;
- s.onerror = () => reject(new Error("go2rtc video-rtc.js не загрузился"));
- document.head.appendChild(s);
- });
- return _rtcLoaded;
-}
// ── встроенный плеер: hevc.js (open-source WASM HEVC) в Web Worker, рендер в OffscreenCanvas ──
// Декод H.265 (включая тайлы) + рендер целиком в отдельном потоке: главный поток свободен,
// сетка из нескольких камер не тормозит. Сервер отдаёт поток как есть (-c:v copy → 0% CPU).
@@ -219,8 +206,10 @@ const HEVC_WASM_JS = "/static/vendor/hevcjs/hevc-decode.esm.js";
const HEVC_WASM_BIN = "/static/vendor/hevcjs/dist/wasm/hevc-decode.wasm";
class HevcWasmPlayer {
- constructor(box, wsUrl, useMain, onFail) {
+ // external:true — не открывать свой WS; FLV-байты подаются методом feed() (SmartFlvPlayer)
+ constructor(box, wsUrl, useMain, onFail, opts) {
this.box = box; this.wsUrl = wsUrl; this.onFail = onFail || null;
+ this.external = !!(opts && opts.external); this.ready = false; this.queue = [];
this.closed = false; this.worker = null; this.ws = null;
const cv = document.createElement("canvas");
cv.className = "wc-canvas";
@@ -234,8 +223,11 @@ class HevcWasmPlayer {
try { this.worker = new Worker(HEVC_WORKER_URL, { type: "module" }); } catch (e) { this._fail("worker"); return; }
this.worker.onmessage = (e) => {
const m = e.data;
- if (m.type === "ready") this._openWS();
- else if (m.type === "err") console.warn("HEVC worker:", m.msg);
+ if (m.type === "ready") {
+ this.ready = true;
+ if (this.external) { for (const b of this.queue) this._send(b); this.queue = []; }
+ else this._openWS();
+ } else if (m.type === "err") console.warn("HEVC worker:", m.msg);
};
this.worker.onerror = (e) => { console.warn("HEVC worker.onerror:", e && e.message); this._fail("worker-error"); };
this.worker.postMessage(
@@ -251,6 +243,9 @@ class HevcWasmPlayer {
this.ws.onclose = () => { /**/ };
} catch (e) { /**/ }
}
+ _send(buf) { if (!this.closed && this.worker) try { this.worker.postMessage({ type: "data", buf }, [buf]); } catch (e) { /**/ } }
+ // внешняя подача FLV-байт (external-режим): до готовности воркера — в очередь
+ feed(buf) { if (this.closed) return; if (this.ready) this._send(buf); else this.queue.push(buf); }
_fail(reason) {
if (this.closed) return;
const f = this.onFail; this.onFail = null;
@@ -293,55 +288,7 @@ function bindFullscreenKey() {
});
}
-// умеет ли браузер декодировать HEVC (H.265) сам — для нативного просмотра
-let _hevc = null;
-function hevcSupported() {
- if (_hevc !== null) return _hevc;
- let ok = false;
- try {
- const v = document.createElement("video");
- const t = v.canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"');
- if (t === "probably" || t === "maybe") ok = true;
- if (!ok && window.MediaSource && MediaSource.isTypeSupported) {
- ok = MediaSource.isTypeSupported('video/mp4; codecs="hvc1.1.6.L93.B0"') ||
- MediaSource.isTypeSupported('video/mp4; codecs="hev1.1.6.L93.B0"');
- }
- } catch (e) { /* ignore */ }
- _hevc = ok;
- return ok;
-}
-
-// нативный плеер go2rtc (open-source video-rtc.js): MSE/WebRTC, поддерживает H.265
-function makePlayer(go2rtcUrl, srcName, transcode) {
- const el = document.createElement("video-stream");
- // mse раньше webrtc — в Chrome HEVC доступен только в MSE; webrtc — фолбэк (Safari)
- el.setAttribute("mode", transcode ? "webrtc,mse" : "mse,webrtc");
- el.setAttribute("background", "false");
- const base = go2rtcUrl.replace(/\/$/, "");
- el.src = new URL(base + "/api/ws?src=" + encodeURIComponent(srcName));
- return el;
-}
-
-// ── свой WebCodecs-плеер H.265 (аппаратный декод на GPU клиента, 0% CPU сервера) ──
-// есть ли в браузере аппаратный HEVC-декодер через WebCodecs (кэш Promise)
-let _hevcHw = null;
-function hevcHwSupported() {
- if (_hevcHw) return _hevcHw;
- _hevcHw = (async () => {
- if (!("VideoDecoder" in window) || !window.isSecureContext) return false;
- const cands = ["hvc1.1.6.L153.B0", "hev1.1.6.L153.B0", "hvc1.2.4.L153.B0"];
- for (const codec of cands) {
- try {
- const r = await VideoDecoder.isConfigSupported(
- { codec, codedWidth: 3200, codedHeight: 1800, hardwareAcceleration: "prefer-hardware" });
- if (r && r.supported) return true;
- } catch (e) { /* пробуем следующий */ }
- }
- return false;
- })();
- return _hevcHw;
-}
-
+// ── WebCodecs-плеер: декод на клиенте (для H.264 — единственный путь, H.265 идёт в WASM) ──
// строка кодека из HEVCDecoderConfigurationRecord (hvcC)
function hevcCodecFromHvcC(hvcC) {
try {
@@ -352,11 +299,21 @@ function hevcCodecFromHvcC(hvcC) {
} catch (e) { return "hvc1.1.6.L153.B0"; }
}
-// демуксер FLV (enhanced-RTMP) → hvcC + length-prefixed HEVC-кадры
+// строка кодека из AVCDecoderConfigurationRecord (avcC):
+// [1]=profile_idc, [2]=constraint_flags, [3]=level_idc → "avc1.PPCCLL" (hex)
+function avcCodecFromAvcC(avcC) {
+ try {
+ const h2 = (n) => n.toString(16).padStart(2, "0");
+ return `avc1.${h2(avcC[1])}${h2(avcC[2])}${h2(avcC[3])}`;
+ } catch (e) { return "avc1.4d0033"; }
+}
+
+// демуксер FLV (enhanced-RTMP) → hvcC/avcC + length-prefixed кадры (HEVC или AVC)
class FlvHevcDemuxer {
constructor({ onConfig, onSample }) {
this.onConfig = onConfig; this.onSample = onSample;
this.buf = new Uint8Array(0); this.headerDone = false; this.gotConfig = false;
+ this.codecType = "hevc"; // 'hevc' | 'avc' — определяется из видеотега
}
push(ab) {
const inc = new Uint8Array(ab);
@@ -395,7 +352,9 @@ class FlvHevcDemuxer {
if (isEx) {
const frameType = (b0 >> 4) & 0x07, packetType = b0 & 0x0f;
const fourcc = String.fromCharCode(body[1], body[2], body[3], body[4]);
- if (fourcc !== "hvc1" && fourcc !== "hev1") return;
+ if (fourcc === "hvc1" || fourcc === "hev1") this.codecType = "hevc";
+ else if (fourcc === "avc1") this.codecType = "avc";
+ else return;
let off = 5;
if (packetType === 1) off += 3; // CompositionTime
const payload = body.subarray(off);
@@ -403,16 +362,25 @@ class FlvHevcDemuxer {
else if (packetType === 1 || packetType === 3) this._frames(payload, frameType === 1, tsMs);
} else {
const codecId = b0 & 0x0f, frameType = (b0 >> 4) & 0x0f;
- if (codecId !== 12 && codecId !== 7) return; // 12=HEVC legacy, 7=AVC
+ if (codecId === 12) this.codecType = "hevc"; // 12=HEVC legacy
+ else if (codecId === 7) this.codecType = "avc"; // 7=AVC (H.264)
+ else return;
const pkt = body[1], payload = body.subarray(5);
if (pkt === 0) this._config(payload);
else if (pkt === 1) this._frames(payload, frameType === 1, tsMs);
}
}
- _config(hvcC) {
- if (this.gotConfig || hvcC.length < 13) return;
- this.gotConfig = true;
- this.onConfig(hvcC.slice(), hevcCodecFromHvcC(hvcC));
+ _config(rec) {
+ if (this.gotConfig) return;
+ if (this.codecType === "avc") {
+ if (rec.length < 7) return;
+ this.gotConfig = true;
+ this.onConfig(rec.slice(), avcCodecFromAvcC(rec), "avc");
+ } else {
+ if (rec.length < 13) return;
+ this.gotConfig = true;
+ this.onConfig(rec.slice(), hevcCodecFromHvcC(rec), "hevc");
+ }
}
_frames(payload, isKey, tsMs) {
if (!this.gotConfig || payload.length === 0) return;
@@ -421,9 +389,12 @@ class FlvHevcDemuxer {
}
// плеер: FLV(ws) → FlvHevcDemuxer → VideoDecoder (HW) →