Skip to content

Commit 0151499

Browse files
authored
Merge pull request #2 from veillette/claude/fix-opencv-tracker-hang-tISsY
Fix OpenCV tracker hang: handle Promise-based WASM export and add timeout
2 parents d40e8af + 02195d2 commit 0151499

2 files changed

Lines changed: 65 additions & 34 deletions

File tree

src/screen-name/view/AutoTrackerNode.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ export class AutoTrackerNode extends Node {
147147
if ( region.w > 4 && region.h > 4 ) {
148148
// initFromVideo is async (loads WASM on first call); tracking begins
149149
// automatically once `ready` becomes true.
150-
this.tracker.initFromVideo( videoElement, region );
150+
this.tracker.initFromVideo( videoElement, region ).catch( err => {
151+
console.error( '[AutoTracker] Tracking initialisation failed:', err );
152+
this.hintText.visible = true;
153+
} );
151154
}
152155
else {
153156
this.hintText.visible = true;

src/tracking/OpenCVTracker.ts

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,39 @@
11
// cv type is 'any' — the OpenCV.js WASM API is dynamic and not fully typed.
22
let cvPromise: Promise<any> | null = null;
33

4+
const CV_LOAD_TIMEOUT_MS = 30_000;
5+
46
function 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

Comments
 (0)