buzz-me-in 2.2.0-beta.2 → 2.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -58,10 +58,13 @@ SHUTDOWN_GRACE_MS=10000
58
58
  # How long the Twilio call is held open after the HomeKit live view closes,
59
59
  # before the call is hung up. Backing out of the camera view to the room view
60
60
  # tears the stream down, and the lock tile there needs the call to still be up
61
- # to send its DTMF. Raise this if you find yourself reaching the lock too late;
62
- # the only cost is that much silence for the caller after you are done.
63
- # Tapping unlock restarts the window, so the digits are never cut short.
64
- HOMEKIT_HANGUP_GRACE_MS=3000
61
+ # to send its DTMF. Tapping unlock restarts the window, so the digits are never
62
+ # cut short. 8 s is measured, not guessed: closing the view and then reaching
63
+ # the lock tile under Other took 2.9 s unhurried, and 3 s left 78 ms of margin.
64
+ # The cost of a longer window is that much silence for the caller after you are
65
+ # done, so lower it only if that silence bothers a caller more than a rushed
66
+ # unlock bothers you.
67
+ HOMEKIT_HANGUP_GRACE_MS=8000
65
68
 
66
69
  # Logging
67
70
  # LOG_LEVEL controls minimum emitted level: debug | info | warn | error
package/README.md CHANGED
@@ -448,12 +448,12 @@ Open the live view, then dismiss it on the iPhone (tap the X / end button).
448
448
  **Pass (server log):**
449
449
 
450
450
  ```text
451
- {"ts":"...","level":"info","message":"Hangup scheduled after grace period","component":"homekit","event":"hangup-scheduled","reason":"homekit-session-stopped","callSid":"CA...","sessionId":"...","graceMs":3000}
451
+ {"ts":"...","level":"info","message":"Hangup scheduled after grace period","component":"homekit","event":"hangup-scheduled","reason":"homekit-session-stopped","callSid":"CA...","sessionId":"...","graceMs":8000}
452
452
  {"ts":"...","level":"info","message":"Inbound ffmpeg exited","component":"homekit","event":"ffin-exit","reason":"nonzero-exit","exitCode":null,"sessionId":"..."}
453
453
  {"ts":"...","level":"info","message":"Hanging up call","component":"twilio-api","event":"hangup","callSid":"CA..."}
454
454
  ```
455
455
 
456
- The call is not hung up the instant the view closes. It is held open for `HOMEKIT_HANGUP_GRACE_MS` (default 3000) so the lock tile still has a call to send DTMF to, then hung up. Reopening the view inside that window logs `hangup-cancelled` instead and the call continues; if the caller hangs up first, the pending hangup logs `hangup-skipped` and does nothing.
456
+ The call is not hung up the instant the view closes. It is held open for `HOMEKIT_HANGUP_GRACE_MS` (default 8000) so the lock tile still has a call to send DTMF to, then hung up. Reopening the view inside that window logs `hangup-cancelled` instead and the call continues; if the caller hangs up first, the pending hangup logs `hangup-skipped` and does nothing.
457
457
 
458
458
  **Pass (Twilio):** The call shows as completed in the [Twilio Console call log](https://console.twilio.com/us1/monitor/logs/calls).
459
459
 
package/homekit.js CHANGED
@@ -158,6 +158,7 @@ const activeSessions = new Map();
158
158
  // PassThrough stream set by server.js each time a Twilio call connects.
159
159
  let currentMulawStream = null;
160
160
  let onHapSessionStarted = null;
161
+ let onHapSessionEnded = null;
161
162
 
162
163
  function getActiveCall() {
163
164
  return state.getActiveCall();
@@ -558,7 +559,19 @@ function _stopSession(sessionID, hangUp) {
558
559
  if (s.ffOut) killFfmpeg(s.ffOut);
559
560
  if (s.sdpPath) removeReturnAudioSdp(s.sdpPath);
560
561
 
562
+ // Only the last session ending means the user is finished. HomeKit runs up
563
+ // to cameraStreamCount sessions at once, and moving between the room view's
564
+ // camera tile and the full camera view starts a second one, so a STOP here
565
+ // routinely arrives while another session is still streaming. Hanging up on
566
+ // it kills a live call: seen on 2026-09-12, where session ac5c8c21 stopping
567
+ // ended the call under session d332e46a three seconds into the stream.
561
568
  const activeCall = getActiveCall();
569
+ if (activeSessions.size !== 0) return;
570
+
571
+ // ffIn is dead and unpiped by now, so tell the media stream to stop writing
572
+ // into a PassThrough with no consumer. See markHomekitSessionEnded.
573
+ if (activeCall && onHapSessionEnded) onHapSessionEnded(activeCall.callSid);
574
+
562
575
  if (hangUp && activeCall) {
563
576
  scheduleHangUp(activeCall.callSid, sessionID);
564
577
  }
@@ -882,6 +895,10 @@ function setOnHapSessionStarted(handler) {
882
895
  onHapSessionStarted = typeof handler === 'function' ? handler : null;
883
896
  }
884
897
 
898
+ function setOnHapSessionEnded(handler) {
899
+ onHapSessionEnded = typeof handler === 'function' ? handler : null;
900
+ }
901
+
885
902
  /**
886
903
  * Graceful shutdown: tear down streaming sessions and unpublish the
887
904
  * accessory so the mDNS advertisement does not linger after exit.
@@ -917,11 +934,16 @@ module.exports = {
917
934
  clearMulawPassthrough,
918
935
  endHapSession,
919
936
  setOnHapSessionStarted,
937
+ setOnHapSessionEnded,
920
938
  shutdown,
921
939
  // Exported for tests/homekit-hangup-grace.test.cjs. Reaching the deferred
922
940
  // hangup through the streaming delegate would mean standing up a full SRTP
923
- // session, which tests nothing about the timer.
941
+ // session, which tests nothing about the timer. _stopSession and the session
942
+ // map go with them: driving only scheduleHangUp tests when the timer fires
943
+ // but not whether it should have been set, which is where the live bug was.
924
944
  scheduleHangUp,
925
945
  cancelPendingHangUp,
926
946
  deferPendingHangUp,
947
+ _stopSession,
948
+ activeSessions,
927
949
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buzz-me-in",
3
- "version": "2.2.0-beta.2",
3
+ "version": "2.2.0-beta.4",
4
4
  "description": "HomeKit-compatible apartment intercom via Twilio media streams",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -67,6 +67,11 @@ homekit.setOnHapSessionStarted((callSid) => {
67
67
  if (mediaStream) mediaStream.markHomekitSessionStarted();
68
68
  });
69
69
 
70
+ homekit.setOnHapSessionEnded((callSid) => {
71
+ const mediaStream = activeMediaStreamsByCallSid.get(callSid);
72
+ if (mediaStream) mediaStream.markHomekitSessionEnded();
73
+ });
74
+
70
75
  // ---------------------------------------------------------------------------
71
76
  // Ringtone — one UK-style ringback cycle (400Hz+450Hz dual tone), generated
72
77
  // in-process as a WAV buffer. Served at GET /ringtone for debug/manual
@@ -138,7 +138,7 @@ module.exports = {
138
138
  streamStartTimeoutMs: parsePositiveInt('STREAM_START_TIMEOUT_MS', '3000'),
139
139
  twilioMediaPayloadMaxBytes: parsePositiveInt('TWILIO_MEDIA_PAYLOAD_MAX_BYTES', '512'),
140
140
  shutdownGraceMs: parsePositiveInt('SHUTDOWN_GRACE_MS', '10000'),
141
- homekitHangupGraceMs: parsePositiveInt('HOMEKIT_HANGUP_GRACE_MS', '3000'),
141
+ homekitHangupGraceMs: parsePositiveInt('HOMEKIT_HANGUP_GRACE_MS', '8000'),
142
142
  streamAuthSecret,
143
143
  streamAuthTtlSec: 90,
144
144
  statusApiToken,
@@ -293,6 +293,36 @@ class MediaStream {
293
293
  this.stopRingback('homekit-session-started');
294
294
  }
295
295
 
296
+ /**
297
+ * The counterpart to markHomekitSessionStarted, called once the last HomeKit
298
+ * streaming session stops.
299
+ *
300
+ * Without it, forwarding stayed on for the life of the call while the
301
+ * PassThrough's consumer was gone: _stopSession kills ffIn and unpipes it, so
302
+ * Twilio audio accumulated in a buffer nobody drained. That matters twice
303
+ * over. The buffer holds 4 s (32 KB of 8 kHz mu-law), so a longer gap logs
304
+ * media-frames-dropped for audio that was never going anywhere; and on
305
+ * reopening the live view the new ffIn inherits the backlog and drains it
306
+ * faster than real time, which is the "buffered startup audio burst" the
307
+ * inbound spawn comment already warns about. Either way a rejoin starts
308
+ * behind live instead of at it.
309
+ *
310
+ * This restores the invariant the write path documents: forward live-view
311
+ * audio only.
312
+ */
313
+ markHomekitSessionEnded() {
314
+ if (!this.hasHomekitSession) return;
315
+ this.hasHomekitSession = false;
316
+ // Otherwise a later resume would log against a run of drops that ended
317
+ // when forwarding stopped.
318
+ this.droppingFrames = false;
319
+ this.logger.info('HomeKit session ended; no longer forwarding media', {
320
+ callSid: this.currentCallSid || undefined,
321
+ event: 'homekit-session-ended',
322
+ reason: 'no-active-hap-session',
323
+ });
324
+ }
325
+
296
326
  sendRingbackFrame() {
297
327
  if (!this.currentCallSid || !this.started || this.closed) return;
298
328