From 77e0845a714914058383258d6ccadcc9b4a7e212 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 14 Aug 2026 21:53:36 +1000 Subject: [PATCH] =?UTF-8?q?v3:=20hardened=20opencv.js=20loader=20=E2=80=94?= =?UTF-8?q?=20fetch=20with=20download=20progress,=20robust=20runtime=20det?= =?UTF-8?q?ection,=2045s=20failover,=20retryable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/ar/solve-cv.js | 80 ++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/public/js/ar/solve-cv.js b/public/js/ar/solve-cv.js index 7b79ee3..4e2a295 100644 --- a/public/js/ar/solve-cv.js +++ b/public/js/ar/solve-cv.js @@ -26,32 +26,66 @@ export function isCvReady() { return !!(_cv && _cv.Mat); } export function getCv() { return _cv; } export function _injectCv(m) { _cv = m; } // test hook (node) -export function loadOpenCV(url = '/vendor/opencv.js') { +export function loadOpenCV(url = '/vendor/opencv.js', onProgress = null) { if (isCvReady()) return Promise.resolve(_cv); if (_loading) return _loading; - _loading = new Promise((resolve, reject) => { - const s = document.createElement('script'); - s.src = url; - s.async = true; - s.onerror = () => reject(new Error('opencv.js failed to load')); - s.onload = () => { + _loading = (async () => { + // 1) download with progress (13MB — visitors on exhibit wifi deserve a %) + const resp = await fetch(url); + if (!resp.ok) throw new Error(`opencv.js HTTP ${resp.status}`); + const total = +resp.headers.get('content-length') || 0; + let blob; + if (resp.body && resp.body.getReader) { + const reader = resp.body.getReader(); + const chunks = []; + let got = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); got += value.length; + if (onProgress) onProgress(total ? Math.round(got / total * 100) : Math.round(got / 1e6) + 'MB'); + } + blob = new Blob(chunks, { type: 'text/javascript' }); + } else { + blob = await resp.blob(); + } + // 2) evaluate + if (onProgress) onProgress('init'); + const src = URL.createObjectURL(blob); + try { + await new Promise((res, rej) => { + const s = document.createElement('script'); + s.src = src; + s.onload = res; + s.onerror = () => rej(new Error('opencv.js eval failed')); + document.head.appendChild(s); + }); + } finally { + setTimeout(() => URL.revokeObjectURL(src), 5000); + } + // 3) resolve the runtime — window.cv may be the module, a thenable that + // resolves to it, or get swapped mid-init depending on the emscripten + // build. Poll fresh every tick; hard 45s ceiling so a hang becomes a + // visible failover instead of an eternal "loading". + const t0 = performance.now(); + let thenAttached = false; + for (;;) { const mod = window.cv; - const finish = (m) => { _cv = m; resolve(m); }; - if (!mod) return reject(new Error('opencv.js loaded but window.cv missing')); - if (mod.Mat) return finish(mod); // already initialized - if (typeof mod.then === 'function') return mod.then(finish, reject); // promise build - mod.onRuntimeInitialized = () => finish(mod); // classic emscripten - // safety: poll in case onRuntimeInitialized was already consumed - const t0 = performance.now(); - (function poll() { - if (_cv) return; - if (mod.Mat) return finish(mod); - if (performance.now() - t0 > 30000) return reject(new Error('opencv.js init timeout')); - setTimeout(poll, 100); - })(); - }; - document.head.appendChild(s); - }); + if (mod && mod.Mat) { _cv = mod; return mod; } + if (mod && typeof mod.then === 'function' && !thenAttached) { + thenAttached = true; + mod.then((m) => { if (m && m.Mat) { window.cv = m; } }, () => {}); + } + if (mod && !mod.Mat && typeof mod.then !== 'function' && !mod.__nbxHook) { + mod.__nbxHook = true; + const prev = mod.onRuntimeInitialized; + mod.onRuntimeInitialized = () => { if (typeof prev === 'function') prev(); }; + } + if (performance.now() - t0 > 45000) throw new Error('opencv.js init timeout'); + await new Promise(r => setTimeout(r, 100)); + } + })(); + _loading.catch(() => { _loading = null; }); // allow retry after failure return _loading; }