From c5e4538bc4b63f02bf89f62aa25a37b9eb0c0316 Mon Sep 17 00:00:00 2001 From: Akemi Date: Mon, 12 Feb 2018 12:28:19 +0100 Subject: cocoa-cb: initial implementation via opengl-cb API this is meant to replace the old and not properly working vo_gpu/opengl cocoa backend in the future. the problems are various shortcomings of Apple's opengl implementation and buggy behaviour in certain circumstances that couldn't be properly worked around. there are also certain regressions on newer macOS versions from 10.11 onwards. - awful opengl performance with a none layer backed context - huge amount of dropped frames with an early context flush - flickering of system elements like the dock or volume indicator - double buffering not properly working with a none layer backed context - bad performance in fullscreen because of system optimisations all the problems were caused by using a normal opengl context, that seems somewhat abandoned by apple, and are fixed by using a layer backed opengl context instead. problems that couldn't be fixed could be properly worked around. this has all features our old backend has sans the wid embedding, the possibility to disable the automatic GPU switching and taking screenshots of the window content. the first was deemed unnecessary by me for now, since i just use the libmpv API that others can use anyway. second is technically not possible atm because we have to pre-allocate our opengl context at a time the config isn't read yet, so we can't get the needed property. third one is a bit tricky because of deadlocking and it needed to be in sync, hopefully i can work around that in the future. this also has at least one additional feature or eye-candy. a properly working fullscreen animation with the native fs. also since this is a direct port of the old backend of the parts that could be used, though with adaptions and improvements, this looks a lot cleaner and easier to understand. some credit goes to @pigoz for the initial swift build support which i could improve upon. Fixes: #5478, #5393, #5152, #5151, #4615, #4476, #3978, #3746, #3739, #2392, #2217 --- DOCS/interface-changes.rst | 2 + DOCS/man/options.rst | 2 +- DOCS/man/vo.rst | 4 +- osdep/macOS_mpv_helper.swift | 255 ++++++++++++++++++ osdep/macOS_swift_bridge.h | 54 ++++ osdep/macosx_application.m | 80 ++++-- osdep/macosx_application_objc.h | 6 + osdep/macosx_events.m | 21 +- osdep/macosx_menubar.m | 18 +- player/client.c | 24 +- player/client.h | 10 + video/out/cocoa-cb/events_view.swift | 266 +++++++++++++++++++ video/out/cocoa-cb/video_layer.swift | 210 +++++++++++++++ video/out/cocoa-cb/window.swift | 456 ++++++++++++++++++++++++++++++++ video/out/cocoa_cb_common.swift | 485 ++++++++++++++++++++++++++++++++++ video/out/opengl/context_cocoa.c | 2 + video/out/vo.h | 5 + video/out/vo_opengl_cb.c | 88 ++++-- waftools/checks/custom.py | 14 +- waftools/detections/compiler_swift.py | 71 +++++ waftools/generators/sources.py | 11 +- wscript | 15 +- wscript_build.py | 37 +++ 23 files changed, 2054 insertions(+), 82 deletions(-) create mode 100644 osdep/macOS_mpv_helper.swift create mode 100644 osdep/macOS_swift_bridge.h create mode 100644 video/out/cocoa-cb/events_view.swift create mode 100644 video/out/cocoa-cb/video_layer.swift create mode 100644 video/out/cocoa-cb/window.swift create mode 100644 video/out/cocoa_cb_common.swift create mode 100644 waftools/detections/compiler_swift.py diff --git a/DOCS/interface-changes.rst b/DOCS/interface-changes.rst index da47ef6ba8..ba1a25c273 100644 --- a/DOCS/interface-changes.rst +++ b/DOCS/interface-changes.rst @@ -70,6 +70,8 @@ Interface changes pad must be connected either to another filter, or to a video/audio track or video/audio output). If they are disconnected at runtime, the stream will probably stall. + - deprecate the OpenGL cocoa backend, option choice --gpu-context=cocoa + when used with --gpu-api=opengl (use --vo=opengl-cb) --- mpv 0.28.0 --- - rename --hwdec=mediacodec option to mediacodec-copy, to reflect conventions followed by other hardware video decoding APIs diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 9f0c672919..c12453ae5f 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -4876,7 +4876,7 @@ The following video options are currently all specific to ``--vo=gpu`` and auto auto-select (default) cocoa - Cocoa/OS X + Cocoa/OS X (deprecated, use --vo=opengl-cb instead) win Win32/WGL winvk diff --git a/DOCS/man/vo.rst b/DOCS/man/vo.rst index 20e406dd4e..d5a4ef9b26 100644 --- a/DOCS/man/vo.rst +++ b/DOCS/man/vo.rst @@ -429,7 +429,9 @@ Available video output drivers are: Specify the directory to save the image files to (default: ``./``). ``opengl-cb`` - For use with libmpv direct OpenGL embedding; useless in any other contexts. + For use with libmpv direct OpenGL embedding. As a special case, on OS X it + is used like a normal VO within mpv (cocoa-cb). Otherwise useless in any + other contexts. (See ````.) This also supports many of the options the ``gpu`` VO has. diff --git a/osdep/macOS_mpv_helper.swift b/osdep/macOS_mpv_helper.swift new file mode 100644 index 0000000000..39be9cc8c7 --- /dev/null +++ b/osdep/macOS_mpv_helper.swift @@ -0,0 +1,255 @@ +/* + * 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 . + */ + +import Cocoa +import OpenGL.GL +import OpenGL.GL3 + +class MPVHelper: NSObject { + + var mpvHandle: OpaquePointer? + var mpvGLCBContext: OpaquePointer? + var mpvLog: OpaquePointer? + var inputContext: OpaquePointer? + var mpctx: UnsafeMutablePointer? + + init(_ mpv: OpaquePointer) { + super.init() + mpvHandle = mpv + mpvLog = mp_log_new(UnsafeMutablePointer(mpvHandle), + mp_client_get_log(mpvHandle), "cocoacb") + mpctx = UnsafeMutablePointer(mp_client_get_core(mpvHandle)) + inputContext = mpctx!.pointee.input + + mpv_observe_property(mpvHandle, 0, "ontop", MPV_FORMAT_FLAG) + mpv_observe_property(mpvHandle, 0, "border", MPV_FORMAT_FLAG) + mpv_observe_property(mpvHandle, 0, "keepaspect-window", MPV_FORMAT_FLAG) + } + + func setGLCB() { + if mpvHandle == nil { + sendError("No mpv handle available.") + exit(1) + } + mpvGLCBContext = OpaquePointer(mp_get_sub_api2(mpvHandle, MPV_SUB_API_OPENGL_CB, false)) + if mpvGLCBContext == nil { + sendError("libmpv does not have the opengl-cb sub-API.") + exit(1) + } + } + + func initGLCB() { + if mpvGLCBContext == nil { + setGLCB() + } + if mpv_opengl_cb_init_gl(mpvGLCBContext, nil, getProcAddress, nil) < 0 { + sendError("GL init has failed.") + exit(1) + } + } + + let getProcAddress: mpv_opengl_cb_get_proc_address_fn = { + (ctx: UnsafeMutableRawPointer?, name: UnsafePointer?) -> UnsafeMutableRawPointer? in + let symbol: CFString = CFStringCreateWithCString( + kCFAllocatorDefault, name, kCFStringEncodingASCII) + let indentifier = CFBundleGetBundleWithIdentifier("com.apple.opengl" as CFString) + let addr = CFBundleGetFunctionPointerForName(indentifier, symbol) + + if symbol as String == "glFlush" { + return glDummyPtr() + } + + return addr + } + + func setGLCBUpdateCallback(_ callback: @escaping mpv_opengl_cb_update_fn, context object: AnyObject) { + if mpvGLCBContext == nil { + sendWarning("Init mpv opengl-cb first.") + } else { + mpv_opengl_cb_set_update_callback(mpvGLCBContext, callback, MPVHelper.bridge(obj: object)) + } + } + + func setGLCBControlCallback(_ callback: @escaping mpv_opengl_cb_control_fn, context object: AnyObject) { + if mpvGLCBContext == nil { + sendWarning("Init mpv opengl-cb first.") + } else { + mp_client_set_control_callback(mpvGLCBContext, callback, MPVHelper.bridge(obj: object)) + } + } + + func reportGLCBFlip() { + if mpvGLCBContext == nil { return } + mpv_opengl_cb_report_flip(mpvGLCBContext, 0) + } + + func drawGLCB(_ surface: NSSize) { + if mpvGLCBContext != nil { + var i: GLint = 0 + glGetIntegerv(GLenum(GL_DRAW_FRAMEBUFFER_BINDING), &i) + + mpv_opengl_cb_draw(mpvGLCBContext, i, Int32(surface.width), Int32(-surface.height)) + } else { + glClearColor(0, 0, 0, 1) + glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) + } + } + + func setGLCBICCProfile(_ profile: NSColorSpace) { + if mpvGLCBContext == nil { return } + var iccData = profile.iccProfileData + iccData!.withUnsafeMutableBytes { (u8Ptr: UnsafeMutablePointer) in + let icc = bstrdup(nil, bstr(start: u8Ptr, len: iccData!.count)) + mp_client_set_icc_profile(mpvGLCBContext, icc) + } + } + + func setGLCBLux(_ lux: Int) { + if mpvGLCBContext == nil { return } + mp_client_set_ambient_lux(mpvGLCBContext, Int32(lux)) + } + + func command(_ cmd: String) { + if mpvHandle == nil { return } + mpv_command_string(mpvHandle, cmd) + } + + func commandAsync(_ cmd: [String?], id: UInt64 = 1) { + if mpvHandle == nil { return } + var mCmd = cmd + mCmd.append(nil) + var cargs = mCmd.map { $0.flatMap { UnsafePointer(strdup($0)) } } + mpv_command_async(mpvHandle, id, &cargs) + for ptr in cargs { free(UnsafeMutablePointer(mutating: ptr)) } + } + + func getBoolProperty(_ name: String) -> Bool { + if mpvHandle == nil { return false } + var value = Int32() + mpv_get_property(mpvHandle, name, MPV_FORMAT_FLAG, &value) + return value > 0 + } + + func getIntProperty(_ name: String) -> Int { + if mpvHandle == nil { return 0 } + var value = Int64() + mpv_get_property(mpvHandle, name, MPV_FORMAT_INT64, &value) + return Int(value) + } + + func getStringProperty(_ name: String) -> String? { + if mpvHandle == nil { return nil } + let value = mpv_get_property_string(mpvHandle, name) + let str = value == nil ? nil : String(cString: value!) + mpv_free(value) + return str + } + + func canBeDraggedAt(_ pos: NSPoint) -> Bool { + if inputContext == nil { return false } + let canDrag = !mp_input_test_dragging(inputContext!, Int32(pos.x), Int32(pos.y)) + return canDrag + } + + func setMousePosition(_ pos: NSPoint) { + if inputContext == nil { return } + mp_input_set_mouse_pos(inputContext!, Int32(pos.x), Int32(pos.y)) + } + + func putAxis(_ mpkey: Int32, delta: Double) { + if inputContext == nil { return } + mp_input_put_wheel(inputContext!, mpkey, delta) + } + + func sendVerbose(_ msg: String) { + send(message: msg, type: MSGL_V) + } + + func sendInfo(_ msg: String) { + send(message: msg, type: MSGL_INFO) + } + + func sendWarning(_ msg: String) { + send(message: msg, type: MSGL_WARN) + } + + func sendError(_ msg: String) { + send(message: msg, type: MSGL_ERR) + } + + func send(message msg: String, type t: Int) { + if mpvLog == nil { + sendFallback(message: msg, type: t) + } else { + let args: [CVarArg] = [ (msg as NSString).utf8String! ] + mp_msg_va(mpvLog, Int32(t), "%s\n", getVaList(args)) + } + } + + func sendFallback(message msg: String, type t: Int) { + var level = "\u{001B}" + switch t { + case MSGL_V: + level += "[0;30m[VERBOSE]" + case MSGL_INFO: + level += "[0;30m[INFO]" + case MSGL_WARN: + level += "[0;33m" + case MSGL_ERR: + level += "[0;31m" + default: + level += "[0;30m" + } + + print("\(level)[osx/cocoacb] \(msg)\u{001B}[0;30m") + } + + func deinitGLCB() { + mpv_opengl_cb_set_update_callback(mpvGLCBContext, nil, nil) + mp_client_set_control_callback(mpvGLCBContext, nil, nil) + mpv_opengl_cb_uninit_gl(mpvGLCBContext) + mpvGLCBContext = nil + } + + func deinitMPV() { + mpvHandle = nil + mpvLog = nil + inputContext = nil + mpctx = nil + } + + // (__bridge void*) + class func bridge(obj: T) -> UnsafeMutableRawPointer { + return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque()) + } + + // (__bridge T*) + class func bridge(ptr: UnsafeRawPointer) -> T { + return Unmanaged.fromOpaque(ptr).takeUnretainedValue() + } + + // *(char **) MPV_FORMAT_STRING on mpv_event_property + class func mpvStringArrayToString(_ obj: UnsafeMutableRawPointer) -> String? { + let cstr = UnsafeMutablePointer>(OpaquePointer(obj)) + return String(cString: cstr[0]) + } + + // MPV_FORMAT_FLAG + class func mpvFlagToBool(_ obj: UnsafeMutableRawPointer) -> Bool? { + return UnsafePointer(OpaquePointer(obj))?.pointee + } +} diff --git a/osdep/macOS_swift_bridge.h b/osdep/macOS_swift_bridge.h new file mode 100644 index 0000000000..d662dbcf4e --- /dev/null +++ b/osdep/macOS_swift_bridge.h @@ -0,0 +1,54 @@ +/* + * 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 . + */ + +// including IOKit here again doesn't make sense, but otherwise the swift +// compiler doesn't include the needed header in our generated header file +#import + +#include "player/client.h" +#include "libmpv/opengl_cb.h" + +#include "player/core.h" +#include "input/input.h" +#include "video/out/win_state.h" + +#include "osdep/macosx_application_objc.h" +#include "osdep/macosx_events_objc.h" + + +// complex macros won't get imported to Swift so we have to reassign them +static int SWIFT_MBTN_LEFT = MP_MBTN_LEFT; +static int SWIFT_MBTN_MID = MP_MBTN_MID; +static int SWIFT_MBTN_RIGHT = MP_MBTN_RIGHT; +static int SWIFT_WHEEL_UP = MP_WHEEL_UP; +static int SWIFT_WHEEL_DOWN = MP_WHEEL_DOWN; +static int SWIFT_WHEEL_LEFT = MP_WHEEL_LEFT; +static int SWIFT_WHEEL_RIGHT = MP_WHEEL_RIGHT; +static int SWIFT_MBTN_BACK = MP_MBTN_BACK; +static int SWIFT_MBTN_FORWARD = MP_MBTN_FORWARD; +static int SWIFT_MBTN9 = MP_MBTN9; + +static int SWIFT_KEY_CLOSE_WIN = MP_KEY_CLOSE_WIN; +static int SWIFT_KEY_MOUSE_LEAVE = MP_KEY_MOUSE_LEAVE; +static int SWIFT_KEY_MOUSE_ENTER = MP_KEY_MOUSE_ENTER; +static int SWIFT_KEY_STATE_DOWN = MP_KEY_STATE_DOWN; +static int SWIFT_KEY_STATE_UP = MP_KEY_STATE_UP; + +// dummy function to override glFlush() +static void glDummy() {} +static void *glDummyPtr(void) __attribute__((unused)); +static void *glDummyPtr() { return &glDummy; } diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m index d070401621..a65910ec9e 100644 --- a/osdep/macosx_application.m +++ b/osdep/macosx_application.m @@ -33,6 +33,9 @@ #if HAVE_MACOS_TOUCHBAR #import "osdep/macosx_touchbar.h" #endif +#if HAVE_MACOS_COCOA_CB +#include "osdep/macOS_swift.h" +#endif #define MPV_PROTOCOL @"mpv://" @@ -40,7 +43,13 @@ // running in libmpv mode, and cocoa_main() was never called. static bool application_instantiated; +struct playback_thread_ctx { + int *argc; + char ***argv; +}; + static pthread_t playback_thread_id; +static struct playback_thread_ctx thread_ctx = {0}; @interface Application () { @@ -60,9 +69,22 @@ static void terminate_cocoa_application(void) [NSApp terminate:NSApp]; } +static void *playback_thread(void *ctx_obj) +{ + mpthread_set_name("playback core (OSX)"); + @autoreleasepool { + struct playback_thread_ctx *ctx = (struct playback_thread_ctx*) ctx_obj; + int r = mpv_main(*ctx->argc, *ctx->argv); + terminate_cocoa_application(); + // normally never reached - unless the cocoa mainloop hasn't started yet + exit(r); + } +} + @implementation Application @synthesize menuBar = _menu_bar; @synthesize openCount = _open_count; +@synthesize cocoaCB = _cocoa_cb; - (void)sendEvent:(NSEvent *)event { @@ -96,6 +118,24 @@ static void terminate_cocoa_application(void) [super dealloc]; } +- (void)initMPVCore +{ + pthread_create(&playback_thread_id, NULL, playback_thread, &thread_ctx); + [[EventsResponder sharedInstance] waitForInputContext]; +} + +static const char macosx_icon[] = +#include "osdep/macosx_icon.inc" +; + +- (NSImage *)getMPVIcon +{ + NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon + length:sizeof(macosx_icon) + freeWhenDone:NO]; + return [[NSImage alloc] initWithData:icon_data]; +} + #if HAVE_MACOS_TOUCHBAR - (NSTouchBar *)makeTouchBar { @@ -117,6 +157,16 @@ static void terminate_cocoa_application(void) if ([self respondsToSelector:@selector(touchBar)]) [(TouchBar *)self.touchBar processEvent:event]; #endif + if (_cocoa_cb) { + [_cocoa_cb processEvent:event]; + } +} + +- (void)setMpvHandle:(struct mpv_handle *)ctx +{ + if (_cocoa_cb) { + [_cocoa_cb setMpvHandle:ctx]; + } } - (void)queueCommand:(char *)cmd @@ -182,11 +232,6 @@ static void terminate_cocoa_application(void) } @end -struct playback_thread_ctx { - int *argc; - char ***argv; -}; - static void cocoa_run_runloop(void) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; @@ -194,18 +239,6 @@ static void cocoa_run_runloop(void) [pool drain]; } -static void *playback_thread(void *ctx_obj) -{ - mpthread_set_name("playback core (OSX)"); - @autoreleasepool { - struct playback_thread_ctx *ctx = (struct playback_thread_ctx*) ctx_obj; - int r = mpv_main(*ctx->argc, *ctx->argv); - terminate_cocoa_application(); - // normally never reached - unless the cocoa mainloop hasn't started yet - exit(r); - } -} - void cocoa_register_menu_item_action(MPMenuKey key, void* action) { if (application_instantiated) @@ -218,6 +251,10 @@ static void init_cocoa_application(bool regular) [NSApp setDelegate:NSApp]; [NSApp setMenuBar:[[MenuBar alloc] init]]; +#if HAVE_MACOS_COCOA_CB + [NSApp setCocoaCB:[[CocoaCB alloc] init]]; +#endif + // Will be set to Regular from cocoa_common during UI creation so that we // don't create an icon when playing audio only files. [NSApp setActivationPolicy: regular ? @@ -279,9 +316,8 @@ int cocoa_main(int argc, char *argv[]) application_instantiated = true; [[EventsResponder sharedInstance] setIsApplication:YES]; - struct playback_thread_ctx ctx = {0}; - ctx.argc = &argc; - ctx.argv = &argv; + thread_ctx.argc = &argc; + thread_ctx.argv = &argv; if (bundle_started_from_finder(argv)) { setup_bundle(&argc, argv); @@ -294,8 +330,8 @@ int cocoa_main(int argc, char *argv[]) init_cocoa_application(false); } - pthread_create(&playback_thread_id, NULL, playback_thread, &ctx); - [[EventsResponder sharedInstance] waitForInputContext]; + if (![NSApp cocoaCB]) + [NSApp initMPVCore]; cocoa_run_runloop(); // This should never be reached: cocoa_run_runloop blocks until the diff --git a/osdep/macosx_application_objc.h b/osdep/macosx_application_objc.h index c12a8b8c64..22e6f7e525 100644 --- a/osdep/macosx_application_objc.h +++ b/osdep/macosx_application_objc.h @@ -19,15 +19,21 @@ #include "osdep/macosx_application.h" #import "osdep/macosx_menubar_objc.h" +@class CocoaCB; struct mpv_event; +struct mpv_handle; @interface Application : NSApplication +- (NSImage *)getMPVIcon; - (void)processEvent:(struct mpv_event *)event; - (void)queueCommand:(char *)cmd; - (void)stopMPV:(char *)cmd; - (void)openFiles:(NSArray *)filenames; +- (void)setMpvHandle:(struct mpv_handle *)ctx; +- (void)initMPVCore; @property(nonatomic, retain) MenuBar *menuBar; @property(nonatomic, assign) size_t openCount; +@property(nonatomic, retain) CocoaCB *cocoaCB; @end diff --git a/osdep/macosx_events.m b/osdep/macosx_events.m index a23bd56278..fef7e95053 100644 --- a/osdep/macosx_events.m +++ b/osdep/macosx_events.m @@ -39,6 +39,10 @@ #include "config.h" +#if HAVE_MACOS_COCOA_CB +#include "osdep/macOS_swift.h" +#endif + @interface EventsResponder () { struct input_ctx *_inputContext; @@ -304,7 +308,10 @@ void cocoa_set_mpv_handle(struct mpv_handle *ctx) - (BOOL)setMpvHandle:(struct mpv_handle *)ctx { if (_is_application) { - dispatch_sync(dispatch_get_main_queue(), ^{ _ctx = ctx; }); + dispatch_sync(dispatch_get_main_queue(), ^{ + _ctx = ctx; + [NSApp setMpvHandle:ctx]; + }); return YES; } else { mpv_detach_destroy(ctx); @@ -326,17 +333,21 @@ void cocoa_set_mpv_handle(struct mpv_handle *ctx) -(void)processEvent:(struct mpv_event *)event { + if(_is_application) { + [NSApp processEvent:event]; + } + switch (event->event_id) { case MPV_EVENT_SHUTDOWN: { + #if HAVE_MACOS_COCOA_CB + if ([(Application *)NSApp cocoaCB].isShuttingDown) + return; + #endif mpv_detach_destroy(_ctx); _ctx = nil; break; } } - - if(_is_application) { - [NSApp processEvent:event]; - } } - (void)startAppleRemote diff --git a/osdep/macosx_menubar.m b/osdep/macosx_menubar.m index 92bd3fa991..931079a552 100644 --- a/osdep/macosx_menubar.m +++ b/osdep/macosx_menubar.m @@ -646,7 +646,7 @@ { NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: @"mpv", @"ApplicationName", - [self getMPVIcon], @"ApplicationIcon", + [(Application *)NSApp getMPVIcon], @"ApplicationIcon", [NSString stringWithUTF8String:mpv_copyright], @"Copyright", [NSString stringWithUTF8String:mpv_version], @"ApplicationVersion", nil]; @@ -709,7 +709,7 @@ [alert setMessageText:@"Open URL"]; [alert addButtonWithTitle:@"Ok"]; [alert addButtonWithTitle:@"Cancel"]; - [alert setIcon:[self getMPVIcon]]; + [alert setIcon:[(Application *)NSApp getMPVIcon]]; NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)]; [input setPlaceholderString:@"URL"]; @@ -737,25 +737,13 @@ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; } -static const char macosx_icon[] = -#include "osdep/macosx_icon.inc" -; - -- (NSImage *)getMPVIcon -{ - NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon - length:sizeof(macosx_icon) - freeWhenDone:NO]; - return [[NSImage alloc] initWithData:icon_data]; -} - - (void)alertWithTitle:(NSString *)title andText:(NSString *)text { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:title]; [alert setInformativeText:text]; [alert addButtonWithTitle:@"Ok"]; - [alert setIcon:[self getMPVIcon]]; + [alert setIcon:[(Application *)NSApp getMPVIcon]]; [alert runModal]; } diff --git a/player/client.c b/player/client.c index 618dbc4165..1df7e659d8 100644 --- a/player/client.c +++ b/player/client.c @@ -1726,6 +1726,17 @@ int mpv_opengl_cb_uninit_gl(mpv_opengl_cb_context *ctx) { return MPV_ERROR_NOT_IMPLEMENTED; } +void mp_client_set_control_callback(struct mpv_opengl_cb_context *ctx, + mpv_opengl_cb_control_fn callback, + void *callback_ctx) +{ +} +void mp_client_set_icc_profile(struct mpv_opengl_cb_context *ctx, bstr icc_data) +{ +} +void mp_client_set_ambient_lux(struct mpv_opengl_cb_context *ctx, int lux) +{ +} #endif int mpv_opengl_cb_render(mpv_opengl_cb_context *ctx, int fbo, int vp[4]) @@ -1733,22 +1744,29 @@ int mpv_opengl_cb_render(mpv_opengl_cb_context *ctx, int fbo, int vp[4]) return mpv_opengl_cb_draw(ctx, fbo, vp[2], vp[3]); } -void *mpv_get_sub_api(mpv_handle *ctx, mpv_sub_api sub_api) +void *mp_get_sub_api2(mpv_handle *ctx, mpv_sub_api sub_api, bool lock) { if (!ctx->mpctx->initialized) return NULL; void *res = NULL; - lock_core(ctx); + if (lock) + lock_core(ctx); switch (sub_api) { case MPV_SUB_API_OPENGL_CB: res = opengl_cb_get_context(ctx); break; default:; } - unlock_core(ctx); + if (lock) + unlock_core(ctx); return res; } +void *mpv_get_sub_api(mpv_handle *ctx, mpv_sub_api sub_api) +{ + return mp_get_sub_api2(ctx, sub_api, true); +} + struct mp_custom_protocol { char *protocol; void *user_data; diff --git a/player/client.h b/player/client.h index 67b287b67f..042934cde3 100644 --- a/player/client.h +++ b/player/client.h @@ -6,6 +6,7 @@ #include "libmpv/client.h" #include "libmpv/stream_cb.h" +#include "misc/bstr.h" struct MPContext; struct mpv_handle; @@ -34,6 +35,7 @@ struct mpv_handle *mp_new_client(struct mp_client_api *clients, const char *name struct mp_log *mp_client_get_log(struct mpv_handle *ctx); struct MPContext *mp_client_get_core(struct mpv_handle *ctx); struct MPContext *mp_client_api_get_core(struct mp_client_api *api); +void *mp_get_sub_api2(mpv_handle *ctx, mpv_sub_api sub_api, bool lock); // m_option.c void *node_get_alloc(struct mpv_node *node); @@ -49,4 +51,12 @@ void kill_video(struct mp_client_api *client_api); bool mp_streamcb_lookup(struct mpv_global *g, const char *protocol, void **out_user_data, mpv_stream_cb_open_ro_fn *out_fn); +typedef int (*mpv_opengl_cb_control_fn)(void *cb_ctx, int *events, uint32_t request, void *data); + +void mp_client_set_control_callback(struct mpv_opengl_cb_context *ctx, + mpv_opengl_cb_control_fn callback, + void *callback_ctx); +void mp_client_set_icc_profile(struct mpv_opengl_cb_context *ctx, bstr icc_data); +void mp_client_set_ambient_lux(struct mpv_opengl_cb_context *ctx, int lux); + #endif diff --git a/video/out/cocoa-cb/events_view.swift b/video/out/cocoa-cb/events_view.swift new file mode 100644 index 0000000000..4cb154c64a --- /dev/null +++ b/video/out/cocoa-cb/events_view.swift @@ -0,0 +1,266 @@ +/* + * 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 . + */ + +import Cocoa + +class EventsView: NSView { + + weak var cocoaCB: CocoaCB! + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + var tracker: NSTrackingArea? + var hasMouseDown: Bool = false + + override var isFlipped: Bool { return true } + override var acceptsFirstResponder: Bool { return true } + + + init(frame frameRect: NSRect, cocoaCB ccb: CocoaCB) { + cocoaCB = ccb + super.init(frame: frameRect) + autoresizingMask = [.viewWidthSizable, .viewHeightSizable] + wantsBestResolutionOpenGLSurface = true + register(forDraggedTypes: [NSFilenamesPboardType, NSURLPboardType]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateTrackingAreas() { + if tracker != nil { + removeTrackingArea(tracker!) + } + + if mpv != nil && !mpv.getBoolProperty("input-cursor") { + return + } + + tracker = NSTrackingArea(rect: self.bounds, + options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved, .enabledDuringMouseDrag], + owner: self, userInfo: nil) + addTrackingArea(tracker!) + + if containsMouseLocation() { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0) + } + } + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + guard let types = sender.draggingPasteboard().types else { return [] } + if types.contains(NSFilenamesPboardType) || types.contains(NSURLPboardType) { + return .copy + } + return [] + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pb = sender.draggingPasteboard() + guard let types = sender.draggingPasteboard().types else { return false } + if types.contains(NSFilenamesPboardType) { + if let files = pb.propertyList(forType: NSFilenamesPboardType) as? [Any] { + EventsResponder.sharedInstance().handleFilesArray(files) + return true + } + } else if types.contains(NSURLPboardType) { + if let url = pb.propertyList(forType: NSURLPboardType) as? [Any] { + EventsResponder.sharedInstance().handleFilesArray(url) + return true + } + } + return false + } + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + return mpv.getBoolProperty("input-cursor") + } + + override func becomeFirstResponder() -> Bool { + return mpv.getBoolProperty("input-cursor") || + mpv.getBoolProperty("input-vo-keyboard") + } + + override func resignFirstResponder() -> Bool { + return true + } + + override func mouseEntered(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_ENTER, 0) + } + } + + override func mouseExited(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0) + } + } + + override func mouseMoved(with event: NSEvent) { + if mpv != nil && mpv.getBoolProperty("input-cursor") { + signalMouseMovement(event) + } + } + + override func mouseDragged(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseMovement(event) + } + } + + override func mouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func mouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + cocoaCB.window.isMoving = false + } + + override func rightMouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func rightMouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + } + + override func otherMouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func otherMouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + } + + func signalMouseDown(_ event: NSEvent) { + signalMouseEvent(event, SWIFT_KEY_STATE_DOWN) + if event.clickCount > 1 { + signalMouseEvent(event, SWIFT_KEY_STATE_UP) + } + } + + func signalMouseUp(_ event: NSEvent) { + signalMouseEvent(event, SWIFT_KEY_STATE_UP) + } + + func signalMouseEvent(_ event: NSEvent, _ state: Int32) { + hasMouseDown = state == SWIFT_KEY_STATE_DOWN + let mpkey = getMpvButton(event) + cocoa_put_key_with_modifiers((mpkey | state), Int32(event.modifierFlags.rawValue)); + } + + func signalMouseMovement(_ event: NSEvent) { + var point = convert(event.locationInWindow, from: nil) + point = convertToBacking(point) + point.y = -point.y + + cocoaCB.window.updateMovableBackground(point) + if !cocoaCB.window.isMoving { + mpv.setMousePosition(point) + } + } + + func preciseScroll(_ event: NSEvent) { + var delta: Double + var cmd: Int32 + + if fabs(event.deltaY) >= fabs(event.deltaX) { + delta = Double(event.deltaY) * 0.1; + cmd = delta > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN; + } else { + delta = Double(event.deltaX) * 0.1; + cmd = delta > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT; + } + + mpv.putAxis(cmd, delta: fabs(delta)) + } + + override func scrollWheel(with event: NSEvent) { + if !mpv.getBoolProperty("input-cursor") { + return + } + + if event.hasPreciseScrollingDeltas { + preciseScroll(event) + } else { + let modifiers = event.modifierFlags + let deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX + let deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY + var mpkey: Int32 + + if fabs(deltaY) >= fabs(deltaX) { + mpkey = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN; + } else { + mpkey = deltaX > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT; + } + + cocoa_put_key_with_modifiers(mpkey, Int32(modifiers.rawValue)) + } + } + + func containsMouseLocation() -> Bool { + if cocoaCB == nil { return false } + var topMargin: CGFloat = 0.0 + let menuBarHeight = NSApp.mainMenu!.menuBarHeight + + if cocoaCB.window.isInFullscreen && (menuBarHeight > 0) { + let titleBar = NSWindow.frameRect(forContentRect: CGRect.zero, styleMask: .titled) + topMargin = titleBar.size.height + 1 + menuBarHeight + } + + var vF = window!.screen!.frame + vF.size.height -= topMargin + + let vFW = window!.convertFromScreen(vF) + let vFV = convert(vFW, from: nil) + let pt = convert(window!.mouseLocationOutsideOfEventStream, from: nil) + + let clippedBounds = bounds.intersection(vFV) + return clippedBounds.contains(pt) + } + + func canHideCursor() -> Bool { + return !hasMouseDown && containsMouseLocation() && window!.isKeyWindow + } + + func getMpvButton(_ event: NSEvent) -> Int32 { + let buttonNumber = event.buttonNumber + switch (buttonNumber) { + case 0: return SWIFT_MBTN_LEFT; + case 1: return SWIFT_MBTN_RIGHT; + case 2: return SWIFT_MBTN_MID; + case 3: return SWIFT_MBTN_BACK; + case 4: return SWIFT_MBTN_FORWARD; + default: return SWIFT_MBTN9 + Int32(buttonNumber - 5); + } + } +} diff --git a/video/out/cocoa-cb/video_layer.swift b/video/out/cocoa-cb/video_layer.swift new file mode 100644 index 0000000000..05f0894159 --- /dev/null +++ b/video/out/cocoa-cb/video_layer.swift @@ -0,0 +1,210 @@ +/* + * 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 . + */ + +import Cocoa +import OpenGL.GL +import OpenGL.GL3 + +class VideoLayer: CAOpenGLLayer { + + weak var cocoaCB: CocoaCB! + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + let videoLock = NSLock() + var hasVideo: Bool = false + var neededFlips: Int = 0 + var cglContext: CGLContextObj? = nil + + var canDrawOffScreen: Bool = false + var lastThread: Thread? = nil + + var needsICCUpdate: Bool = false { + didSet { + if needsICCUpdate == true { + neededFlips += 1 + } + } + } + + let surfaceLock = NSLock() + var surfaceSize: NSSize? + + var inLiveResize: Bool = false { + didSet { + if inLiveResize == false { + isAsynchronous = false + display() + } else { + surfaceLock.lock() + updateSurfaceSize() + surfaceLock.unlock() + isAsynchronous = true + } + } + } + + init(cocoaCB ccb: CocoaCB) { + cocoaCB = ccb + super.init() + autoresizingMask = [.layerWidthSizable, .layerHeightSizable] + backgroundColor = NSColor.black.cgColor + contentsScale = cocoaCB.window.backingScaleFactor + } + + override init(layer: Any) { + let oldLayer = layer as! VideoLayer + cocoaCB = oldLayer.cocoaCB + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setUpGLCB() { + self.mpv.initGLCB() + self.mpv.setGLCBUpdateCallback(self.updateCallback, context: self) + self.mpv.setGLCBControlCallback(self.cocoaCB.controlCallback, context: self.cocoaCB) + } + + override func canDraw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer?) -> Bool { + return mpv != nil && cocoaCB.backendState == .init + } + + override func draw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer?) { + neededFlips = 0 + canDrawOffScreen = Thread.current == lastThread + lastThread = Thread.current + draw(ctx) + } + + func draw(_ ctx: CGLContextObj) { + surfaceLock.lock() + if inLiveResize == false { + updateSurfaceSize() + } + + mpv.drawGLCB(surfaceSize!) + surfaceLock.unlock() + CGLFlushDrawable(ctx) + + if needsICCUpdate { + needsICCUpdate = false + cocoaCB.updateICCProfile() + } + } + + func updateSurfaceSize() { + surfaceSize = bounds.size + surfaceSize!.width *= contentsScale + surfaceSize!.height *= contentsScale + } + + override func copyCGLPixelFormat(forDisplayMask mask: UInt32) -> CGLPixelFormatObj { + let glVersions: [CGLOpenGLProfile] = [ + kCGLOGLPVersion_3_2_Core, + kCGLOGLPVersion_Legacy + ] + + var pix: CGLPixelFormatObj? + var err: CGLError = CGLError(rawValue: 0) + var npix: GLint = 0 + + verLoop : for ver in glVersions { + var glAttributes: [CGLPixelFormatAttribute] = [ + kCGLPFAOpenGLProfile, CGLPixelFormatAttribute(ver.rawValue), + kCGLPFAAccelerated, + kCGLPFADoubleBuffer, + kCGLPFABackingStore, + kCGLPFAAllowOfflineRenderers, + kCGLPFASupportsAutomaticGraphicsSwitching, + _CGLPixelFormatAttribute(rawValue: 0) + ] + + for index in stride(from: glAttributes.count-2, through: 4, by: -1) { + err = CGLChoosePixelFormat(glAttributes, &pix, &npix) + if err == kCGLBadAttribute { + glAttributes.remove(at: index) + } else { + break verLoop + } + } + } + + if err != kCGLNoError { + fatalError("Couldn't create CGL pixel format: \(CGLErrorString(err)) (\(err))") + } + return pix! + } + + override func copyCGLContext(forPixelFormat pf: CGLPixelFormatObj) -> CGLContextObj { + let ctx = super.copyCGLContext(forPixelFormat: pf) + var i: GLint = 1 + CGLSetParameter(ctx, kCGLCPSwapInterval, &i) + CGLSetCurrentContext(ctx) + cglContext = ctx + + if let app = NSApp as? Application { + app.initMPVCore() + } + return ctx + } + + let updateCallback: mpv_opengl_cb_update_fn = { (ctx) in + let layer: VideoLayer = MPVHelper.bridge(ptr: ctx!) + layer.neededFlips += 1 + } + + override func display() { + super.display() + if !isAsynchronous { + CATransaction.flush() + } + } + + func setVideo(_ state: Bool) { + videoLock.lock() + hasVideo = state + neededFlips = 0 + videoLock.unlock() + } + + func reportFlip() { + mpv.reportGLCBFlip() + videoLock.lock() + if !isAsynchronous && neededFlips > 0 && hasVideo { + if !cocoaCB.window.occlusionState.contains(.visible) && + neededFlips > 1 && canDrawOffScreen + { + draw(cglContext!) + display() + } else { + display() + } + } + videoLock.unlock() + } + +} diff --git a/video/out/cocoa-cb/window.swift b/video/out/cocoa-cb/window.swift new file mode 100644 index 0000000000..c93537c03a --- /dev/null +++ b/video/out/cocoa-cb/window.swift @@ -0,0 +1,456 @@ +/* + * 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 . + */ + +import Cocoa + +class Window: NSWindow, NSWindowDelegate { + + weak var cocoaCB: CocoaCB! = nil + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + var targetScreen: NSScreen? + var previousScreen: NSScreen? + var currentScreen: NSScreen? + var unfScreen: NSScreen? + + var unfsContentFrame: NSRect? + var isInFullscreen: Bool = false + var isAnimating: Bool = false + var isMoving: Bool = false + var forceTargetScreen: Bool = false + + var keepAspect: Bool = true { + didSet { + if !isInFullscreen { + unfsContentFrame = convertToScreen(contentView!.frame) + } + + if keepAspect { + contentAspectRatio = unfsContentFrame!.size + } else { + resizeIncrements = NSSize(width: 1.0, height: 1.0) + } + } + } + + override var canBecomeKey: Bool { return true } + override var canBecomeMain: Bool { return true } + + override var styleMask: NSWindowStyleMask { + get { return super.styleMask } + set { + let responder = firstResponder + let windowTitle = title + super.styleMask = newValue + makeFirstResponder(responder) + title = windowTitle + } + } + + convenience init(cocoaCB ccb: CocoaCB) { + self.init(contentRect: NSMakeRect(0, 0, 960, 480), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, defer: false, screen: NSScreen.main()) + cocoaCB = ccb + title = "mpv" + } + + convenience init(contentRect: NSRect, styleMask style: NSWindowStyleMask, + screen: NSScreen?, cocoaCB ccb: CocoaCB) + { + self.init(contentRect: contentRect, styleMask: style, + backing: .buffered, defer: false, screen: screen) + cocoaCB = ccb + minSize = NSMakeSize(160, 90) + collectionBehavior = .fullScreenPrimary + delegate = self + + unfsContentFrame = convertToScreen(contentView!.frame) + targetScreen = screen! + currentScreen = screen! + unfScreen = screen! + + if let app = NSApp as? Application { + app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE) + app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE) + app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE) + app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE) + app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM) + } + } + + override func toggleFullScreen(_ sender: Any?) { + if isAnimating { + return + } + + isAnimating = true + + targetScreen = cocoaCB.getTargetScreen(forFullscreen: !isInFullscreen) + if targetScreen == nil && previousScreen == nil { + targetScreen = screen + } else if targetScreen == nil { + targetScreen = previousScreen + previousScreen = nil + } else { + previousScreen = screen + } + + if !isInFullscreen { + unfsContentFrame = convertToScreen(contentView!.frame) + unfScreen = screen + } + // move window to target screen when going to fullscreen + if !isInFullscreen && (targetScreen != screen) { + let frame = calculateWindowPosition(for: targetScreen!, withoutBounds: false) + setFrame(frame, display: true) + } + + if mpv.getBoolProperty("native-fs") { + super.toggleFullScreen(sender) + } else { + if !isInFullscreen { + setToFullScreen() + } + else { + setToWindow() + } + } + } + + func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? { + return [window] + } + + func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? { + return [window] + } + + func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) { + var newFrame = targetScreen!.frame + let cRect = contentRect(forFrameRect: frame) + newFrame.size.height = newFrame.size.height - (frame.size.height - cRect.size.height) + let intermediateFrame = aspectFit(rect: cRect, in: newFrame) + + NSAnimationContext.runAnimationGroup({ (context) -> Void in + context.duration = duration-0.1 + window.animator().setFrame(intermediateFrame, display: true) + }, completionHandler: { }) + } + + func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + let intermediateFrame = aspectFit(rect: newFrame, in: screen!.frame) + setFrame(intermediateFrame, display: true) + cocoaCB.layer.display() + + NSAnimationContext.runAnimationGroup({ (context) -> Void in + context.duration = duration-0.1 + window.animator().setFrame(newFrame, display: true) + }, completionHandler: { }) + } + + func windowDidEnterFullScreen(_ notification: Notification) { + isInFullscreen = true + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.updateCusorVisibility() + endAnimation() + } + + func windowDidExitFullScreen(_ notification: Notification) { + isInFullscreen = false + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + endAnimation() + } + + func windowDidFailToEnterFullScreen(_ window: NSWindow) { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + setFrame(newFrame, display: true) + endAnimation() + } + + func windowDidFailToExitFullScreen(_ window: NSWindow) { + let newFrame = targetScreen!.frame + setFrame(newFrame, display: true) + endAnimation() + } + + func endAnimation() { + isAnimating = false + cocoaCB.isShuttingDown = false + } + + func setToFullScreen() { + styleMask.insert(.fullScreen) + NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock] + setFrame(targetScreen!.frame, display: true) + endAnimation() + isInFullscreen = true + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.layer.neededFlips += 1 + } + + func setToWindow() { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + NSApp.presentationOptions = [] + setFrame(newFrame, display: true) + styleMask.remove(.fullScreen) + endAnimation() + isInFullscreen = false + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.layer.neededFlips += 1 + } + + func setBorder(_ state: Bool) { + if styleMask.contains(.titled) != state { + if state { + styleMask.remove(.borderless) + styleMask.insert(.titled) + } else { + styleMask.remove(.titled) + styleMask.insert(.borderless) + } + } + } + + func setOnTop(_ state: Bool) { + if state { + let ontopLevel = mpv.getStringProperty("ontop-level") ?? "window" + switch ontopLevel { + case "window": + level = Int(CGWindowLevelForKey(.floatingWindow)) + case "system": + level = Int(CGWindowLevelForKey(.statusWindow))+1 + default: + level = Int(ontopLevel)! + } + collectionBehavior.remove(.transient) + collectionBehavior.insert(.managed) + } else { + level = Int(CGWindowLevelForKey(.normalWindow)) + } + } + + func updateMovableBackground(_ pos: NSPoint) { + if !isInFullscreen { + isMovableByWindowBackground = mpv.canBeDraggedAt(pos) + } else { + isMovableByWindowBackground = false + } + } + + func updateFrame(_ rect: NSRect) { + if rect != frame { + let cRect = frameRect(forContentRect: rect) + setFrame(cRect, display: true) + unfsContentFrame = rect + } + } + + func updateSize(_ size: NSSize) { + if size != contentView!.frame.size { + let newContentFrame = centeredContentSize(for: frame, size: size) + if !isInFullscreen { + updateFrame(newContentFrame) + } else { + unfsContentFrame = newContentFrame + } + } + } + + override func setFrame(_ frameRect: NSRect, display flag: Bool) { + super.setFrame(frameRect, display: flag) + if keepAspect { + contentAspectRatio = unfsContentFrame!.size + } + } + + func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect { + let cRect = contentRect(forFrameRect: rect) + let dx = (cRect.size.width - sz.width) / 2 + let dy = (cRect.size.height - sz.height) / 2 + return NSInsetRect(cRect, dx, dy) + } + + func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect { + var s = rTarget.width / r.width; + if r.height*s > rTarget.height { + s = rTarget.height / r.height + } + let w = r.width * s + let h = r.height * s + return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h) + } + + func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect { + var newFrame = frameRect(forContentRect: unfsContentFrame!) + let targetFrame = tScreen.frame + let targetVisibleFrame = tScreen.visibleFrame + let unfsScreenFrame = unfScreen!.frame + let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame) + + // calculate visible area of every side + let left = newFrame.origin.x - unfsScreenFrame.origin.x + let right = unfsScreenFrame.size.width - + (newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width) + let bottom = newFrame.origin.y - unfsScreenFrame.origin.y + let top = unfsScreenFrame.size.height - + (newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height) + + // normalize visible areas, decide which one to take horizontal/vertical + var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width) + var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height) + if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer } + if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer } + + // calculate visible area for every side for target screen + let xNewLeft = targetFrame.origin.x + + (targetFrame.size.width - visibleWindow.size.width) * xPer + let xNewRight = targetFrame.origin.x + targetFrame.size.width - + (targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width + let yNewBottom = targetFrame.origin.y + + (targetFrame.size.height - visibleWindow.size.height) * yPer + let yNewTop = targetFrame.origin.y + targetFrame.size.height - + (targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height + + // calculate new coordinates, decide which one to take horizontal/vertical + newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight + newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop + + // don't place new window on top of a visible menubar + let topMar = targetFrame.size.height - + (newFrame.origin.y - targetFrame.origin.y + newFrame.size.height) + let menuBarHeight = targetFrame.size.height - + (targetVisibleFrame.size.height + targetVisibleFrame.origin.y) + if topMar < menuBarHeight { + newFrame.origin.y -= top - menuBarHeight + } + + if withoutBounds { + return newFrame + } + + // screen bounds right and left + if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width { + newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width + } + if newFrame.origin.x < targetFrame.origin.x { + newFrame.origin.x = targetFrame.origin.x + } + + // screen bounds top and bottom + if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height { + newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height + } + if newFrame.origin.y < targetFrame.origin.y { + newFrame.origin.y = targetFrame.origin.y + } + return newFrame + } + + override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect { + if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen) { + return frameRect + } + + var nf: NSRect = frameRect + let ts: NSScreen = tScreen ?? screen ?? NSScreen.main()! + let of: NSRect = frame + let vf: NSRect = (isAnimating ? targetScreen! : ts).visibleFrame + let ncf: NSRect = contentRect(forFrameRect: nf) + + // screen bounds top and bottom + if NSMaxY(nf) > NSMaxY(vf) { + nf.origin.y = NSMaxY(vf) - NSHeight(nf) + } + if NSMaxY(ncf) < NSMinY(vf) { + nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf) + } + + // screen bounds right and left + if NSMinX(nf) > NSMaxX(vf) { + nf.origin.x = NSMaxX(vf) - NSWidth(nf) + } + if NSMaxX(nf) < NSMinX(vf) { + nf.origin.x = NSMinX(vf) + } + + if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen { + // If the window height is smaller than the visible frame, but it was + // bigger previously recenter the smaller window vertically. This is + // needed to counter the 'snap to top' behaviour. + nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2 + } + return nf + } + + func setNormalWindowSize() { setWindowScale(1.0) } + func setHalfWindowSize() { setWindowScale(0.5) } + func setDoubleWindowSize() { setWindowScale(2.0) } + + func setWindowScale(_ scale: Double) { + mpv.commandAsync(["osd-auto", "set", "window-scale", "\(scale)"]) + } + + func windowDidChangeScreen(_ notification: Notification) { + if screen == nil { + return + } + if !isAnimating && (currentScreen != screen) { + previousScreen = screen + } + if currentScreen != screen { + cocoaCB.updateDisplaylink() + } + currentScreen = screen + } + + func windowDidChangeScreenProfile(_ notification: Notification) { + cocoaCB.layer.needsICCUpdate = true + } + + func windowDidChangeBackingProperties(_ notification: Notification) { + cocoaCB.layer.contentsScale = backingScaleFactor + } + + func windowWillStartLiveResize(_ notification: Notification) { + cocoaCB.layer.inLiveResize = true + } + + func windowDidEndLiveResize(_ notification: Notification) { + cocoaCB.layer.inLiveResize = false + } + + func windowShouldClose(_ sender: Any) -> Bool { + cocoa_put_key(SWIFT_KEY_CLOSE_WIN) + return false + } + + func windowDidResignKey(_ notification: Notification) { + cocoaCB.setCursorVisiblility(true) + } + + func windowDidBecomeKey(_ notification: Notification) { + cocoaCB.updateCusorVisibility() + } + + func windowWillMove(_ notification: Notification) { + isMoving = true + } +} diff --git a/video/out/cocoa_cb_common.swift b/video/out/cocoa_cb_common.swift new file mode 100644 index 0000000000..9a6da82ecf --- /dev/null +++ b/video/out/cocoa_cb_common.swift @@ -0,0 +1,485 @@ +/* + * 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 . + */ + +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 + } + + func setMpvHandle(_ ctx: OpaquePointer) { + mpv = MPVHelper(ctx) + layer.setUpGLCB() + } + + func preinit() { + if backendState == .uninit { + backendState = .needsInit + DispatchQueue.main.async { + self.updateICCProfile() + } + startDisplayLink() + } else { + layer.setVideo(true) + } + } + + func uninit() { + layer.setVideo(false) + window.orderOut(nil) + } + + func reconfig() { + if backendState == .needsInit { + initBackend() + } else { + updateWindowSize() + layer.neededFlips += 1 + } + } + + 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.setBorder(mpv.getBoolProperty("border")) + win.keepAspect = mpv.getBoolProperty("keepaspect-window") + window.close() + window = win + window.contentView!.addSubview(view) + view.frame = window.contentView!.frame + + setAppIcon() + window.isRestorable = false + window.makeMain() + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + layer.setVideo(true) + + if self.mpv.getBoolProperty("fullscreen") { + window.toggleFullScreen(nil) + } else { + window.isMovableByWindowBackground = true + } + + initLightSensor() + addDisplayReconfigureObserver() + backendState = .init + } + + func updateWindowSize() { + if layer.hasVideo { + let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main() + let wr = getWindowGeometry(forScreen: targetScreen!, videoOut: mpv.mpctx!.pointee.video_out) + if !window.isVisible { + window.makeKeyAndOrderFront(nil) + } + window.updateSize(wr.size) +