Skip to content

fix: reject recording when the selected display is disconnected - #185

Merged
jsattler merged 1 commit into
jsattler:mainfrom
krishna2206:fix/stale-display-selection
Jul 30, 2026
Merged

fix: reject recording when the selected display is disconnected#185
jsattler merged 1 commit into
jsattler:mainfrom
krishna2206:fix/stale-display-selection

Conversation

@krishna2206

Copy link
Copy Markdown
Contributor

Fixes #184.

Recording against a display that was disconnected after being selected produces a file with audio tracks and no video track, reported to the user as a normal "Recording Saved". This makes that impossible in two independent places.

Changes

1. Validate the selected display before creating the stream

ContentFilterService.isSelectedDisplayConnected(_:) checks the filter's displayID against the currently connected displays. CaptureEngine.startCapture fails with a new CaptureError.selectedDisplayDisconnected instead of starting a stream that will never deliver frames.

RecorderViewModel.startRecording then clears the stale selection, so pressing record again opens the picker rather than failing the same way. Since lastError has no UI representation anywhere in the app, the reason is surfaced as a notification — otherwise the record button would simply do nothing.

Two deliberate scoping decisions, both easy to change if you'd prefer otherwise:

  • The check is limited to filter.style == .display. Window and application filters aren't bound to a display, and their includedDisplays snapshot could name a display the window has since moved off, which would block a capture that would actually work.
  • If SCShareableContent can't be read, validation is skipped and capture proceeds. A validation step shouldn't become a new way to fail.

2. Report the video frame count from finishWriting

The noFramesWritten guard only checks hasStartedSession, and appendAudioSample sets that flag too — so with audio enabled it never fires no matter how many video frames were dropped. finishWriting() now returns (url:videoFrameCount:), and stopRecording sends a distinct "Recording Saved Without Video" notification when the count is zero.

The file is deliberately kept, not cancelled: cancel() deletes the output, and in the reported case that would have discarded nearly two hours of otherwise-valid audio. The notification uses the existing saved category so "Show in Finder" still works.

This second change is the backstop — it catches any future cause of a video-less recording, not just a disconnected display.

Tests

New AssetWriterTests covering the frame-count reporting:

  • audioOnlyRecordingReportsZeroVideoFrames — feeds only audio, asserts the count is zero, the file still exists, and AVURLAsset reports no video track. This reproduces the reported failure directly.
  • recordingWithVideoReportsFramesWritten — positive control with synthesized frames marked SCFrameStatus.complete.
  • recordingWithoutAnySampleThrows — confirms the existing noFramesWritten behaviour is unchanged.

.selectedDisplayDisconnected added to the ErrorTests description coverage. Full suite: 96 tests passing on macOS 15.7.7 / Xcode 26.3.

Note on verification: the display-validation path itself isn't unit tested, because SCShareableContent requires screen recording TCC, which the test runner doesn't have — it throws -3801 there, which is exactly the skip-validation path. I verified the frame-count half end to end via the tests above and the validation half by inspection. Happy to add coverage if you have a preferred way to fake SCShareableContent in this project.

I have SwiftLint unavailable locally, so the lint run is on CI — let me know if anything trips and I'll fix it.

Not addressed

Disconnecting a display during an active recording is still unhandled — the video track just stops partway while audio continues. That needs display-reconfiguration observation, which felt like a separate change; happy to follow up if you want it.

A display picked from the content picker is captured by its `displayID`.
Disconnecting the display leaves the filter intact, so recording starts
normally but the stream never delivers a frame. With audio enabled the
writer session is started by the first audio buffer, which satisfies the
`noFramesWritten` guard, and the recording is finalized and reported as
saved with audio tracks but no video track at all.

Validate the selected display against the connected displays before
creating the stream, and clear the stale selection so the next attempt
opens the picker. Report the number of video frames from `finishWriting`
so a recording that ends up without video is saved but flagged to the
user instead of announced as a normal success.

Closes jsattler#184
Copilot AI review requested due to automatic review settings July 28, 2026 09:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@jsattler

Copy link
Copy Markdown
Owner

@krishna2206 thanks for contributing this fix. I'm going to have a look. In case there are no major blockers it should be available in the next release.

@jsattler
jsattler merged commit 4bce0e8 into jsattler:main Jul 30, 2026
3 checks passed
@krishna2206
krishna2206 deleted the fix/stale-display-selection branch July 30, 2026 17:23
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.

Recording silently produces an audio-only file when the selected display is disconnected

3 participants