Files
yattee/Yattee/Views/Player/MPVReflectionView.swift
Arkadiusz Fal 83895a1fd9 Reflect the video below the hinge in the tabletop layout
The iPhone Duo tabletop layout mirrors every presented frame into the
lower half, blurred and fading out, like a screen standing on a glossy
table. The render view blits a quarter-resolution copy of the display
framebuffer into a ring of IOSurface-backed buffers (no Y flip, so the
surface is already mirrored) and hands it to a CALayer hosted by
MPVReflectionView; the simulator's software renderer feeds the same
layer a flipped CGImage. Off during PiP, for audio-only streams, and
via the new Playback → Foldable → "Reflection Below Video" setting.
2026-10-01 20:59:48 +02:00

80 lines
2.3 KiB
Swift

//
// MPVReflectionView.swift
// Yattee
//
// Hosts the backend's mirrored frame layer below the video in the iPhone
// Duo tabletop layout, so the lower half shows a dim "reflection" of the
// upper half like a screen standing on a glossy table.
//
import SwiftUI
#if os(iOS)
import UIKit
/// Shows the mirrored video frames the MPV render view captures while the
/// view is on screen. Sized to the fitted video; place it directly below the
/// hinge line and fade it with `tabletopReflectionOverlay`.
struct MPVReflectionView: UIViewRepresentable {
let backend: MPVBackend
func makeUIView(context: Context) -> ReflectionHostView {
let view = ReflectionHostView()
view.backend = backend
view.attach(backend.reflectionLayer)
backend.setReflectionEnabled(true)
return view
}
func updateUIView(_ uiView: ReflectionHostView, context: Context) {
// The backend's player view can be recreated; re-attach the current layer.
uiView.backend = backend
uiView.attach(backend.reflectionLayer)
backend.setReflectionEnabled(true)
}
static func dismantleUIView(_ uiView: ReflectionHostView, coordinator: ()) {
uiView.backend?.setReflectionEnabled(false)
uiView.attach(nil)
}
final class ReflectionHostView: UIView {
private weak var hostedLayer: CALayer?
weak var backend: MPVBackend?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func attach(_ newLayer: CALayer?) {
guard newLayer !== hostedLayer else { return }
hostedLayer?.removeFromSuperlayer()
hostedLayer = newLayer
if let newLayer {
newLayer.frame = bounds
layer.addSublayer(newLayer)
}
}
override func layoutSubviews() {
super.layoutSubviews()
CATransaction.begin()
CATransaction.setDisableActions(true)
hostedLayer?.frame = bounds
CATransaction.commit()
}
deinit {
hostedLayer?.removeFromSuperlayer()
}
}
}
#endif