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
44let isNodeWorker = false ;
55let parent = null ;
66
@@ -13,6 +13,22 @@ let offscreenCtx = null;
1313let canvasW = 0 ;
1414let 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+
1632if ( 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
4056function 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
106198onMessage ( 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