Skip to content

Allow local Program recording without a Zoom meeting (#1009) - #1025

Merged
iamfatness merged 2 commits into
mainfrom
codex/rc-1009
Oct 23, 2026
Merged

iamfatness merged 2 commits into
mainfrom
codex/rc-1009

Conversation

@iamfatness

@iamfatness iamfatness commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1009. This is a draft: the installed-app checks are not done (see "Not verified").

Cause

Two predicates required Settings.IsInMeeting:

  • StudioViewModel.CanToggleRecording, which gates the Studio Record button.
  • CanSetRecording, which gates the control API's transport.record.set and the resume after a core restart.

Program recording records whatever Program renders, including webcam and screen capture. The meeting term therefore blocked local recording for no reason.

Change

  • Record gate. Added RecordingControlPolicy (ViewModels/Transport), and both predicates now call it. It keeps the in-flight guard and the "Stop and close" (outputs-closing) guard. It has no meeting input. CanToggleCapture (Engine on/off) stays meeting-gated.
  • Start order in TransportCoordinator.SetRecordingAsync.
    • A start now brings the core up (EnsureMediaCoreRunningAsync) and checks MediaCoreOnAirPolicy.RecordBlockReason before the Record intent is armed.
    • The reason: before this PR, being in a meeting implied a running core. Now the core can be down when Record is pressed, and arming first would let a production sync carry recording=true to a core that is then refused.
    • A core with no encoder is refused, and the reason is shown.
    • A core that cannot start fails the start ("Recording start failed: ...") before anything is armed, so there is nothing to roll back.
    • The rollback paths after arming are unchanged.
  • Crash resume (PENDING OWNER RULING). In OutputResumeAfterCoreRestart, the parameter requiresMeeting is renamed requiresZoomRejoin.
    • A recording that was running with no meeting when the core crashed (CoreOnly) is now resumed.
    • Before, it was "NOT resumed: there is no meeting to record", which stops being true once offline recording is allowed.
    • A recording whose Zoom rejoin failed is still not resumed, and the status still says so.
    • This changes the Recording does not resume after the media core restarts, and the operator is not told #732 rule. core-restart-drill.md and core-on-air-policy.md mark it pending owner ruling and state the old rule.
  • Readiness text. It no longer says the operator must join Zoom first.
  • ISO is unchanged. A Zoom guest's ISO still needs the guest in the roster (BuildIsoSourceTargets) and Zoom capture on (IsoCapturePreflight).

Leave/rejoin finding

I checked the real leave and rejoin paths by reading them. The tests cover only the coordinator, the snapshot merge and the stub core.

What the real paths do:

  • Shell, Leave. SettingsViewModel.LeaveZoomAsync calls onBeforeLeaveMeeting, which calls UnsubscribeZoomCapture (Engine off). It then calls _bridge.LeaveZoomAsync (zoom-leave). The core stays up. None of these steps touches RecordingRequested.
  • Production sync. It sends Recording = RecordingRequested whatever the meeting state.
  • Clearing the intent. RecordingRequested is cleared only on three things: a core-reported failure or interruption, a core restart, or a deliberate core stop.
  • Core. MediaCore::leaveZoom and ZoomEngineRuntime::leave reset only Zoom state. Join does not touch outputs.

The old gate had one effect on leave: with no meeting, the Record button was disabled. A recording still running after a leave could therefore not be stopped from the Studio button until a rejoin. A stop through the API was still allowed. Removing the meeting term removes that too.

What the tests cover, and what they do not:

  • CoordinatorEngineOff_DoesNotStopAnArmedRecording_FakeHost: only the Engine-off path in TransportCoordinator, against a fake host. The real UnsubscribeZoomCapture is not run.
  • LeaveAndRejoinSnapshotsKeepTheProgramRecordingObservedAndArmed (MediaCore): only how the shell reads a recording across leave/rejoin snapshots (ZoomCaptureSnapshotMerger plus LiveProductionSync).
  • StubCoreZoomJoinLeaveRejoinDoesNotStopAnOfflineStartedRecording (native): the real JsonRpcServer zoom-join and zoom-leave handlers on stub modules. MediaCore takes its stub-meeting branch, so ZoomEngineRuntime::leave and join are not run.

I could not add a test of the real leave sequence without StudioViewModel:

  • The real UnsubscribeZoomCapture is a private method of StudioViewModel, which cannot be constructed in tests.
  • SettingsViewModel cannot be constructed either:
    • It takes the concrete MediaCoreBridgeService, not the IMediaCoreBridge seam.
    • It calls DispatcherQueue.GetForCurrentThread() at field init.
    • Its constructor reads and writes the real telemetry-consent and recent-meetings files under %LOCALAPPDATA%\CoreVideoPro, and starts the telemetry service.
  • A test there would need a DI seam in SettingsViewModel first. That refactor is outside Local Program recording is incorrectly gated on joining a Zoom meeting #1009.

Tests

Suite Result
WinUI 1732 passed, 0 failed
MediaCore 2641 passed, 0 failed
Control 102 passed, 0 failed (run on the first commit; Control is unchanged since)
Native stub build (COREVIDEO_STUB=ON, Release), EncoderRecordingSession.* 16/16
Native stub build, JsonRpcServer.* 21/21 (on the first commit)

New WinUI tests:

  • start and stop available offline
  • the in-flight and outputs-closing guards are kept
  • the Studio predicates contain no meeting term (a source-text check)
  • transport.record.set starts with no meeting
  • coordinator Engine-off keeps an armed recording
  • an offline start against a core with no encoder is refused
  • an offline start when the core cannot start
  • the core starts before Record is armed
  • a Zoom guest's ISO needs the meeting while a capture ISO does not

The full native suite and the dev/real-adapter tier were not run.

Not verified

  • The installed app's Studio controls: Record start/stop with Zoom offline from the real button. The app was not launched.
  • That an offline recording ends up as a finished, playable video+audio file. The native test uses a counting encoder sink, not Media Foundation.
  • Leave and rejoin while recording, on the real-engine path or in a live Zoom meeting. This was checked only by reading the code (see above).
  • The CoreOnly crash-resume change. It needs owner sign-off and was not drilled against a real core crash.

🤖 Generated with Claude Code

iamfatness and others added 2 commits October 23, 2026 07:50
The Studio Record button (CanToggleRecording) and the control API's
transport.record.set (CanSetRecording) both required Settings.IsInMeeting,
so a show of webcam/screen/media sources could not record with Zoom
offline. Both now go through RecordingControlPolicy, which keeps only the
in-flight and outputs-closing guards. Engine on/off stays meeting-gated;
Zoom-guest ISO still needs the guest in the roster and Zoom capture on.

- TransportCoordinator.SetRecordingAsync re-asks RecordBlockReason of the
  core it just started, so an offline start against a core whose encoder
  did not construct is refused with the reason instead of armed.
- OutputResumeAfterCoreRestart: a recording made with no meeting (CoreOnly)
  now resumes after a core crash; a recording whose Zoom rejoin failed is
  still not resumed and says so.
- Show readiness text no longer says Zoom must be joined first.
- Leave/rejoin: traced; nothing stops a recording. Pinned by shell
  (coordinator, snapshot merge) and native (stub core join/leave/rejoin)
  tests. The old gate did disable the Record button after a leave, so a
  recording running at leave could not be stopped from the Studio button
  until a rejoin (the API stop was still allowed); that is gone too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sts (#1009)

- SetRecordingAsync starts the core and asks RecordBlockReason BEFORE
  arming the Record intent, so no production sync can carry
  recording=true to a core that is then refused. A core that cannot
  start fails the start before anything is armed. The in-try Ensure is
  kept for the stop path and as a no-op on a start.
- Leave/rejoin tests renamed and re-commented to say what they cover:
  CoordinatorEngineOff_..._FakeHost (coordinator, fake host),
  LeaveAndRejoinSnapshots... (snapshot merge/mapping),
  StubCoreZoomJoinLeaveRejoin... (stub core; ZoomEngineRuntime not run).
- New test: OfflineStart_StartsTheCoreBeforeArming.
- core-restart-drill.md and core-on-air-policy.md: the recording resume
  rule from #1025 is marked PENDING OWNER RULING; the old rule is stated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@iamfatness
iamfatness marked this pull request as ready for review October 23, 2026 12:44
@iamfatness
iamfatness merged commit b6883be into main Oct 23, 2026
17 checks passed
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.

Local Program recording is incorrectly gated on joining a Zoom meeting

1 participant