This commit is contained in:
Alex Kim
2025-05-29 18:30:30 +10:00
parent 3a8fa09881
commit de95b2dd18
5 changed files with 861 additions and 20 deletions

View File

@@ -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);

View File

@@ -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();
};

View File

@@ -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();

View File

@@ -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

View 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, &params) < 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, &params)
}
}
}
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()
}
}