11// cv type is 'any' — the OpenCV.js WASM API is dynamic and not fully typed.
22let cvPromise : Promise < any > | null = null ;
33
4+ const CV_LOAD_TIMEOUT_MS = 30_000 ;
5+
46function loadCV ( ) : Promise < any > {
57 if ( ! cvPromise ) {
6- cvPromise = import ( '@techstark/opencv-js' ) . then ( mod => {
7- const cv = ( mod as any ) . default ?? mod ;
8- // WASM may already be ready (e.g. in test environments)
9- if ( typeof cv . Mat === 'function' ) return cv ;
10- return new Promise < any > ( resolve => {
11- cv . onRuntimeInitialized = ( ) => resolve ( cv ) ;
8+ cvPromise = import ( '@techstark/opencv-js' ) . then ( async mod => {
9+ let cv = ( mod as any ) . default ?? mod ;
10+
11+ // The default export may itself be a Promise (v4.12.0+).
12+ if ( cv instanceof Promise ) {
13+ cv = await cv ;
14+ }
15+
16+ // WASM may already be ready (e.g. in test environments).
17+ if ( typeof cv . Mat === 'function' ) {
18+ return cv ;
19+ }
20+
21+ // Wait for the Emscripten runtime to initialise, with a timeout so we
22+ // never hang indefinitely.
23+ return new Promise < any > ( ( resolve , reject ) => {
24+ const timer = setTimeout ( ( ) => {
25+ reject ( new Error ( 'OpenCV WASM initialisation timed out' ) ) ;
26+ } , CV_LOAD_TIMEOUT_MS ) ;
27+
28+ cv . onRuntimeInitialized = ( ) => {
29+ clearTimeout ( timer ) ;
30+ resolve ( cv ) ;
31+ } ;
1232 } ) ;
1333 } ) ;
34+
35+ // If loading fails, clear the cached promise so the next attempt can retry.
36+ cvPromise . catch ( ( ) => { cvPromise = null ; } ) ;
1437 }
1538 return cvPromise ;
1639}
@@ -50,20 +73,23 @@ export class OpenCVTracker {
5073 const imageData = this . ctx . getImageData ( 0 , 0 , this . offscreen . width , this . offscreen . height ) ;
5174 const frame = this . cv . matFromImageData ( imageData ) ;
5275 const gray = new this . cv . Mat ( ) ;
53- this . cv . cvtColor ( frame , gray , this . cv . COLOR_RGBA2GRAY ) ;
54-
55- if ( this . templateMat ) this . templateMat . delete ( ) ;
56-
57- const roi = new this . cv . Rect (
58- Math . round ( Math . max ( 0 , region . x ) ) ,
59- Math . round ( Math . max ( 0 , region . y ) ) ,
60- Math . round ( Math . min ( region . w , this . offscreen . width - region . x ) ) ,
61- Math . round ( Math . min ( region . h , this . offscreen . height - region . y ) )
62- ) ;
63- this . templateMat = gray . roi ( roi ) . clone ( ) ;
64-
65- frame . delete ( ) ;
66- gray . delete ( ) ;
76+ try {
77+ this . cv . cvtColor ( frame , gray , this . cv . COLOR_RGBA2GRAY ) ;
78+
79+ if ( this . templateMat ) this . templateMat . delete ( ) ;
80+
81+ const roi = new this . cv . Rect (
82+ Math . round ( Math . max ( 0 , region . x ) ) ,
83+ Math . round ( Math . max ( 0 , region . y ) ) ,
84+ Math . round ( Math . min ( region . w , this . offscreen . width - region . x ) ) ,
85+ Math . round ( Math . min ( region . h , this . offscreen . height - region . y ) )
86+ ) ;
87+ this . templateMat = gray . roi ( roi ) . clone ( ) ;
88+ }
89+ finally {
90+ frame . delete ( ) ;
91+ gray . delete ( ) ;
92+ }
6793 }
6894
6995 /**
@@ -77,20 +103,22 @@ export class OpenCVTracker {
77103 const imageData = this . ctx . getImageData ( 0 , 0 , this . offscreen . width , this . offscreen . height ) ;
78104 const frame = this . cv . matFromImageData ( imageData ) ;
79105 const gray = new this . cv . Mat ( ) ;
80- this . cv . cvtColor ( frame , gray , this . cv . COLOR_RGBA2GRAY ) ;
81-
82106 const result = new this . cv . Mat ( ) ;
83- this . cv . matchTemplate ( gray , this . templateMat , result , this . cv . TM_CCOEFF_NORMED ) ;
84- const { maxLoc } = this . cv . minMaxLoc ( result ) ;
85-
86- frame . delete ( ) ;
87- gray . delete ( ) ;
88- result . delete ( ) ;
89-
90- return {
91- x : maxLoc . x + this . templateMat . cols / 2 ,
92- y : maxLoc . y + this . templateMat . rows / 2 ,
93- } ;
107+ try {
108+ this . cv . cvtColor ( frame , gray , this . cv . COLOR_RGBA2GRAY ) ;
109+ this . cv . matchTemplate ( gray , this . templateMat , result , this . cv . TM_CCOEFF_NORMED ) ;
110+ const { maxLoc } = this . cv . minMaxLoc ( result ) ;
111+
112+ return {
113+ x : maxLoc . x + this . templateMat . cols / 2 ,
114+ y : maxLoc . y + this . templateMat . rows / 2 ,
115+ } ;
116+ }
117+ finally {
118+ frame . delete ( ) ;
119+ gray . delete ( ) ;
120+ result . delete ( ) ;
121+ }
94122 }
95123
96124 public dispose ( ) : void {
0 commit comments