Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680/* * 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 Cocoaimport IOKit.pwr_mgt
class Common: NSObject { var option: OptionHelper var input: InputHelper? var log: LogHelper let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")
@objc var window: Window? var view: View? var titleBar: TitleBar?
var link: CVDisplayLink?
let eventsLock = NSLock() var vo: UnsafeMutablePointer<vo>? var events: Int = 0
var lightSensor: io_connect_t = 0 var lastLmu: Double = 0 var lightSensorIOPort: IONotificationPortRef?
var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)
var appNotificationObservers: [NSObjectProtocol] = []
var cursorVisibilityWanted: Bool = true
var title: String = "mpv" { didSet { if let window = window { window.title = title } } }
init(_ option: OptionHelper, _ log: LogHelper) { self.option = option self.log = log }
func initMisc(_ vo: UnsafeMutablePointer<vo>) { startDisplayLink(vo) initLightSensor() addDisplayReconfigureObserver() addAppNotifications() option.setMacOptionCallback(macOptsWakeupCallback, context: self) }
func initApp() { var policy: NSApplication.ActivationPolicy = .regular switch option.mac.macos_app_activation_policy { case 0: policy = .regular case 1: policy = .accessory case 2: policy = .prohibited default: break }
NSApp.setActivationPolicy(policy) setAppIcon() }
func initWindow(_ vo: UnsafeMutablePointer<vo>, _ previousActiveApp: NSRunningApplication?) { let (targetScreen, wr, _) = getInitProperties(vo)
guard let view = self.view else { log.error("Something went wrong, no View was initialized") exit(1) }
window = Window(contentRect: wr, screen: targetScreen, view: view, common: self) guard let window = self.window else { log.error("Something went wrong, no Window was initialized") exit(1) }
window.setOnTop(Bool(option.vo.ontop), Int(option.vo.ontop_level)) window.setOnAllWorkspaces(Bool(option.vo.all_workspaces)) window.keepAspect = Bool(option.vo.keepaspect_window) window.title = title window.border = Bool(option.vo.border)
titleBar = TitleBar(frame: wr, window: window, common: self)
let maximized = Bool(option.vo.window_maximized) let minimized = Bool(option.vo.window_minimized) window.isRestorable = false window.isReleasedWhenClosed = false window.setMaximized((minimized || !maximized) ? window.isZoomed : maximized) window.setMinimized(minimized) window.makeMain() window.makeKey()
view.layer?.contentsScale = window.backingScaleFactor
if !minimized { window.orderFront(nil) }
NSApp.activate(ignoringOtherApps: option.vo.focus_on >= 1)
// workaround for macOS 10.15 to refocus the previous App if option.vo.focus_on == 0 { previousActiveApp?.activate() } }
func initView(_ vo: UnsafeMutablePointer<vo>, _ layer: CALayer) { let (_, wr, _) = getInitProperties(vo)
view = View(frame: wr, common: self) guard let view = self.view else { log.error("Something went wrong, no View was initialized") exit(1) }
view.layer = layer view.wantsLayer = true view.layerContentsPlacement = .scaleProportionallyToFit layer.delegate = view }
func initWindowState() { if option.vo.fullscreen { DispatchQueue.main.async { self.window?.toggleFullScreen(nil) } } }
func uninitCommon() { eventsLock.withLock { self.vo = nil } setCursorVisibility(true) stopDisplaylink() uninitLightSensor() removeDisplayReconfigureObserver() removeAppNotifications() enableDisplaySleep() window?.orderOut(nil)
titleBar?.removeFromSuperview() view?.removeFromSuperview() }
func displayLinkCallback(_ displayLink: CVDisplayLink, _ inNow: UnsafePointer<CVTimeStamp>, _ inOutputTime: UnsafePointer<CVTimeStamp>, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer<CVOptionFlags>) -> CVReturn { return kCVReturnSuccess }
func startDisplayLink(_ vo: UnsafeMutablePointer<vo>) { CVDisplayLinkCreateWithActiveCGDisplays(&link)
guard let screen = getTargetScreen(forFullscreen: false) ?? NSScreen.main, let link = self.link else { log.warning("Couldn't start DisplayLink, no Screen or DisplayLink available") return }
CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID) CVDisplayLinkSetOutputHandler(link) { link, now, out, inFlags, outFlags -> CVReturn in return self.displayLinkCallback(link, now, out, inFlags, outFlags) } CVDisplayLinkStart(link) }
func stopDisplaylink() { if let link = self.link, CVDisplayLinkIsRunning(link) { CVDisplayLinkStop(link) } }
func updateDisplaylink() { guard let screen = window?.screen, let link = self.link else { log.warning("Couldn't update DisplayLink, no Screen or DisplayLink available") return }
CVDisplayLinkSetCurrentCGDisplay(link, screen.displayID) queue.asyncAfter(deadline: DispatchTime.now() + 0.1) { self.flagEvents(VO_EVENT_WIN_STATE) } }
func currentFps() -> Double { if let link = self.link { 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 { log.verbose("Falling back to nominal display refresh rate: \(nominalFps)") return nominalFps } else { return actualFps } } } else { log.warning("No DisplayLink available") }
log.warning("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 lmuToLux(_ lmu: Double) -> Double { // the polynomial 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: Double = 1 / pow(10, 27) let power_c3: Double = 1 / pow(10, 19) let power_c2: Double = 1 / pow(10, 12) let power_c1: Double = 1 / pow(10, 5)
let term4: Double = -3.0 * power_c4 * pow(lmu, 4.0) let term3: Double = 2.6 * power_c3 * pow(lmu, 3.0) let term2: Double = -3.4 * power_c2 * pow(lmu, 2.0) let term1: Double = 3.9 * power_c1 * lmu
let lux = ceil(term4 + term3 + term2 + term1 - 0.19) return lux > 0 ? lux : 0 }
var lightSensorCallback: IOServiceInterestCallback = { (ctx, _ service, _ messageType, _ messageArgument) in let com = unsafeBitCast(ctx, to: Common.self)
var outputs: UInt32 = 2 var values: [UInt64] = [0, 0]
var kr = IOConnectCallMethod(com.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil) if kr == KERN_SUCCESS { var mean: Double = (Double(values[0]) + Double(values[1])) / 2 if com.lastLmu != mean { com.lastLmu = mean com.lightSensorUpdate() } } }
func lightSensorUpdate() { flagEvents(VO_EVENT_AMBIENT_LIGHTING_CHANGED) }
func initLightSensor() { let mainPort: mach_port_t if #available(macOS 12.0, *) { mainPort = kIOMainPortDefault } else { mainPort = kIOMasterPortDefault }
let srv = IOServiceGetMatchingService(mainPort, IOServiceMatching("AppleLMUController")) if srv == IO_OBJECT_NULL { log.verbose("Can't find an ambient light sensor") return }
lightSensorIOPort = IONotificationPortCreate(mainPort) IONotificationPortSetDispatchQueue(lightSensorIOPort, queue) var n = io_object_t() IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback, TypeHelper.bridge(obj: self), &n) let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor) IOObjectRelease(srv)
if kr != KERN_SUCCESS { log.verbose("Can't start ambient light sensor connection") return } lightSensorCallback(TypeHelper.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 com = unsafeBitCast(userInfo, to: Common.self) let displayID = com.window?.screen?.displayID ?? display
if displayID == display { com.log.verbose("Detected display mode change, updating screen refresh rate") com.flagEvents(VO_EVENT_WIN_STATE) } } }
func addDisplayReconfigureObserver() { CGDisplayRegisterReconfigurationCallback(reconfigureCallback, TypeHelper.bridge(obj: self)) }
func removeDisplayReconfigureObserver() { CGDisplayRemoveReconfigurationCallback(reconfigureCallback, TypeHelper.bridge(obj: self)) }
func addAppNotifications() { appNotificationObservers.append(NotificationCenter.default.addObserver( forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main, using: { [weak self] (_) in self?.appDidBecomeActive() } )) appNotificationObservers.append(NotificationCenter.default.addObserver( forName: NSApplication.didResignActiveNotification, object: nil, queue: .main, using: { [weak self] (_) in self?.appDidResignActive() } )) }
func removeAppNotifications() { appNotificationObservers.forEach { NotificationCenter.default.removeObserver($0) } appNotificationObservers.removeAll() }
func appDidBecomeActive() { flagEvents(VO_EVENT_FOCUS) }
func appDidResignActive() { flagEvents(VO_EVENT_FOCUS) }
func setAppIcon() { if !AppHub.shared.isBundle { NSApp.applicationIconImage = AppHub.shared.getIcon() } }
func updateCursorVisibility() { setCursorVisibility(cursorVisibilityWanted) }
func setCursorVisibility(_ visible: Bool) { NSCursor.setHiddenUntilMouseMoves(!visible && (view?.canHideCursor() ?? false)) }
func updateICCProfile() { log.warning("updateICCProfile not implemented") }
func getScreenBy(id screenID: Int) -> NSScreen? { if screenID >= NSScreen.screens.count { log.info("Screen ID \(screenID) does not exist, falling back to current device") return nil } else if screenID < 0 { return nil } return NSScreen.screens[screenID] }
func getScreenBy(name screenName: String?) -> NSScreen? { for screen in NSScreen.screens where [screen.localizedName, screen.name, screen.uniqueName, screen.serialNumber].contains(screenName) { return screen } return nil }
func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? { let screenID = fs ? option.vo.fsscreen_id : option.vo.screen_id var name: String? if let screenName = fs ? option.vo.fsscreen_name : option.vo.screen_name { name = String(cString: screenName) } return getScreenBy(id: Int(screenID)) ?? getScreenBy(name: name) }
func getCurrentScreen() -> NSScreen? { return window != nil ? window?.screen : getTargetScreen(forFullscreen: false) ?? NSScreen.main }
func getWindowGeometry(forScreen screen: NSScreen, videoOut vo: UnsafeMutablePointer<vo>) -> (NSRect, Bool) { let r = screen.convertRectToBacking(screen.frame) let targetFrame = option.mac.macos_geometry_calculation == FRAME_VISIBLE ? screen.visibleFrame : screen.frame let rv = screen.convertRectToBacking(targetFrame)
// convert origin to be relative to target screen var originY = rv.origin.y - r.origin.y let originX = rv.origin.x - r.origin.x // flip the y origin, mp_rect expects the origin at the top-left // macOS' windowing system operates from the bottom-left originY = -(originY + rv.size.height) var screenRC: mp_rect = mp_rect(x0: Int32(originX), y0: Int32(originY), x1: Int32(originX + rv.size.width), y1: Int32(originY + rv.size.height))
var geo: vo_win_geometry = vo_win_geometry() vo_calc_window_geometry(vo, &screenRC, &screenRC, Double(screen.backingScaleFactor), false, &geo) vo_apply_window_geometry(vo, &geo)
let height = CGFloat(geo.win.y1 - geo.win.y0) let width = CGFloat(geo.win.x1 - geo.win.x0) // flip the y origin again let y = CGFloat(-geo.win.y1) let x = CGFloat(geo.win.x0) let wr = screen.convertRectFromBacking(NSRect(x: x, y: y, width: width, height: height)) return (wr, Bool(geo.flags & Int32(VO_WIN_FORCE_POS))) }
func getInitProperties(_ vo: UnsafeMutablePointer<vo>) -> (NSScreen, NSRect, Bool) { guard let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main else { log.error("Something went wrong, no Screen was found") exit(1) }
let (wr, forcePosition) = getWindowGeometry(forScreen: targetScreen, videoOut: vo)
return (targetScreen, wr, forcePosition) }
// call before initApp, because on macOS +10.15 it changes the active App func getActiveApp() -> NSRunningApplication? { return NSWorkspace.shared.runningApplications.first(where: {$0.isActive}) }
func flagEvents(_ ev: Int) { eventsLock.withLock { events |= ev guard let vo = vo else { return } vo_wakeup(vo) } }
func checkEvents() -> Int { eventsLock.withLock { let ev = events events = 0 return ev } }
func windowDidEndAnimation() {} func windowSetToFullScreen() {} func windowSetToWindow() {} func windowDidUpdateFrame() {} func windowDidChangeScreen() {} func windowDidChangeScreenProfile() {} func windowDidChangeBackingProperties() {} func windowWillStartLiveResize() {} func windowDidEndLiveResize() {} func windowDidResize() {} func windowDidChangeOcclusionState() {}
@objc func control(_ vo: UnsafeMutablePointer<vo>, events: UnsafeMutablePointer<Int32>, request: UInt32, data: UnsafeMutableRawPointer?) -> Int32 { switch mp_voctrl(request) { case VOCTRL_CHECK_EVENTS: events.pointee |= Int32(checkEvents()) return VO_TRUE case VOCTRL_VO_OPTS_CHANGED: var opt: UnsafeMutableRawPointer? while option.nextChangedOption(property: &opt) { switch opt { case TypeHelper.toPointer(&option.voPtr.pointee.border): DispatchQueue.main.async { self.window?.border = Bool(self.option.vo.border) } case TypeHelper.toPointer(&option.voPtr.pointee.fullscreen): DispatchQueue.main.async { self.window?.toggleFullScreen(nil) } case TypeHelper.toPointer(&option.voPtr.pointee.ontop), TypeHelper.toPointer(&option.voPtr.pointee.ontop_level): DispatchQueue.main.async { self.window?.setOnTop(Bool(self.option.vo.ontop), Int(self.option.vo.ontop_level)) } case TypeHelper.toPointer(&option.voPtr.pointee.all_workspaces): DispatchQueue.main.async { self.window?.setOnAllWorkspaces(Bool(self.option.vo.all_workspaces)) } case TypeHelper.toPointer(&option.voPtr.pointee.keepaspect_window): DispatchQueue.main.async { self.window?.keepAspect = Bool(self.option.vo.keepaspect_window) } case TypeHelper.toPointer(&option.voPtr.pointee.window_minimized): DispatchQueue.main.async { self.window?.setMinimized(Bool(self.option.vo.window_minimized)) } case TypeHelper.toPointer(&option.voPtr.pointee.window_maximized): DispatchQueue.main.async { self.window?.setMaximized(Bool(self.option.vo.window_maximized)) } case TypeHelper.toPointer(&option.voPtr.pointee.cursor_passthrough): DispatchQueue.main.async { self.window?.ignoresMouseEvents = self.option.vo.cursor_passthrough } case TypeHelper.toPointer(&option.voPtr.pointee.geometry), TypeHelper.toPointer(&option.voPtr.pointee.autofit), TypeHelper.toPointer(&option.voPtr.pointee.autofit_smaller), TypeHelper.toPointer(&option.voPtr.pointee.autofit_larger): DispatchQueue.main.async { let (_, wr, forcePosition) = self.getInitProperties(vo) forcePosition ? self.window?.updateFrame(wr) : self.window?.updateSize(wr.size) } default: break } } return VO_TRUE case VOCTRL_GET_DISPLAY_FPS: let fps = data!.assumingMemoryBound(to: CDouble.self) fps.pointee = currentFps() return VO_TRUE case VOCTRL_GET_WINDOW_ID: guard let window = window else { return VO_NOTAVAIL } let wid = data!.assumingMemoryBound(to: Int64.self) wid.pointee = unsafeBitCast(window, to: Int64.self) return VO_TRUE case VOCTRL_GET_HIDPI_SCALE: let scaleFactor = data!.assumingMemoryBound(to: CDouble.self) let screen = getCurrentScreen() let factor = window?.backingScaleFactor ?? screen?.backingScaleFactor ?? 1.0 scaleFactor.pointee = Double(factor) return VO_TRUE case VOCTRL_RESTORE_SCREENSAVER: enableDisplaySleep() return VO_TRUE case VOCTRL_KILL_SCREENSAVER: disableDisplaySleep() return VO_TRUE case VOCTRL_SET_CURSOR_VISIBILITY: let cursorVisibility = data!.assumingMemoryBound(to: CBool.self) cursorVisibilityWanted = cursorVisibility.pointee DispatchQueue.main.async { self.setCursorVisibility(self.cursorVisibilityWanted) } return VO_TRUE case VOCTRL_GET_ICC_PROFILE: let screen = getCurrentScreen() guard var iccData = screen?.colorSpace?.iccProfileData else { log.warning("No Screen available to retrieve ICC profile") return VO_TRUE }
let icc = data!.assumingMemoryBound(to: bstr.self) iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return } let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self) icc.pointee = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count)) } return VO_TRUE case VOCTRL_GET_AMBIENT_LUX: if lightSensor != 0 { let lux = data!.assumingMemoryBound(to: CDouble.self) lux.pointee = lmuToLux(lastLmu) return VO_TRUE } return VO_NOTIMPL case VOCTRL_GET_UNFS_WINDOW_SIZE: let sizeData = data!.assumingMemoryBound(to: Int32.self) let size = UnsafeMutableBufferPointer(start: sizeData, count: 2) let rect = (Bool(option.vo.hidpi_window_scale) ? window?.unfsContentFrame : window?.unfsContentFramePixel) ?? NSRect(x: 0, y: 0, width: 1280, height: 720)
size[0] = Int32(rect.size.width) size[1] = Int32(rect.size.height) 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 = NSRect(x: 0, y: 0, width: CGFloat(size[0]), height: CGFloat(size[1])) DispatchQueue.main.async { if let screen = self.window?.currentScreen, !Bool(self.option.vo.hidpi_window_scale) { rect = screen.convertRectFromBacking(rect) } self.window?.updateSize(rect.size) } return VO_TRUE case VOCTRL_GET_DISPLAY_NAMES: let dnames = data!.assumingMemoryBound(to: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?.self) var array: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>? var count: Int32 = 0 let displayName = getCurrentScreen()?.uniqueName ?? "Unknown"
app_bridge_tarray_append(nil, &array, &count, ta_xstrdup(nil, displayName)) app_bridge_tarray_append(nil, &array, &count, nil) dnames.pointee = array return VO_TRUE case VOCTRL_GET_DISPLAY_RES: guard let screen = getCurrentScreen() else { log.warning("No Screen available to retrieve frame") return VO_NOTAVAIL } let sizeData = data!.assumingMemoryBound(to: Int32.self) let size = UnsafeMutableBufferPointer(start: sizeData, count: 2) let frame = screen.convertRectToBacking(screen.frame) size[0] = Int32(frame.size.width) size[1] = Int32(frame.size.height) return VO_TRUE case VOCTRL_GET_FOCUSED: let focus = data!.assumingMemoryBound(to: CBool.self) focus.pointee = NSApp.isActive return VO_TRUE case VOCTRL_UPDATE_WINDOW_TITLE: let title = String(cString: data!.assumingMemoryBound(to: CChar.self)) DispatchQueue.main.async { self.title = title } return VO_TRUE case VOCTRL_BEGIN_DRAGGING: self.window?.startDragging() return VO_TRUE default: return VO_NOTIMPL } }
let macOptsWakeupCallback: OptionHelper.WakeupCallback = { ( ctx ) in let com = unsafeBitCast(ctx, to: Common.self) DispatchQueue.main.async { com.macOptsUpdate() } }
func macOptsUpdate() { var opt: UnsafeMutableRawPointer? while option.nextChangedMacOption(property: &opt) { switch opt { case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_appearance): titleBar?.set(appearance: option.mac.macos_title_bar_appearance) case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_material): titleBar?.set(material: option.mac.macos_title_bar_material) case TypeHelper.toPointer(&option.macPtr.pointee.macos_title_bar_color): titleBar?.set(color: option.mac.macos_title_bar_color) case TypeHelper.toPointer(&option.macPtr.pointee.cocoa_cb_output_csp): updateICCProfile() default: break } } }}