mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 14:32:28 +00:00
fix ios player rotation via patched libmpv
This commit is contained in:
parent
39241ae746
commit
fb805b738b
6 changed files with 121 additions and 8 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -28,3 +28,6 @@ asset
|
|||
scripts/scrape_android_compose_animation_docs.py
|
||||
tools
|
||||
AGENTS.md
|
||||
|
||||
# Local MPVKit iOS build environment (sparse APFS image, see MPVKit docs)
|
||||
.mpvkit-build.sparseimage
|
||||
|
|
|
|||
2
MPVKit
2
MPVKit
|
|
@ -1 +1 @@
|
|||
Subproject commit 20afe97c34e46fce08a3794e2afa64613ea62794
|
||||
Subproject commit be2268feded4d8b34aa0631d72df0b70ba9a2fcb
|
||||
|
|
@ -45,6 +45,7 @@ interface NuvioPlayerBridge {
|
|||
fun setPlaybackSpeed(speed: Float)
|
||||
fun setMuted(muted: Boolean)
|
||||
fun setResizeMode(mode: Int) // 0=Fit, 1=Fill, 2=Zoom
|
||||
fun syncVideoSurfaceLayout(width: Double, height: Double)
|
||||
fun getAudioTrackCount(): Int
|
||||
fun getAudioTrackIndex(at: Int): Int
|
||||
fun getAudioTrackId(at: Int): String
|
||||
|
|
|
|||
|
|
@ -18,12 +18,15 @@ import androidx.compose.material.icons.rounded.PlayArrow
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.interop.UIKitViewController
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.useContents
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
|
@ -58,6 +61,7 @@ actual fun PlatformPlayerSurface(
|
|||
val latestOnControllerReady = rememberUpdatedState(onControllerReady)
|
||||
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
||||
val latestOnError = rememberUpdatedState(onError)
|
||||
val density = LocalDensity.current
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
val playerSettings by PlayerSettingsRepository.uiState.collectAsStateWithLifecycle()
|
||||
val latestPlayerSettings = rememberUpdatedState(playerSettings)
|
||||
|
|
@ -341,7 +345,25 @@ actual fun PlatformPlayerSurface(
|
|||
Box(modifier = modifier) {
|
||||
UIKitViewController(
|
||||
factory = { bridge.createPlayerViewController() },
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onSizeChanged { size ->
|
||||
if (size.width > 1 && size.height > 1) {
|
||||
bridge.syncVideoSurfaceLayout(
|
||||
width = with(density) { size.width.toDp().value.toDouble() },
|
||||
height = with(density) { size.height.toDp().value.toDouble() },
|
||||
)
|
||||
}
|
||||
},
|
||||
onResize = { viewController, rect ->
|
||||
viewController.view.setFrame(rect)
|
||||
rect.useContents {
|
||||
bridge.syncVideoSurfaceLayout(
|
||||
width = size.width,
|
||||
height = size.height,
|
||||
)
|
||||
}
|
||||
},
|
||||
interactive = false,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ final class MPVPlayerBridgeImpl: NSObject, NuvioPlayerBridge {
|
|||
func setPlaybackSpeed(speed: Float) { playerVC?.setSpeed(speed) }
|
||||
func setMuted(muted: Bool) { playerVC?.setMuted(muted) }
|
||||
func setResizeMode(mode: Int32) { playerVC?.setResize(Int(mode)) }
|
||||
func syncVideoSurfaceLayout(width: Double, height: Double) {
|
||||
playerVC?.syncVideoSurfaceLayout(size: CGSize(width: width, height: height))
|
||||
}
|
||||
|
||||
// Audio tracks
|
||||
func getAudioTrackCount() -> Int32 { Int32(playerVC?.audioTracks.count ?? 0) }
|
||||
|
|
@ -253,6 +256,8 @@ final class MPVPlayerViewController: UIViewController {
|
|||
private let errorStateLock = NSLock()
|
||||
private var metalLayer = MetalLayer()
|
||||
private var lastAppliedDrawableSize: CGSize = .zero
|
||||
private var externallyManagedViewSize: CGSize?
|
||||
private var pendingSurfaceLayoutWorkItems: [DispatchWorkItem] = []
|
||||
private var pendingLoadRequest: PendingLoadRequest?
|
||||
private var pendingLoadRetryWorkItem: DispatchWorkItem?
|
||||
private var mpv: OpaquePointer?
|
||||
|
|
@ -313,6 +318,8 @@ final class MPVPlayerViewController: UIViewController {
|
|||
metalLayer.framebufferOnly = true
|
||||
metalLayer.backgroundColor = UIColor.black.cgColor
|
||||
metalLayer.wantsExtendedDynamicRangeContent = true
|
||||
metalLayer.anchorPoint = CGPoint(x: 0, y: 0)
|
||||
metalLayer.position = .zero
|
||||
view.layer.addSublayer(metalLayer)
|
||||
layoutMetalLayer()
|
||||
|
||||
|
|
@ -339,19 +346,92 @@ final class MPVPlayerViewController: UIViewController {
|
|||
becomeFirstResponder()
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
publishCachedNowPlayingInfoIfNeeded()
|
||||
layoutMetalLayer()
|
||||
syncVideoSurfaceLayout()
|
||||
attemptStartPendingLoad()
|
||||
}
|
||||
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
layoutMetalLayer()
|
||||
syncVideoSurfaceLayout()
|
||||
refreshImmersiveSystemUI()
|
||||
attemptStartPendingLoad()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
syncVideoSurfaceLayoutNow(scheduleDeferredPasses: false)
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.syncVideoSurfaceLayoutNow(scheduleDeferredPasses: false)
|
||||
}, completion: { [weak self] _ in
|
||||
self?.syncVideoSurfaceLayout()
|
||||
self?.attemptStartPendingLoad()
|
||||
})
|
||||
}
|
||||
|
||||
func syncVideoSurfaceLayout(size: CGSize) {
|
||||
if Thread.isMainThread {
|
||||
syncVideoSurfaceLayoutNow(size: size, scheduleDeferredPasses: true)
|
||||
} else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.syncVideoSurfaceLayoutNow(size: size, scheduleDeferredPasses: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncVideoSurfaceLayout() {
|
||||
if Thread.isMainThread {
|
||||
syncVideoSurfaceLayoutNow(size: nil, scheduleDeferredPasses: true)
|
||||
} else {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.syncVideoSurfaceLayoutNow(size: nil, scheduleDeferredPasses: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func syncVideoSurfaceLayoutNow(size: CGSize? = nil, scheduleDeferredPasses: Bool) {
|
||||
guard isViewLoaded else { return }
|
||||
if let size, size.width > 1, size.height > 1 {
|
||||
externallyManagedViewSize = size
|
||||
applyExternallyManagedViewSize(size)
|
||||
}
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
layoutMetalLayer()
|
||||
|
||||
if scheduleDeferredPasses {
|
||||
scheduleDeferredSurfaceLayoutPasses()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleDeferredSurfaceLayoutPasses() {
|
||||
pendingSurfaceLayoutWorkItems.forEach { $0.cancel() }
|
||||
pendingSurfaceLayoutWorkItems.removeAll(keepingCapacity: true)
|
||||
|
||||
[0.0, 0.05, 0.15, 0.35].forEach { delay in
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.syncVideoSurfaceLayoutNow(scheduleDeferredPasses: false)
|
||||
}
|
||||
pendingSurfaceLayoutWorkItems.append(workItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyExternallyManagedViewSize(_ size: CGSize) {
|
||||
let targetBounds = CGRect(origin: .zero, size: size)
|
||||
if view.bounds != targetBounds {
|
||||
view.bounds = targetBounds
|
||||
}
|
||||
|
||||
var targetFrame = view.frame
|
||||
if targetFrame.size != size {
|
||||
targetFrame.size = size
|
||||
view.frame = targetFrame
|
||||
}
|
||||
}
|
||||
|
||||
private func layoutMetalLayer() {
|
||||
let bounds = view.bounds
|
||||
let bounds = CGRect(origin: .zero, size: externallyManagedViewSize ?? view.bounds.size)
|
||||
guard bounds.width > 1, bounds.height > 1 else { return }
|
||||
|
||||
let scale = view.window?.screen.nativeScale ?? UIScreen.main.nativeScale
|
||||
|
|
@ -363,8 +443,10 @@ final class MPVPlayerViewController: UIViewController {
|
|||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
metalLayer.contentsScale = scale
|
||||
metalLayer.frame = CGRect(origin: .zero, size: bounds.size)
|
||||
metalLayer.position = .zero
|
||||
metalLayer.bounds = CGRect(origin: .zero, size: bounds.size)
|
||||
if drawableSize != lastAppliedDrawableSize {
|
||||
// mpv's moltenvk context polls drawableSize and resizes its swapchain.
|
||||
metalLayer.drawableSize = drawableSize
|
||||
lastAppliedDrawableSize = drawableSize
|
||||
}
|
||||
|
|
@ -382,7 +464,8 @@ final class MPVPlayerViewController: UIViewController {
|
|||
|
||||
checkError(mpv_request_log_messages(mpv, "warn"))
|
||||
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &metalLayer))
|
||||
var layerPointer = Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layerPointer))
|
||||
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk"))
|
||||
|
|
@ -747,6 +830,8 @@ final class MPVPlayerViewController: UIViewController {
|
|||
resignFirstResponder()
|
||||
pendingLoadRetryWorkItem?.cancel()
|
||||
pendingLoadRetryWorkItem = nil
|
||||
pendingSurfaceLayoutWorkItems.forEach { $0.cancel() }
|
||||
pendingSurfaceLayoutWorkItems.removeAll(keepingCapacity: false)
|
||||
pendingLoadRequest = nil
|
||||
nowPlayingController.invalidate()
|
||||
clearPlaybackError()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ class MetalLayer: CAMetalLayer {
|
|||
if Thread.isMainThread {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
// mpv's vo thread sets this during video-output init while it holds the
|
||||
// core lock; a sync hop here deadlocks against main-thread property reads.
|
||||
DispatchQueue.main.async {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue