Skip to content

Commit 7f5051b

Browse files
committed
feat: enhance ARToolKit integration with module URL support and improved initialization handling
1 parent 84f4f8c commit 7f5051b

File tree

3 files changed

+143
-41
lines changed

3 files changed

+143
-41
lines changed

examples/simple-marker/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ <h3>Event Log:</h3>
7070
async function start() {
7171
try {
7272
log('Creating ArtoolkitPlugin instance...');
73-
plugin = new ArtoolkitPlugin({ worker: true });
73+
plugin = new ArtoolkitPlugin({ worker: true,
74+
// ESM bundle path served by your dev server
75+
artoolkitModuleUrl: '../../node_modules/@ar-js-org/artoolkit5-js/dist/ARToolkit.js',
76+
});
7477
await plugin.init(core);
7578
await plugin.enable();
7679

src/plugin.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,14 @@ export class ArtoolkitPlugin {
157157

158158
// If worker supports postMessage init, send init
159159
try {
160-
this._worker.postMessage?.({ type: 'init' });
160+
this._worker.postMessage?.({
161+
type: 'init',
162+
payload: {
163+
moduleUrl: this.options.artoolkitModuleUrl || null,
164+
cameraParametersUrl: this.options.cameraParametersUrl || null,
165+
wasmBaseUrl: this.options.wasmBaseUrl || null
166+
}
167+
});
161168
} catch (e) {
162169
// ignore
163170
}

src/worker/worker.js

Lines changed: 131 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Cross-platform worker integrating ARToolKit in browser Workers.
22
// - Browser: processes ImageBitmap → OffscreenCanvas → ARToolKit.process(canvas)
3-
// - Node: keeps stub behavior
3+
// - Node: keeps stub behavior if needed
44
let isNodeWorker = false;
55
let parent = null;
66

@@ -13,6 +13,22 @@ let offscreenCtx = null;
1313
let canvasW = 0;
1414
let canvasH = 0;
1515

16+
// Init backoff state
17+
let initInProgress = null; // Promise | null
18+
let initFailCount = 0; // increases on each failure
19+
let initFailedUntil = 0; // timestamp when next retry is allowed
20+
21+
// Marker cache/dedupe
22+
const loadedMarkers = new Map(); // patternUrl -> markerId
23+
const loadingMarkers = new Map(); // patternUrl -> Promise<markerId>
24+
25+
// Init-time options (can be overridden via init payload if you already set this up)
26+
let INIT_OPTS = {
27+
moduleUrl: null,
28+
cameraParametersUrl: null,
29+
wasmBaseUrl: null
30+
};
31+
1632
if (typeof self === 'undefined') {
1733
try {
1834
const wt = await import('node:worker_threads').catch(() => null);
@@ -36,7 +52,7 @@ function sendMessage(msg) {
3652
else self.postMessage(msg);
3753
}
3854

39-
// AR.js-style getMarker event serializer
55+
// Serialize AR.js-style getMarker event into a transferable payload
4056
function serializeGetMarkerEvent(ev) {
4157
try {
4258
const data = ev?.data || {};
@@ -73,40 +89,122 @@ function attachGetMarkerForwarder() {
7389
getMarkerForwarderAttached = true;
7490
}
7591

76-
async function initArtoolkit(width = 640, height = 480, cameraParametersUrl) {
92+
// IMPORTANT: this function should be the only place that initializes ARToolKit.
93+
// It is guarded by initInProgress and a failure backoff.
94+
async function initArtoolkit(width = 640, height = 480) {
7795
if (arControllerInitialized) return true;
78-
try {
79-
// Lazy import to keep worker module-light
8096

81-
const cdn = 'https://cdn.jsdelivr.net/npm/@ar-js-org/[email protected]/dist/ARToolkit.min.js';
82-
console.log('[Worker] Trying CDN import:', cdn);
83-
// const jsartoolkit = await import(cdn);
84-
await import(cdn);
85-
// const { ARController } = jsartoolkit;
86-
// console.log(ARToolkit)
97+
// Respect backoff window
98+
const now = Date.now();
99+
if (now < initFailedUntil) {
100+
const waitMs = initFailedUntil - now;
101+
console.warn('[Worker] initArtoolkit skipped due to backoff (ms):', waitMs);
102+
return false;
103+
}
104+
105+
// If an init is already in-flight, await it
106+
if (initInProgress) {
107+
try {
108+
await initInProgress;
109+
return arControllerInitialized;
110+
} catch {
111+
return false;
112+
}
113+
}
114+
115+
// Start a new init attempt
116+
initInProgress = (async () => {
117+
try {
118+
const jsartoolkit = await (async () => {
119+
if (INIT_OPTS.moduleUrl) {
120+
console.log('[Worker] Loading artoolkit from moduleUrl:', INIT_OPTS.moduleUrl);
121+
return await import(INIT_OPTS.moduleUrl);
122+
}
123+
// Fallback to bare import if your environment supports it (import map/bundler)
124+
return await import('@ar-js-org/artoolkit5-js');
125+
})();
87126

88-
const camUrl = cameraParametersUrl
89-
|| 'https://raw.githack.com/AR-js-org/AR.js/master/data/data/camera_para.dat';
127+
const { ARController } = jsartoolkit;
90128

91-
console.log('[Worker] ARToolKit init', { width, height, camUrl });
92-
arController = await ARToolkit.ARController.initWithDimensions(width, height, camUrl, {});
93-
arControllerInitialized = !!arController;
94-
console.log('[Worker] ARToolKit initialized:', arControllerInitialized);
129+
if (INIT_OPTS.wasmBaseUrl && ARController) {
130+
try {
131+
ARController.baseURL = INIT_OPTS.wasmBaseUrl.endsWith('/') ? INIT_OPTS.wasmBaseUrl : INIT_OPTS.wasmBaseUrl + '/';
132+
} catch {}
133+
}
95134

96-
attachGetMarkerForwarder();
97-
return true;
98-
} catch (err) {
99-
console.error('[Worker] ARToolKit init failed:', err);
100-
arController = null;
101-
arControllerInitialized = false;
102-
return false;
135+
const camUrl = INIT_OPTS.cameraParametersUrl
136+
|| 'https://raw.githack.com/AR-js-org/AR.js/master/data/data/camera_para.dat';
137+
138+
console.log('[Worker] ARToolKit init', { width, height, camUrl });
139+
arController = await ARToolkit.ARController.initWithDimensions(width, height, camUrl, {});
140+
arControllerInitialized = !!arController;
141+
console.log('[Worker] ARToolKit initialized:', arControllerInitialized);
142+
143+
if (!arControllerInitialized) throw new Error('ARController.initWithDimensions returned falsy controller');
144+
145+
attachGetMarkerForwarder();
146+
147+
// Reset failure state
148+
initFailCount = 0;
149+
initFailedUntil = 0;
150+
} catch (err) {
151+
console.error('[Worker] ARToolKit init failed:', err);
152+
arController = null;
153+
arControllerInitialized = false;
154+
155+
// Exponential backoff up to 30s
156+
initFailCount = Math.min(initFailCount + 1, 6); // caps at ~64x
157+
const delay = Math.min(30000, 1000 * Math.pow(2, initFailCount)); // 1s,2s,4s,8s,16s,30s
158+
initFailedUntil = Date.now() + delay;
159+
160+
// Surface a single error to main thread (optional)
161+
sendMessage({ type: 'error', payload: { message: `ARToolKit init failed (${err?.message || err}). Retrying in ${delay}ms.` } });
162+
throw err;
163+
} finally {
164+
// Mark as done (success or failure)
165+
const ok = arControllerInitialized;
166+
initInProgress = null;
167+
return ok;
168+
}
169+
})();
170+
171+
try {
172+
await initInProgress;
173+
} catch {
174+
// already handled
103175
}
176+
return arControllerInitialized;
177+
}
178+
179+
// Dedupe marker loading by URL
180+
async function loadPatternOnce(patternUrl) {
181+
if (loadedMarkers.has(patternUrl)) return loadedMarkers.get(patternUrl);
182+
if (loadingMarkers.has(patternUrl)) return loadingMarkers.get(patternUrl);
183+
184+
const p = (async () => {
185+
const id = await arController.loadMarker(patternUrl);
186+
loadedMarkers.set(patternUrl, id);
187+
loadingMarkers.delete(patternUrl);
188+
return id;
189+
})().catch((e) => {
190+
loadingMarkers.delete(patternUrl);
191+
throw e;
192+
});
193+
194+
loadingMarkers.set(patternUrl, p);
195+
return p;
104196
}
105197

106198
onMessage(async (ev) => {
107199
const { type, payload } = ev || {};
108200
try {
109201
if (type === 'init') {
202+
// Accept init overrides
203+
if (payload && typeof payload === 'object') {
204+
INIT_OPTS.moduleUrl = payload.moduleUrl || INIT_OPTS.moduleUrl;
205+
INIT_OPTS.cameraParametersUrl = payload.cameraParametersUrl || INIT_OPTS.cameraParametersUrl;
206+
INIT_OPTS.wasmBaseUrl = payload.wasmBaseUrl || INIT_OPTS.wasmBaseUrl;
207+
}
110208
sendMessage({ type: 'ready' });
111209
return;
112210
}
@@ -118,12 +216,10 @@ onMessage(async (ev) => {
118216
return;
119217
}
120218
try {
121-
if (!arControllerInitialized) {
122-
// Initialize with some defaults; will be resized on first frame as needed
123-
const ok = await initArtoolkit(640, 480);
124-
if (!ok) throw new Error('Failed to initialize ARToolKit');
125-
}
126-
const markerId = await arController.loadMarker(patternUrl);
219+
const ok = await initArtoolkit(640, 480);
220+
if (!ok) throw new Error('ARToolKit not initialized');
221+
222+
const markerId = await loadPatternOnce(patternUrl);
127223
if (typeof arController.trackPatternMarkerId === 'function') {
128224
arController.trackPatternMarkerId(markerId, size);
129225
} else if (typeof arController.trackPatternMarker === 'function') {
@@ -139,18 +235,16 @@ onMessage(async (ev) => {
139235

140236
if (type === 'processFrame') {
141237
const { imageBitmap, width, height } = payload || {};
142-
// In browser: drive ARToolKit processing so it emits getMarker (with the real matrix)
238+
239+
// Browser path: only attempt init at controlled cadence (guard handles backoff)
143240
if (!isNodeWorker && imageBitmap) {
144241
try {
145242
const w = width || imageBitmap.width || 640;
146243
const h = height || imageBitmap.height || 480;
147244

148-
// Initialize ARToolKit with actual frame size (first time)
149-
if (!arControllerInitialized) {
150-
await initArtoolkit(w, h);
151-
}
245+
// Attempt init once; if it fails, guard prevents hammering it per-frame
246+
await initArtoolkit(w, h);
152247

153-
// Prepare OffscreenCanvas
154248
if (!offscreenCanvas || canvasW !== w || canvasH !== h) {
155249
canvasW = w; canvasH = h;
156250
offscreenCanvas = new OffscreenCanvas(canvasW, canvasH);
@@ -163,10 +257,8 @@ onMessage(async (ev) => {
163257

164258
if (arControllerInitialized && arController) {
165259
try {
166-
// Prefer passing canvas to ARToolKit so it can compute matrix and emit getMarker
167260
arController.process(offscreenCanvas);
168261
} catch (e) {
169-
// Fallback: pass ImageData if this build requires it
170262
try {
171263
const imgData = offscreenCtx.getImageData(0, 0, canvasW, canvasH);
172264
arController.process(imgData);
@@ -177,7 +269,7 @@ onMessage(async (ev) => {
177269
}
178270
} catch (err) {
179271
console.error('[Worker] processFrame error:', err);
180-
sendMessage({ type: 'error', payload: { message: err?.message || String(err) } });
272+
// No spam: let initArtoolkit handle error posting and backoff logging
181273
}
182274
return;
183275
}

0 commit comments

Comments
 (0)