← back to Dw Photo Capture
Wire ended + mute listeners on _tsTrack, scoped by generation (TK-12128)
0e42bafa15e7d7dcd63722aea4e4a117d97ad2be · 2026-09-24 09:04:00 -0700 · Steve Abrams
Cody-found gap from the TK-12124 red-team pass: the two-shot flow had no
mute/ended listener on _tsTrack. On iOS, a LATER camera request elsewhere
(another app, FaceTime) can mute or end an earlier stream's track without any
getUserMedia call of ours — the preview freezes while _tsLive stays true and
the shutter would save a frozen frame.
batch.html's reference handler (line ~310) only covers 'ended'. The failure
Cody actually described is specifically a MUTE (the track never ends, it just
stops delivering frames) — wiring 'ended' alone per the literal instruction
would have left the named bug unfixed, so both events are wired here.
On either event: _tsLive resets to false, the shutter disables, a toast fires
("Camera stopped — tap Front/Back to restart"), and the stream is stopped +
cleared so a retap goes through the existing bounded acquire path cleanly.
Scoped to the CURRENT generation via the _tsStreamGen counter (from the
TK-12124 pass) plus a direct track-identity check, so an old, already-replaced
track's late event — e.g. from the .stop() call the next acquire makes on it —
can never kill a newer, genuinely-live stream.
Negative-tested: confirmed both 'ended' and 'mute' correctly reset _tsLive/
disable the shutter/toast (mute dispatched synthetically since it's exclusively
OS-triggered with no script-callable trigger; 'ended' likewise synthetic since
MediaStreamTrack.stop() does NOT fire 'ended' per spec — only an external end
does), confirmed a retap re-acquires cleanly afterward, and confirmed a STALE
old-generation track firing either event does NOT affect a newer live stream.
12/12 new assertions pass; the existing 25 TK-12124 assertions are unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXUyzc9vybUz39rhnNJdwY
Files touched
Diff
commit 0e42bafa15e7d7dcd63722aea4e4a117d97ad2be
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 09:04:00 2026 -0700
Wire ended + mute listeners on _tsTrack, scoped by generation (TK-12128)
Cody-found gap from the TK-12124 red-team pass: the two-shot flow had no
mute/ended listener on _tsTrack. On iOS, a LATER camera request elsewhere
(another app, FaceTime) can mute or end an earlier stream's track without any
getUserMedia call of ours — the preview freezes while _tsLive stays true and
the shutter would save a frozen frame.
batch.html's reference handler (line ~310) only covers 'ended'. The failure
Cody actually described is specifically a MUTE (the track never ends, it just
stops delivering frames) — wiring 'ended' alone per the literal instruction
would have left the named bug unfixed, so both events are wired here.
On either event: _tsLive resets to false, the shutter disables, a toast fires
("Camera stopped — tap Front/Back to restart"), and the stream is stopped +
cleared so a retap goes through the existing bounded acquire path cleanly.
Scoped to the CURRENT generation via the _tsStreamGen counter (from the
TK-12124 pass) plus a direct track-identity check, so an old, already-replaced
track's late event — e.g. from the .stop() call the next acquire makes on it —
can never kill a newer, genuinely-live stream.
Negative-tested: confirmed both 'ended' and 'mute' correctly reset _tsLive/
disable the shutter/toast (mute dispatched synthetically since it's exclusively
OS-triggered with no script-callable trigger; 'ended' likewise synthetic since
MediaStreamTrack.stop() does NOT fire 'ended' per spec — only an external end
does), confirmed a retap re-acquires cleanly afterward, and confirmed a STALE
old-generation track firing either event does NOT affect a newer live stream.
12/12 new assertions pass; the existing 25 TK-12124 assertions are unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXUyzc9vybUz39rhnNJdwY
---
public/index.html | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/public/index.html b/public/index.html
index 968a11d..6a186bc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2324,6 +2324,23 @@ async function startTsStream(){
_tsStream.getTracks().forEach(t=>t.stop()); _tsStream=null; v.srcObject=null; // release the held camera immediately — don't leave the light on through the fallback
throw new Error('camera-timeout');
}
+ // TK-12128 (Cody-found gap): nothing watched _tsTrack for 'ended' OR 'mute' — on iOS a LATER camera
+ // request elsewhere can mute (or end) THIS track without any getUserMedia call of ours, so the preview
+ // freezes while _tsLive stays true and the shutter would save a frozen frame. batch.html's reference
+ // handler only covers 'ended'; the failure Cody actually named is a MUTE (track never ends, just stops
+ // delivering frames), so both are wired here — 'ended'-only would leave the named bug unfixed.
+ // Scoped to THIS generation (myGen + a direct track-identity check) so an old, now-replaced track's
+ // late event (e.g. from the .stop() call above on the NEXT acquire) can never kill a newer live stream.
+ const _tsTrackGen=myGen, _tsTrackRef=_tsTrack;
+ const onTrackDead=()=>{
+ if(_tsTrackGen!==_tsStreamGen || _tsTrack!==_tsTrackRef) return; // stale — a newer generation already took over
+ _tsLive=false; if(_sb) _sb.disabled=true;
+ toast('Camera stopped — tap Front/Back to restart');
+ if(_tsStream){ _tsStream.getTracks().forEach(t=>t.stop()); _tsStream=null; }
+ v.srcObject=null;
+ };
+ _tsTrack.addEventListener('ended',onTrackDead);
+ _tsTrack.addEventListener('mute',onTrackDead);
} finally { _tsAcquiring=false; }
}
async function tsRequestWake(){ try{ if('wakeLock' in navigator){ _tsWake=await navigator.wakeLock.request('screen'); } }catch(e){} }
← b2bd893 Add persistent "applies at capture, not preview" hint on the
·
back to Dw Photo Capture
·
Extract shared acquireCamera() helper, adopt across 5 unboun a1b8ee3 →