From 09a384763aec3fcef33d495c9461f4eeee3ba2ff Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 9 Oct 2026 23:59:58 +0200 Subject: [PATCH] Stretch the mini player's video preview to fill its frame Wide videos showed letterbox bars in the tiny iOS mini player preview right after the dismiss morph, which lands the video filling the preview. The container that takes the shared player view now sets mpv's keepaspect, so the mini player fills its frame and every other surface keeps the aspect ratio. --- Yattee/Services/Player/MPVBackend.swift | 11 +++++++++++ .../Views/Player/MPVRenderViewRepresentable.swift | 13 +++++++++++++ Yattee/Views/Player/MiniPlayerView.swift | 5 +++++ 3 files changed, 29 insertions(+) diff --git a/Yattee/Services/Player/MPVBackend.swift b/Yattee/Services/Player/MPVBackend.swift index 6e08f32d..c46209f5 100644 --- a/Yattee/Services/Player/MPVBackend.swift +++ b/Yattee/Services/Player/MPVBackend.swift @@ -68,6 +68,9 @@ final class MPVBackend: PlayerBackend { // MARK: - Private Properties private var mpvClient: MPVClient? + #if os(iOS) + private var videoFillsFrame = false + #endif #if os(macOS) private var renderView: MPVOGLView? #elseif targetEnvironment(simulator) && (os(iOS) || os(tvOS)) @@ -173,6 +176,14 @@ final class MPVBackend: PlayerBackend { _playerView?.pauseRendering() } + /// Stretch the video over the whole view (ignoring its aspect ratio) + /// instead of letterboxing it. + func setVideoFillsFrame(_ fills: Bool) { + guard fills != videoFillsFrame else { return } + videoFillsFrame = fills + mpvClient?.setProperty("keepaspect", !fills) + } + /// Resume video rendering func resumeRendering() { MPVLogging.log("MPVBackend.resumeRendering called") diff --git a/Yattee/Views/Player/MPVRenderViewRepresentable.swift b/Yattee/Views/Player/MPVRenderViewRepresentable.swift index 0f84221d..e680f0a4 100644 --- a/Yattee/Views/Player/MPVRenderViewRepresentable.swift +++ b/Yattee/Views/Player/MPVRenderViewRepresentable.swift @@ -18,10 +18,16 @@ struct MPVRenderViewRepresentable: UIViewRepresentable { /// Optional player state to update PiP availability var playerState: PlayerState? + /// Stretch the video to fill the view instead of letterboxing it (the mini + /// player's tiny preview, where bars look like a glitch). + var fillsFrame = false + func makeUIView(context: Context) -> UIView { // Create a container that will hold the actual player view let container = MPVContainerView() container.backgroundColor = .black + container.backend = backend + container.fillsFrame = fillsFrame MPVLogging.log("MPVRenderViewRepresentable.makeUIView: creating container", details: "hasPlayerView:\(backend.playerView != nil)") @@ -64,6 +70,10 @@ private class MPVContainerView: UIView { private weak var currentPlayerView: UIView? private let containerID = UUID() + /// Applied to the backend whenever this container takes the player view + weak var backend: MPVBackend? + var fillsFrame = false + // Track all living containers to enable view transfer on deinit private static var livingContainers = NSHashTable.weakObjects() @@ -159,6 +169,9 @@ private class MPVContainerView: UIView { playerView.trailingAnchor.constraint(equalTo: trailingAnchor) ]) currentPlayerView = playerView + #if os(iOS) + backend?.setVideoFillsFrame(fillsFrame) + #endif // Size it (and re-render its framebuffer) before anyone reacts to the move if !bounds.isEmpty { layoutIfNeeded() diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index 2a7f7892..64a502f8 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -344,8 +344,13 @@ struct MiniPlayerView: View { if let backend = playerService?.currentBackend as? MPVBackend, let playerState, shouldMountVideoView { + #if os(iOS) + MPVRenderViewRepresentable(backend: backend, playerState: playerState, fillsFrame: true) + .allowsHitTesting(false) + #else MPVRenderViewRepresentable(backend: backend, playerState: playerState) .allowsHitTesting(false) + #endif } // Thumbnail layer - on top so it can cover the black MPV view during PiP