mirror of
https://github.com/mpv-player/mpv
synced 2024-12-25 00:02:13 +00:00
965ba23303
we rendered on the displaylink thread which wasn't the best idea. if rendering took too long or was blocking it also blocked the displaylink callback. when that happened new vsyncs were reported delayed or not at all. consequently the mpv_render_context_report_swap function wasn't called consistently and that could cause bad video playback. so the rendering is moved to a dedicated dispatch queue. furthermore the update callback starts a layer update directly instead of the displaylink callback, making the rendering a bit more consistent.
516 lines
17 KiB
Swift
516 lines
17 KiB
Swift
/*
|
|
* This file is part of mpv.
|
|
*
|
|
* mpv is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* mpv is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import Cocoa
|
|
import IOKit.pwr_mgt
|
|
|
|
class CocoaCB: NSObject {
|
|
|
|
var mpv: MPVHelper!
|
|
var window: Window!
|
|
var view: EventsView!
|
|
var layer: VideoLayer!
|
|
var link: CVDisplayLink?
|
|
|
|
var cursorHidden: Bool = false
|
|
var cursorVisibilityWanted: Bool = true
|
|
var isShuttingDown: Bool = false
|
|
|
|
enum State {
|
|
case uninit
|
|
case needsInit
|
|
case `init`
|
|
}
|
|
var backendState: State = .uninit
|
|
|
|
let eventsLock = NSLock()
|
|
var events: Int = 0
|
|
|
|
var lightSensor: io_connect_t = 0
|
|
var lastLmu: UInt64 = 0
|
|
var lightSensorIOPort: IONotificationPortRef?
|
|
var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)
|
|
|
|
let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")
|
|
|
|
override init() {
|
|
super.init()
|
|
window = Window(cocoaCB: self)
|
|
|
|
view = EventsView(frame: window.contentView!.bounds, cocoaCB: self)
|
|
window.contentView!.addSubview(view)
|
|
|
|
layer = VideoLayer(cocoaCB: self)
|
|
view.layer = layer
|
|
view.wantsLayer = true
|
|
view.layerContentsPlacement = .scaleProportionallyToFit
|
|
}
|
|
|
|
func setMpvHandle(_ ctx: OpaquePointer) {
|
|
mpv = MPVHelper(ctx)
|
|
layer.setUpRender()
|
|
}
|
|
|
|
func preinit() {
|
|
if backendState == .uninit {
|
|
backendState = .needsInit
|
|
DispatchQueue.main.async {
|
|
self.updateICCProfile()
|
|
}
|
|
startDisplayLink()
|
|
}
|
|
}
|
|
|
|
func uninit() {
|
|
layer.setVideo(false)
|
|
window.orderOut(nil)
|
|
}
|
|
|
|
func reconfig() {
|
|
if backendState == .needsInit {
|
|
initBackend()
|
|
} else {
|
|
layer.setVideo(true)
|
|
updateWindowSize()
|
|
layer.update()
|
|
}
|
|
}
|
|
|
|
func initBackend() {
|
|
NSApp.setActivationPolicy(.regular)
|
|
|
|
let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main()
|
|
let wr = getWindowGeometry(forScreen: targetScreen!, videoOut: mpv.mpctx!.pointee.video_out)
|
|
let win = Window(contentRect: wr, styleMask: window.styleMask,
|
|
screen: targetScreen, cocoaCB: self)
|
|
win.title = window.title
|
|
win.setOnTop(mpv.getBoolProperty("ontop"))
|
|
win.keepAspect = mpv.getBoolProperty("keepaspect-window")
|
|
window.close()
|
|
window = win
|
|
window.contentView!.addSubview(view)
|
|
view.frame = window.contentView!.frame
|
|
window.initTitleBar()
|
|
|
|
setAppIcon()
|
|
window.isRestorable = false
|
|
window.makeMain()
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
layer.setVideo(true)
|
|
|
|
if mpv.getBoolProperty("fullscreen") {
|
|
DispatchQueue.main.async {
|
|
self.window.toggleFullScreen(nil)
|
|
}
|
|
} else {
|
|
window.isMovableByWindowBackground = true
|
|
}
|
|
|
|
initLightSensor()
|
|
addDisplayReconfigureObserver()
|
|
backendState = .init
|
|
}
|
|
|
|
func updateWindowSize() {
|
|
let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main()
|
|
let wr = getWindowGeometry(forScreen: targetScreen!, videoOut: mpv.mpctx!.pointee.video_out)
|
|
if !window.isVisible {
|
|
window.makeKeyAndOrderFront(nil)
|
|
}
|
|
layer.atomicDrawingStart()
|
|
window.updateSize(wr.size)
|
|
}
|
|
|
|
func setAppIcon() {
|
|
if let app = NSApp as? Application {
|
|
NSApp.applicationIconImage = app.getMPVIcon()
|
|
}
|
|
}
|
|
|
|
let linkCallback: CVDisplayLinkOutputCallback = {
|
|
(displayLink: CVDisplayLink,
|
|
inNow: UnsafePointer<CVTimeStamp>,
|
|
inOutputTime: UnsafePointer<CVTimeStamp>,
|
|
flagsIn: CVOptionFlags,
|
|
flagsOut: UnsafeMutablePointer<CVOptionFlags>,
|
|
displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn in
|
|
let ccb: CocoaCB = MPVHelper.bridge(ptr: displayLinkContext!)
|
|
ccb.mpv.reportRenderFlip()
|
|
return kCVReturnSuccess
|
|
}
|
|
|
|
func startDisplayLink() {
|
|
let displayId = UInt32(window.screen!.deviceDescription["NSScreenNumber"] as! Int)
|
|
CVDisplayLinkCreateWithActiveCGDisplays(&link)
|
|
CVDisplayLinkSetCurrentCGDisplay(link!, displayId)
|
|
if #available(macOS 10.12, *) {
|
|
CVDisplayLinkSetOutputHandler(link!) { link, now, out, inFlags, outFlags -> CVReturn in
|
|
self.mpv.reportRenderFlip()
|
|
return kCVReturnSuccess
|
|
}
|
|
} else {
|
|
CVDisplayLinkSetOutputCallback(link!, linkCallback, MPVHelper.bridge(obj: self))
|
|
}
|
|
CVDisplayLinkStart(link!)
|
|
}
|
|
|
|
func stopDisplaylink() {
|
|
if link != nil && CVDisplayLinkIsRunning(link!) {
|
|
CVDisplayLinkStop(link!)
|
|
}
|
|
}
|
|
|
|
func updateDisplaylink() {
|
|
let displayId = UInt32(window.screen!.deviceDescription["NSScreenNumber"] as! Int)
|
|
CVDisplayLinkSetCurrentCGDisplay(link!, displayId)
|
|
|
|
queue.asyncAfter(deadline: DispatchTime.now() + 0.1) {
|
|
self.flagEvents(VO_EVENT_WIN_STATE)
|
|
}
|
|
}
|
|
|
|
func currentFps() -> Double {
|
|
var actualFps = CVDisplayLinkGetActualOutputVideoRefreshPeriod(link!)
|
|
let nominalData = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link!)
|
|
|
|
if (nominalData.flags & Int32(CVTimeFlags.isIndefinite.rawValue)) < 1 {
|
|
let nominalFps = Double(nominalData.timeScale) / Double(nominalData.timeValue)
|
|
|
|
if actualFps > 0 {
|
|
actualFps = 1/actualFps
|
|
}
|
|
|
|
if fabs(actualFps - nominalFps) > 0.1 {
|
|
mpv.sendVerbose("Falling back to nominal display refresh rate: \(nominalFps)")
|
|
return nominalFps
|
|
} else {
|
|
return actualFps
|
|
}
|
|
}
|
|
mpv.sendWarning("Falling back to standard display refresh rate: 60Hz")
|
|
return 60.0
|
|
}
|
|
|
|
func enableDisplaySleep() {
|
|
IOPMAssertionRelease(displaySleepAssertion)
|
|
displaySleepAssertion = IOPMAssertionID(0)
|
|
}
|
|
|
|
func disableDisplaySleep() {
|
|
if displaySleepAssertion != IOPMAssertionID(0) { return }
|
|
IOPMAssertionCreateWithName(
|
|
kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
|
|
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
|
"io.mpv.video_playing_back" as CFString,
|
|
&displaySleepAssertion)
|
|
}
|
|
|
|
func updateCusorVisibility() {
|
|
setCursorVisiblility(cursorVisibilityWanted)
|
|
}
|
|
|
|
func setCursorVisiblility(_ visible: Bool) {
|
|
let visibility = visible ? true : !view.canHideCursor()
|
|
|
|
if visibility && cursorHidden {
|
|
NSCursor.unhide()
|
|
cursorHidden = false;
|
|
} else if !visibility && !cursorHidden {
|
|
NSCursor.hide()
|
|
cursorHidden = true
|
|
}
|
|
}
|
|
|
|
func updateICCProfile() {
|
|
mpv.setRenderICCProfile(window.screen!.colorSpace!)
|
|
layer.colorspace = window.screen!.colorSpace!.cgColorSpace!
|
|
}
|
|
|
|
func lmuToLux(_ v: UInt64) -> Int {
|
|
// the polinomial approximation for apple lmu value -> lux was empirically
|
|
// derived by firefox developers (Apple provides no documentation).
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=793728
|
|
let power_c4 = 1 / pow(10, 27)
|
|
let power_c3 = 1 / pow(10, 19)
|
|
let power_c2 = 1 / pow(10, 12)
|
|
let power_c1 = 1 / pow(10, 5)
|
|
|
|
let term4 = -3.0 * power_c4 * pow(Decimal(v), 4)
|
|
let term3 = 2.6 * power_c3 * pow(Decimal(v), 3)
|
|
let term2 = -3.4 * power_c2 * pow(Decimal(v), 2)
|
|
let term1 = 3.9 * power_c1 * Decimal(v)
|
|
|
|
let lux = Int(ceil( Double((term4 + term3 + term2 + term1 - 0.19) as NSNumber)))
|
|
return Int(lux > 0 ? lux : 0)
|
|
}
|
|
|
|
var lightSensorCallback: IOServiceInterestCallback = { (ctx, service, messageType, messageArgument) -> Void in
|
|
let ccb: CocoaCB = MPVHelper.bridge(ptr: ctx!)
|
|
|
|
var outputs: UInt32 = 2
|
|
var values: [UInt64] = [0, 0]
|
|
|
|
var kr = IOConnectCallMethod(ccb.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil)
|
|
if kr == KERN_SUCCESS {
|
|
var mean = (values[0] + values[1]) / 2
|
|
if ccb.lastLmu != mean {
|
|
ccb.lastLmu = mean
|
|
ccb.mpv.setRenderLux(ccb.lmuToLux(ccb.lastLmu))
|
|
}
|
|
}
|
|
}
|
|
|
|
func initLightSensor() {
|
|
let srv = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController"))
|
|
if srv == IO_OBJECT_NULL {
|
|
mpv.sendVerbose("Can't find an ambient light sensor")
|
|
return
|
|
}
|
|
|
|
lightSensorIOPort = IONotificationPortCreate(kIOMasterPortDefault)
|
|
IONotificationPortSetDispatchQueue(lightSensorIOPort, queue)
|
|
var n = io_object_t()
|
|
IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback, MPVHelper.bridge(obj: self), &n)
|
|
let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor)
|
|
IOObjectRelease(srv)
|
|
|
|
if kr != KERN_SUCCESS {
|
|
mpv.sendVerbose("Can't start ambient light sensor connection")
|
|
return
|
|
}
|
|
lightSensorCallback(MPVHelper.bridge(obj: self), 0, 0, nil)
|
|
}
|
|
|
|
func uninitLightSensor() {
|
|
if lightSensorIOPort != nil {
|
|
IONotificationPortDestroy(lightSensorIOPort)
|
|
IOObjectRelease(lightSensor)
|
|
}
|
|
}
|
|
|
|
var reconfigureCallback: CGDisplayReconfigurationCallBack = { (display, flags, userInfo) in
|
|
if flags.contains(.setModeFlag) {
|
|
let ccb: CocoaCB = MPVHelper.bridge(ptr: userInfo!)
|
|
let displayID = (ccb.window.screen!.deviceDescription["NSScreenNumber"] as! NSNumber).intValue
|
|
if UInt32(displayID) == display {
|
|
ccb.mpv.sendVerbose("Detected display mode change, updating screen refresh rate\n");
|
|
ccb.flagEvents(VO_EVENT_WIN_STATE)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addDisplayReconfigureObserver() {
|
|
CGDisplayRegisterReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
|
|
}
|
|
|
|
func removeDisplayReconfigureObserver() {
|
|
CGDisplayRemoveReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
|
|
}
|
|
|
|
func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? {
|
|
let screenID = fs ? mpv.getStringProperty("fs-screen") ?? "current":
|
|
mpv.getStringProperty("screen") ?? "current"
|
|
|
|
switch screenID {
|
|
case "current", "default", "all":
|
|
return getScreenBy(id: -1)
|
|
default:
|
|
return getScreenBy(id: Int(screenID)!)
|
|
}
|
|
}
|
|
|
|
func getScreenBy(id screenID: Int) -> NSScreen? {
|
|
let screens = NSScreen.screens()
|
|
if screenID >= screens!.count {
|
|
mpv.sendInfo("Screen ID \(screenID) does not exist, falling back to current device")
|
|
return nil
|
|
} else if screenID < 0 {
|
|
return nil
|
|
}
|
|
return screens![screenID]
|
|
}
|
|
|
|
func getWindowGeometry(forScreen targetScreen: NSScreen,
|
|
videoOut vo: UnsafeMutablePointer<vo>) -> NSRect {
|
|
let r = targetScreen.convertRectToBacking(targetScreen.frame)
|
|
var screenRC: mp_rect = mp_rect(x0: Int32(0),
|
|
y0: Int32(0),
|
|
x1: Int32(r.size.width),
|
|
y1: Int32(r.size.height))
|
|
|
|
var geo: vo_win_geometry = vo_win_geometry()
|
|
vo_calc_window_geometry2(vo, &screenRC, Double(targetScreen.backingScaleFactor), &geo)
|
|
|
|
// flip y coordinates
|
|
geo.win.y1 = Int32(r.size.height) - geo.win.y1
|
|
geo.win.y0 = Int32(r.size.height) - geo.win.y0
|
|
|
|
let wr = NSMakeRect(CGFloat(geo.win.x0), CGFloat(geo.win.y1),
|
|
CGFloat(geo.win.x1 - geo.win.x0),
|
|
CGFloat(geo.win.y0 - geo.win.y1))
|
|
return targetScreen.convertRectFromBacking(wr)
|
|
}
|
|
|
|
func flagEvents(_ ev: Int) {
|
|
eventsLock.lock()
|
|
events |= ev
|
|
eventsLock.unlock()
|
|
}
|
|
|
|
func checkEvents() -> Int {
|
|
eventsLock.lock()
|
|
let ev = events
|
|
events = 0
|
|
eventsLock.unlock()
|
|
return ev
|
|
}
|
|
|
|
var controlCallback: mp_render_cb_control_fn = { ( ctx, events, request, data ) -> Int32 in
|
|
let ccb: CocoaCB = MPVHelper.bridge(ptr: ctx!)
|
|
|
|
switch mp_voctrl(request) {
|
|
case VOCTRL_CHECK_EVENTS:
|
|
events!.pointee = Int32(ccb.checkEvents())
|
|
return VO_TRUE
|
|
case VOCTRL_FULLSCREEN:
|
|
DispatchQueue.main.async {
|
|
ccb.window.toggleFullScreen(nil)
|
|
}
|
|
return VO_TRUE
|
|
case VOCTRL_GET_FULLSCREEN:
|
|
let fsData = data!.assumingMemoryBound(to: Int32.self)
|
|
fsData.pointee = ccb.window.isInFullscreen ? 1 : 0
|
|
return VO_TRUE
|
|
case VOCTRL_GET_DISPLAY_FPS:
|
|
let fps = data!.assumingMemoryBound(to: CDouble.self)
|
|
fps.pointee = ccb.currentFps()
|
|
return VO_TRUE
|
|
case VOCTRL_RESTORE_SCREENSAVER:
|
|
ccb.enableDisplaySleep()
|
|
return VO_TRUE
|
|
case VOCTRL_KILL_SCREENSAVER:
|
|
ccb.disableDisplaySleep()
|
|
return VO_TRUE
|
|
case VOCTRL_SET_CURSOR_VISIBILITY:
|
|
ccb.cursorVisibilityWanted = data!.assumingMemoryBound(to: CBool.self).pointee
|
|
DispatchQueue.main.async {
|
|
ccb.setCursorVisiblility(ccb.cursorVisibilityWanted)
|
|
}
|
|
return VO_TRUE
|
|
case VOCTRL_SET_UNFS_WINDOW_SIZE:
|
|
let sizeData = data!.assumingMemoryBound(to: Int32.self)
|
|
let size = UnsafeBufferPointer(start: sizeData, count: 2)
|
|
var rect = NSMakeRect(0, 0, CGFloat(size[0]), CGFloat(size[1]))
|
|
DispatchQueue.main.async {
|
|
if !ccb.mpv.getBoolProperty("hidpi-window-scale") {
|
|
rect = ccb.window.currentScreen!.convertRectFromBacking(rect)
|
|
}
|
|
ccb.window.updateSize(rect.size)
|
|
}
|
|
return VO_TRUE
|
|
case VOCTRL_GET_WIN_STATE:
|
|
let minimized = data!.assumingMemoryBound(to: Int32.self)
|
|
minimized.pointee = ccb.window.isMiniaturized ? VO_WIN_STATE_MINIMIZED : Int32(0)
|
|
return VO_TRUE
|
|
case VOCTRL_UPDATE_WINDOW_TITLE:
|
|
let titleData = data!.assumingMemoryBound(to: Int8.self)
|
|
let title = String(cString: titleData)
|
|
DispatchQueue.main.async {
|
|
ccb.window.title = String(cString: titleData)
|
|
}
|
|
return VO_TRUE
|
|
case VOCTRL_PREINIT:
|
|
ccb.preinit()
|
|
return VO_TRUE
|
|
case VOCTRL_UNINIT:
|
|
DispatchQueue.main.async {
|
|
ccb.uninit()
|
|
}
|
|
return VO_TRUE
|
|
case VOCTRL_RECONFIG:
|
|
DispatchQueue.main.async {
|
|
ccb.reconfig()
|
|
}
|
|
return VO_TRUE
|
|
default:
|
|
return VO_NOTIMPL
|
|
}
|
|
}
|
|
|
|
func shutdown(_ destroy: Bool = false) {
|
|
setCursorVisiblility(true)
|
|
layer.setVideo(false)
|
|
stopDisplaylink()
|
|
uninitLightSensor()
|
|
removeDisplayReconfigureObserver()
|
|
mpv.deinitRender()
|
|
mpv.deinitMPV(destroy)
|
|
}
|
|
|
|
func checkShutdown() {
|
|
if isShuttingDown {
|
|
shutdown(true)
|
|
}
|
|
}
|
|
|
|
func processEvent(_ event: UnsafePointer<mpv_event>) {
|
|
switch event.pointee.event_id {
|
|
case MPV_EVENT_SHUTDOWN:
|
|
if window.isAnimating {
|
|
isShuttingDown = true
|
|
return
|
|
}
|
|
shutdown()
|
|
case MPV_EVENT_PROPERTY_CHANGE:
|
|
if backendState == .init {
|
|
handlePropertyChange(event)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func handlePropertyChange(_ event: UnsafePointer<mpv_event>) {
|
|
let pData = OpaquePointer(event.pointee.data)
|
|
guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else {
|
|
return
|
|
}
|
|
|
|
switch String(cString: property.name) {
|
|
case "border":
|
|
if let data = MPVHelper.mpvFlagToBool(property.data) {
|
|
window.border = data
|
|
}
|
|
case "ontop":
|
|
if let data = MPVHelper.mpvFlagToBool(property.data) {
|
|
window.setOnTop(data)
|
|
}
|
|
case "keepaspect-window":
|
|
if let data = MPVHelper.mpvFlagToBool(property.data) {
|
|
window.keepAspect = data
|
|
}
|
|
case "macos-title-bar-style":
|
|
if let data = MPVHelper.mpvStringArrayToString(property.data) {
|
|
window.setTitleBarStyle(data)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|