mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Lazy load open cv (#6236)
Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
co-authored by
EthanHealy01
parent
4e4918b91e
commit
a26b15b1fe
@@ -19,8 +19,5 @@
|
|||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="src/index.tsx"></script>
|
<script type="module" src="src/index.tsx"></script>
|
||||||
<!-- jscanify and OpenCV for mobile scanner - loaded after React for non-blocking page load -->
|
|
||||||
<script src="/vendor/jscanify/opencv.js" async></script>
|
|
||||||
<script src="/vendor/jscanify/jscanify.js" async></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,14 +20,7 @@ import PhotoCameraRoundedIcon from "@mui/icons-material/PhotoCameraRounded";
|
|||||||
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
|
import UploadRoundedIcon from "@mui/icons-material/UploadRounded";
|
||||||
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
|
import AddPhotoAlternateRoundedIcon from "@mui/icons-material/AddPhotoAlternateRounded";
|
||||||
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
|
import CheckCircleRoundedIcon from "@mui/icons-material/CheckCircleRounded";
|
||||||
|
import { loadJscanify } from "@app/utils/loadJscanify";
|
||||||
// jscanify is loaded via script tag in index.html as a global
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
jscanify: any;
|
|
||||||
cv: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MobileScannerPage
|
* MobileScannerPage
|
||||||
@@ -136,69 +129,36 @@ export default function MobileScannerPage() {
|
|||||||
validateSession();
|
validateSession();
|
||||||
}, [sessionId, t]);
|
}, [sessionId, t]);
|
||||||
|
|
||||||
// Initialize jscanify scanner and wait for OpenCV (loaded via script tags in index.html)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let retryCount = 0;
|
let cancelled = false;
|
||||||
const MAX_RETRIES = 50; // 5 seconds max wait
|
|
||||||
|
|
||||||
const initScanner = () => {
|
loadJscanify({
|
||||||
// Check if both OpenCV and jscanify are loaded
|
onStatus: (status) => {
|
||||||
if (!(window as any).cv || !(window as any).cv.Mat) {
|
if (!cancelled) setLoadingStatus(status);
|
||||||
retryCount++;
|
},
|
||||||
if (retryCount < MAX_RETRIES) {
|
})
|
||||||
if (retryCount % 10 === 1) {
|
.then(() => {
|
||||||
setLoadingStatus(
|
if (cancelled) return;
|
||||||
`Loading OpenCV... (${retryCount}/${MAX_RETRIES})`,
|
try {
|
||||||
);
|
scannerRef.current = new window.jscanify!();
|
||||||
console.log(
|
setOpenCvReady(true);
|
||||||
`[${retryCount}/${MAX_RETRIES}] Waiting for OpenCV to load...`,
|
console.log("✓ jscanify initialized with OpenCV");
|
||||||
);
|
} catch (err) {
|
||||||
}
|
setLoadingStatus("jscanify init failed ✗");
|
||||||
setTimeout(initScanner, 100);
|
console.error("Failed to initialize jscanify:", err);
|
||||||
} else {
|
|
||||||
const error =
|
|
||||||
"OpenCV failed to load after 5 seconds. Check that /vendor/jscanify/opencv.js is accessible.";
|
|
||||||
setLoadingStatus("OpenCV load failed ✗");
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
return;
|
})
|
||||||
}
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setLoadingStatus(
|
||||||
|
`Scanner library failed to load ✗: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
|
console.error("Failed to load jscanify:", err);
|
||||||
|
});
|
||||||
|
|
||||||
if (!window.jscanify) {
|
return () => {
|
||||||
retryCount++;
|
cancelled = true;
|
||||||
if (retryCount < MAX_RETRIES) {
|
|
||||||
if (retryCount % 10 === 1) {
|
|
||||||
setLoadingStatus(
|
|
||||||
`Loading jscanify... (${retryCount}/${MAX_RETRIES})`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[${retryCount}/${MAX_RETRIES}] Waiting for jscanify to load...`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setTimeout(initScanner, 100);
|
|
||||||
} else {
|
|
||||||
const error =
|
|
||||||
"jscanify failed to load after 5 seconds. Check that /vendor/jscanify/jscanify.js is accessible.";
|
|
||||||
setLoadingStatus("jscanify load failed ✗");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
scannerRef.current = new window.jscanify();
|
|
||||||
setOpenCvReady(true);
|
|
||||||
// Don't set status here - let camera/detection effects control status from now on
|
|
||||||
console.log("✓ jscanify initialized with OpenCV");
|
|
||||||
} catch (err) {
|
|
||||||
setLoadingStatus("jscanify init failed ✗");
|
|
||||||
console.error("Failed to initialize jscanify:", err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start initialization
|
|
||||||
setLoadingStatus("Loading OpenCV...");
|
|
||||||
initScanner();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initialize camera
|
// Initialize camera
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
cv?: any;
|
||||||
|
jscanify?: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const OPENCV_SRC = "/vendor/jscanify/opencv.js";
|
||||||
|
const JSCANIFY_SRC = "/vendor/jscanify/jscanify.js";
|
||||||
|
|
||||||
|
let loadPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
function injectScript(src: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// data-loaded: load event fired. data-loading: in flight.
|
||||||
|
// Neither: foreign tag (e.g. HMR-preserved); trust the downstream
|
||||||
|
// global check rather than wait on a load event that may already
|
||||||
|
// have dispatched.
|
||||||
|
const existing = document.querySelector<HTMLScriptElement>(
|
||||||
|
`script[src="${src}"]`,
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
if (existing.dataset.loaded === "true") {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing.dataset.loading === "true") {
|
||||||
|
existing.addEventListener("load", () => resolve(), { once: true });
|
||||||
|
existing.addEventListener(
|
||||||
|
"error",
|
||||||
|
() => reject(new Error(`Failed to load ${src}`)),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = src;
|
||||||
|
script.async = true;
|
||||||
|
script.dataset.loading = "true";
|
||||||
|
script.onload = () => {
|
||||||
|
script.dataset.loaded = "true";
|
||||||
|
delete script.dataset.loading;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
script.onerror = () => {
|
||||||
|
delete script.dataset.loading;
|
||||||
|
reject(new Error(`Failed to load ${src}`));
|
||||||
|
};
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForGlobal(
|
||||||
|
check: () => boolean,
|
||||||
|
name: string,
|
||||||
|
timeoutMs: number,
|
||||||
|
onProgress?: (elapsedMs: number) => void,
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const tick = () => {
|
||||||
|
if (check()) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const elapsed = Date.now() - start;
|
||||||
|
if (elapsed > timeoutMs) {
|
||||||
|
reject(new Error(`Timed out waiting for ${name}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onProgress?.(elapsed);
|
||||||
|
setTimeout(tick, 100);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadJscanifyOptions {
|
||||||
|
onStatus?: (status: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadJscanify(options: LoadJscanifyOptions = {}): Promise<void> {
|
||||||
|
const { onStatus } = options;
|
||||||
|
|
||||||
|
if (loadPromise) return loadPromise;
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
onStatus?.("Loading OpenCV...");
|
||||||
|
await injectScript(OPENCV_SRC);
|
||||||
|
// OpenCV's script load event fires before the WASM runtime is ready.
|
||||||
|
await waitForGlobal(
|
||||||
|
() => !!window.cv && !!window.cv.Mat,
|
||||||
|
"OpenCV runtime (cv.Mat)",
|
||||||
|
15000,
|
||||||
|
(elapsed) => {
|
||||||
|
if (elapsed > 2000) onStatus?.("Initializing OpenCV runtime...");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onStatus?.("Loading jscanify...");
|
||||||
|
await injectScript(JSCANIFY_SRC);
|
||||||
|
await waitForGlobal(() => !!window.jscanify, "jscanify global", 5000);
|
||||||
|
|
||||||
|
onStatus?.("Scanner ready");
|
||||||
|
})().catch((err) => {
|
||||||
|
loadPromise = null;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user