yattee/Model/Player/Backends/MPVClient.swift

560 lines
17 KiB
Swift
Raw Normal View History

2022-02-27 20:31:17 +00:00
import CoreMedia
2022-07-02 10:49:57 +00:00
import Defaults
2022-02-16 20:23:11 +00:00
import Foundation
import Logging
2023-06-26 04:18:28 +00:00
import MPVKit
2022-02-16 20:23:11 +00:00
#if !os(macOS)
import Siesta
import UIKit
#endif
final class MPVClient: ObservableObject {
2022-07-06 22:08:38 +00:00
static var logFile: URL {
YatteeApp.logsDirectory.appendingPathComponent("yattee-\(YatteeApp.build)-mpv-log.txt")
}
2022-02-16 20:23:11 +00:00
private var logger = Logger(label: "mpv-client")
var mpv: OpaquePointer!
var mpvGL: OpaquePointer!
var queue: DispatchQueue!
2022-02-27 20:31:17 +00:00
#if os(macOS)
var layer: VideoLayer!
var link: CVDisplayLink!
#else
var glView: MPVOGLView!
#endif
2022-02-16 20:23:11 +00:00
var backend: MPVBackend!
2022-02-16 21:51:37 +00:00
var seeking = false
2022-02-27 20:31:17 +00:00
func create(frame: CGRect? = nil) {
#if !os(macOS)
2022-09-28 14:27:01 +00:00
if let frame {
2022-03-19 23:05:09 +00:00
glView = MPVOGLView(frame: frame)
}
2022-02-27 20:31:17 +00:00
#endif
2022-02-16 20:23:11 +00:00
mpv = mpv_create()
if mpv == nil {
print("failed creating context\n")
exit(1)
}
2022-07-06 22:08:38 +00:00
if Defaults[.mpvEnableLogging] {
checkError(mpv_set_option_string(
mpv,
"log-file",
Self.logFile.absoluteString.replacingOccurrences(of: "file://", with: "")
))
checkError(mpv_request_log_messages(mpv, "debug"))
2022-07-06 22:08:38 +00:00
} else {
#if DEBUG
checkError(mpv_request_log_messages(mpv, "debug"))
#else
checkError(mpv_request_log_messages(mpv, "no"))
#endif
}
2022-02-27 20:31:17 +00:00
#if os(macOS)
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
#endif
// CACHING //
checkError(mpv_set_option_string(mpv, "cache-pause-initial", Defaults[.mpvCachePauseInital] ? "yes" : "no"))
2022-07-02 10:49:57 +00:00
checkError(mpv_set_option_string(mpv, "cache-secs", Defaults[.mpvCacheSecs]))
checkError(mpv_set_option_string(mpv, "cache-pause-wait", Defaults[.mpvCachePauseWait]))
// PLAYBACK //
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
// GPU //
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
2022-02-16 20:23:11 +00:00
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
// We set set everything to OpenGL so MPV doesn't have to probe for other APIs.
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "0"))
#if !os(macOS)
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
#endif
// We set this to ordered since we use OpenGL and Apple's implementation is ancient.
checkError(mpv_set_option_string(mpv, "dither", "ordered"))
// DEMUXER //
// We request to test for lavf first and skip probing other demuxer.
checkError(mpv_set_option_string(mpv, "demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "audio-demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "sub-demuxer", "lavf"))
2022-12-09 00:15:19 +00:00
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
2022-02-16 20:23:11 +00:00
2022-02-27 20:31:17 +00:00
checkError(mpv_initialize(mpv))
2022-02-16 20:23:11 +00:00
let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
var initParams = mpv_opengl_init_params(
2022-02-27 20:31:17 +00:00
get_proc_address: getProcAddress,
2023-06-26 04:18:28 +00:00
get_proc_address_ctx: nil
2022-02-16 20:23:11 +00:00
)
2022-12-26 18:41:37 +00:00
queue = DispatchQueue(label: "mpv")
2022-02-27 20:31:17 +00:00
2022-02-16 20:23:11 +00:00
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 {
2023-07-22 17:34:28 +00:00
print("failed to initialize mpv GL context")
2022-02-16 20:23:11 +00:00
exit(1)
}
2022-02-27 20:31:17 +00:00
#if os(macOS)
mpv_render_context_set_update_callback(
mpvGL,
glUpdate,
UnsafeMutableRawPointer(Unmanaged.passUnretained(layer).toOpaque())
)
#else
glView.mpvGL = UnsafeMutableRawPointer(mpvGL)
mpv_render_context_set_update_callback(
mpvGL,
glUpdate(_:),
UnsafeMutableRawPointer(Unmanaged.passUnretained(glView).toOpaque())
)
#endif
2022-02-16 20:23:11 +00:00
}
2023-09-23 14:42:46 +00:00
mpv_set_wakeup_callback(mpv, wakeUp, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()))
mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG)
2023-09-23 16:05:13 +00:00
mpv_observe_property(mpv, 0, "core-idle", MPV_FORMAT_FLAG)
2022-02-16 20:23:11 +00:00
}
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
}
2022-06-29 22:03:36 +00:00
backend?.handle(event)
2022-02-16 20:23:11 +00:00
}
}
}
2022-08-20 21:25:06 +00:00
func loadFile(
_ url: URL,
audio: URL? = nil,
bitrate: Int? = nil,
kind: Stream.Kind,
2022-08-20 21:25:06 +00:00
sub: URL? = nil,
time: CMTime? = nil,
forceSeekable: Bool = false,
completionHandler: ((Int32) -> Void)? = nil
) {
2022-02-16 20:23:11 +00:00
var args = [url.absoluteString]
var options = [String]()
2022-12-09 00:15:19 +00:00
args.append("replace")
// needed since mpvkit 0.38.0
// https://github.com/mpv-player/mpv/issues/13806#issuecomment-2029818905
args.append("-1")
2022-12-09 00:15:19 +00:00
if let time, time.seconds > 0 {
options.append("start=\(Int(time.seconds))")
}
if let audioURL = audio?.absoluteString {
options.append("audio-files-append=\"\(audioURL)\"")
2022-02-16 20:23:11 +00:00
}
2022-07-05 17:20:25 +00:00
if let subURL = sub?.absoluteString {
options.append("sub-files-append=\"\(subURL)\"")
}
2022-08-20 21:25:06 +00:00
if forceSeekable {
2022-12-17 13:26:36 +00:00
options.append("force-seekable=yes")
// this is needed for peertube?
// options.append("stream-lavf-o=seekable=0")
2022-08-20 21:25:06 +00:00
}
2022-07-21 22:44:21 +00:00
2022-08-20 21:05:40 +00:00
if !options.isEmpty {
args.append(options.joined(separator: ","))
}
if kind == .hls, bitrate != 0 {
checkError(mpv_set_option_string(mpv, "hls-bitrate", String(describing: bitrate)))
}
2022-02-16 20:23:11 +00:00
command("loadfile", args: args, returnValueCallback: completionHandler)
}
func play() {
setFlagAsync("pause", false)
}
func pause() {
setFlagAsync("pause", true)
}
func togglePlay() {
command("cycle", args: ["pause"])
}
func stop() {
command("stop")
}
var currentTime: CMTime {
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("time-pos"))
2022-02-16 20:23:11 +00:00
}
2022-06-16 17:44:39 +00:00
var frameDropCount: Int {
mpv.isNil ? 0 : getInt("frame-drop-count")
}
var outputFps: Double {
mpv.isNil ? 0.0 : getDouble("estimated-vf-fps")
}
var hwDecoder: String {
mpv.isNil ? "unknown" : (getString("hwdec-current") ?? "unknown")
}
var bufferingState: Double {
mpv.isNil ? 0.0 : getDouble("cache-buffering-state")
}
var cacheDuration: Double {
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration")
}
2022-11-10 17:11:28 +00:00
var videoFormat: String {
stringOrUnknown("video-format")
}
var videoCodec: String {
stringOrUnknown("video-codec")
}
var currentVo: String {
stringOrUnknown("current-vo")
}
var width: String {
stringOrUnknown("width")
}
var height: String {
stringOrUnknown("height")
}
var videoBitrate: Double {
mpv.isNil ? 0.0 : getDouble("video-bitrate")
}
var audioFormat: String {
stringOrUnknown("audio-params/format")
}
var audioCodec: String {
stringOrUnknown("audio-codec")
}
var currentAo: String {
stringOrUnknown("current-ao")
}
var audioChannels: String {
stringOrUnknown("audio-params/channels")
}
var audioSampleRate: String {
stringOrUnknown("audio-params/samplerate")
}
2022-07-09 00:21:04 +00:00
var aspectRatio: Double {
guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio }
let aspect = getDouble("video-params/aspect")
return aspect.isZero ? VideoPlayerView.defaultAspectRatio : aspect
}
var dh: Double {
let defaultDh = 500.0
guard !mpv.isNil else { return defaultDh }
let dh = getDouble("video-params/dh")
return dh.isZero ? defaultDh : dh
}
2022-02-16 20:23:11 +00:00
var duration: CMTime {
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
2022-02-16 20:23:11 +00:00
}
var pausedForCache: Bool {
mpv.isNil ? false : getFlag("paused-for-cache")
}
2022-07-03 21:18:27 +00:00
var eofReached: Bool {
mpv.isNil ? false : getFlag("eof-reached")
}
2022-02-16 20:23:11 +00:00
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
2022-02-16 21:51:37 +00:00
guard !seeking else {
logger.warning("ignoring seek, another in progress")
return
}
seeking = true
command("seek", args: [String(time.seconds)]) { [weak self] _ in
self?.seeking = false
2022-02-16 20:23:11 +00:00
completionHandler?(true)
}
}
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
2022-02-16 21:51:37 +00:00
guard !seeking else {
logger.warning("ignoring seek, another in progress")
return
}
seeking = true
command("seek", args: [String(time.seconds), "absolute"]) { [weak self] _ in
self?.seeking = false
2022-02-16 20:23:11 +00:00
completionHandler?(true)
}
}
func setSize(_ width: Double, _ height: Double) {
2022-03-27 11:42:20 +00:00
let roundedWidth = width.rounded()
let roundedHeight = height.rounded()
guard width > 0, height > 0 else {
return
}
logger.info("setting player size to \(roundedWidth),\(roundedHeight)")
2022-02-16 20:23:11 +00:00
#if !os(macOS)
2022-03-27 11:42:20 +00:00
guard roundedWidth <= UIScreen.main.bounds.width, roundedHeight <= UIScreen.main.bounds.height else {
2022-02-16 20:23:11 +00:00
logger.info("requested size is greater than screen size, ignoring")
2022-03-27 11:42:20 +00:00
logger.info("width: \(roundedWidth) <= \(UIScreen.main.bounds.width)")
logger.info("height: \(roundedHeight) <= \(UIScreen.main.bounds.height)")
2022-02-16 20:23:11 +00:00
return
}
2022-07-09 00:21:04 +00:00
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let model = self.backend.model
let aspectRatio = self.aspectRatio > 0 && self.aspectRatio < VideoPlayerView.defaultAspectRatio ? self.aspectRatio : VideoPlayerView.defaultAspectRatio
let height = [model.playerSize.height, model.playerSize.width / aspectRatio].min()!
var insets = 0.0
#if os(iOS)
insets = OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? SafeAreaModel.shared.safeArea.bottom : 0
#endif
let offsetY = max(0, model.playingFullScreen ? ((model.playerSize.height / 2.0) - ((height + insets) / 2)) : 0)
2022-07-09 00:21:04 +00:00
UIView.animate(withDuration: 0.2, animations: {
self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height)
}) { completion in
if completion {
self.logger.info("setting player size to \(roundedWidth),\(roundedHeight) FINISHED")
self.glView?.queue.async {
self.glView.display()
}
self.backend?.controls.objectWillChange.send()
2022-07-09 00:21:04 +00:00
}
}
}
2022-02-27 20:31:17 +00:00
#endif
2022-02-16 20:23:11 +00:00
}
func setNeedsDrawing(_ needsDrawing: Bool) {
logger.info("needs drawing: \(needsDrawing)")
2022-02-27 20:31:17 +00:00
#if !os(macOS)
2022-09-02 16:50:59 +00:00
glView?.needsDrawing = needsDrawing
2022-02-27 20:31:17 +00:00
#endif
2022-02-16 20:23:11 +00:00
}
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!))
}
}
logger.info("\(command) -- \(args)")
let returnValue = mpv_command(mpv, &cargs)
if checkForErrors {
checkError(returnValue)
}
if let cb = returnValueCallback {
cb(returnValue)
}
}
2022-06-07 21:20:24 +00:00
func addVideoTrack(_ url: URL) {
command("video-add", args: [url.absoluteString])
}
2022-07-05 17:20:25 +00:00
func addSubTrack(_ url: URL) {
command("sub-add", args: [url.absoluteString])
}
func removeSubs() {
command("sub-remove")
}
2022-06-07 21:20:24 +00:00
func setVideoToAuto() {
2022-06-07 22:05:30 +00:00
setString("video", "1")
2022-06-07 21:20:24 +00:00
}
func setVideoToNo() {
setString("video", "no")
}
var tracksCount: Int {
Int(getString("track-list/count") ?? "-1") ?? -1
}
private func getFlag(_ name: String) -> Bool {
var data = Int64()
mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &data)
return data > 0
}
2022-02-16 20:23:11 +00:00
private func setFlagAsync(_ name: String, _ flag: Bool) {
2022-09-02 16:50:59 +00:00
guard mpv != nil else { return }
2022-02-16 20:23:11 +00:00
var data: Int = flag ? 1 : 0
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_FLAG, &data)
}
2022-04-16 20:50:37 +00:00
func setDoubleAsync(_ name: String, _ value: Double) {
2022-11-10 22:26:25 +00:00
guard mpv != nil else { return }
2022-04-16 20:50:37 +00:00
var data = value
mpv_set_property_async(mpv, 0, name, MPV_FORMAT_DOUBLE, &data)
}
2022-02-16 20:23:11 +00:00
private func getDouble(_ name: String) -> Double {
2022-11-10 22:26:25 +00:00
guard mpv != nil else { return 0.0 }
2022-02-16 20:23:11 +00:00
var data = Double()
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
return data
}
2022-06-07 21:20:24 +00:00
private func getInt(_ name: String) -> Int {
2022-11-10 22:26:25 +00:00
guard mpv != nil else { return 0 }
2022-06-07 21:20:24 +00:00
var data = Int64()
mpv_get_property(mpv, name, MPV_FORMAT_INT64, &data)
return Int(data)
}
func getString(_ name: String) -> String? {
2023-09-23 14:42:46 +00:00
guard mpv != nil else { return nil }
2022-06-07 21:20:24 +00:00
let cstr = mpv_get_property_string(mpv, name)
let str: String? = cstr == nil ? nil : String(cString: cstr!)
mpv_free(cstr)
return str
}
private func setString(_ name: String, _ value: String) {
2022-12-21 17:13:41 +00:00
guard mpv != nil else { return }
2022-06-07 21:20:24 +00:00
mpv_set_property_string(mpv, name, value)
}
2022-02-16 20:23:11 +00:00
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
}
2022-06-26 22:23:34 +00:00
private func checkError(_ status: CInt) {
2022-02-16 20:23:11 +00:00
if status < 0 {
logger.error(.init(stringLiteral: "MPV API error: \(String(cString: mpv_error_string(status)))\n"))
}
}
2022-06-26 22:23:34 +00:00
2022-11-10 17:11:28 +00:00
private func stringOrUnknown(_ name: String) -> String {
mpv.isNil ? "unknown" : (getString(name) ?? "unknown")
}
2022-06-26 22:23:34 +00:00
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)!
}
2023-06-17 12:09:51 +00:00
return String(data: data, encoding: .isoLatin1)!
2022-06-26 22:23:34 +00:00
}
}
2022-02-16 20:23:11 +00:00
}
2022-02-27 20:31:17 +00:00
#if os(macOS)
func getProcAddress(_: UnsafeMutableRawPointer?, _ name: UnsafePointer<Int8>?) -> UnsafeMutableRawPointer? {
let symbolName = CFStringCreateWithCString(kCFAllocatorDefault, name, CFStringBuiltInEncodings.ASCII.rawValue)
let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengl" as CFString)
2022-02-16 20:23:11 +00:00
2022-02-27 20:31:17 +00:00
return CFBundleGetFunctionPointerForName(identifier, symbolName)
}
2022-02-16 20:23:11 +00:00
2022-02-27 20:31:17 +00:00
func glUpdate(_ ctx: UnsafeMutableRawPointer?) {
let videoLayer = unsafeBitCast(ctx, to: VideoLayer.self)
2022-02-16 20:23:11 +00:00
2022-02-27 20:31:17 +00:00
videoLayer.client?.queue?.async {
if !videoLayer.isAsynchronous {
videoLayer.display()
}
}
2022-02-16 20:23:11 +00:00
}
2022-02-27 20:31:17 +00:00
#else
func getProcAddress(_: UnsafeMutableRawPointer?, _ name: UnsafePointer<Int8>?) -> UnsafeMutableRawPointer? {
let symbolName = CFStringCreateWithCString(kCFAllocatorDefault, name, CFStringBuiltInEncodings.ASCII.rawValue)
let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengles" as CFString)
2022-02-16 20:23:11 +00:00
2022-02-27 20:31:17 +00:00
return CFBundleGetFunctionPointerForName(identifier, symbolName)
2022-02-16 20:23:11 +00:00
}
2022-02-27 20:31:17 +00:00
private func glUpdate(_ ctx: UnsafeMutableRawPointer?) {
let glView = unsafeBitCast(ctx, to: MPVOGLView.self)
guard glView.needsDrawing else {
return
}
2022-06-16 17:44:39 +00:00
glView.queue.async {
2022-06-16 00:03:15 +00:00
glView.display()
2022-02-27 20:31:17 +00:00
}
}
#endif
2022-02-16 20:23:11 +00:00
private func wakeUp(_ context: UnsafeMutableRawPointer?) {
let client = unsafeBitCast(context, to: MPVClient.self)
client.readEvents()
}