Files
CoRE.Vision/frontend/static/hevctest.html
T
2026-05-30 00:08:59 +05:00

217 lines
9.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>hevc.js (WASM, тайлы) — CoRE.Vision</title>
<style>
body { background:#0d0d0d; color:#ddd; font:13px system-ui,Arial; margin:0; padding:12px; }
#cv { background:#000; border:1px solid #333; display:block; margin:10px 0; width:100%; max-width:900px; height:auto; }
#log { white-space:pre-wrap; font:12px Consolas,monospace; color:#9f9; max-height:240px; overflow:auto; }
button { font:13px system-ui; padding:4px 10px; }
#stat { color:#ffd479; font-weight:bold; margin-left:12px; }
</style>
<!-- index.js статически импортит "mp4box" (для FMP4Demuxer, не используем) → подменяем заглушкой -->
<script type="importmap">
{ "imports": { "mp4box": "/static/vendor/hevcjs/mp4box-stub.js" } }
</script>
<!-- glue эмскриптена: UMD, выставляет глобал HEVCDecoderModule; create() его подхватит -->
<script src="/static/vendor/hevcjs/dist/wasm/hevc-decode.js"></script>
</head>
<body>
<div>
<button id="gomain">▶ main 3200×1800</button>
<button id="gosub">▶ sub 640×360</button>
<button id="stop">⏹ Stop</button>
<span id="stat"></span>
</div>
<canvas id="cv" width="960" height="540"></canvas>
<div id="log"></div>
<script type="module">
import { HEVCDecoder, FrameRenderer } from "/static/vendor/hevcjs/hevc-core.js";
const logEl = document.getElementById("log"), statEl = document.getElementById("stat");
const log = (...a) => { logEl.textContent += a.map((x) => typeof x === "object" ? JSON.stringify(x) : String(x)).join(" ") + "\n"; logEl.scrollTop = 1e9; console.log("[hevctest]", ...a); };
window.onerror = (m, s, l) => log("‼ onerror", m, "@", l);
window.onunhandledrejection = (e) => log("‼ promise", e.reason && (e.reason.message || e.reason));
// ── строка кодека из hvcC (для лога) ──
function hevcCodecFromHvcC(hvcC) {
try { const p = hvcC[1] & 0x1f, t = ((hvcC[1] >> 5) & 1) ? "H" : "L", lv = hvcC[12] || 153; return `hvc1.${p}.6.${t}${lv}.B0`; }
catch (e) { return "hvc1.1.6.L153.B0"; }
}
// ── параметр-сеты (VPS/SPS/PPS) из hvcC → Annex-B ──
function hvccParamSetsToAnnexB(hvcC) {
if (hvcC.length < 23) return new Uint8Array(0);
let o = 22; const numArrays = hvcC[o++]; const chunks = [];
for (let a = 0; a < numArrays; a++) {
o++; // байт: array_completeness(1)|reserved(1)|NAL_unit_type(6) — тип не нужен
const num = (hvcC[o] << 8) | hvcC[o + 1]; o += 2;
for (let n = 0; n < num; n++) {
const len = (hvcC[o] << 8) | hvcC[o + 1]; o += 2;
chunks.push(hvcC.subarray(o, o + len)); o += len;
}
}
let total = 0; for (const c of chunks) total += 4 + c.length;
const out = new Uint8Array(total); let p = 0;
for (const c of chunks) { out[p + 3] = 1; p += 4; out.set(c, p); p += c.length; }
return out;
}
// ── length-prefixed (4 байта длины) NAL → Annex-B (00 00 00 01), размер не меняется ──
function toAnnexB(u8) {
const out = new Uint8Array(u8.length); out.set(u8);
let i = 0;
while (i + 4 <= out.length) {
const len = ((u8[i] << 24) | (u8[i + 1] << 16) | (u8[i + 2] << 8) | u8[i + 3]) >>> 0;
out[i] = 0; out[i + 1] = 0; out[i + 2] = 0; out[i + 3] = 1;
if (len === 0) break; i += 4 + len;
}
return out;
}
// ── демуксер FLV (enhanced-RTMP) → hvcC + length-prefixed HEVC-кадры (из app.js) ──
class FlvHevcDemuxer {
constructor({ onConfig, onSample }) {
this.onConfig = onConfig; this.onSample = onSample;
this.buf = new Uint8Array(0); this.headerDone = false; this.gotConfig = false;
}
push(ab) {
const inc = new Uint8Array(ab);
if (this.buf.length === 0) { this.buf = inc; }
else { const m = new Uint8Array(this.buf.length + inc.length); m.set(this.buf, 0); m.set(inc, this.buf.length); this.buf = m; }
this._parse();
}
_u32(o) { return ((this.buf[o] << 24) | (this.buf[o + 1] << 16) | (this.buf[o + 2] << 8) | this.buf[o + 3]) >>> 0; }
_parse() {
const b = this.buf, len = b.length;
if (!this.headerDone) {
if (len < 9) return;
const dataOffset = this._u32(5);
this.buf = b.slice(dataOffset); this.headerDone = true;
return this._parse();
}
let o = 0;
while (true) {
if (o + 4 + 11 > len) break;
const ts = o + 4;
const tagType = b[ts] & 0x1f;
const dataSize = (b[ts + 1] << 16) | (b[ts + 2] << 8) | b[ts + 3];
const bodyStart = ts + 11, bodyEnd = bodyStart + dataSize;
if (bodyEnd > len) break;
if (tagType === 9) {
const tsMs = ((b[ts + 7] << 24) >>> 0) | (b[ts + 4] << 16) | (b[ts + 5] << 8) | b[ts + 6];
this._videoTag(b.subarray(bodyStart, bodyEnd), tsMs);
}
o = bodyEnd;
}
if (o > 0) this.buf = b.slice(o);
}
_videoTag(body, tsMs) {
if (body.length < 5) return;
const b0 = body[0], isEx = (b0 & 0x80) !== 0;
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;
let off = 5;
if (packetType === 1) off += 3;
const payload = body.subarray(off);
if (packetType === 0) this._config(payload);
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;
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));
}
_frames(payload, isKey, tsMs) {
if (!this.gotConfig || payload.length === 0) return;
this.onSample({ data: payload.slice(), type: isKey ? "key" : "delta", timestampUs: tsMs * 1000 });
}
}
// ── состояние ──
let decoder = null, renderer = null, demux = null, ws = null;
let statTimer = null, rafId = 0, latest = null;
let fedAU = 0, decoded = 0, rendered = 0, gotInfo = false;
function stop() {
if (statTimer) { clearInterval(statTimer); statTimer = null; }
if (rafId) { cancelAnimationFrame(rafId); rafId = 0; }
try { ws && ws.close(); } catch (e) {} ws = null;
try { renderer && renderer.destroy(); } catch (e) {} renderer = null;
try { decoder && decoder.destroy(); } catch (e) {} decoder = null;
demux = null; latest = null;
fedAU = decoded = rendered = 0; gotInfo = false;
statEl.textContent = "";
}
async function start(stream) {
stop();
const cv = document.getElementById("cv");
log("создаю декодер…");
try {
decoder = await HEVCDecoder.create({
wasmUrl: "/static/vendor/hevcjs/dist/wasm/hevc-decode.js",
wasmBinaryUrl: "/static/vendor/hevcjs/dist/wasm/hevc-decode.wasm",
});
} catch (e) { log("‼ create:", e.message || e); return; }
log("декодер готов");
renderer = new FrameRenderer();
renderer.initCanvas(cv); // принудительно WebGL-путь (без MediaStreamTrackGenerator)
log("FrameRenderer WebGL ок; supportsMediaStream=", FrameRenderer.supportsMediaStream);
demux = new FlvHevcDemuxer({
onConfig: (hvcC, codec) => {
log("config: codec=", codec, "hvcClen=", hvcC.length);
const ps = hvccParamSetsToAnnexB(hvcC);
log("param-sets Annex-B=", ps.length, "байт");
try { if (ps.length) { decoder.feed(ps); pump(); } } catch (e) { log("‼ feed(ps):", e.message || e); }
},
onSample: ({ data, type, timestampUs }) => {
fedAU++;
try { decoder.feed(toAnnexB(data)); } catch (e) { log("‼ feed:", e.message || e); return; }
pump();
},
});
const proto = location.protocol === "https:" ? "wss" : "ws";
const url = `${proto}://${location.host}/api/cameras/cam32/flv?stream=${stream}`;
log("WS →", url);
ws = new WebSocket(url); ws.binaryType = "arraybuffer";
ws.onopen = () => log("WS open");
ws.onmessage = (e) => { try { demux.push(e.data); } catch (err) { log("‼ demux:", err.message || err); } };
ws.onerror = () => log("WS error");
ws.onclose = (ev) => log("WS closed", ev.code);
statTimer = setInterval(() => {
if (!gotInfo && decoder && decoder.info) { gotInfo = true; log("INFO:", JSON.stringify(decoder.info)); }
statEl.textContent = `AU fed=${fedAU} декод=${decoded}/с показ=${rendered}/с`;
log(`декод=${decoded}/с показ=${rendered}/с (AU=${fedAU})`);
decoded = 0; rendered = 0;
}, 1000);
}
function pump() {
let frames;
try { frames = decoder.drain(); } catch (e) { log("‼ drain:", e.message || e); return; }
for (const f of frames) { decoded++; try { renderer.renderFrame(f, decoded * 40000); rendered++; } catch (e) { log("‼ render:", e.message || e); } }
}
document.getElementById("gomain").onclick = () => start("main");
document.getElementById("gosub").onclick = () => start("sub");
document.getElementById("stop").onclick = stop;
log("загружено. secure=", isSecureContext, "| COI=", crossOriginIsolated, "| HEVCDecoderModule=", typeof globalThis.HEVCDecoderModule);
</script>
</body>
</html>