fix(video): surface a full-screen toggle on the macOS player - #3
Conversation
AVPlayerView.showsFullScreenToggleButton defaults to NO, and VideoPlayerSurface never set it, so the Mac video surface had no full-screen affordance at all — concerts played in a ~480pt letterbox wedged between the sidebar, the transport bar, and the 340pt inspector, with no way out. Turning it on gets AVKit's own control: it appears in the floating control bar, flips to a shrink icon to exit, and Esc exits too. No app-drawn chrome, which would have needed the unrelated NSView.enterFullScreenMode path since AVPlayerView exposes no programmatic full-screen entry point. Also correct the doc comment, which asserted AVPlayerView "supplies the native scrubber, volume, fullscreen, ..." — that false claim is why the gap went unnoticed. iOS is untouched: AVPlayerViewController already draws its expand button and already carries the delegate + OrientationGate rotation contract. Co-Authored-By: Claude Opus 5 <[email protected]>
Review follow-up on the full-screen toggle. When a video plays to the end, the didPlayToEndTime handler relinquishes the arbiter, which calls audio.endExternalPlayback(resume:) and restarts the audio queue if it had been playing. Inline that is fine — the app is visible and the change is legible. Under AVKit's full-screen window it is not: music starts with no visible UI and no obvious cause, behind a surface the viewer can't see past. AVPlayerView offers no way to fix this the way iOS does. It has no method to enter or exit full screen and no property reporting the state — only the four AVPlayerViewDelegate notifications — so there is no macOS equivalent of iOS's exitsFullScreenWhenPlaybackEnds, and a video that ends full screen leaves the viewer there until they press Esc. What we can control is where the hand-back happens, so track full-screen state via the delegate and defer it to the exit. The deferral is skipped if the viewer replayed the video while still full screen, since that re-claims the arbiter and is playing again; stop() clears the pending flag so a dismissal can't leave one armed. Co-Authored-By: Claude Opus 5 <[email protected]>
Review follow-up folded in (e39222d)Addresses finding 1 from review — but not the way the review proposed, because that turned out to be impossible. The correctionThe review suggested exiting full screen on end-of-item, mirroring iOS's What was fixedThe half that actually causes harm. So the surface now adopts
The coordinator uses Also softened the doc comment per finding 2 — it no longer asserts the shrink-icon and Esc behaviors I couldn't verify. Verification
|
What
One line in
AppleNugs/macOS/VideoPlayerSurface.swift:…plus a correction to the doc comment above it.
Why
The Mac video surface had no full-screen affordance at all.
AVPlayerView.showsFullScreenToggleButtonis documented asDefault is NO(AVPlayerView.h:198-201), andVideoPlayerSurfacenever set it — so AVKit drew no full-screen control, andNSWindow.toggleFullScreenis never called anywhere either. The button wasn't hidden; it was never enabled.That matters because the inline 16:9 box lives in the
NavigationSplitViewdetail column, squeezed between the sidebar, theTransportBarin a bottomsafeAreaInset, and a 340pt inspector — so a concert plays in a roughly 480pt letterbox with no way to escape it.The stale doc comment is the second half of the fix. It asserted that
AVPlayerView"supplies the native scrubber, volume, fullscreen, AirPlay, and (free) Picture-in-Picture" — a false claim that made the gap invisible to anyone reading the file.Behavior
All from AVKit, no app-drawn chrome:
An always-visible custom button was considered and rejected: neither AVKit surface exposes a programmatic full-screen entry point, so it would have required the unrelated
NSView.enterFullScreenModemechanism — a second full-screen path that AVKit's own state doesn't know about.iOS is untouched.
AVPlayerViewControlleralready draws its expand button inline and already carries a fullAVPlayerViewControllerDelegate+OrientationGaterotation contract (AppleNugs/iOS/VideoPlayerSurface.swift:39-72).Verification
AppleNugs(macOS) andAppleNugs-iOSboth build clean with fresh-derivedDataPathunderSWIFT_STRICT_CONCURRENCY=complete. Only warning is the pre-existingappintentsmetadataprocessorone.AppleNugsTests: 81/81 pass.swiftc+NSHostingViewharness is blind here: AVKit renders.floatingcontrols out of process. In a real, visible, hovered window the AVPlayerView subtree is onlyAVDesktopPlayerViewContentView → …ContentContainerView → …VideoContentView → …AnalysisPlayerLayerView— no control bar, no child windows,AXUnknownwith zero children — and toggling the property changes the in-process tree not at all.Reviewer notes
Worth eyeballing in a real build, since these only surface live:
VideoDetailView.swift:68is.onDisappear { app.video.stop() }, andstop()relinquishes Now Playing and resumes the audio queue. AVKit reparents the view into its own window while the SwiftUI tree stays mounted, so this shouldn't fire — but it's the one thing that would break the feature outright..aspectRatio(16:9)and.clipShape(RoundedRectangle(cornerRadius: 8)); confirm no clipped corners in full screen and a correct inline box afterward.liveBadgeand theloadErrorview are SwiftUI siblings, not AVPlayerView subviews, so they won't appear in full screen (fine — AVKit shows its own chrome). Confirm the badge returns on exit.If 1 or 2 misbehave, the fix is an
AVPlayerViewDelegatecoordinator (macOS 12+) guarding the teardown viaplayerViewWillEnterFullScreen/playerViewDidExitFullScreen— the same shape the iOS surface already uses. Not written speculatively.🤖 Generated with Claude Code