mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 22:02:10 +00:00
Fix macOS PiP window not resizing when switching videos
Switching videos during active PiP left the window at the old video's size with white space around the new content, for two reasons: - MPV reports the new video's width and height as separate property events while the old values are kept across switches, so a transient mixed size (new width with old height) could reach the PiP capture path and AVKit sized the window from that frame. Coalesce the PiP capture-size/aspect update with a short debounce so only settled dimensions are used. - On macOS nothing told AVKit about the new aspect: unlike iOS, the bridge never flushed the sample buffer renderer on an aspect change during active PiP, and AVKit doesn't resize the macOS PiP window on its own. Flush the renderer, reset the format description, and resize the PiP window to the new aspect ratio explicitly.
This commit is contained in:
@@ -222,21 +222,40 @@ final class MPVPiPBridge: NSObject {
|
||||
/// - Parameter aspectRatio: Video width divided by height (e.g., 16/9 = 1.777...)
|
||||
func updateVideoAspectRatio(_ aspectRatio: CGFloat) {
|
||||
guard aspectRatio > 0 else { return }
|
||||
#if os(macOS)
|
||||
let previousAspectRatio = videoAspectRatio
|
||||
#endif
|
||||
videoAspectRatio = aspectRatio
|
||||
|
||||
#if os(macOS)
|
||||
// On macOS, update layer bounds to match aspect ratio
|
||||
// This helps AVKit size the PiP window correctly
|
||||
let currentBounds = sampleBufferLayer.bounds
|
||||
let newHeight = currentBounds.width / aspectRatio
|
||||
let newBounds = CGRect(x: 0, y: 0, width: currentBounds.width, height: newHeight)
|
||||
if isPiPActive {
|
||||
// Video changed while PiP is active. AVKit sizes the PiP window from
|
||||
// the format of the initially enqueued frames and doesn't resize it
|
||||
// when frames with new dimensions arrive, leaving empty space around
|
||||
// the new video. Flush stale frames, force a fresh format description
|
||||
// so AVKit re-reads the new dimensions, and resize the PiP window to
|
||||
// the new aspect ratio ourselves.
|
||||
let ratioChange = abs(aspectRatio - previousAspectRatio) / previousAspectRatio
|
||||
if ratioChange > 0.01 {
|
||||
sampleBufferLayer.sampleBufferRenderer.flush()
|
||||
currentFormatDescription = nil
|
||||
resizePiPWindow(toAspectRatio: aspectRatio)
|
||||
LoggingService.shared.debug("MPVPiPBridge: Aspect ratio changed during PiP \(previousAspectRatio) -> \(aspectRatio), flushed buffer and resized PiP window", category: .mpv)
|
||||
}
|
||||
} else {
|
||||
// On macOS, update layer bounds to match aspect ratio
|
||||
// This helps AVKit size the PiP window correctly
|
||||
let currentBounds = sampleBufferLayer.bounds
|
||||
let newHeight = currentBounds.width / aspectRatio
|
||||
let newBounds = CGRect(x: 0, y: 0, width: currentBounds.width, height: newHeight)
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
sampleBufferLayer.bounds = newBounds
|
||||
CATransaction.commit()
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
sampleBufferLayer.bounds = newBounds
|
||||
CATransaction.commit()
|
||||
|
||||
LoggingService.shared.debug("MPVPiPBridge: Updated aspect ratio to \(aspectRatio), layer bounds: \(newBounds)", category: .mpv)
|
||||
LoggingService.shared.debug("MPVPiPBridge: Updated aspect ratio to \(aspectRatio), layer bounds: \(newBounds)", category: .mpv)
|
||||
}
|
||||
#else
|
||||
// On iOS, don't modify bounds when PiP is inactive - this causes frame misalignment
|
||||
// (negative Y offset) which breaks the system's PiP restore UI positioning.
|
||||
@@ -857,6 +876,29 @@ extension MPVPiPBridge {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Resize the PiP window to match a new video aspect ratio, keeping the
|
||||
/// current width and top edge. AVKit doesn't resize the window itself when
|
||||
/// the enqueued frame dimensions change (e.g. switching videos during PiP).
|
||||
func resizePiPWindow(toAspectRatio aspectRatio: CGFloat) {
|
||||
guard aspectRatio > 0, let pipWindow = findPiPWindow() else {
|
||||
LoggingService.shared.debug("MPVPiPBridge: resizePiPWindow - PiP window not found", category: .mpv)
|
||||
return
|
||||
}
|
||||
|
||||
let contentRect = pipWindow.contentRect(forFrameRect: pipWindow.frame)
|
||||
guard contentRect.width > 0 else { return }
|
||||
let newContentHeight = contentRect.width / aspectRatio
|
||||
let heightDelta = newContentHeight - contentRect.height
|
||||
guard abs(heightDelta) >= 1 else { return }
|
||||
|
||||
var frame = pipWindow.frame
|
||||
frame.size.height += heightDelta
|
||||
frame.origin.y -= heightDelta // keep the top edge in place
|
||||
pipWindow.setFrame(frame, display: true, animate: false)
|
||||
|
||||
LoggingService.shared.debug("MPVPiPBridge: Resized PiP window for aspect \(aspectRatio): \(frame)", category: .mpv)
|
||||
}
|
||||
|
||||
/// Recursively log view hierarchy for debugging
|
||||
private func logViewHierarchy(_ view: NSView, depth: Int) {
|
||||
let indent = String(repeating: " ", count: depth)
|
||||
|
||||
@@ -127,6 +127,8 @@ final class MPVBackend: PlayerBackend {
|
||||
// Video dimensions for aspect ratio detection
|
||||
private var videoWidth: Int = 0
|
||||
private var videoHeight: Int = 0
|
||||
// Coalesces separate width/height property events into one PiP size update
|
||||
private var pipVideoSizeUpdateTask: Task<Void, Never>?
|
||||
// Cached video FPS to avoid sync fetch on main thread
|
||||
private var containerFps: Double = 0
|
||||
// Cached cache state to avoid sync fetch on main thread
|
||||
@@ -1732,17 +1734,28 @@ extension MPVBackend: MPVClientDelegate {
|
||||
LoggingService.shared.debug("MPV: Video size detected: \(videoWidth)x\(videoHeight)", category: .mpv)
|
||||
delegate?.backend(self, didUpdateVideoSize: videoWidth, height: videoHeight)
|
||||
|
||||
// Update PiP bridge with video aspect ratio for proper window sizing
|
||||
// Update PiP capture dimensions and aspect ratio, coalesced.
|
||||
// Width and height arrive as separate MPV property events and the old
|
||||
// values are kept across video switches, so right after a switch one of
|
||||
// them can still be stale (e.g. new width paired with old height).
|
||||
// Debounce so capture buffers and the PiP window never see that
|
||||
// transient mixed size.
|
||||
#if os(iOS) || os(macOS)
|
||||
let aspectRatio = CGFloat(videoWidth) / CGFloat(videoHeight)
|
||||
pipBridge?.updateVideoAspectRatio(aspectRatio)
|
||||
#endif
|
||||
|
||||
// Update render view with video content dimensions for accurate PiP capture
|
||||
// (avoids capturing letterbox/pillarbox black bars)
|
||||
#if os(iOS) || os(macOS)
|
||||
renderView?.videoContentWidth = videoWidth
|
||||
renderView?.videoContentHeight = videoHeight
|
||||
pipVideoSizeUpdateTask?.cancel()
|
||||
pipVideoSizeUpdateTask = Task { [weak self] in
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
guard let self, !Task.isCancelled else { return }
|
||||
let width = self.videoWidth
|
||||
let height = self.videoHeight
|
||||
guard width > 0, height > 0 else { return }
|
||||
// Content dimensions must update before the aspect ratio: on macOS
|
||||
// the aspect update flushes the PiP sample buffer during active PiP,
|
||||
// and a stale-sized frame captured afterwards would make AVKit
|
||||
// re-read the old dimensions.
|
||||
self.renderView?.videoContentWidth = width
|
||||
self.renderView?.videoContentHeight = height
|
||||
self.pipBridge?.updateVideoAspectRatio(CGFloat(width) / CGFloat(height))
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update render view with video FPS for display link frame rate matching
|
||||
|
||||
Reference in New Issue
Block a user