/** * 视频右侧栏 OCR 结果 canvas 复刻绘制。 * 数据源:CDN 上的分P全量 JSON({space:[w,h], recs:[[ts,[[x1,y1,x2,y2,text],...]],...]}) * 或服务器注入的单帧快照({ts, lines:[[x1,y1,x2,y2,text],...]})。 */ (function () { // 按框坐标 1:1 复刻:黑底、黄框、白字,字号按框高自适应 function drawFrame(canvas, space, lines) { const sw = space[0], sh = space[1]; const cssWidth = canvas.clientWidth || 200; const scale = cssWidth / sw; const cssHeight = Math.max(1, Math.round(sh * scale)); const dpr = window.devicePixelRatio || 1; canvas.width = Math.round(cssWidth * dpr); canvas.height = Math.round(cssHeight * dpr); canvas.style.height = cssHeight + "px"; const ctx = canvas.getContext("2d"); ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0); ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, sw, sh); ctx.strokeStyle = "#eab308"; ctx.fillStyle = "#ffffff"; ctx.textBaseline = "middle"; lines.forEach(function (line) { const x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3], text = line[4]; ctx.lineWidth = 2 / scale; ctx.strokeRect(x1, y1, x2 - x1, y2 - y1); if (text) { ctx.font = Math.max(4, (y2 - y1) * 0.75) + "px sans-serif"; ctx.fillText(text, x1 + 2 / scale, (y1 + y2) / 2, x2 - x1 - 4 / scale); } }); } function formatHms(seconds) { seconds = Math.max(0, Math.floor(seconds)); const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; const pad = (n) => String(n).padStart(2, "0"); return h + ":" + pad(m) + ":" + pad(s); } // 单帧 space:按本帧最大框坐标向上取整到 16 的倍数(与导出端一致) function frameSpace(lines) { let maxX = 0, maxY = 0; lines.forEach(function (line) { maxX = Math.max(maxX, line[2]); maxY = Math.max(maxY, line[3]); }); return [Math.max(16, Math.ceil(maxX / 16) * 16), Math.max(16, Math.ceil(maxY / 16) * 16)]; } // 稿件详情页:CDN 全量 + 进度条联动(展示 ts<=t 最近一条) function initPlayer(panel) { const urls = JSON.parse(panel.dataset.urls || "{}"); const canvas = panel.querySelector("canvas"); const slider = panel.querySelector("input[type=range]"); const timeLabel = panel.querySelector("[data-ocr-time]"); const status = panel.querySelector("[data-ocr-status]"); const tabs = panel.querySelectorAll("[data-ocr-part]"); let data = null; let loadSeq = 0; function showStatus(text) { if (status) { status.textContent = text; status.style.display = text ? "" : "none"; } } function currentIndex(t) { // 二分:ts<=t 的最后一条;t 早于首条时展示首条 const recs = data.recs; let lo = 0, hi = recs.length - 1, ans = 0; while (lo <= hi) { const mid = (lo + hi) >> 1; if (recs[mid][0] <= t) { ans = mid; lo = mid + 1; } else { hi = mid - 1; } } return ans; } function render() { if (!data || data.recs.length === 0) { return; } const t = parseFloat(slider.value); const idx = currentIndex(t); timeLabel.textContent = formatHms(t) + "(OCR @" + formatHms(data.recs[idx][0]) + ")"; drawFrame(canvas, data.space, data.recs[idx][1]); } function load(partNum) { const seq = ++loadSeq; data = null; showStatus("加载中…"); tabs.forEach(function (tab) { tab.classList.toggle("font-bold", tab.dataset.ocrPart === String(partNum)); }); fetch(urls[partNum]) .then((response) => { if (!response.ok) { throw new Error("HTTP " + response.status); } return response.json(); }) .then((payload) => { if (seq !== loadSeq) { return; } data = payload; slider.max = payload.recs.length ? Math.floor(payload.recs[payload.recs.length - 1][0]) : 0; showStatus(""); render(); }) .catch(() => { if (seq !== loadSeq) { return; } showStatus("OCR 数据加载失败"); }); } slider.addEventListener("input", render); tabs.forEach(function (tab) { tab.addEventListener("click", function () { slider.value = 0; load(tab.dataset.ocrPart); }); }); const firstPart = Object.keys(urls)[0]; if (firstPart !== undefined) { load(firstPart); } } // 节目详情页:服务器注入的单帧快照 function initSnapshots(scope) { (scope || document).querySelectorAll("canvas[data-ocr-frame]").forEach(function (canvas) { const frame = JSON.parse(canvas.dataset.ocrFrame); drawFrame(canvas, frameSpace(frame.lines), frame.lines); }); } window.OcrCanvas = { drawFrame: drawFrame, initPlayer: initPlayer, initSnapshots: initSnapshots, }; })();