Skip to content

fix(core): stop forwarding Twilio audio once the live view closes - #95

Merged
atdr merged 1 commit into
fix/hangup-only-on-last-sessionfrom
fix/stop-buffering-after-session-ends
Sep 27, 2026
Merged

atdr merged 1 commit into
fix/hangup-only-on-last-sessionfrom
fix/stop-buffering-after-session-ends

Conversation

@atdr

@atdr atdr commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • markHomekitSessionStarted() had no counterpart, so hasHomekitSession was set once and never cleared. From the moment the live view closed, Twilio audio was written into a PassThrough with no consumer for the rest of the call.
  • Two observed consequences: media-frames-dropped warnings for audio that was never going anywhere, and a backlog the next ffmpeg bursts through on reopen, so a rejoin begins behind live rather than at it.
  • Adds markHomekitSessionEnded(), wired through a setOnHapSessionEnded seam mirroring the existing setOnHapSessionStarted.

Stacked on #89

Third layer of the stack: #88 (grace window) → #89 (only hang up on the last session) → this.

What was happening

_stopSession kills ffIn and unpipes it from the mu-law PassThrough. Nothing told MediaStream to stop writing to it, so it kept forwarding for the remainder of the call.

The PassThrough is new PassThrough({ highWaterMark: 32768 }). At 8 kHz mu-law that is 4.096 seconds of audio, after which writableNeedDrain trips and frames are dropped by design.

So with the live view closed:

  • gaps under ~4 s silently accumulate a backlog
  • gaps over ~4 s additionally log media-frames-dropped at warn, seen on the Pi at 19:36:36.870, moments after a hangup
  • on reopening, the new ffIn is piped to the same PassThrough and inherits the backlog, draining it far faster than real time

That last one is the "buffered startup audio burst" the inbound spawn comment in homekit.js already warns about, and it is the reported symptom: silence on rejoining, clearing after roughly the length of the gap.

The grace window did not cause this. Before it, the call ended the instant the view closed, so barely any audio arrived and the asymmetry was unreachable. Holding the call open for 8 s turned it into seconds of pointless buffering on every call that ends this way.

Changes

src/core/media-stream.js

markHomekitSessionEnded() clears the flag and resets droppingFrames so a later resume cannot log against a run of drops that ended when forwarding stopped. Logs homekit-session-ended, because the previous behaviour left no trace either way.

homekit.js, server.js

setOnHapSessionEnded mirrors setOnHapSessionStarted, fired from _stopSession and, like the hangup, only when the last session stops. A stop while another session is still streaming must not turn forwarding off, or the surviving live view goes silent. That rule is now enforced in one place for both concerns.

Docs

docs/architecture.md records the forwarding stop alongside the deferred hangup. The playbook's event inventory gains homekit-session-ended, plus media-ws-handshake-verified and media-ws-rejected, which had been missed when they were added.

Test Plan

  • All five gates pass (typecheck, lint, format:check, check, test — 150 tests, up from 146).
  • Three cases in tests/media-stream.test.cjs: forwarding stops when the session ends and nothing accumulates afterwards; reopening resumes from live with no backlog from the gap; ending a session that never started is a no-op. The first also asserts markActivity still fires, so the stale reaper cannot take a call being deliberately held open.
  • One case in tests/homekit-hangup-grace.test.cjs: the media stream is told only when the last session ends.
  • Verified by mutation. Never clearing the flag fails 2 cases, notifying on every stop rather than the last fails 2, never notifying fails 1. Both source files confirmed byte-identical afterwards.
  • Live call with a close/reopen gap (2026-09-27 18:54 UTC, grace raised to 60000). homekit-session-ended on close, no media-frames-dropped, and a temporary per-hop audio probe showed nothing written to ffIn during the gap and live frames straight after mulaw-stream-bound on reopen. The gap was 3.5 s rather than the planned >4.096 s, so the buffer was never at risk of filling; the probe shows the mechanism directly instead.

Honest uncertainty (resolved 2026-09-27)

Resolved: it is session startup, and this PR is correct on its own terms but not the fix for what was heard. With no backlog possible, every session start still shows ffIn silent for ~3 s after mulaw-stream-bound, then ~200 packets (≈4 s of audio) in one second. That is constant, not gap-dependent. Tracked in #102.

Original note:

The reported symptom was silence on rejoin, clearing shortly after. The backlog-burst explanation fits and the buffer arithmetic matches the gap lengths, but it has not been isolated: fresh ffmpeg processes and SRTP renegotiation on every rejoin carry their own startup cost, which would also produce brief silence.

The test above separates them. If the silence scales with gap length, it was the backlog and this fixes it. If it is constant regardless, it is session startup and this PR is still correct on its own terms (no dropped-frame warnings, no pointless buffering) but is not the fix for what was heard.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rq7wnggNTrhwGMADfJ5yvf

markHomekitSessionStarted had no counterpart, so hasHomekitSession was
set once and never cleared. _stopSession kills ffIn and unpipes it, so
from the moment the live view closed, Twilio audio was written into a
PassThrough with no consumer for the rest of the call.

Two consequences, both observed. The buffer holds 4 s (32 KB of 8 kHz
mu-law), so a gap longer than that logs media-frames-dropped at warn for
audio that was never going anywhere: seen on the Pi at 19:36:36, just
after a hangup. And on reopening the view the new ffIn inherits whatever
accumulated and drains it faster than real time, which is the buffered
startup burst the inbound spawn comment already warns about, so a rejoin
begins behind live rather than at it.

The grace window did not cause this, but it made it reachable. Before
it, the call ended the instant the view closed and barely any audio
arrived; holding the call open for 8 s turned a theoretical asymmetry
into seconds of pointless buffering on every call.

markHomekitSessionEnded restores the invariant the write path already
states, namely that only live-view audio is forwarded. It is wired via a
setOnHapSessionEnded seam mirroring setOnHapSessionStarted, and fires
only when the last session stops, on the same reasoning as the hangup.

Verified by mutation, never clearing the flag fails 2, notifying on every
stop rather than the last fails 2, never notifying fails 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rq7wnggNTrhwGMADfJ5yvf
@atdr
atdr added this pull request to stack #90 September 17, 2026 19:49
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.47%. Comparing base (54caf05) to head (0494107).

Files with missing lines Patch % Lines
server.js 60.00% 2 Missing ⚠️
Additional details and impacted files
@@                         Coverage Diff                         @@
##           fix/hangup-only-on-last-session      #95      +/-   ##
===================================================================
+ Coverage                            69.08%   69.47%   +0.38%     
===================================================================
  Files                                   14       14              
  Lines                                 3193     3240      +47     
===================================================================
+ Hits                                  2206     2251      +45     
- Misses                                 987      989       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@atdr
atdr deployed to npm-prerelease September 17, 2026 20:09 — with GitHub Actions Active
@atdr
atdr merged commit 11c1820 into main Sep 27, 2026
12 checks passed
@atdr
atdr deleted the fix/stop-buffering-after-session-ends branch September 27, 2026 19:21

This branch was successfully deployed

1 active deployment
npm-prerelease — 0494107d Deployed Sep 17, 2026 by atdr via Publish prerelease to npm #7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant