mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-22 08:44:41 +01:00
Working prototype
This commit is contained in:
@@ -7,14 +7,10 @@ Pod::Spec.new do |s|
|
||||
s.source = { git: '' }
|
||||
s.homepage = 'https://github.com/mpvkit/MPVKit'
|
||||
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
spm_dependency(s,
|
||||
url: 'https://github.com/mpvkit/MPVKit.git',
|
||||
requirement: {kind: 'upToNextMajorVersion', minimumVersion: '0.40.0'},
|
||||
products: ['MPVKit']
|
||||
)
|
||||
s.dependency 'MPVKit', '~> 0.40.6'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
|
||||
@@ -5,9 +5,7 @@ import UIKit
|
||||
|
||||
// MARK: - Metal Layer
|
||||
class MetalLayer: CAMetalLayer {
|
||||
// workaround for a MoltenVK that sets the drawableSize to 1x1 to forcefully complete
|
||||
// the presentation, this causes flicker and the drawableSize possibly staying at 1x1
|
||||
// https://github.com/mpv-player/mpv/pull/13651
|
||||
// Workaround for MoltenVK issue that sets drawableSize to 1x1
|
||||
override var drawableSize: CGSize {
|
||||
get { return super.drawableSize }
|
||||
set {
|
||||
@@ -17,13 +15,10 @@ class MetalLayer: CAMetalLayer {
|
||||
}
|
||||
}
|
||||
|
||||
// Hack for fix [target-colorspace-hint] option:
|
||||
// Update wantsExtendedDynamicRangeContent only available in iOS 16.0+
|
||||
// Handle extended dynamic range content on iOS 16+
|
||||
@available(iOS 16.0, *)
|
||||
override var wantsExtendedDynamicRangeContent: Bool {
|
||||
get {
|
||||
return super.wantsExtendedDynamicRangeContent
|
||||
}
|
||||
get { return super.wantsExtendedDynamicRangeContent }
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
@@ -35,7 +30,7 @@ class MetalLayer: CAMetalLayer {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to conditionally set HDR content
|
||||
// Helper to set HDR content safely
|
||||
func setHDRContent(_ enabled: Bool) {
|
||||
if #available(iOS 16.0, *) {
|
||||
if Thread.isMainThread {
|
||||
@@ -49,14 +44,31 @@ class MetalLayer: CAMetalLayer {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Properties
|
||||
enum MpvProperty {
|
||||
static let timePosition = "time-pos"
|
||||
static let duration = "duration"
|
||||
static let pause = "pause"
|
||||
static let pausedForCache = "paused-for-cache"
|
||||
static let videoParamsSigPeak = "video-params/sig-peak"
|
||||
}
|
||||
|
||||
// MARK: - Protocol
|
||||
protocol MpvPlayerDelegate: AnyObject {
|
||||
func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?)
|
||||
}
|
||||
|
||||
// MARK: - MPV Player View
|
||||
class MpvPlayerView: ExpoView {
|
||||
private var mpvViewController: MpvMetalViewController?
|
||||
private var coordinator: MpvMetalPlayerView.Coordinator?
|
||||
// MARK: - Properties
|
||||
|
||||
private var playerController: MpvMetalViewController?
|
||||
private var coordinator: MpvMetalPlayerView.Coordinator?
|
||||
private var hostingController: UIHostingController<MpvMetalPlayerView>?
|
||||
private var source: [String: Any]?
|
||||
|
||||
// Event emitters
|
||||
// MARK: - Event Emitters
|
||||
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@@ -65,42 +77,48 @@ class MpvPlayerView: ExpoView {
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
setupView()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupView() {
|
||||
backgroundColor = .black
|
||||
|
||||
// Create coordinator
|
||||
// Create coordinator and configure property change handling
|
||||
let coordinator = MpvMetalPlayerView.Coordinator()
|
||||
coordinator.onPropertyChange = { [weak self] _, propertyName, data in
|
||||
self?.handlePropertyChange(propertyName: propertyName, data: data)
|
||||
coordinator.onPropertyChange = { [weak self] _, propertyName, value in
|
||||
DispatchQueue.main.async {
|
||||
self?.handlePropertyChange(propertyName: propertyName, value: value)
|
||||
}
|
||||
}
|
||||
self.coordinator = coordinator
|
||||
|
||||
// Create MPV controller
|
||||
let mpvController = MpvMetalViewController()
|
||||
mpvController.playDelegate = coordinator
|
||||
coordinator.player = mpvController
|
||||
// Create player controller
|
||||
let controller = MpvMetalViewController()
|
||||
controller.delegate = coordinator
|
||||
coordinator.player = controller
|
||||
playerController = controller
|
||||
|
||||
mpvViewController = mpvController
|
||||
|
||||
// Add to view hierarchy
|
||||
let hostingController = UIHostingController(
|
||||
// Create and add SwiftUI hosting controller
|
||||
let hostController = UIHostingController(
|
||||
rootView: MpvMetalPlayerView(coordinator: coordinator)
|
||||
)
|
||||
self.hostingController = hostController
|
||||
|
||||
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostingController.view.backgroundColor = .clear
|
||||
hostController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostController.view.backgroundColor = .clear
|
||||
|
||||
addSubview(hostingController.view)
|
||||
addSubview(hostController.view)
|
||||
NSLayoutConstraint.activate([
|
||||
hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
hostingController.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
hostController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hostController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
hostController.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hostController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -125,78 +143,96 @@ class MpvPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
self.onPipStarted?(["pipStarted": false, "target": self.reactTag as Any])
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.onPipStarted?(["pipStarted": false, "target": self.reactTag as Any])
|
||||
}
|
||||
}
|
||||
|
||||
func play() {
|
||||
mpvViewController?.play()
|
||||
playerController?.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
mpvViewController?.pause()
|
||||
playerController?.pause()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
mpvViewController?.command("stop", args: [])
|
||||
playerController?.command("stop", args: [])
|
||||
}
|
||||
|
||||
func seekTo(_ time: Int32) {
|
||||
let seconds = Double(time) / 1000.0
|
||||
mpvViewController?.command("seek", args: ["\(seconds)"])
|
||||
playerController?.command("seek", args: ["\(seconds)"])
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackIndex: Int) {
|
||||
mpvViewController?.command("set", args: ["aid", "\(trackIndex)"])
|
||||
playerController?.command("set", args: ["aid", "\(trackIndex)"])
|
||||
}
|
||||
|
||||
func getAudioTracks() -> [[String: Any]]? {
|
||||
func getAudioTracks() -> [[String: Any]] {
|
||||
// Implementation would go here
|
||||
return []
|
||||
}
|
||||
|
||||
func setSubtitleTrack(_ trackIndex: Int) {
|
||||
mpvViewController?.command("set", args: ["sid", "\(trackIndex)"])
|
||||
playerController?.command("set", args: ["sid", "\(trackIndex)"])
|
||||
}
|
||||
|
||||
func getSubtitleTracks() -> [[String: Any]]? {
|
||||
func getSubtitleTracks() -> [[String: Any]] {
|
||||
// Implementation would go here
|
||||
return []
|
||||
}
|
||||
|
||||
func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||
guard let url = URL(string: subtitleURL) else { return }
|
||||
mpvViewController?.command("sub-add", args: [url.absoluteString])
|
||||
playerController?.command("sub-add", args: [url.absoluteString])
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func handlePropertyChange(propertyName: String, data: Any?) {
|
||||
private func handlePropertyChange(propertyName: String, value: Any?) {
|
||||
guard let playerController = playerController else { return }
|
||||
|
||||
switch propertyName {
|
||||
case MpvProperty.pausedForCache:
|
||||
let isBuffering = data as? Bool ?? false
|
||||
onVideoStateChange?(["isBuffering": isBuffering, "target": reactTag as Any])
|
||||
|
||||
case MpvProperty.timePosition:
|
||||
if let position = data as? Double {
|
||||
let timeMs = position * 1000
|
||||
onVideoProgress?([
|
||||
"currentTime": timeMs,
|
||||
"duration": getVideoDuration() * 1000,
|
||||
"isPlaying": !isPaused(),
|
||||
"isBuffering": isBuffering(),
|
||||
"target": reactTag as Any,
|
||||
let isBuffering = value as? Bool ?? false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.onVideoStateChange?([
|
||||
"isBuffering": isBuffering, "target": self.reactTag as Any,
|
||||
])
|
||||
}
|
||||
|
||||
case MpvProperty.timePosition:
|
||||
if let position = value as? Double {
|
||||
let timeMs = position * 1000
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.onVideoProgress?([
|
||||
"currentTime": timeMs,
|
||||
"duration": self.getVideoDuration() * 1000,
|
||||
"isPlaying": !self.isPaused(),
|
||||
"isBuffering": self.isBuffering(),
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
case MpvProperty.pause:
|
||||
if let isPaused = data as? Bool {
|
||||
if let isPaused = value as? Bool {
|
||||
let state = isPaused ? "Paused" : "Playing"
|
||||
onPlaybackStateChanged?([
|
||||
"state": state,
|
||||
"isPlaying": !isPaused,
|
||||
"isBuffering": isBuffering(),
|
||||
"currentTime": getCurrentTime() * 1000,
|
||||
"duration": getVideoDuration() * 1000,
|
||||
"target": reactTag as Any,
|
||||
])
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.onPlaybackStateChanged?([
|
||||
"state": state,
|
||||
"isPlaying": !isPaused,
|
||||
"isBuffering": self.isBuffering(),
|
||||
"currentTime": self.getCurrentTime() * 1000,
|
||||
"duration": self.getVideoDuration() * 1000,
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -205,60 +241,80 @@ class MpvPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
private func isPaused() -> Bool {
|
||||
return mpvViewController?.getFlag(MpvProperty.pause) ?? true
|
||||
return playerController?.getFlag(MpvProperty.pause) ?? true
|
||||
}
|
||||
|
||||
private func isBuffering() -> Bool {
|
||||
return mpvViewController?.getFlag(MpvProperty.pausedForCache) ?? false
|
||||
return playerController?.getFlag(MpvProperty.pausedForCache) ?? false
|
||||
}
|
||||
|
||||
private func getCurrentTime() -> Double {
|
||||
return mpvViewController?.getDouble(MpvProperty.timePosition) ?? 0
|
||||
return playerController?.getDouble(MpvProperty.timePosition) ?? 0
|
||||
}
|
||||
|
||||
private func getVideoDuration() -> Double {
|
||||
return mpvViewController?.getDouble(MpvProperty.duration) ?? 0
|
||||
return playerController?.getDouble(MpvProperty.duration) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Properties and Protocol
|
||||
enum MpvProperty {
|
||||
static let timePosition = "time-pos"
|
||||
static let duration = "duration"
|
||||
static let pause = "pause"
|
||||
static let pausedForCache = "paused-for-cache"
|
||||
static let videoParamsSigPeak = "video-params/sig-peak"
|
||||
}
|
||||
// MARK: - Cleanup
|
||||
|
||||
protocol MpvPlayerDelegate: AnyObject {
|
||||
func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?)
|
||||
override func removeFromSuperview() {
|
||||
cleanup()
|
||||
super.removeFromSuperview()
|
||||
}
|
||||
|
||||
private func cleanup() {
|
||||
// Ensure everything completes before cleanup
|
||||
playerController?.mpvQueue.sync {}
|
||||
|
||||
// Stop playback
|
||||
stop()
|
||||
|
||||
// Break reference cycles
|
||||
coordinator?.player = nil
|
||||
coordinator?.onPropertyChange = nil
|
||||
playerController?.delegate = nil
|
||||
|
||||
// Remove from view hierarchy
|
||||
hostingController?.view.removeFromSuperview()
|
||||
hostingController = nil
|
||||
coordinator = nil
|
||||
playerController = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SwiftUI Wrapper
|
||||
struct MpvMetalPlayerView: UIViewControllerRepresentable {
|
||||
@ObservedObject var coordinator: Coordinator
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let mpv = MpvMetalViewController()
|
||||
mpv.playDelegate = coordinator
|
||||
mpv.playUrl = coordinator.playUrl
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
let controller = MpvMetalViewController()
|
||||
controller.delegate = coordinator
|
||||
controller.playUrl = coordinator.playUrl
|
||||
|
||||
context.coordinator.player = mpv
|
||||
return mpv
|
||||
coordinator.player = controller
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
|
||||
// Updates if needed
|
||||
}
|
||||
|
||||
public func makeCoordinator() -> Coordinator {
|
||||
func makeCoordinator() -> Coordinator {
|
||||
coordinator
|
||||
}
|
||||
|
||||
// Method for playing media
|
||||
func play(_ url: URL) -> Self {
|
||||
coordinator.playUrl = url
|
||||
return self
|
||||
}
|
||||
|
||||
// Method for handling property changes
|
||||
func onPropertyChange(_ handler: @escaping (MpvMetalViewController, String, Any?) -> Void)
|
||||
-> Self
|
||||
{
|
||||
@@ -267,9 +323,8 @@ struct MpvMetalPlayerView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class Coordinator: MpvPlayerDelegate, ObservableObject {
|
||||
final class Coordinator: MpvPlayerDelegate, ObservableObject {
|
||||
weak var player: MpvMetalViewController?
|
||||
|
||||
var playUrl: URL?
|
||||
var onPropertyChange: ((MpvMetalViewController, String, Any?) -> Void)?
|
||||
|
||||
@@ -277,58 +332,62 @@ struct MpvMetalPlayerView: UIViewControllerRepresentable {
|
||||
player?.loadFile(url)
|
||||
}
|
||||
|
||||
func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?) {
|
||||
func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) {
|
||||
guard let player = player else { return }
|
||||
|
||||
self.onPropertyChange?(player, propertyName, data)
|
||||
onPropertyChange?(player, propertyName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Metal View Controller
|
||||
// MARK: - Player Controller
|
||||
final class MpvMetalViewController: UIViewController {
|
||||
// MARK: - Properties
|
||||
|
||||
var metalLayer = MetalLayer()
|
||||
var mpv: OpaquePointer!
|
||||
weak var playDelegate: MpvPlayerDelegate?
|
||||
lazy var queue = DispatchQueue(label: "mpv", qos: .userInitiated)
|
||||
var mpv: OpaquePointer?
|
||||
weak var delegate: MpvPlayerDelegate?
|
||||
let mpvQueue = DispatchQueue(label: "mpv.queue", qos: .userInitiated)
|
||||
|
||||
private var isBeingDeallocated = false
|
||||
private var contextPointer: UnsafeMutableRawPointer?
|
||||
|
||||
var playUrl: URL?
|
||||
|
||||
var hdrAvailable: Bool {
|
||||
if #available(iOS 16.0, *) {
|
||||
let maxEDRRange = view.window?.screen.potentialEDRHeadroom ?? 1.0
|
||||
let sigPeak = getDouble(MpvProperty.videoParamsSigPeak)
|
||||
// display screen support HDR and current playing HDR video
|
||||
return maxEDRRange > 1.0 && sigPeak > 1.0
|
||||
} else {
|
||||
return false // HDR not available on iOS < 16.0
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var hdrEnabled = false {
|
||||
didSet {
|
||||
// FIXME: target-colorspace-hint does not support being changed at runtime.
|
||||
// this option should be set as early as possible otherwise can cause issues
|
||||
// not recommended to use this way.
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
if hdrEnabled {
|
||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
|
||||
mpv_set_option_string(mpv, "target-colorspace-hint", "yes")
|
||||
metalLayer.setHDRContent(true)
|
||||
} else {
|
||||
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "no"))
|
||||
mpv_set_option_string(mpv, "target-colorspace-hint", "no")
|
||||
metalLayer.setHDRContent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new property to track shutdown state
|
||||
private var isShuttingDown = false
|
||||
private let syncQueue = DispatchQueue(label: "com.mpv.sync", qos: .userInitiated)
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
metalLayer.frame = view.frame
|
||||
metalLayer.contentsScale = UIScreen.main.nativeScale
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
view.layer.addSublayer(metalLayer)
|
||||
|
||||
setupMpv()
|
||||
setupMetalLayer()
|
||||
setupMPV()
|
||||
|
||||
if let url = playUrl {
|
||||
loadFile(url)
|
||||
@@ -337,53 +396,128 @@ final class MpvMetalViewController: UIViewController {
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
metalLayer.frame = view.frame
|
||||
metalLayer.frame = view.bounds
|
||||
}
|
||||
|
||||
func setupMpv() {
|
||||
mpv = mpv_create()
|
||||
if mpv == nil {
|
||||
print("failed creating context\n")
|
||||
exit(1)
|
||||
deinit {
|
||||
// Use syncQueue to ensure thread safety during shutdown
|
||||
syncQueue.sync {
|
||||
// Mark as shutting down to prevent new callbacks from running
|
||||
isShuttingDown = true
|
||||
isBeingDeallocated = true
|
||||
|
||||
// Make sure to handle this on the mpv queue
|
||||
mpvQueue.sync {
|
||||
// First remove the wakeup callback to prevent any new callbacks
|
||||
if let mpv = self.mpv {
|
||||
mpv_set_wakeup_callback(mpv, nil, nil)
|
||||
}
|
||||
|
||||
// Release the container
|
||||
if let contextPtr = contextPointer {
|
||||
let container = Unmanaged<WeakContainer<MpvMetalViewController>>.fromOpaque(
|
||||
contextPtr
|
||||
).takeUnretainedValue()
|
||||
container.invalidate()
|
||||
Unmanaged<WeakContainer<MpvMetalViewController>>.fromOpaque(contextPtr)
|
||||
.release()
|
||||
contextPointer = nil
|
||||
}
|
||||
|
||||
// Terminate and destroy mpv as the final step
|
||||
if let mpv = self.mpv {
|
||||
mpv_terminate_destroy(mpv)
|
||||
self.mpv = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupMetalLayer() {
|
||||
metalLayer.frame = view.bounds
|
||||
metalLayer.contentsScale = UIScreen.main.nativeScale
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
view.layer.addSublayer(metalLayer)
|
||||
}
|
||||
|
||||
private func setupMPV() {
|
||||
guard let mpvHandle = mpv_create() else {
|
||||
print("Failed to create MPV instance")
|
||||
return
|
||||
}
|
||||
|
||||
// https://mpv.io/manual/stable/#options
|
||||
mpv = mpvHandle
|
||||
|
||||
// Configure mpv options
|
||||
#if DEBUG
|
||||
checkError(mpv_request_log_messages(mpv, "debug"))
|
||||
mpv_request_log_messages(mpvHandle, "debug")
|
||||
#else
|
||||
checkError(mpv_request_log_messages(mpv, "no"))
|
||||
mpv_request_log_messages(mpvHandle, "no")
|
||||
#endif
|
||||
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &metalLayer))
|
||||
checkError(mpv_set_option_string(mpv, "subs-match-os-language", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "subs-fallback", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
|
||||
checkError(mpv_set_option_string(mpv, "video-rotate", "no"))
|
||||
checkError(mpv_set_option_string(mpv, "ytdl", "no"))
|
||||
mpv_set_option(mpvHandle, "wid", MPV_FORMAT_INT64, &metalLayer)
|
||||
mpv_set_option_string(mpvHandle, "subs-match-os-language", "yes")
|
||||
mpv_set_option_string(mpvHandle, "subs-fallback", "yes")
|
||||
mpv_set_option_string(mpvHandle, "vo", "gpu-next")
|
||||
mpv_set_option_string(mpvHandle, "gpu-api", "vulkan")
|
||||
mpv_set_option_string(mpvHandle, "hwdec", "videotoolbox")
|
||||
mpv_set_option_string(mpvHandle, "video-rotate", "no")
|
||||
mpv_set_option_string(mpvHandle, "ytdl", "no")
|
||||
|
||||
checkError(mpv_initialize(mpv))
|
||||
// Initialize mpv
|
||||
let status = mpv_initialize(mpvHandle)
|
||||
if status < 0 {
|
||||
print("Failed to initialize MPV: \(String(cString: mpv_error_string(status)))")
|
||||
mpv_terminate_destroy(mpvHandle)
|
||||
mpv = nil
|
||||
return
|
||||
}
|
||||
|
||||
mpv_observe_property(mpv, 0, MpvProperty.videoParamsSigPeak, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpv, 0, MpvProperty.pausedForCache, MPV_FORMAT_FLAG)
|
||||
mpv_observe_property(mpv, 0, MpvProperty.timePosition, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpv, 0, MpvProperty.duration, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpv, 0, MpvProperty.pause, MPV_FORMAT_FLAG)
|
||||
// Observe properties
|
||||
mpv_observe_property(mpvHandle, 0, MpvProperty.videoParamsSigPeak, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpvHandle, 0, MpvProperty.pausedForCache, MPV_FORMAT_FLAG)
|
||||
mpv_observe_property(mpvHandle, 0, MpvProperty.timePosition, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpvHandle, 0, MpvProperty.duration, MPV_FORMAT_DOUBLE)
|
||||
mpv_observe_property(mpvHandle, 0, MpvProperty.pause, MPV_FORMAT_FLAG)
|
||||
|
||||
// Set up weak reference for callback with improved safety
|
||||
let container = WeakContainer(value: self)
|
||||
contextPointer = Unmanaged.passRetained(container).toOpaque()
|
||||
|
||||
// Set wakeup callback with safer checking
|
||||
mpv_set_wakeup_callback(
|
||||
self.mpv,
|
||||
{ (ctx) in
|
||||
let client = unsafeBitCast(ctx, to: MpvMetalViewController.self)
|
||||
client.readEvents()
|
||||
}, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
|
||||
mpvHandle,
|
||||
{ pointer in
|
||||
guard let ptr = pointer else { return }
|
||||
|
||||
// Get the container safely
|
||||
let container = Unmanaged<WeakContainer<MpvMetalViewController>>.fromOpaque(ptr)
|
||||
.takeUnretainedValue()
|
||||
|
||||
// Access the value with additional safety checks
|
||||
DispatchQueue.main.async {
|
||||
if let controller = container.value {
|
||||
if !controller.isBeingDeallocated {
|
||||
controller.processEvents()
|
||||
} else {
|
||||
// If the controller is being deallocated, invalidate the container
|
||||
container.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, contextPointer)
|
||||
}
|
||||
|
||||
func loadFile(_ url: URL) {
|
||||
var args = [url.absoluteString]
|
||||
args.append("replace")
|
||||
// MARK: - MPV Methods
|
||||
|
||||
func loadFile(_ url: URL) {
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
var args = [url.absoluteString, "replace"]
|
||||
command("loadfile", args: args)
|
||||
}
|
||||
|
||||
@@ -400,152 +534,181 @@ final class MpvMetalViewController: UIViewController {
|
||||
}
|
||||
|
||||
func getDouble(_ name: String) -> Double {
|
||||
guard mpv != nil else { return 0.0 }
|
||||
var data = Double()
|
||||
guard let mpv = mpv else { return 0.0 }
|
||||
|
||||
var data = 0.0
|
||||
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
|
||||
return data
|
||||
}
|
||||
|
||||
func getString(_ name: String) -> String? {
|
||||
guard mpv != nil else { return nil }
|
||||
let cstr = mpv_get_property_string(mpv, name)
|
||||
let str: String? = cstr == nil ? nil : String(cString: cstr!)
|
||||
mpv_free(cstr)
|
||||
return str
|
||||
guard let mpv = mpv else { return nil }
|
||||
|
||||
guard let cString = mpv_get_property_string(mpv, name) else { return nil }
|
||||
let string = String(cString: cString)
|
||||
mpv_free(UnsafeMutableRawPointer(mutating: cString))
|
||||
return string
|
||||
}
|
||||
|
||||
func getFlag(_ name: String) -> Bool {
|
||||
var data = Int64()
|
||||
guard let mpv = mpv else { return false }
|
||||
|
||||
var data: Int64 = 0
|
||||
mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
||||
return data > 0
|
||||
}
|
||||
|
||||
func setFlag(_ name: String, _ flag: Bool) {
|
||||
guard mpv != nil else { return }
|
||||
var data: Int = flag ? 1 : 0
|
||||
func setFlag(_ name: String, _ value: Bool) {
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
var data: Int = value ? 1 : 0
|
||||
mpv_set_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
||||
}
|
||||
|
||||
func command(
|
||||
_ command: String,
|
||||
args: [String?] = [],
|
||||
checkForErrors: Bool = true,
|
||||
returnValueCallback: ((Int32) -> Void)? = nil
|
||||
args: [String] = [],
|
||||
checkErrors: Bool = true,
|
||||
completion: ((Int32) -> Void)? = nil
|
||||
) {
|
||||
guard mpv != nil else {
|
||||
guard let mpv = mpv else {
|
||||
completion?(-1)
|
||||
return
|
||||
}
|
||||
var cargs = makeCArgs(command, args).map { $0.flatMap { UnsafePointer<CChar>(strdup($0)) } }
|
||||
defer {
|
||||
for ptr in cargs where ptr != nil {
|
||||
free(UnsafeMutablePointer(mutating: ptr!))
|
||||
|
||||
// Create the C-style command array manually with the correct type
|
||||
let cStrings = [command] + args
|
||||
|
||||
// Create array of C string pointers with the correct type
|
||||
let count = cStrings.count
|
||||
let cArray = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: count + 1)
|
||||
|
||||
// Fill the array
|
||||
for i in 0..<count {
|
||||
let cString = (cStrings[i] as NSString).utf8String
|
||||
cArray[i] = cString
|
||||
}
|
||||
|
||||
// Set last element to nil
|
||||
cArray[count] = nil
|
||||
|
||||
// Execute the command with the properly typed array
|
||||
let status = mpv_command(mpv, cArray)
|
||||
|
||||
// Clean up
|
||||
cArray.deallocate()
|
||||
|
||||
if checkErrors && status < 0 {
|
||||
print("MPV command error: \(String(cString: mpv_error_string(status)))")
|
||||
}
|
||||
|
||||
completion?(status)
|
||||
}
|
||||
|
||||
// MARK: - Event Processing
|
||||
|
||||
private func processEvents() {
|
||||
// Check shutdown state first before proceeding
|
||||
if syncQueue.sync(execute: { isShuttingDown }) {
|
||||
return
|
||||
}
|
||||
|
||||
mpvQueue.async { [weak self] in
|
||||
guard let self = self,
|
||||
let mpv = self.mpv,
|
||||
!self.isBeingDeallocated,
|
||||
!self.syncQueue.sync(execute: { self.isShuttingDown })
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
let returnValue = mpv_command(mpv, &cargs)
|
||||
if checkForErrors {
|
||||
checkError(returnValue)
|
||||
}
|
||||
if let cb = returnValueCallback {
|
||||
cb(returnValue)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCArgs(_ command: String, _ args: [String?]) -> [String?] {
|
||||
if !args.isEmpty, args.last == nil {
|
||||
fatalError("Command do not need a nil suffix")
|
||||
}
|
||||
while self.mpv != nil && !self.isBeingDeallocated
|
||||
&& !self.syncQueue.sync(execute: { self.isShuttingDown })
|
||||
{
|
||||
guard let event = mpv_wait_event(mpv, 0) else { break }
|
||||
if event.pointee.event_id == MPV_EVENT_NONE { break }
|
||||
|
||||
var strArgs = args
|
||||
strArgs.insert(command, at: 0)
|
||||
strArgs.append(nil)
|
||||
|
||||
return strArgs
|
||||
}
|
||||
|
||||
func readEvents() {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
while self.mpv != nil {
|
||||
let event = mpv_wait_event(self.mpv, 0)
|
||||
if event?.pointee.event_id == MPV_EVENT_NONE {
|
||||
break
|
||||
}
|
||||
|
||||
switch event!.pointee.event_id {
|
||||
case MPV_EVENT_PROPERTY_CHANGE:
|
||||
let dataOpaquePtr = OpaquePointer(event!.pointee.data)
|
||||
if let property = UnsafePointer<mpv_event_property>(dataOpaquePtr)?.pointee {
|
||||
let propertyName = String(cString: property.name)
|
||||
|
||||
switch propertyName {
|
||||
case MpvProperty.pausedForCache:
|
||||
let buffering =
|
||||
UnsafePointer<Bool>(OpaquePointer(property.data))?.pointee ?? true
|
||||
DispatchQueue.main.async {
|
||||
self.playDelegate?.propertyChange(
|
||||
mpv: self.mpv, propertyName: propertyName, data: buffering)
|
||||
}
|
||||
case MpvProperty.timePosition:
|
||||
if let data = property.data,
|
||||
let position = UnsafePointer<Double>(OpaquePointer(data))?.pointee
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.playDelegate?.propertyChange(
|
||||
mpv: self.mpv, propertyName: propertyName, data: position)
|
||||
}
|
||||
}
|
||||
case MpvProperty.pause:
|
||||
if let data = property.data,
|
||||
let paused = UnsafePointer<Bool>(OpaquePointer(data))?.pointee
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.playDelegate?.propertyChange(
|
||||
mpv: self.mpv, propertyName: propertyName, data: paused)
|
||||
}
|
||||
}
|
||||
case MpvProperty.duration:
|
||||
if let data = property.data,
|
||||
let duration = UnsafePointer<Double>(OpaquePointer(data))?.pointee
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.playDelegate?.propertyChange(
|
||||
mpv: self.mpv, propertyName: propertyName, data: duration)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case MPV_EVENT_SHUTDOWN:
|
||||
print("event: shutdown\n")
|
||||
mpv_terminate_destroy(mpv)
|
||||
mpv = nil
|
||||
break
|
||||
case MPV_EVENT_LOG_MESSAGE:
|
||||
let msg = UnsafeMutablePointer<mpv_event_log_message>(
|
||||
OpaquePointer(event!.pointee.data))
|
||||
print(
|
||||
"[\(String(cString: (msg!.pointee.prefix)!))] \(String(cString: (msg!.pointee.level)!)): \(String(cString: (msg!.pointee.text)!))",
|
||||
terminator: "")
|
||||
default:
|
||||
let eventName = mpv_event_name(event!.pointee.event_id)
|
||||
print("event: \(String(cString: (eventName)!))")
|
||||
}
|
||||
self.handleEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkError(_ status: CInt) {
|
||||
if status < 0 {
|
||||
print("MPV API error: \(String(cString: mpv_error_string(status)))\n")
|
||||
private func handleEvent(_ event: UnsafePointer<mpv_event>) {
|
||||
// Exit early if we're shutting down
|
||||
if syncQueue.sync(execute: { isShuttingDown }) || isBeingDeallocated {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if mpv != nil {
|
||||
mpv_terminate_destroy(mpv)
|
||||
mpv = nil
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
switch event.pointee.event_id {
|
||||
case MPV_EVENT_PROPERTY_CHANGE:
|
||||
guard let propertyData = event.pointee.data else { break }
|
||||
let property = UnsafePointer<mpv_event_property>(OpaquePointer(propertyData)).pointee
|
||||
let propertyName = String(cString: property.name)
|
||||
|
||||
var value: Any?
|
||||
|
||||
switch propertyName {
|
||||
case MpvProperty.pausedForCache, MpvProperty.pause:
|
||||
if let data = property.data,
|
||||
let boolValue = UnsafePointer<Bool>(OpaquePointer(data))?.pointee
|
||||
{
|
||||
value = boolValue
|
||||
}
|
||||
|
||||
case MpvProperty.timePosition, MpvProperty.duration:
|
||||
if let data = property.data,
|
||||
let doubleValue = UnsafePointer<Double>(OpaquePointer(data))?.pointee
|
||||
{
|
||||
value = doubleValue
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Notify delegate on main thread
|
||||
if let value = value {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, !self.isBeingDeallocated else { return }
|
||||
self.delegate?.propertyChanged(
|
||||
mpv: mpv, propertyName: propertyName, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
case MPV_EVENT_SHUTDOWN:
|
||||
print("MPV shutdown event received")
|
||||
mpvQueue.async { [weak self] in
|
||||
guard let self = self, self.mpv != nil else { return }
|
||||
mpv_terminate_destroy(self.mpv)
|
||||
self.mpv = nil
|
||||
}
|
||||
|
||||
default:
|
||||
if let eventName = mpv_event_name(event.pointee.event_id) {
|
||||
print("MPV event: \(String(cString: eventName))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Improved WeakContainer
|
||||
class WeakContainer<T: AnyObject> {
|
||||
private weak var _value: T?
|
||||
private var _isValid = true
|
||||
|
||||
var value: T? {
|
||||
guard _isValid else { return nil }
|
||||
return _value
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
_isValid = false
|
||||
_value = nil
|
||||
}
|
||||
|
||||
init(value: T) {
|
||||
self._value = value
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user