forked from Ninjalama/streamyfin_mirror
Add more
This commit is contained in:
@@ -401,19 +401,19 @@ export default function page() {
|
||||
`--sub-text-scale=${settings.subtitleSize}`,
|
||||
`--start=${startPosition}`,
|
||||
];
|
||||
if (
|
||||
chosenSubtitleTrack &&
|
||||
(notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
||||
) {
|
||||
const finalIndex = notTranscoding
|
||||
? allSubs.indexOf(chosenSubtitleTrack)
|
||||
: textSubs.indexOf(chosenSubtitleTrack);
|
||||
initOptions.push(`--sub-track=${finalIndex}`);
|
||||
}
|
||||
// if (
|
||||
// chosenSubtitleTrack &&
|
||||
// (notTranscoding || chosenSubtitleTrack.IsTextSubtitleStream)
|
||||
// ) {
|
||||
// const finalIndex = notTranscoding
|
||||
// ? allSubs.indexOf(chosenSubtitleTrack)
|
||||
// : textSubs.indexOf(chosenSubtitleTrack);
|
||||
// initOptions.push(`--sub-track=${finalIndex}`);
|
||||
// }
|
||||
|
||||
if (notTranscoding && chosenAudioTrack) {
|
||||
initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||
}
|
||||
// if (notTranscoding && chosenAudioTrack) {
|
||||
// initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`);
|
||||
// }
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
|
||||
@@ -455,9 +455,9 @@ export const Controls: FC<Props> = ({
|
||||
|
||||
const onClose = async () => {
|
||||
lightHapticFeedback();
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
);
|
||||
// await ScreenOrientation.lockAsync(
|
||||
// ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
// );
|
||||
router.back();
|
||||
};
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
if (getSubtitleTracks) {
|
||||
const subtitleData = await getSubtitleTracks();
|
||||
|
||||
console.log("subtitleData", subtitleData);
|
||||
|
||||
// Step 1: Move external subs to the end, because VLC puts external subs at the end
|
||||
const sortedSubs = allSubs.sort(
|
||||
(a, b) => Number(a.IsExternal) - Number(b.IsExternal),
|
||||
@@ -170,6 +172,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
|
||||
});
|
||||
setSubtitleTracks(subtitles);
|
||||
}
|
||||
|
||||
if (getAudioTracks) {
|
||||
const audioData = await getAudioTracks();
|
||||
|
||||
|
||||
@@ -498,11 +498,14 @@ final class MpvMetalViewController: UIViewController {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupMetalLayer()
|
||||
setupMPV()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||
self?.setupMPV()
|
||||
|
||||
if let url = playUrl {
|
||||
loadFile(url)
|
||||
if let url = self?.playUrl {
|
||||
self?.loadFile(url)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
@@ -580,9 +583,11 @@ final class MpvMetalViewController: UIViewController {
|
||||
mpv_set_option_string(mpvHandle, "reset-on-next-file", "all")
|
||||
|
||||
// Set rendering options
|
||||
mpv_set_option(mpvHandle, "wid", MPV_FORMAT_INT64, &metalLayer)
|
||||
|
||||
var layerPtr = Unmanaged.passUnretained(metalLayer).toOpaque()
|
||||
mpv_set_option(mpvHandle, "wid", MPV_FORMAT_INT64, &layerPtr)
|
||||
mpv_set_option_string(mpvHandle, "vo", "gpu-next")
|
||||
mpv_set_option_string(mpvHandle, "gpu-api", "vulkan")
|
||||
mpv_set_option_string(mpvHandle, "gpu-api", "metal")
|
||||
mpv_set_option_string(mpvHandle, "gpu-context", "auto")
|
||||
mpv_set_option_string(mpvHandle, "hwdec", "videotoolbox")
|
||||
|
||||
@@ -629,6 +634,8 @@ final class MpvMetalViewController: UIViewController {
|
||||
mpv_set_wakeup_callback(
|
||||
mpvHandle, MpvMetalViewController.mpvWakeupCallback,
|
||||
UnsafeMutableRawPointer(bitPattern: controllerId))
|
||||
|
||||
print("MPV initialized")
|
||||
}
|
||||
|
||||
// Static callback function - no WeakContainer needed
|
||||
|
||||
831
modules/mpv-player/ios/MpvPlayerViewGL.swift
Normal file
831
modules/mpv-player/ios/MpvPlayerViewGL.swift
Normal file
@@ -0,0 +1,831 @@
|
||||
import ExpoModulesCore
|
||||
import Foundation
|
||||
import GLKit
|
||||
import Libmpv
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// 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 {
|
||||
// MARK: - Properties
|
||||
|
||||
private var playerController: MpvGLViewController?
|
||||
private var source: [String: Any]?
|
||||
private var externalSubtitles: [[String: String]]?
|
||||
|
||||
// MARK: - Event Emitters
|
||||
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@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
|
||||
|
||||
print("Setting up MPV GL view")
|
||||
|
||||
// Create player controller - IMPORTANT: Use init(nibName:bundle:) to ensure proper GLKView setup
|
||||
let controller = MpvGLViewController(nibName: nil, bundle: nil)
|
||||
|
||||
// Force view loading immediately
|
||||
_ = controller.view
|
||||
|
||||
// Configure player delegate
|
||||
controller.mpvDelegate = self
|
||||
playerController = controller
|
||||
|
||||
// Make sure controller view is properly set up as GLKView
|
||||
controller.view.backgroundColor = .black
|
||||
|
||||
// Set explicit frame to ensure it's visible
|
||||
controller.view.frame = bounds
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
// Add to hierarchy
|
||||
addSubview(controller.view)
|
||||
|
||||
// Use constraints to ensure proper sizing
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
controller.view.topAnchor.constraint(equalTo: topAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// Override layoutSubviews to make sure the player view is properly sized
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
playerController?.view.frame = bounds
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
func setSource(_ source: [String: Any]) {
|
||||
self.source = source
|
||||
|
||||
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) {
|
||||
print("Loading file: \(url.absoluteString)")
|
||||
self.playerController?.playUrl = url
|
||||
|
||||
// Set start position if available
|
||||
if let startPosition = source["startPosition"] as? Double {
|
||||
self.playerController?.startPosition = startPosition
|
||||
}
|
||||
|
||||
self.playerController?.loadFile(url)
|
||||
|
||||
// Set video to fill the screen
|
||||
self.setVideoScalingMode("cover")
|
||||
|
||||
// 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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.onPipStarted?(["pipStarted": false, "target": self.reactTag as Any])
|
||||
}
|
||||
}
|
||||
|
||||
func play() {
|
||||
playerController?.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
playerController?.pause()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
playerController?.command("stop", args: [])
|
||||
}
|
||||
|
||||
func seekTo(_ time: Int32) {
|
||||
let seconds = Double(time) / 1000.0
|
||||
print("Seeking to absolute position: \(seconds) seconds")
|
||||
playerController?.command("seek", args: ["\(seconds)", "absolute"])
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackIndex: Int) {
|
||||
playerController?.command("set", args: ["aid", "\(trackIndex)"])
|
||||
}
|
||||
|
||||
func getAudioTracks() -> [[String: Any]] {
|
||||
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) {
|
||||
playerController?.command("set", args: ["sid", "\(trackIndex)"])
|
||||
}
|
||||
|
||||
func getSubtitleTracks() -> [[String: Any]] {
|
||||
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])
|
||||
}
|
||||
|
||||
@objc
|
||||
func setVideoScalingMode(_ mode: String) {
|
||||
// Mode can be: "contain" (letterbox), "cover" (crop/fill), or "stretch"
|
||||
|
||||
guard let playerController = playerController else { return }
|
||||
|
||||
switch mode.lowercased() {
|
||||
case "cover", "fill", "crop":
|
||||
// Fill the screen, cropping if necessary
|
||||
playerController.command("set", args: ["panscan", "1.0"])
|
||||
playerController.command("set", args: ["video-unscaled", "no"])
|
||||
playerController.command("set", args: ["video-aspect-override", "no"])
|
||||
// Center the crop
|
||||
playerController.command("set", args: ["video-align-x", "0.5"])
|
||||
playerController.command("set", args: ["video-align-y", "0.5"])
|
||||
case "stretch":
|
||||
// Stretch to fill without maintaining aspect ratio
|
||||
playerController.command("set", args: ["panscan", "0.0"])
|
||||
playerController.command("set", args: ["video-unscaled", "no"])
|
||||
playerController.command("set", args: ["video-aspect-override", "-1"])
|
||||
// No need for alignment as it stretches to fill entire area
|
||||
case "contain", "letterbox", "fit":
|
||||
// Keep aspect ratio, fit within screen (letterbox)
|
||||
playerController.command("set", args: ["panscan", "0.0"])
|
||||
playerController.command("set", args: ["video-unscaled", "no"])
|
||||
playerController.command("set", args: ["video-aspect-override", "no"])
|
||||
// Set alignment to center
|
||||
playerController.command("set", args: ["video-align-x", "0.5"])
|
||||
playerController.command("set", args: ["video-align-y", "0.5"])
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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 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() {
|
||||
// Check if we already cleaned up
|
||||
|
||||
print("Cleaning up player")
|
||||
guard playerController != nil else { return }
|
||||
|
||||
// First stop playback
|
||||
stop()
|
||||
|
||||
// Break reference cycles
|
||||
playerController?.mpvDelegate = nil
|
||||
|
||||
// Remove from view hierarchy
|
||||
playerController?.view.removeFromSuperview()
|
||||
|
||||
// Release references
|
||||
playerController = nil
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Move the static properties to class level
|
||||
private static var lastTimePositionUpdate = Date(timeIntervalSince1970: 0)
|
||||
|
||||
func propertyChanged(mpv: OpaquePointer, propertyName: String, value: Any?) {
|
||||
// Add throttling for frequently updated properties
|
||||
switch propertyName {
|
||||
case MpvProperty.timePosition:
|
||||
// Throttle timePosition updates to once per second
|
||||
let now = Date()
|
||||
if now.timeIntervalSince(MpvPlayerView.lastTimePositionUpdate) < 1.0 {
|
||||
return
|
||||
}
|
||||
MpvPlayerView.lastTimePositionUpdate = now
|
||||
|
||||
if let position = value as? Double {
|
||||
let timeMs = position * 1000
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
print("IsPlaying: \(!self.isPaused())")
|
||||
self.onVideoProgress?([
|
||||
"currentTime": timeMs,
|
||||
"duration": self.getVideoDuration() * 1000,
|
||||
"isPlaying": !self.isPaused(),
|
||||
"isBuffering": self.isBuffering(),
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
case MpvProperty.pausedForCache:
|
||||
// We want to respond immediately to buffering state changes
|
||||
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,
|
||||
"isPlaying": !self.isPaused(),
|
||||
"state": self.isPaused() ? "Paused" : "Playing",
|
||||
])
|
||||
}
|
||||
|
||||
case MpvProperty.pause:
|
||||
// We want to respond immediately to play/pause state changes
|
||||
if let isPaused = value as? Bool {
|
||||
let state = isPaused ? "Paused" : "Playing"
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
print("onPlaybackStateChanged: \(state)")
|
||||
self.onPlaybackStateChanged?([
|
||||
"state": state,
|
||||
"isPlaying": !isPaused,
|
||||
"isBuffering": self.isBuffering(),
|
||||
"currentTime": self.getCurrentTime() * 1000,
|
||||
"duration": self.getVideoDuration() * 1000,
|
||||
"target": self.reactTag as Any,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Player Controller
|
||||
final class MpvGLViewController: GLKViewController {
|
||||
// MARK: - Properties
|
||||
var mpv: OpaquePointer!
|
||||
var mpvGL: OpaquePointer!
|
||||
weak var mpvDelegate: MpvPlayerDelegate?
|
||||
var queue: DispatchQueue = DispatchQueue(label: "mpv", qos: .userInteractive)
|
||||
private var defaultFBO: GLint = -1
|
||||
|
||||
var playUrl: URL?
|
||||
var startPosition: Double?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupContext()
|
||||
setupMpv()
|
||||
|
||||
if let url = playUrl {
|
||||
self.loadFile(url)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
print("GLKViewController viewWillAppear")
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
print("GLKViewController viewDidAppear")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Clean up on deallocation
|
||||
if mpvGL != nil {
|
||||
mpv_render_context_free(mpvGL)
|
||||
mpvGL = nil
|
||||
}
|
||||
|
||||
if mpv != nil {
|
||||
mpv_terminate_destroy(mpv)
|
||||
mpv = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
func setupContext() {
|
||||
print("Setting up OpenGL ES context")
|
||||
|
||||
let context = EAGLContext(api: .openGLES3)!
|
||||
if context == nil {
|
||||
print("ERROR: Failed to create OpenGL ES context")
|
||||
return
|
||||
}
|
||||
|
||||
let isSuccess = EAGLContext.setCurrent(context)
|
||||
if !isSuccess {
|
||||
print("ERROR: Failed to set current GL context")
|
||||
return
|
||||
}
|
||||
|
||||
// Set the context on our GLKView
|
||||
let glkView = self.view as! GLKView
|
||||
glkView.context = context
|
||||
|
||||
print("Successfully set up OpenGL ES context")
|
||||
}
|
||||
|
||||
func setupMpv() {
|
||||
print("Setting up MPV")
|
||||
|
||||
mpv = mpv_create()
|
||||
if mpv == nil {
|
||||
print("ERROR: failed creating mpv context\n")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// https://mpv.io/manual/stable/#options
|
||||
#if DEBUG
|
||||
checkError(mpv_request_log_messages(mpv, "debug"))
|
||||
#else
|
||||
checkError(mpv_request_log_messages(mpv, "no"))
|
||||
#endif
|
||||
#if os(macOS)
|
||||
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
|
||||
#endif
|
||||
|
||||
// Set options
|
||||
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, "hwdec", "auto-copy"))
|
||||
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
|
||||
checkError(mpv_set_option_string(mpv, "profile", "gpu-hq"))
|
||||
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
|
||||
|
||||
// Add in setupMpv before initialization
|
||||
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "opengl-version", "3"))
|
||||
|
||||
// Initialize MPV
|
||||
checkError(mpv_initialize(mpv))
|
||||
|
||||
// Set starting point if available
|
||||
if let startPos = startPosition {
|
||||
let startPosString = String(format: "%.1f", startPos)
|
||||
print("Setting initial start position to \(startPosString)")
|
||||
checkError(mpv_set_option_string(mpv, "start", startPosString))
|
||||
}
|
||||
|
||||
// Set up rendering
|
||||
print("Setting up MPV GL rendering context")
|
||||
let api = UnsafeMutableRawPointer(
|
||||
mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
|
||||
var initParams = mpv_opengl_init_params(
|
||||
get_proc_address: {
|
||||
(ctx, name) in
|
||||
return MpvGLViewController.getProcAddress(ctx, name)
|
||||
},
|
||||
get_proc_address_ctx: nil
|
||||
)
|
||||
|
||||
withUnsafeMutablePointer(to: &initParams) { initParams in
|
||||
var params = [
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data: initParams),
|
||||
mpv_render_param(),
|
||||
]
|
||||
|
||||
if mpv_render_context_create(&mpvGL, mpv, ¶ms) < 0 {
|
||||
puts("ERROR: failed to initialize mpv GL context")
|
||||
exit(1)
|
||||
}
|
||||
|
||||
print("Successfully created MPV GL render context")
|
||||
|
||||
mpv_render_context_set_update_callback(
|
||||
mpvGL,
|
||||
mpvGLUpdate,
|
||||
UnsafeMutableRawPointer(Unmanaged.passUnretained(self.view).toOpaque())
|
||||
)
|
||||
}
|
||||
|
||||
// Observe properties
|
||||
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)
|
||||
|
||||
// Set wakeup callback
|
||||
mpv_set_wakeup_callback(
|
||||
self.mpv,
|
||||
{ (ctx) in
|
||||
let client = unsafeBitCast(ctx, to: MpvGLViewController.self)
|
||||
client.readEvents()
|
||||
}, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
|
||||
|
||||
print("MPV setup complete")
|
||||
|
||||
// Configure GLKView properly for better performance
|
||||
let glkView = self.view as! GLKView
|
||||
glkView.enableSetNeedsDisplay = false // Allow continuous rendering
|
||||
glkView.drawableMultisample = .multisample4X // Might help or hurt - test both
|
||||
glkView.drawableColorFormat = .RGBA8888
|
||||
|
||||
// Set higher preferred frame rate
|
||||
self.preferredFramesPerSecond = 60 // Or even higher on newer devices
|
||||
}
|
||||
|
||||
// MARK: - MPV Methods
|
||||
|
||||
func loadFile(_ url: URL) {
|
||||
print("Loading file: \(url.absoluteString)")
|
||||
|
||||
var args = [url.absoluteString]
|
||||
args.append("replace")
|
||||
|
||||
print("MPV Command: loadfile with args \(args)")
|
||||
command("loadfile", args: args.map { $0 as String? })
|
||||
|
||||
// Set video settings for visibility
|
||||
command("set", args: ["video-unscaled", "no"])
|
||||
command("set", args: ["panscan", "1.0"]) // Ensure video fills screen
|
||||
}
|
||||
|
||||
func togglePause() {
|
||||
getFlag(MpvProperty.pause) ? play() : pause()
|
||||
}
|
||||
|
||||
func play() {
|
||||
setFlag(MpvProperty.pause, false)
|
||||
}
|
||||
|
||||
func pause() {
|
||||
setFlag(MpvProperty.pause, true)
|
||||
}
|
||||
|
||||
func getDouble(_ name: String) -> Double {
|
||||
var data = 0.0
|
||||
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
|
||||
return data
|
||||
}
|
||||
|
||||
func getNode(_ name: String) -> String? {
|
||||
guard let cString = mpv_get_property_string(mpv, name) else { return nil }
|
||||
defer {
|
||||
mpv_free(UnsafeMutableRawPointer(mutating: cString))
|
||||
}
|
||||
return String(cString: cString)
|
||||
}
|
||||
|
||||
func getFlag(_ name: String) -> Bool {
|
||||
var data = Int64()
|
||||
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
|
||||
mpv_set_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
||||
}
|
||||
|
||||
func command(
|
||||
_ command: String,
|
||||
args: [String?] = [],
|
||||
checkForErrors: Bool = true,
|
||||
returnValueCallback: ((Int32) -> Void)? = nil
|
||||
) {
|
||||
guard mpv != nil else {
|
||||
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!))
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
var strArgs = args
|
||||
strArgs.insert(command, at: 0)
|
||||
strArgs.append(nil)
|
||||
|
||||
return strArgs
|
||||
}
|
||||
|
||||
// MARK: - Event Processing
|
||||
|
||||
func readEvents() {
|
||||
queue.async { [self] in
|
||||
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)
|
||||
|
||||
// Handle different property types
|
||||
var value: Any?
|
||||
|
||||
switch propertyName {
|
||||
case MpvProperty.pausedForCache, MpvProperty.pause:
|
||||
if property.format == MPV_FORMAT_FLAG,
|
||||
let data = property.data
|
||||
{
|
||||
let boolValue =
|
||||
UnsafePointer<Bool>(OpaquePointer(data))?.pointee ?? false
|
||||
value = boolValue
|
||||
}
|
||||
|
||||
case MpvProperty.timePosition, MpvProperty.duration:
|
||||
if property.format == MPV_FORMAT_DOUBLE,
|
||||
let data = property.data
|
||||
{
|
||||
let doubleValue =
|
||||
UnsafePointer<Double>(OpaquePointer(data))?.pointee ?? 0.0
|
||||
value = doubleValue
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Notify delegate if we have a value
|
||||
if let value = value {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.mpvDelegate?.propertyChanged(
|
||||
mpv: self.mpv, propertyName: propertyName, value: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
case MPV_EVENT_SHUTDOWN:
|
||||
mpv_render_context_free(mpvGL)
|
||||
mpv_terminate_destroy(mpv)
|
||||
mpv = nil
|
||||
print("event: shutdown\n")
|
||||
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)!))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkError(_ status: CInt) {
|
||||
if status < 0 {
|
||||
print("MPV API error: \(String(cString: mpv_error_string(status)))\n")
|
||||
}
|
||||
}
|
||||
|
||||
private var machine: String {
|
||||
var systeminfo = utsname()
|
||||
uname(&systeminfo)
|
||||
return withUnsafeBytes(of: &systeminfo.machine) { bufPtr -> String in
|
||||
let data = Data(bufPtr)
|
||||
if let lastIndex = data.lastIndex(where: { $0 != 0 }) {
|
||||
return String(data: data[0...lastIndex], encoding: .isoLatin1)!
|
||||
} else {
|
||||
return String(data: data, encoding: .isoLatin1)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - GL Rendering
|
||||
|
||||
override func glkView(_ view: GLKView, drawIn rect: CGRect) {
|
||||
guard let mpvGL else {
|
||||
return
|
||||
}
|
||||
|
||||
// fill black background
|
||||
glClearColor(0, 0, 0, 0)
|
||||
glClear(UInt32(GL_COLOR_BUFFER_BIT))
|
||||
|
||||
glGetIntegerv(UInt32(GL_FRAMEBUFFER_BINDING), &defaultFBO)
|
||||
|
||||
var dims: [GLint] = [0, 0, 0, 0]
|
||||
glGetIntegerv(GLenum(GL_VIEWPORT), &dims)
|
||||
|
||||
var data = mpv_opengl_fbo(
|
||||
fbo: Int32(defaultFBO),
|
||||
w: Int32(dims[2]),
|
||||
h: Int32(dims[3]),
|
||||
internal_format: 0
|
||||
)
|
||||
|
||||
var flip: CInt = 1
|
||||
withUnsafeMutablePointer(to: &flip) { flip in
|
||||
withUnsafeMutablePointer(to: &data) { data in
|
||||
var params = [
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: data),
|
||||
mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: flip),
|
||||
mpv_render_param(),
|
||||
]
|
||||
mpv_render_context_render(mpvGL, ¶ms)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static func getProcAddress(_: UnsafeMutableRawPointer?, _ name: UnsafePointer<Int8>?)
|
||||
-> UnsafeMutableRawPointer?
|
||||
{
|
||||
let symbolName = CFStringCreateWithCString(
|
||||
kCFAllocatorDefault, name, CFStringBuiltInEncodings.ASCII.rawValue)
|
||||
let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengles" as CFString)
|
||||
|
||||
return CFBundleGetFunctionPointerForName(identifier, symbolName)
|
||||
}
|
||||
}
|
||||
|
||||
private func mpvGLUpdate(_ ctx: UnsafeMutableRawPointer?) {
|
||||
let glView = unsafeBitCast(ctx, to: GLKView.self)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
glView.display()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user