Improvement: HKSV recording stream AbortSignal support and graceful generator termination.#1110
Merged
Merged
Conversation
Pull Request Test Coverage Report for Build 23367641817Details
💛 - Coveralls |
There was a problem hiding this comment.
Pull request overview
This PR improves HomeKit Secure Video (HKSV) recording stream shutdown behavior by introducing cooperative cancellation via AbortSignal, adding a protocol-level generator termination backstop, and making close handling idempotent to avoid duplicate delegate side effects.
Changes:
- Add an optional
AbortSignalparameter toCameraRecordingDelegate.handleRecordingStreamRequest()to support immediate cooperative cancellation. - In
CameraRecordingStream, abort the signal and invokegenerator.return()during close handling, plus guardhandleClosed()for idempotency. - Downgrade the “delegate yielded after close” log from
console.errortodebug().
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/lib/controller/CameraController.ts |
Extends the recording delegate API with an optional AbortSignal to support cooperative cancellation. |
src/lib/camera/RecordingManagement.ts |
Implements abort + generator termination on close and prevents double-close side effects; adjusts logging level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
donavanbecker
previously approved these changes
Mar 20, 2026
…enerator termination.
hjdhjd
force-pushed
the
hksv-abort-signal-recording-stream
branch
from
March 21, 2026 00:13
eaacd5c to
f4f5872
Compare
donavanbecker
approved these changes
Mar 21, 2026
hjdhjd
enabled auto-merge (squash)
March 21, 2026 01:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
AbortSignalsupport toCameraRecordingDelegate.handleRecordingStreamRequest()for immediate, cooperative cancellation of recording stream generators when an HDS stream closes.generator.return()inhandleClosed()as a protocol-level backstop to ensure generator termination.handleClosed()to prevent double invocation from batched TCP messages.console.errortodebug().homebridge-unifi-protectand validated as working correctly.Background
When an HDS recording stream closes,
handleClosed()setsthis.closed = trueand callscloseRecordingStream()on the delegate. The delegate's async generator is expected to stop yielding. However, if the generator is suspended in an async operation (e.g. a pacing delay or FFmpeg wait), it can't observe the close signal until that operation completes — which may be seconds later. By then, the generator yields one more fragment to a closed stream, triggering the "Delegate yielded fragment after stream was already closed" error.This is an inherent race condition in the async generator protocol:
handleClosed()can't preemptively stop a generator that has a pending.next()call. The consumer must wait for the generator to yield before it can break the loop.Additionally,
handleClosed()lacked an idempotency guard — unlikeclose()which checksthis.closedbefore proceeding,handleClosed()could be invoked multiple times when multiple close-related messages arrive in the same TCP chunk and are processed in theforEachloop withinonSocketData. This resulted incloseRecordingStream()being called twice for the same stream, causing duplicate side effects in delegate implementations.Approach
Four changes, each independently tested and validated:
AbortSignal—handleClosed()aborts the signal immediately alongside settingthis.closed. Delegates that make their async operations abortable (e.g. usingnode:timers/promisessetTimeoutwith a signal) wake up instantly and can return without yielding. This is the modern platform pattern for cooperative async cancellation.generator.return()— Called inhandleClosed()as a fire-and-forget signal. Per the async generator spec, the return is queued and takes effect after the generator's current await completes. This ensures termination even for delegates that don't adopt theAbortSignal.handleClosed()idempotency guard — Matches the existing pattern inclose(). Prevents double invocation when batched TCP messages trigger multiple close events for the same stream in a singleonSocketDataprocessing cycle.debug()downgrade — The post-close yield is an inherent race for delegates that haven't adopted the signal. The fragment is already discarded and the loop breaks. Logging it asconsole.errorimplies a programming mistake when it's actually a protocol-level timing condition.Backwards compatibility
The
signalparameter onhandleRecordingStreamRequest()is optional. Existing delegates that don't declare it continue working exactly as before — the signal is passed but ignored. Thegenerator.return()backstop ensures termination regardless of delegate adoption.Test plan
console.errorno longer appears in production logs.setTimeoutwith signal) exit immediately on stream close without yielding post-close fragments.generator.return()backstop.handleClosed()idempotency guard prevents duplicatecloseRecordingStream()calls from batched TCP messages.Backport
Once merged, we should be cherry-picked to the
release-0.xbranch. The recording management code is identical between 0.14.x and 2.x for the paths we're modifying — the cherry-pick applies cleanly (verified).