1
0
mirror of https://github.com/mpv-player/mpv synced 2024-12-25 00:02:13 +00:00
mpv/video/out/cocoa_cb_common.swift
Akemi 965ba23303 cocoa-cb: render on a dedicated dispatch queue
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.
2018-03-25 16:24:23 -07:00

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