mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
Updated
This commit is contained in:
@@ -63,9 +63,8 @@ class MpvPlayerView: ExpoView {
|
||||
// MARK: - Properties
|
||||
|
||||
private var playerController: MpvMetalViewController?
|
||||
private var coordinator: MpvMetalPlayerView.Coordinator?
|
||||
private var hostingController: UIHostingController<MpvMetalPlayerView>?
|
||||
private var source: [String: Any]?
|
||||
private var externalSubtitles: [[String: String]]?
|
||||
|
||||
// MARK: - Event Emitters
|
||||
|
||||
@@ -89,36 +88,25 @@ class MpvPlayerView: ExpoView {
|
||||
private func setupView() {
|
||||
backgroundColor = .black
|
||||
|
||||
// Create coordinator and configure property change handling
|
||||
let coordinator = MpvMetalPlayerView.Coordinator()
|
||||
coordinator.onPropertyChange = { [weak self] _, propertyName, value in
|
||||
DispatchQueue.main.async {
|
||||
self?.handlePropertyChange(propertyName: propertyName, value: value)
|
||||
}
|
||||
}
|
||||
self.coordinator = coordinator
|
||||
print("Setting up direct MPV view")
|
||||
|
||||
// Create player controller
|
||||
let controller = MpvMetalViewController()
|
||||
controller.delegate = coordinator
|
||||
coordinator.player = controller
|
||||
|
||||
// Configure player delegate
|
||||
controller.delegate = self
|
||||
playerController = controller
|
||||
|
||||
// Create and add SwiftUI hosting controller
|
||||
let hostController = UIHostingController(
|
||||
rootView: MpvMetalPlayerView(coordinator: coordinator, existingController: controller)
|
||||
)
|
||||
self.hostingController = hostController
|
||||
// Add the controller's view to our view hierarchy
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
controller.view.backgroundColor = .clear
|
||||
|
||||
hostController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
hostController.view.backgroundColor = .clear
|
||||
|
||||
addSubview(hostController.view)
|
||||
addSubview(controller.view)
|
||||
NSLayoutConstraint.activate([
|
||||
hostController.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
hostController.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
hostController.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
hostController.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
controller.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
controller.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -129,12 +117,19 @@ class MpvPlayerView: ExpoView {
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.onVideoLoadStart?(["target": self.reactTag as Any])
|
||||
|
||||
// Store external subtitle data
|
||||
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
||||
|
||||
if let uri = source["uri"] as? String, let url = URL(string: uri) {
|
||||
self.coordinator?.playUrl = url
|
||||
self.coordinator?.play(url)
|
||||
print("Loading file: \(url.absoluteString)")
|
||||
self.playerController?.playUrl = url
|
||||
self.playerController?.loadFile(url)
|
||||
|
||||
// Add external subtitles after the video is loaded
|
||||
self.setInitialExternalSubtitles()
|
||||
|
||||
self.onVideoLoadEnd?(["target": self.reactTag as Any])
|
||||
} else {
|
||||
self.onVideoError?(["error": "Invalid or empty URI"])
|
||||
@@ -172,8 +167,41 @@ class MpvPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
func getAudioTracks() -> [[String: Any]] {
|
||||
// Implementation would go here
|
||||
return []
|
||||
guard let playerController = playerController else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get track list as a node
|
||||
guard let trackListStr = playerController.getNode("track-list") else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Parse the JSON string into an array
|
||||
guard let data = trackListStr.data(using: .utf8),
|
||||
let trackList = try? JSONSerialization.jsonObject(with: data) as? [Any]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Filter to audio tracks only
|
||||
var audioTracks: [[String: Any]] = []
|
||||
for case let track as [String: Any] in trackList {
|
||||
if let type = track["type"] as? String, type == "audio" {
|
||||
let id = track["id"] as? Int ?? 0
|
||||
let title = track["title"] as? String ?? "Audio \(id)"
|
||||
let lang = track["lang"] as? String ?? "unknown"
|
||||
let selected = track["selected"] as? Bool ?? false
|
||||
|
||||
audioTracks.append([
|
||||
"id": id,
|
||||
"title": title,
|
||||
"language": lang,
|
||||
"selected": selected,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return audioTracks
|
||||
}
|
||||
|
||||
func setSubtitleTrack(_ trackIndex: Int) {
|
||||
@@ -181,20 +209,152 @@ class MpvPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
func getSubtitleTracks() -> [[String: Any]] {
|
||||
// Implementation would go here
|
||||
return []
|
||||
guard let playerController = playerController else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get track list as a node
|
||||
guard let trackListStr = playerController.getNode("track-list") else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Parse the JSON string into an array
|
||||
guard let data = trackListStr.data(using: .utf8),
|
||||
let trackList = try? JSONSerialization.jsonObject(with: data) as? [Any]
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Filter to subtitle tracks only
|
||||
var subtitleTracks: [[String: Any]] = []
|
||||
for case let track as [String: Any] in trackList {
|
||||
if let type = track["type"] as? String, type == "sub" {
|
||||
let id = track["id"] as? Int ?? 0
|
||||
let title = track["title"] as? String ?? "Subtitle \(id)"
|
||||
let lang = track["lang"] as? String ?? "unknown"
|
||||
let selected = track["selected"] as? Bool ?? false
|
||||
|
||||
subtitleTracks.append([
|
||||
"id": id,
|
||||
"title": title,
|
||||
"language": lang,
|
||||
"selected": selected,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return subtitleTracks
|
||||
}
|
||||
|
||||
func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||
guard let url = URL(string: subtitleURL) else { return }
|
||||
|
||||
print("Adding subtitle: \(name) from \(subtitleURL)")
|
||||
|
||||
// Add the subtitle file
|
||||
playerController?.command("sub-add", args: [url.absoluteString])
|
||||
}
|
||||
|
||||
private func setInitialExternalSubtitles() {
|
||||
if let externalSubtitles = self.externalSubtitles {
|
||||
for subtitle in externalSubtitles {
|
||||
if let subtitleName = subtitle["name"],
|
||||
let subtitleURL = subtitle["DeliveryUrl"]
|
||||
{
|
||||
print("Adding external subtitle: \(subtitleName) from \(subtitleURL)")
|
||||
setSubtitleURL(subtitleURL, name: subtitleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func handlePropertyChange(propertyName: String, value: Any?) {
|
||||
guard let playerController = playerController else { return }
|
||||
private func isPaused() -> Bool {
|
||||
print("isPaused: \(playerController?.getFlag(MpvProperty.pause) ?? true)")
|
||||
return playerController?.getFlag(MpvProperty.pause) ?? true
|
||||
}
|
||||
|
||||
private func isBuffering() -> Bool {
|
||||
return playerController?.getFlag(MpvProperty.pausedForCache) ?? false
|
||||
}
|
||||
|
||||
private func getCurrentTime() -> Double {
|
||||
return playerController?.getDouble(MpvProperty.timePosition) ?? 0
|
||||
}
|
||||
|
||||
private func getVideoDuration() -> Double {
|
||||
return playerController?.getDouble(MpvProperty.duration) ?? 0
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
override func removeFromSuperview() {
|
||||
cleanup()
|
||||
super.removeFromSuperview()
|
||||
}
|
||||
|
||||
private func cleanup() {
|
||||
// Check if we already cleaned up
|
||||
guard playerController != nil else { return }
|
||||
|
||||
// First stop playback
|
||||
stop()
|
||||
|
||||
// Break reference cycles
|
||||
playerController?.delegate = nil
|
||||
|
||||
// Remove from view hierarchy
|
||||
playerController?.view.removeFromSuperview()
|
||||
|
||||
// Release references
|
||||
playerController = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// Reset the player when experiencing black screen or other issues
|
||||
func resetPlayer() {
|
||||
// Store current source
|
||||
let currentSource = source
|
||||
|
||||
// Clean up existing player
|
||||
cleanup()
|
||||
|
||||
// Create a new player
|
||||
setupView()
|
||||
|
||||
// If we had a source, reload it
|
||||
if let source = currentSource {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.setSource(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if player needs reset when the view appears
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
|
||||
// If we're returning to the window and player is missing, reset
|
||||
if window != nil && playerController == nil {
|
||||
setupView()
|
||||
|
||||
// Reload previous source if available
|
||||
if let source = source {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.setSource(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MPV Player Delegate
|
||||
extension MpvPlayerView: MpvPlayerDelegate {
|
||||
func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) {
|
||||
switch propertyName {
|
||||
case MpvProperty.pausedForCache:
|
||||
let isBuffering = value as? Bool ?? false
|
||||
@@ -241,105 +401,6 @@ class MpvPlayerView: ExpoView {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func isPaused() -> Bool {
|
||||
return playerController?.getFlag(MpvProperty.pause) ?? true
|
||||
}
|
||||
|
||||
private func isBuffering() -> Bool {
|
||||
return playerController?.getFlag(MpvProperty.pausedForCache) ?? false
|
||||
}
|
||||
|
||||
private func getCurrentTime() -> Double {
|
||||
return playerController?.getDouble(MpvProperty.timePosition) ?? 0
|
||||
}
|
||||
|
||||
private func getVideoDuration() -> Double {
|
||||
return playerController?.getDouble(MpvProperty.duration) ?? 0
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
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
|
||||
let existingController: MpvMetalViewController
|
||||
|
||||
init(coordinator: Coordinator, existingController: MpvMetalViewController) {
|
||||
self.coordinator = coordinator
|
||||
self.existingController = existingController
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
return existingController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
|
||||
// Updates if needed
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
coordinator.onPropertyChange = handler
|
||||
return self
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class Coordinator: MpvPlayerDelegate, ObservableObject {
|
||||
weak var player: MpvMetalViewController?
|
||||
var playUrl: URL?
|
||||
var onPropertyChange: ((MpvMetalViewController, String, Any?) -> Void)?
|
||||
|
||||
func play(_ url: URL) {
|
||||
player?.loadFile(url)
|
||||
}
|
||||
|
||||
func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) {
|
||||
guard let player = player else { return }
|
||||
onPropertyChange?(player, propertyName, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player Controller
|
||||
@@ -403,36 +464,31 @@ final class MpvMetalViewController: UIViewController {
|
||||
}
|
||||
|
||||
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
|
||||
// Flag that we're being deinitialized to prevent new callbacks
|
||||
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)
|
||||
}
|
||||
// Remove the wakeup callback first 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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// Terminate and destroy mpv as the final step
|
||||
if let mpv = self.mpv {
|
||||
// Unobserve all properties
|
||||
mpv_unobserve_property(mpv, 0)
|
||||
mpv_terminate_destroy(mpv)
|
||||
self.mpv = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,12 +518,22 @@ final class MpvMetalViewController: UIViewController {
|
||||
mpv_request_log_messages(mpvHandle, "no")
|
||||
#endif
|
||||
|
||||
// Force a proper window setup to prevent black screens
|
||||
mpv_set_option_string(mpvHandle, "force-window", "yes")
|
||||
mpv_set_option_string(mpvHandle, "reset-on-next-file", "all")
|
||||
|
||||
// Set rendering options
|
||||
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, "gpu-context", "auto")
|
||||
mpv_set_option_string(mpvHandle, "hwdec", "videotoolbox")
|
||||
|
||||
// Set subtitle options
|
||||
mpv_set_option_string(mpvHandle, "subs-match-os-language", "yes")
|
||||
mpv_set_option_string(mpvHandle, "subs-fallback", "yes")
|
||||
|
||||
// Set video options
|
||||
mpv_set_option_string(mpvHandle, "video-rotate", "no")
|
||||
mpv_set_option_string(mpvHandle, "ytdl", "no")
|
||||
|
||||
@@ -487,29 +553,21 @@ final class MpvMetalViewController: UIViewController {
|
||||
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
|
||||
// Set up weak reference for callback
|
||||
let container = WeakContainer(value: self)
|
||||
contextPointer = Unmanaged.passRetained(container).toOpaque()
|
||||
|
||||
// Set wakeup callback with safer checking
|
||||
// Set wakeup callback
|
||||
mpv_set_wakeup_callback(
|
||||
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()
|
||||
}
|
||||
if let controller = container.value, !controller.isBeingDeallocated {
|
||||
controller.processEvents()
|
||||
}
|
||||
}
|
||||
}, contextPointer)
|
||||
@@ -520,14 +578,12 @@ final class MpvMetalViewController: UIViewController {
|
||||
func loadFile(_ url: URL) {
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
print("Loading file: \(url.absoluteString)")
|
||||
|
||||
var args = [url.absoluteString, "replace"]
|
||||
command("loadfile", args: args)
|
||||
}
|
||||
|
||||
func togglePause() {
|
||||
getFlag(MpvProperty.pause) ? play() : pause()
|
||||
}
|
||||
|
||||
func play() {
|
||||
setFlag(MpvProperty.pause, false)
|
||||
}
|
||||
@@ -545,6 +601,15 @@ final class MpvMetalViewController: UIViewController {
|
||||
return data
|
||||
}
|
||||
|
||||
func getNode(_ name: String) -> String? {
|
||||
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 getString(_ name: String) -> String? {
|
||||
guard let mpv = mpv else { return nil }
|
||||
|
||||
@@ -613,34 +678,29 @@ final class MpvMetalViewController: UIViewController {
|
||||
// MARK: - Event Processing
|
||||
|
||||
private func processEvents() {
|
||||
// Check shutdown state first before proceeding
|
||||
if syncQueue.sync(execute: { isShuttingDown }) {
|
||||
// Exit if we're being deallocated
|
||||
if isBeingDeallocated {
|
||||
return
|
||||
}
|
||||
|
||||
mpvQueue.async { [weak self] in
|
||||
guard let self = self,
|
||||
let mpv = self.mpv,
|
||||
!self.isBeingDeallocated,
|
||||
!self.syncQueue.sync(execute: { self.isShuttingDown })
|
||||
else {
|
||||
return
|
||||
}
|
||||
guard let mpv = mpv else { return }
|
||||
|
||||
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 }
|
||||
// Process a limited number of events to avoid infinite loops
|
||||
let maxEvents = 10
|
||||
var eventCount = 0
|
||||
|
||||
self.handleEvent(event)
|
||||
}
|
||||
while !isBeingDeallocated && eventCount < maxEvents {
|
||||
guard let event = mpv_wait_event(mpv, 0) else { break }
|
||||
if event.pointee.event_id == MPV_EVENT_NONE { break }
|
||||
|
||||
handleEvent(event)
|
||||
eventCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEvent(_ event: UnsafePointer<mpv_event>) {
|
||||
// Exit early if we're shutting down
|
||||
if syncQueue.sync(execute: { isShuttingDown }) || isBeingDeallocated {
|
||||
// Exit early if we're being deallocated
|
||||
if isBeingDeallocated {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -684,13 +744,12 @@ final class MpvMetalViewController: UIViewController {
|
||||
|
||||
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
|
||||
}
|
||||
// Let the deinit handle cleanup - just mark as deallocating
|
||||
isBeingDeallocated = true
|
||||
|
||||
case MPV_EVENT_LOG_MESSAGE:
|
||||
return
|
||||
|
||||
default:
|
||||
if let eventName = mpv_event_name(event.pointee.event_id) {
|
||||
print("MPV event: \(String(cString: eventName))")
|
||||
|
||||
Reference in New Issue
Block a user