init
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user