@@ -22,6 +22,7 @@
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */; };
|
||||
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */; };
|
||||
B1D51A6A2F00110000000008 /* ExternalDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000007 /* ExternalDisplayManager.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -73,6 +74,7 @@
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = SOURCE_ROOT; };
|
||||
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = SOURCE_ROOT; };
|
||||
B1D51A6A2F00110000000007 /* ExternalDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplayManager.swift; sourceTree = "<group>"; };
|
||||
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -121,6 +123,7 @@
|
||||
children = (
|
||||
B1D51A6A2F00110000000002 /* MpvPlayerCoreBase.swift */,
|
||||
B1D51A6A2F00110000000006 /* MpvPlayerPluginShared.swift */,
|
||||
B1D51A6A2F00110000000007 /* ExternalDisplayManager.swift */,
|
||||
6A8A46222EDB370C0057B88C /* MpvPlayerCore.swift */,
|
||||
6A8A46232EDB370C0057B88C /* MpvPlayerPlugin.swift */,
|
||||
92F969587D0E464D999910F4 /* MpvPipController.swift */,
|
||||
@@ -406,6 +409,7 @@
|
||||
files = (
|
||||
B1D51A6A2F00110000000001 /* MpvPlayerCoreBase.swift in Sources */,
|
||||
B1D51A6A2F00110000000005 /* MpvPlayerPluginShared.swift in Sources */,
|
||||
B1D51A6A2F00110000000008 /* ExternalDisplayManager.swift in Sources */,
|
||||
6A8A46252EDB370C0057B88C /* MpvPlayerPlugin.swift in Sources */,
|
||||
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
|
||||
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
|
||||
|
||||
+23
-1
@@ -70,7 +70,7 @@
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<true/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
@@ -86,6 +86,28 @@
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleExternalDisplay</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ExternalDisplaySceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>external-display</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIWindowSceneSessionRoleExternalDisplayNonInteractive</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ExternalDisplaySceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>external-display</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import UIKit
|
||||
|
||||
final class ExternalDisplayManager {
|
||||
static let shared = ExternalDisplayManager()
|
||||
|
||||
private weak var activeCore: MpvPlayerCore?
|
||||
private var externalScene: UIWindowScene?
|
||||
private var externalWindow: UIWindow?
|
||||
private var externalRootViewController: UIViewController?
|
||||
|
||||
private init() {}
|
||||
|
||||
static func isExternalDisplayRole(_ role: UISceneSession.Role) -> Bool {
|
||||
role.rawValue == "UIWindowSceneSessionRoleExternalDisplay"
|
||||
|| role.rawValue == "UIWindowSceneSessionRoleExternalDisplayNonInteractive"
|
||||
}
|
||||
|
||||
static var hasActiveApplicationScene: Bool {
|
||||
applicationWindowScenes.contains { $0.activationState == .foregroundActive }
|
||||
}
|
||||
|
||||
static func mainApplicationWindow() -> UIWindow? {
|
||||
let scenes = applicationWindowScenes.sorted {
|
||||
scenePriority($0.activationState) < scenePriority($1.activationState)
|
||||
}
|
||||
|
||||
for scene in scenes {
|
||||
if let window = scene.windows.first(where: { $0.isKeyWindow }) {
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
for scene in scenes {
|
||||
if let window = scene.windows.first(where: { !$0.isHidden }) {
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
return scenes.first?.windows.first
|
||||
}
|
||||
|
||||
private static var applicationWindowScenes: [UIWindowScene] {
|
||||
UIApplication.shared.connectedScenes.compactMap { scene in
|
||||
guard let windowScene = scene as? UIWindowScene,
|
||||
!isExternalDisplayRole(windowScene.session.role)
|
||||
else { return nil }
|
||||
return windowScene
|
||||
}
|
||||
}
|
||||
|
||||
private static func scenePriority(_ state: UIScene.ActivationState) -> Int {
|
||||
switch state {
|
||||
case .foregroundActive:
|
||||
return 0
|
||||
case .foregroundInactive:
|
||||
return 1
|
||||
case .background:
|
||||
return 2
|
||||
case .unattached:
|
||||
return 3
|
||||
@unknown default:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
var videoSuperview: UIView? {
|
||||
externalRootViewController?.view
|
||||
}
|
||||
|
||||
func attach(core: MpvPlayerCore) {
|
||||
activeCore = core
|
||||
discoverExistingExternalScene()
|
||||
activeCore?.externalDisplayDidChange()
|
||||
}
|
||||
|
||||
func detach(core: MpvPlayerCore) {
|
||||
if activeCore === core {
|
||||
activeCore = nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func connect(scene: UIWindowScene) -> UIWindow? {
|
||||
guard Self.isExternalDisplayRole(scene.session.role) else { return nil }
|
||||
|
||||
externalScene = scene
|
||||
let window = ensureWindow(for: scene)
|
||||
activeCore?.externalDisplayDidChange()
|
||||
print("[ExternalDisplayManager] External display connected")
|
||||
return window
|
||||
}
|
||||
|
||||
func disconnect(scene: UIScene) {
|
||||
guard scene === externalScene else { return }
|
||||
|
||||
externalWindow?.isHidden = true
|
||||
externalWindow = nil
|
||||
externalRootViewController = nil
|
||||
externalScene = nil
|
||||
activeCore?.externalDisplayDidChange()
|
||||
print("[ExternalDisplayManager] External display disconnected")
|
||||
}
|
||||
|
||||
func update(scene: UIWindowScene) {
|
||||
guard scene === externalScene else { return }
|
||||
externalRootViewController?.view.frame = scene.coordinateSpace.bounds
|
||||
activeCore?.externalDisplayDidChange()
|
||||
}
|
||||
|
||||
private func discoverExistingExternalScene() {
|
||||
guard externalScene == nil else { return }
|
||||
|
||||
for scene in UIApplication.shared.connectedScenes {
|
||||
guard let windowScene = scene as? UIWindowScene,
|
||||
Self.isExternalDisplayRole(windowScene.session.role)
|
||||
else { continue }
|
||||
_ = connect(scene: windowScene)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureWindow(for scene: UIWindowScene) -> UIWindow {
|
||||
if let existing = externalWindow, existing.windowScene === scene {
|
||||
return existing
|
||||
}
|
||||
|
||||
externalWindow?.isHidden = true
|
||||
|
||||
let rootViewController = UIViewController()
|
||||
rootViewController.view.frame = scene.coordinateSpace.bounds
|
||||
rootViewController.view.backgroundColor = .black
|
||||
|
||||
let window = UIWindow(windowScene: scene)
|
||||
window.rootViewController = rootViewController
|
||||
window.backgroundColor = .black
|
||||
window.isHidden = false
|
||||
|
||||
externalRootViewController = rootViewController
|
||||
externalWindow = window
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalDisplaySceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func scene(
|
||||
_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions
|
||||
) {
|
||||
guard let windowScene = scene as? UIWindowScene,
|
||||
ExternalDisplayManager.isExternalDisplayRole(session.role)
|
||||
else { return }
|
||||
|
||||
window = ExternalDisplayManager.shared.connect(scene: windowScene)
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
ExternalDisplayManager.shared.disconnect(scene: scene)
|
||||
window = nil
|
||||
}
|
||||
|
||||
func windowScene(
|
||||
_ windowScene: UIWindowScene,
|
||||
didUpdate previousCoordinateSpace: UICoordinateSpace,
|
||||
interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation,
|
||||
traitCollection previousTraitCollection: UITraitCollection
|
||||
) {
|
||||
ExternalDisplayManager.shared.update(scene: windowScene)
|
||||
}
|
||||
}
|
||||
@@ -95,9 +95,7 @@ import UIKit
|
||||
|
||||
// The sample buffer layer must be in a visible view hierarchy for
|
||||
// isPictureInPicturePossible to become true.
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow })
|
||||
{
|
||||
if let window = ExternalDisplayManager.mainApplicationWindow() {
|
||||
let view = UIView(frame: window.bounds)
|
||||
view.clipsToBounds = true
|
||||
view.isUserInteractionEnabled = false
|
||||
|
||||
@@ -6,6 +6,8 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
private var containerView: UIView?
|
||||
private weak var window: UIWindow?
|
||||
private var mainBlankView: UIView?
|
||||
private var isVisible = false
|
||||
|
||||
var isPipStarting = false
|
||||
|
||||
@@ -23,7 +25,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
let layer = MpvMetalLayer()
|
||||
layer.frame = container.bounds
|
||||
layer.contentsScale = UIScreen.main.nativeScale
|
||||
layer.contentsScale = window.screen.nativeScale
|
||||
layer.framebufferOnly = true
|
||||
layer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
@@ -43,6 +45,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
setupNotifications()
|
||||
ExternalDisplayManager.shared.attach(core: self)
|
||||
|
||||
isInitialized = true
|
||||
print("[MpvPlayerCore] Initialized successfully with MPV")
|
||||
@@ -87,12 +90,10 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
func setVisible(_ visible: Bool) {
|
||||
guard let containerView else { return }
|
||||
|
||||
if visible {
|
||||
containerView.removeFromSuperview()
|
||||
window?.insertSubview(containerView, at: 0)
|
||||
}
|
||||
|
||||
isVisible = visible
|
||||
if visible { refreshExternalDisplayAttachment() }
|
||||
containerView.isHidden = !visible
|
||||
if !visible { mainBlankView?.isHidden = true }
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect? = nil) {
|
||||
@@ -101,18 +102,86 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
if let frame {
|
||||
containerView.frame = frame
|
||||
metalLayer.frame = containerView.bounds
|
||||
} else if let superview = containerView.superview {
|
||||
containerView.frame = superview.bounds
|
||||
metalLayer.frame = containerView.bounds
|
||||
} else if let window {
|
||||
containerView.frame = window.bounds
|
||||
metalLayer.frame = containerView.bounds
|
||||
}
|
||||
|
||||
let scale = UIScreen.main.nativeScale
|
||||
mainBlankView?.frame = window?.bounds ?? .zero
|
||||
|
||||
let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main
|
||||
let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale
|
||||
metalLayer.contentsScale = scale
|
||||
metalLayer.drawableSize = CGSize(
|
||||
width: metalLayer.frame.width * scale,
|
||||
height: metalLayer.frame.height * scale
|
||||
)
|
||||
}
|
||||
|
||||
func externalDisplayDidChange() {
|
||||
refreshExternalDisplayAttachment()
|
||||
}
|
||||
|
||||
private func refreshExternalDisplayAttachment() {
|
||||
guard let containerView else { return }
|
||||
|
||||
let externalSuperview =
|
||||
isVisible && !isPipActive && !isPipStarting
|
||||
? ExternalDisplayManager.shared.videoSuperview
|
||||
: nil
|
||||
|
||||
if let externalSuperview {
|
||||
moveContainerView(to: externalSuperview)
|
||||
setMainBlankViewVisible(true)
|
||||
} else if isVisible, let window {
|
||||
moveContainerView(to: window)
|
||||
setMainBlankViewVisible(false)
|
||||
} else {
|
||||
setMainBlankViewVisible(false)
|
||||
}
|
||||
|
||||
containerView.isHidden = !isVisible
|
||||
updateFrame()
|
||||
}
|
||||
|
||||
private func moveContainerView(to superview: UIView) {
|
||||
guard let containerView else { return }
|
||||
|
||||
if containerView.superview !== superview {
|
||||
containerView.removeFromSuperview()
|
||||
}
|
||||
containerView.frame = superview.bounds
|
||||
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
superview.insertSubview(containerView, at: 0)
|
||||
}
|
||||
|
||||
private func setMainBlankViewVisible(_ visible: Bool) {
|
||||
guard visible, let window else {
|
||||
mainBlankView?.removeFromSuperview()
|
||||
mainBlankView = nil
|
||||
return
|
||||
}
|
||||
|
||||
let blankView = mainBlankView ?? UIView(frame: window.bounds)
|
||||
blankView.backgroundColor = .black
|
||||
blankView.isUserInteractionEnabled = false
|
||||
blankView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
blankView.frame = window.bounds
|
||||
|
||||
if blankView.superview !== window {
|
||||
blankView.removeFromSuperview()
|
||||
window.insertSubview(blankView, at: 0)
|
||||
} else {
|
||||
window.insertSubview(blankView, at: 0)
|
||||
}
|
||||
|
||||
blankView.isHidden = false
|
||||
mainBlankView = blankView
|
||||
}
|
||||
|
||||
/// Nudge mpv to present the current paused frame after switching back from PiP.
|
||||
func forceDraw() {
|
||||
command(["seek", "0", "relative+exact"])
|
||||
@@ -138,12 +207,15 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
func dispose() {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
ExternalDisplayManager.shared.detach(core: self)
|
||||
disposeSharedState(destroySynchronously: false)
|
||||
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
metalLayer = nil
|
||||
containerView?.removeFromSuperview()
|
||||
containerView = nil
|
||||
mainBlankView?.removeFromSuperview()
|
||||
mainBlankView = nil
|
||||
isInitialized = false
|
||||
print("[MpvPlayerCore] Disposed")
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
}
|
||||
|
||||
private var isSceneActive: Bool {
|
||||
UIApplication.shared.connectedScenes.contains { $0.activationState == .foregroundActive }
|
||||
ExternalDisplayManager.hasActiveApplicationScene
|
||||
}
|
||||
|
||||
private func restoreInlinePlayerAfterPip() {
|
||||
@@ -362,12 +362,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
// MARK: - Helpers
|
||||
|
||||
private func findKeyWindow() -> UIWindow? {
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow })
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
return window
|
||||
ExternalDisplayManager.mainApplicationWindow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user