Repository navigation
Conversation
|
Thanks for this, Pavel. Having the delegate and the controller in the engine is the right place for it, and the off-main snapshot design reads well: nothing in the delegate path waits on main. Local runMerged onto current main (
The red CI test
Questions
Smaller points
Known limitation, fine for this PRAny load on the software path ends PiP (next episode with auto-advance, and reloads such as an audio switch). On iOS that means PiP plus auto-advance closes the window and continues audio-only in the background. It is documented, so no change needed here; keeping the display layer across loads for a hand-over like the native path would be a good follow-up. Happy to merge once the test is hardened and your device list has been run. |
SoftwarePiPSource gains makePictureInPictureController(), which returns an AVPictureInPictureController around the display layer (nil where PiP is unsupported), and a playback delegate that maps play, pause, skip and the time range onto the engine's transport. Skip is clamped to the VOD duration and completes once the seek has settled; with no content loaded the range is invalid. The engine invalidates the playback state of the latest controller it made when state or duration changes. The delegate never blocks on main: the source keeps a lock-guarded snapshot of its transport answers, refreshed at init, on main-thread reads and on state or duration changes, and setPlaying hops asynchronously. On macOS the picture fits the PiP window: while PiP is active the layer is laid out at the window's size, found with public AppKit only and followed through its frame-change notification, and the layer is restored when PiP stops. iOS and tvOS are unchanged. docs/api.md, README.md and the CHANGELOG are updated.
…a dedicated thread
…ecision from inside its own teardown stopInternal still clears pictureInPictureActive on every platform, so a gone window is never taken for a live one. On tvOS the flag's didSet could ask for the background decision while the teardown was half done, and only a guard on the software host being present kept that from running. The reset now says it is the teardown's own, so only a window that closes owes the decision.
…e the picture's aspect is known Without a window, the tracker observed every window update for the whole PiP session; it now stops after a five second budget and keeps the fit layout. With the aspect unknown at start any lone new panel was adopted; when the picture size arrives, a window that does not match it is dropped and the match starts again, and a tracker that found nothing gets the aspect.
… window fit with FB22411168
|
Thanks for the careful review and for running it on current main. Everything is in four new commits on top of the PR head, no history rewrite (e22c98b..ecfead5). Red test (3808096). The getter test now runs its background half on a dedicated tvOS re-entrancy (f1c696e). I did not gate the reset to non-tvOS, because the reason for it holds on every platform: a host that drops its controller with the source never sees a did-stop, so a gone window must not count as live for the keepalive policy. Instead the reset inside Q2, iOS fit. Yes, intended: Tracker cost (1c02b1c). The search for the window now has a budget of five seconds ( Unknown aspect (same commit). When the engine learns the picture size during PiP, the tracker gets the aspect: a window adopted without it that does not match is dropped (no area reported, the view's setting restored) and the match starts again, and a tracker that found nothing gets to look with the aspect. Tests cover the tracker and the engine wiring. Docs and CHANGELOG (ecfead5). Known limitation. Agreed, it stays as it is for this PR, and I have put it into the PR text as a possible follow-up (keeping the display layer across loads). Tests on my side after the changes: full |
AVKit resizes the system PiP window to each new picture shape, and the new aspect can arrive before that resize has happened. The tracker treated the mismatch as a lost window: it dropped the adopted window, the layer fell back to the view's fit, and the search had to find the window again. A window matched under a known aspect now stays the PiP window until PiP ends, and its size is followed as it changes. A window adopted before the aspect was known is still checked against the first aspect. Source: parity/pip @ ecfead5
Software-host parity work from #702, piece two: picture in picture.
SoftwarePiPSourcealready published theAVSampleBufferDisplayLayerand transport answers, but every host had to write its ownAVPictureInPictureControllerand playback delegate. This moves that into the engine.What it does
makePictureInPictureController()onSoftwarePiPSourcereturns anAVPictureInPictureControlleraround the display layer, or nil when PiP is unsupported on the device.SoftwarePiPSource.isSupported(new, public) answers the same question without building one.SoftwarePiPPlaybackDelegate(source.playbackDelegate) maps play, pause, skip and the time range onto the engine's transport. Skip is clamped to the VOD duration and its completion handler fires once the seek has settled. With no content loaded the range is invalid, so the window shows no scrubber.invalidatePlaybackState()on the controller it made whenstateordurationchanges, so the play/pause button and the timeline follow the engine.setPlayinghops asynchronously.pictureInPictureActiveis set and a boundAetherPlayerViewshows a software session, the layer is laid out to the picture instead of to the view. On iOS (and on tvOS if a host sets the flag) that is the picture's aspect-fit rectangle, or the aspect-fill rectangle clipped by the view underresizeAspectFill; the view's layer is reshaped andclipsToBoundsis toggled while PiP is active, and both are restored when it stops. On macOS the layer is laid out at the size of the system PiP window, so its ownvideoGravityfits the picture. That window is found with public AppKit only (a new visible panel whose aspect matches the picture, when exactly one does), and its size is followed through the public frame-change notification, with the render-size callback as a second trigger. The search stops watching window updates at the first update after five seconds, and it starts again when the picture's aspect becomes known or changes (a panel taken before the size was known is dropped if it does not match). Observers are removed and the layer is restored when PiP stops. Nothing is hidden, and if no window is found the shaped layout is kept.stopInternalstill resetspictureInPictureActive(a host that drops its controller with the source never sees a did-stop), but on tvOS that reset no longer asks for the background decision from inside the teardown; only a window that closes does.API notes
pictureInPictureActivefrom its callbacks, and rebuilds the controller whenever$softwarePiPSourcechanges (a reload replaces the source, and the content source holds its delegate weakly).docs/api.md,README.mdand the CHANGELOG (Unreleased > Added) are updated.Tested on Mac
swift test, full run: 5107 swift-testing tests in 699 suites plus 815 XCTest (1 skipped), 0 failures.LIBDISPATCH_COOPERATIVE_POOL_STRICT=1 swift test --skip-build --disable-xctest: 5107 tests pass.Thread; it passes alone three times.build-for-testingfor tvOS and iOS,buildfor visionOS; visionOS is build only, PiP is not exercised there).stopInternalis not the one clearing it. The settle itself runs on tvOS only, so that platform is compile-checked, not run.Known issue: system bar (OS defect FB22411168)
On macOS the system draws an opaque bar over the top of the PiP window for sample-buffer sources whenever the window is taller than a height that depends on the video's aspect (roughly 350 pt for 16:9, 467 pt for 2.4:1, 200 pt for 4:3 at the default scale). The bar overlays the top of the picture; there is no public API to remove it or to read its height, so the picture is fitted to the whole window and the bar is left alone. At the default window size, 16:9 and 2.4:1 show no bar; 4:3 loses about 114 pt at the top. The player-layer route has no such bar. Filed as FB22411168.
Known limitations
videoGravity = .resizethe main view shows the picture letterboxed (undistorted) while PiP is active, because the window mirrors the layer's shape; it returns to stretched when PiP ends.resizeAspectandresizeAspectFilllook the same in the main view.Still needs a real window (macOS)
Still needs Apple TV / devices
Not yet tried on hardware, so this stays a draft until these pass with a host that uses the helper:
clipsToBoundsis restored.