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.
This commit is contained in:
Arkadiusz Fal
2026-10-09 23:59:58 +02:00
parent 7b1a24a5b0
commit 09a384763a
3 changed files with 29 additions and 0 deletions

View File

@@ -68,6 +68,9 @@ final class MPVBackend: PlayerBackend {
// MARK: - Private Properties // MARK: - Private Properties
private var mpvClient: MPVClient? private var mpvClient: MPVClient?
#if os(iOS)
private var videoFillsFrame = false
#endif
#if os(macOS) #if os(macOS)
private var renderView: MPVOGLView? private var renderView: MPVOGLView?
#elseif targetEnvironment(simulator) && (os(iOS) || os(tvOS)) #elseif targetEnvironment(simulator) && (os(iOS) || os(tvOS))
@@ -173,6 +176,14 @@ final class MPVBackend: PlayerBackend {
_playerView?.pauseRendering() _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 /// Resume video rendering
func resumeRendering() { func resumeRendering() {
MPVLogging.log("MPVBackend.resumeRendering called") MPVLogging.log("MPVBackend.resumeRendering called")

View File

@@ -18,10 +18,16 @@ struct MPVRenderViewRepresentable: UIViewRepresentable {
/// Optional player state to update PiP availability /// Optional player state to update PiP availability
var playerState: PlayerState? 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 { func makeUIView(context: Context) -> UIView {
// Create a container that will hold the actual player view // Create a container that will hold the actual player view
let container = MPVContainerView() let container = MPVContainerView()
container.backgroundColor = .black container.backgroundColor = .black
container.backend = backend
container.fillsFrame = fillsFrame
MPVLogging.log("MPVRenderViewRepresentable.makeUIView: creating container", MPVLogging.log("MPVRenderViewRepresentable.makeUIView: creating container",
details: "hasPlayerView:\(backend.playerView != nil)") details: "hasPlayerView:\(backend.playerView != nil)")
@@ -64,6 +70,10 @@ private class MPVContainerView: UIView {
private weak var currentPlayerView: UIView? private weak var currentPlayerView: UIView?
private let containerID = UUID() 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 // Track all living containers to enable view transfer on deinit
private static var livingContainers = NSHashTable<MPVContainerView>.weakObjects() private static var livingContainers = NSHashTable<MPVContainerView>.weakObjects()
@@ -159,6 +169,9 @@ private class MPVContainerView: UIView {
playerView.trailingAnchor.constraint(equalTo: trailingAnchor) playerView.trailingAnchor.constraint(equalTo: trailingAnchor)
]) ])
currentPlayerView = playerView currentPlayerView = playerView
#if os(iOS)
backend?.setVideoFillsFrame(fillsFrame)
#endif
// Size it (and re-render its framebuffer) before anyone reacts to the move // Size it (and re-render its framebuffer) before anyone reacts to the move
if !bounds.isEmpty { if !bounds.isEmpty {
layoutIfNeeded() layoutIfNeeded()

View File

@@ -344,8 +344,13 @@ struct MiniPlayerView: View {
if let backend = playerService?.currentBackend as? MPVBackend, if let backend = playerService?.currentBackend as? MPVBackend,
let playerState, let playerState,
shouldMountVideoView { shouldMountVideoView {
#if os(iOS)
MPVRenderViewRepresentable(backend: backend, playerState: playerState, fillsFrame: true)
.allowsHitTesting(false)
#else
MPVRenderViewRepresentable(backend: backend, playerState: playerState) MPVRenderViewRepresentable(backend: backend, playerState: playerState)
.allowsHitTesting(false) .allowsHitTesting(false)
#endif
} }
// Thumbnail layer - on top so it can cover the black MPV view during PiP // Thumbnail layer - on top so it can cover the black MPV view during PiP