@@ -11,7 +11,8 @@ const ASSET_CHANNELS = {
1111const DETECTOR_SIZE = 384 ;
1212const MIN_MATCH_SCORE_DEFAULT = 0.50 ;
1313const PREVIEW_ASPECT = 16 / 9 ;
14- const SCAN_INTERVAL_MS = 900 ;
14+ const SCAN_INTERVAL_DEFAULT_MS = 50 ;
15+ const SCAN_INTERVAL_MAX_MS = 1000 ;
1516const MOBILE_PREVIEW_INTERVAL_MS = 1000 / 15 ;
1617
1718const SOUND_PATHS = {
@@ -25,6 +26,7 @@ const ASSET_STORE_NAME = "assets";
2526const WEBGPU_PREF_KEY = "cv_webgpu_enabled" ;
2627const MATCH_SCORE_KEY = "cv_min_match_score" ;
2728const CORNER_CONFIDENCE_KEY = "cv_min_corner_confidence" ;
29+ const SCAN_INTERVAL_KEY = "cv_scan_interval_ms" ;
2830const ROTATION_INVARIANT_KEY = "cv_rotation_invariant_enabled" ;
2931const MIN_MATCHES_DEFAULT = 2 ;
3032const MATCHES_KEY = "cv_min_matches" ;
@@ -1505,6 +1507,13 @@ function getMinMatchScore() {
15051507 return Number . isFinite ( stored ) ? stored : MIN_MATCH_SCORE_DEFAULT ;
15061508}
15071509
1510+ function getScanIntervalMs ( ) {
1511+ const stored = Number . parseInt ( localStorage . getItem ( SCAN_INTERVAL_KEY ) , 10 ) ;
1512+ return Number . isFinite ( stored )
1513+ ? Math . min ( SCAN_INTERVAL_MAX_MS , Math . max ( 0 , stored ) )
1514+ : SCAN_INTERVAL_DEFAULT_MS ;
1515+ }
1516+
15081517function getMinCornerConfidence ( ) {
15091518 const stored = Number . parseFloat ( localStorage . getItem ( CORNER_CONFIDENCE_KEY ) ) ;
15101519 return Number . isFinite ( stored ) ? Math . min ( 0.1 , Math . max ( 0 , stored ) ) : MIN_CORNER_CONFIDENCE_DEFAULT ;
@@ -1536,6 +1545,25 @@ function setupMatchScoreSlider() {
15361545 } ) ;
15371546}
15381547
1548+ function setupScanIntervalSlider ( onChange ) {
1549+ const slider = document . getElementById ( "scan-interval-slider" ) ;
1550+ const label = document . getElementById ( "scan-interval-value" ) ;
1551+ if ( ! slider || ! label ) return ;
1552+
1553+ const render = ( value ) => {
1554+ slider . value = String ( value ) ;
1555+ label . textContent = value === 0 ? "Max speed" : `${ value } ms` ;
1556+ } ;
1557+ render ( getScanIntervalMs ( ) ) ;
1558+
1559+ slider . addEventListener ( "input" , ( ) => {
1560+ const value = Math . min ( SCAN_INTERVAL_MAX_MS , Math . max ( 0 , Number . parseInt ( slider . value , 10 ) || 0 ) ) ;
1561+ localStorage . setItem ( SCAN_INTERVAL_KEY , String ( value ) ) ;
1562+ render ( value ) ;
1563+ onChange ( value ) ;
1564+ } ) ;
1565+ }
1566+
15391567function setupCornerConfidenceSlider ( scannerWorker = null ) {
15401568 const slider = document . getElementById ( "corner-confidence-slider" ) ;
15411569 const label = document . getElementById ( "corner-confidence-value" ) ;
@@ -1650,7 +1678,7 @@ class PerformanceOverlay {
16501678 const card = data ?. cardPresent ? ( data . cornersValid ? "card" : "bad-quad" ) : "no-card" ;
16511679 const orientation = data ?. orientation ? ` ${ data . orientation } ` : "" ;
16521680 this . el . textContent = [
1653- `scan ${ SCAN_INTERVAL_MS } ms result ${ resultGap } ` ,
1681+ `minimum interval ${ getScanIntervalMs ( ) } ms result ${ resultGap } ` ,
16541682 `total ${ formatMs ( timing . totalMs ) } det ${ formatMs ( timing . detectMs ) } (run ${ formatMs ( timing . detectorRunMs ) } )` ,
16551683 `dew ${ formatMs ( timing . dewarpMs ) } (warp ${ formatMs ( timing . dewarpWarpMs ) } ) emb ${ formatMs ( timing . embedMs ) } (run ${ formatMs ( timing . embedRunMs ) } )` ,
16561684 `prep det ${ formatMs ( timing . detectorInputMs ) } prep emb ${ formatMs ( timing . embedInputMs ) } lookup ${ formatMs ( timing . searchMs ) } ` ,
@@ -1812,6 +1840,7 @@ function createScannerLoop(
18121840 const bucket = new ScanBucket ( ) ;
18131841 let timer = null ;
18141842 let workerBusy = false ;
1843+ let scanIntervalMs = getScanIntervalMs ( ) ;
18151844
18161845 scannerWorker . addEventListener ( "error" , ( event ) => {
18171846 workerBusy = false ;
@@ -1973,8 +2002,8 @@ function createScannerLoop(
19732002 return ;
19742003 }
19752004 setText ( "camera-badge" , "Scanning" ) ;
1976- debugLog . info ( "scan interval" , `${ SCAN_INTERVAL_MS } ms` ) ;
1977- recordBootTrace ( "scan:loop-started" , { intervalMs : SCAN_INTERVAL_MS } ) ;
2005+ debugLog . info ( "scan interval" , `${ scanIntervalMs } ms` ) ;
2006+ recordBootTrace ( "scan:loop-started" , { intervalMs : scanIntervalMs } ) ;
19782007 timer = setInterval ( async ( ) => {
19792008 if ( workerBusy || ! camera . stream ) {
19802009 return ;
@@ -1998,7 +2027,15 @@ function createScannerLoop(
19982027 debugLog . error ( "scan tick failed" , error ) ;
19992028 setText ( "camera-badge" , error ?. message || "Scan error" ) ;
20002029 }
2001- } , SCAN_INTERVAL_MS ) ;
2030+ } , scanIntervalMs ) ;
2031+ } ,
2032+ setIntervalMs ( value ) {
2033+ scanIntervalMs = Math . min ( SCAN_INTERVAL_MAX_MS , Math . max ( 0 , value ) ) ;
2034+ if ( ! timer ) {
2035+ return ;
2036+ }
2037+ this . stop ( ) ;
2038+ this . start ( ) ;
20022039 } ,
20032040 } ;
20042041}
@@ -2241,6 +2278,7 @@ async function boot() {
22412278 const loop = createScannerLoop (
22422279 camera , scannerWorker , enricherWorker , scans , audioBus , manifest , debugLog , diag , captureState , perfOverlay ,
22432280 ) ;
2281+ setupScanIntervalSlider ( ( value ) => loop . setIntervalMs ( value ) ) ;
22442282 camera . bind (
22452283 async ( ) => {
22462284 debugLog . info ( "starting scan loop" ) ;
0 commit comments