mirror of
https://github.com/yattee/yattee.git
synced 2026-07-31 04:12:03 +00:00
Guard PiP bridge geometry writes against non-finite rects
Two build 264 crashes on macOS 26.5.1 hit AppKit's non-finite-frame trap (_NSViewValidateGeometry) on AVKit's PiP sample-buffer host view during a routine layout pass. The NaN is produced inside AVKit from geometry we feed it - either the manual PiP window resize around a video switch or an infinite container rect copied into the sample buffer layer frame. Add a sanitizedGeometry helper that rejects NaN/infinite or degenerate rects and apply it at every layer/window geometry write in the bridge (updateLayerFrame variants, moveLayer, updateVideoAspectRatio, resizePiPWindow, updateLayerFrameToMatchPiPWindow). Rejected writes are logged with their call site so the actual source can be identified when it reproduces, instead of crashing on the next CATransaction commit.
This commit is contained in:
@@ -249,6 +249,7 @@ final class MPVPiPBridge: NSObject {
|
|||||||
let newHeight = currentBounds.width / aspectRatio
|
let newHeight = currentBounds.width / aspectRatio
|
||||||
let newBounds = CGRect(x: 0, y: 0, width: currentBounds.width, height: newHeight)
|
let newBounds = CGRect(x: 0, y: 0, width: currentBounds.width, height: newHeight)
|
||||||
|
|
||||||
|
if let newBounds = sanitizedGeometry(newBounds, from: "updateVideoAspectRatio") {
|
||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
CATransaction.setDisableActions(true)
|
CATransaction.setDisableActions(true)
|
||||||
sampleBufferLayer.bounds = newBounds
|
sampleBufferLayer.bounds = newBounds
|
||||||
@@ -256,6 +257,7 @@ final class MPVPiPBridge: NSObject {
|
|||||||
|
|
||||||
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
|
#else
|
||||||
// On iOS, don't modify bounds when PiP is inactive - this causes frame misalignment
|
// 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.
|
// (negative Y offset) which breaks the system's PiP restore UI positioning.
|
||||||
@@ -371,9 +373,25 @@ final class MPVPiPBridge: NSObject {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/// Validate a rect before it reaches PiP geometry. AppKit traps the whole
|
||||||
|
/// app when AVKit's PiP host view derives a non-finite frame from our
|
||||||
|
/// layer/window geometry (build 264 crash), so reject NaN/infinite or
|
||||||
|
/// degenerate rects and log the site instead of storing poisoned values.
|
||||||
|
private func sanitizedGeometry(_ rect: CGRect, from site: String) -> CGRect? {
|
||||||
|
guard rect.origin.x.isFinite, rect.origin.y.isFinite,
|
||||||
|
rect.width.isFinite, rect.height.isFinite,
|
||||||
|
rect.width > 0, rect.height > 0
|
||||||
|
else {
|
||||||
|
LoggingService.shared.debug("MPVPiPBridge: rejected invalid geometry \(rect) from \(site)", category: .mpv)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rect
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the layer frame when container bounds change.
|
/// Update the layer frame when container bounds change.
|
||||||
/// On macOS, the frame should be relative to the window's content view.
|
/// On macOS, the frame should be relative to the window's content view.
|
||||||
func updateLayerFrame(_ frame: CGRect) {
|
func updateLayerFrame(_ frame: CGRect) {
|
||||||
|
guard let frame = sanitizedGeometry(frame, from: "updateLayerFrame(_:)") else { return }
|
||||||
sampleBufferLayer.frame = frame
|
sampleBufferLayer.frame = frame
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +399,8 @@ final class MPVPiPBridge: NSObject {
|
|||||||
/// Update the layer frame based on container view's bounds.
|
/// Update the layer frame based on container view's bounds.
|
||||||
/// Call this on macOS when the container view's size changes.
|
/// Call this on macOS when the container view's size changes.
|
||||||
func updateLayerFrame(for containerView: NSView) {
|
func updateLayerFrame(for containerView: NSView) {
|
||||||
sampleBufferLayer.frame = containerView.bounds
|
guard let frame = sanitizedGeometry(containerView.bounds, from: "updateLayerFrame(for:)") else { return }
|
||||||
|
sampleBufferLayer.frame = frame
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -390,7 +409,9 @@ final class MPVPiPBridge: NSObject {
|
|||||||
/// as the layer must be in a visible window hierarchy.
|
/// as the layer must be in a visible window hierarchy.
|
||||||
func moveLayer(to containerView: PlatformView) {
|
func moveLayer(to containerView: PlatformView) {
|
||||||
sampleBufferLayer.removeFromSuperlayer()
|
sampleBufferLayer.removeFromSuperlayer()
|
||||||
sampleBufferLayer.frame = containerView.bounds
|
if let frame = sanitizedGeometry(containerView.bounds, from: "moveLayer(to:)") {
|
||||||
|
sampleBufferLayer.frame = frame
|
||||||
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
containerView.layer.addSublayer(sampleBufferLayer)
|
containerView.layer.addSublayer(sampleBufferLayer)
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
@@ -886,14 +907,19 @@ extension MPVPiPBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentRect = pipWindow.contentRect(forFrameRect: pipWindow.frame)
|
let contentRect = pipWindow.contentRect(forFrameRect: pipWindow.frame)
|
||||||
guard contentRect.width > 0 else { return }
|
guard contentRect.width > 0, contentRect.width.isFinite else { return }
|
||||||
let newContentHeight = contentRect.width / aspectRatio
|
let newContentHeight = contentRect.width / aspectRatio
|
||||||
|
guard newContentHeight.isFinite, newContentHeight > 0 else {
|
||||||
|
LoggingService.shared.debug("MPVPiPBridge: resizePiPWindow - rejected invalid content height \(newContentHeight) for aspect \(aspectRatio)", category: .mpv)
|
||||||
|
return
|
||||||
|
}
|
||||||
let heightDelta = newContentHeight - contentRect.height
|
let heightDelta = newContentHeight - contentRect.height
|
||||||
guard abs(heightDelta) >= 1 else { return }
|
guard abs(heightDelta) >= 1 else { return }
|
||||||
|
|
||||||
var frame = pipWindow.frame
|
var frame = pipWindow.frame
|
||||||
frame.size.height += heightDelta
|
frame.size.height += heightDelta
|
||||||
frame.origin.y -= heightDelta // keep the top edge in place
|
frame.origin.y -= heightDelta // keep the top edge in place
|
||||||
|
guard let frame = sanitizedGeometry(frame, from: "resizePiPWindow") else { return }
|
||||||
pipWindow.setFrame(frame, display: true, animate: false)
|
pipWindow.setFrame(frame, display: true, animate: false)
|
||||||
|
|
||||||
LoggingService.shared.debug("MPVPiPBridge: Resized PiP window for aspect \(aspectRatio): \(frame)", category: .mpv)
|
LoggingService.shared.debug("MPVPiPBridge: Resized PiP window for aspect \(aspectRatio): \(frame)", category: .mpv)
|
||||||
@@ -1000,7 +1026,7 @@ extension MPVPiPBridge {
|
|||||||
// Fix mispositioned internal AVKit views that cause the black bar
|
// Fix mispositioned internal AVKit views that cause the black bar
|
||||||
fixPiPLayerHostViewPosition(in: pipWindow)
|
fixPiPLayerHostViewPosition(in: pipWindow)
|
||||||
|
|
||||||
let newFrame = CGRect(origin: .zero, size: windowSize)
|
guard let newFrame = sanitizedGeometry(CGRect(origin: .zero, size: windowSize), from: "updateLayerFrameToMatchPiPWindow") else { return }
|
||||||
|
|
||||||
if sampleBufferLayer.frame.size != newFrame.size {
|
if sampleBufferLayer.frame.size != newFrame.size {
|
||||||
LoggingService.shared.debug("MPVPiPBridge: Resizing layer to match PiP window: \(sampleBufferLayer.frame) -> \(newFrame)", category: .mpv)
|
LoggingService.shared.debug("MPVPiPBridge: Resizing layer to match PiP window: \(sampleBufferLayer.frame) -> \(newFrame)", category: .mpv)
|
||||||
@@ -1012,7 +1038,7 @@ extension MPVPiPBridge {
|
|||||||
} else {
|
} else {
|
||||||
// Fallback: try to match superlayer
|
// Fallback: try to match superlayer
|
||||||
guard let superlayer = sampleBufferLayer.superlayer else { return }
|
guard let superlayer = sampleBufferLayer.superlayer else { return }
|
||||||
let superBounds = superlayer.bounds
|
guard let superBounds = sanitizedGeometry(superlayer.bounds, from: "updateLayerFrameToMatchPiPWindow(superlayer)") else { return }
|
||||||
if sampleBufferLayer.frame != superBounds {
|
if sampleBufferLayer.frame != superBounds {
|
||||||
LoggingService.shared.debug("MPVPiPBridge: Resizing layer to match superlayer: \(sampleBufferLayer.frame) -> \(superBounds)", category: .mpv)
|
LoggingService.shared.debug("MPVPiPBridge: Resizing layer to match superlayer: \(sampleBufferLayer.frame) -> \(superBounds)", category: .mpv)
|
||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
|
|||||||
Reference in New Issue
Block a user