diff options
-rw-r--r-- | DOCS/interface-changes.rst | 2 | ||||
-rw-r--r-- | DOCS/man/options.rst | 2 | ||||
-rw-r--r-- | DOCS/man/vo.rst | 4 | ||||
-rw-r--r-- | osdep/macOS_mpv_helper.swift | 255 | ||||
-rw-r--r-- | osdep/macOS_swift_bridge.h | 54 | ||||
-rw-r--r-- | osdep/macosx_application.m | 80 | ||||
-rw-r--r-- | osdep/macosx_application_objc.h | 6 | ||||
-rw-r--r-- | osdep/macosx_events.m | 21 | ||||
-rw-r--r-- | osdep/macosx_menubar.m | 18 | ||||
-rw-r--r-- | player/client.c | 24 | ||||
-rw-r--r-- | player/client.h | 10 | ||||
-rw-r--r-- | video/out/cocoa-cb/events_view.swift | 266 | ||||
-rw-r--r-- | video/out/cocoa-cb/video_layer.swift | 210 | ||||
-rw-r--r-- | video/out/cocoa-cb/window.swift | 456 | ||||
-rw-r--r-- | video/out/cocoa_cb_common.swift | 485 | ||||
-rw-r--r-- | video/out/opengl/context_cocoa.c | 2 | ||||
-rw-r--r-- | video/out/vo.h | 5 | ||||
-rw-r--r-- | video/out/vo_opengl_cb.c | 88 | ||||
-rw-r--r-- | waftools/checks/custom.py | 14 | ||||
-rw-r--r-- | waftools/detections/compiler_swift.py | 71 | ||||
-rw-r--r-- | waftools/generators/sources.py | 11 | ||||
-rw-r--r-- | wscript | 15 | ||||
-rw-r--r-- | wscript_build.py | 37 |
23 files changed, 2054 insertions, 82 deletions
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 ``<mpv/opengl_cb.h>``.) 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 <http://www.gnu.org/licenses/>. + */ + +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<MPContext>? + + init(_ mpv: OpaquePointer) { + super.init() + mpvHandle = mpv + mpvLog = mp_log_new(UnsafeMutablePointer<MPContext>(mpvHandle), + mp_client_get_log(mpvHandle), "cocoacb") + mpctx = UnsafeMutablePointer<MPContext>(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<Int8>?) -> 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<UInt8>) 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<Int8>(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<T: AnyObject>(obj: T) -> UnsafeMutableRawPointer { + return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque()) + } + + // (__bridge T*) + class func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T { + return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue() + } + + // *(char **) MPV_FORMAT_STRING on mpv_event_property + class func mpvStringArrayToString(_ obj: UnsafeMutableRawPointer) -> String? { + let cstr = UnsafeMutablePointer<UnsafeMutablePointer<Int8>>(OpaquePointer(obj)) + return String(cString: cstr[0]) + } + + // MPV_FORMAT_FLAG + class func mpvFlagToBool(_ obj: UnsafeMutableRawPointer) -> Bool? { + return UnsafePointer<Bool>(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 <http://www.gnu.org/licenses/>. + */ + +// 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 <IOKit/pwr_mgt/IOPMLib.h> + +#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 <http://www.gnu.org/licenses/>. + */ + +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 <http://www.gnu.org/licenses/>. + */ + +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<CVTimeStamp>?) -> Bool { + return mpv != nil && cocoaCB.backendState == .init + } + + override func draw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer<CVTimeStamp>?) { + 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 <http://www.gnu.org/licenses/>. + */ + +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 <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 + } + + 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) + } + } + + func setAppIcon() { + if let app = NSApp as? Application { + NSApp.applicationIconImage = app.getMPVIcon() + } + } + + func startDisplayLink() { + let displayId = UInt32(window.screen!.deviceDescription["NSScreenNumber"] as! Int) + CVDisplayLinkCreateWithActiveCGDisplays(&link) + CVDisplayLinkSetCurrentCGDisplay(link!, displayId) + CVDisplayLinkSetOutputHandler(link!) { link, now, out, inFlags, outFlags -> CVReturn in + self.layer.reportFlip() + return kCVReturnSuccess + } + 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() { + if mpv.getBoolProperty("icc-profile-auto") { + mpv.setGLCBICCProfile(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.setGLCBLux(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: mpv_opengl_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 processEvent(_ event: UnsafePointer<mpv_event>) { + switch event.pointee.event_id { + case MPV_EVENT_SHUTDOWN: + if window.isAnimating { + isShuttingDown = true + return + } + setCursorVisiblility(true) + stopDisplaylink() + uninitLightSensor() + removeDisplayReconfigureObserver() + mpv.deinitGLCB() + mpv.deinitMPV() + 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.setBorder(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 + } + default: + break + } + } +} diff --git a/video/out/opengl/context_cocoa.c b/video/out/opengl/context_cocoa.c index da20f400b2..367c6cd291 100644 --- a/video/out/opengl/context_cocoa.c +++ b/video/out/opengl/context_cocoa.c @@ -171,6 +171,8 @@ static bool cocoa_init(struct ra_ctx *ctx) p->opts = mp_get_config_group(ctx, ctx->global, &cocoa_conf); vo_cocoa_init(ctx->vo); + MP_WARN(ctx->vo, "opengl cocoa backend is deprecated, use opengl-cb instead\n"); + if (!create_gl_context(ctx)) goto fail; diff --git a/video/out/vo.h b/video/out/vo.h index 4ce2cf1371..533eca6a2e 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -73,6 +73,11 @@ enum mp_voctrl { // be updated and redrawn. Optional; emulated if not available. VOCTRL_REDRAW_FRAME, + // Only used internally in vo_opengl_cb + VOCTRL_PREINIT, + VOCTRL_UNINIT, + VOCTRL_RECONFIG, + VOCTRL_FULLSCREEN, VOCTRL_ONTOP, VOCTRL_BORDER, diff --git a/video/out/vo_opengl_cb.c b/video/out/vo_opengl_cb.c index f839f0a197..4be36118a0 100644 --- a/video/out/vo_opengl_cb.c +++ b/video/out/vo_opengl_cb.c @@ -57,6 +57,10 @@ struct mpv_opengl_cb_context { struct mpv_global *global; struct mp_client_api *client_api; + pthread_mutex_t control_lock; + mpv_opengl_cb_control_fn control_cb; + void *control_cb_ctx; + pthread_mutex_t lock; pthread_cond_t wakeup; @@ -78,6 +82,7 @@ struct mpv_opengl_cb_context { bool imgfmt_supported[IMGFMT_END - IMGFMT_START]; bool update_new_opts; struct vo *active; + bool icc_was_set; // --- This is only mutable while initialized=false, during which nothing // except the OpenGL context manager is allowed to access it. @@ -93,7 +98,7 @@ struct mpv_opengl_cb_context { struct mp_vo_opts *vo_opts; }; -static void update(struct vo_priv *p); +static void update(struct mpv_opengl_cb_context *ctx); static void forget_frames(struct mpv_opengl_cb_context *ctx, bool all) { @@ -114,6 +119,7 @@ static void free_ctx(void *ptr) pthread_cond_destroy(&ctx->wakeup); pthread_mutex_destroy(&ctx->lock); + pthread_mutex_destroy(&ctx->control_lock); } struct mpv_opengl_cb_context *mp_opengl_create(struct mpv_global *g, @@ -121,6 +127,7 @@ struct mpv_opengl_cb_context *mp_opengl_create(struct mpv_global *g, { mpv_opengl_cb_context *ctx = talloc_zero(NULL, mpv_opengl_cb_context); talloc_set_destructor(ctx, free_ctx); + pthread_mutex_init(&ctx->control_lock, NULL); pthread_mutex_init(&ctx->lock, NULL); pthread_cond_init(&ctx->wakeup, NULL); @@ -144,6 +151,17 @@ void mpv_opengl_cb_set_update_callback(struct mpv_opengl_cb_context *ctx, pthread_mutex_unlock(&ctx->lock); } + +void mp_client_set_control_callback(struct mpv_opengl_cb_context *ctx, + mpv_opengl_cb_control_fn callback, + void *callback_ctx) +{ + pthread_mutex_lock(&ctx->control_lock); + ctx->control_cb = callback; + ctx->control_cb_ctx = callback_ctx; + pthread_mutex_unlock(&ctx->control_lock); +} + // Reset some GL attributes the user might clobber. For mid-term compatibility // only - we expect both user code and our code to do this correctly. static void reset_gl_state(GL *gl) @@ -223,9 +241,11 @@ int mpv_opengl_cb_uninit_gl(struct mpv_opengl_cb_context *ctx) pthread_mutex_lock(&ctx->lock); forget_frames(ctx, true); ctx->initialized = false; + bool was_active = ctx->active ? true : false; pthread_mutex_unlock(&ctx->lock); - kill_video(ctx->client_api); + if (was_active) + kill_video(ctx->client_api); pthread_mutex_lock(&ctx->lock); assert(!ctx->active); @@ -291,7 +311,7 @@ int mpv_opengl_cb_draw(mpv_opengl_cb_context *ctx, int fbo, int vp_w, int vp_h) &debug); ctx->gl->debug_context = debug; ra_gl_set_debug(ctx->ra_ctx->ra, debug); - if (gl_video_icc_auto_enabled(ctx->renderer)) + if (!ctx->icc_was_set && gl_video_icc_auto_enabled(ctx->renderer)) MP_ERR(ctx, "icc-profile-auto is not available with opengl-cb\n"); } ctx->reconfigured = false; @@ -360,10 +380,10 @@ int mpv_opengl_cb_report_flip(mpv_opengl_cb_context *ctx, int64_t time) } // Called locked. -static void update(struct vo_priv *p) +static void update(struct mpv_opengl_cb_context *ctx) { - if (p->ctx->update_cb) - p->ctx->update_cb(p->ctx->update_cb_ctx); + if (ctx->update_cb) + ctx->update_cb(ctx->update_cb_ctx); } static void draw_frame(struct vo *vo, struct vo_frame *frame) @@ -375,7 +395,7 @@ static void draw_frame(struct vo *vo, struct vo_frame *frame) p->ctx->next_frame = vo_frame_ref(frame); p->ctx->expected_flip_count = p->ctx->flip_count + 1; p->ctx->redrawing = frame->redraw || !frame->current; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); } @@ -442,17 +462,20 @@ static int query_format(struct vo *vo, int format) return ok; } -static int reconfig(struct vo *vo, struct mp_image_params *params) +// Can only be called from the GL thread +void mp_client_set_icc_profile(struct mpv_opengl_cb_context *ctx, bstr icc_data) { - struct vo_priv *p = vo->priv; - - pthread_mutex_lock(&p->ctx->lock); - forget_frames(p->ctx, true); - p->ctx->img_params = *params; - p->ctx->reconfigured = true; - pthread_mutex_unlock(&p->ctx->lock); + gl_video_set_icc_profile(ctx->renderer, icc_data); + pthread_mutex_lock(&ctx->lock); + ctx->icc_was_set = true; + if (ctx->active) + update(ctx); + pthread_mutex_unlock(&ctx->lock); +} - return 0; +void mp_client_set_ambient_lux(struct mpv_opengl_cb_context *ctx, int lux) +{ + gl_video_set_ambient_lux(ctx->renderer, lux); } static int control(struct vo *vo, uint32_t request, void *data) @@ -475,30 +498,54 @@ static int control(struct vo *vo, uint32_t request, void *data) case VOCTRL_SET_PANSCAN: pthread_mutex_lock(&p->ctx->lock); p->ctx->force_update = true; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); return VO_TRUE; case VOCTRL_UPDATE_RENDER_OPTS: pthread_mutex_lock(&p->ctx->lock); p->ctx->update_new_opts = true; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); return VO_TRUE; } - return VO_NOTIMPL; + int r = VO_NOTIMPL; + pthread_mutex_lock(&p->ctx->control_lock); + if (p->ctx->control_cb) { + int events = 0; + r = p->ctx->control_cb(p->ctx->control_cb_ctx, &events, request, data); + vo_event(vo, events); + } + pthread_mutex_unlock(&p->ctx->control_lock); + + return r; +} + +static int reconfig(struct vo *vo, struct mp_image_params *params) +{ + struct vo_priv *p = vo->priv; + + pthread_mutex_lock(&p->ctx->lock); + forget_frames(p->ctx, true); + p->ctx->img_params = *params; + p->ctx->reconfigured = true; + pthread_mutex_unlock(&p->ctx->lock); + control(vo, VOCTRL_RECONFIG, NULL); + + return 0; } static void uninit(struct vo *vo) { struct vo_priv *p = vo->priv; + control(vo, VOCTRL_UNINIT, NULL); pthread_mutex_lock(&p->ctx->lock); forget_frames(p->ctx, true); p->ctx->img_params = (struct mp_image_params){0}; p->ctx->reconfigured = true; p->ctx->active = NULL; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); } @@ -523,6 +570,7 @@ static int preinit(struct vo *vo) pthread_mutex_unlock(&p->ctx->lock); vo->hwdec_devs = p->ctx->hwdec_devs; + control(vo, VOCTRL_PREINIT, NULL); return 0; } diff --git a/waftools/checks/custom.py b/waftools/checks/custom.py index 3af9bdb921..14e59f3f49 100644 --- a/waftools/checks/custom.py +++ b/waftools/checks/custom.py @@ -92,18 +92,6 @@ def check_wl_protocols(ctx, dependency_identifier): return ret return fn(ctx, dependency_identifier) -def _run(cmd): - from waflib import Utils - try: - cmd = Utils.subprocess.Popen(cmd, - stdout=Utils.subprocess.PIPE, - stderr=Utils.subprocess.PIPE, - shell=True) - output = cmd.stdout.read().strip() - return output - except Exception: - return "" - def check_cocoa(ctx, dependency_identifier): fn = check_cc( fragment = load_fragment('cocoa.m'), @@ -117,7 +105,7 @@ def check_cocoa(ctx, dependency_identifier): # linking warnings or errors if res: ctx.env.append_value('LINKFLAGS', [ - '-isysroot', '%s' % _run('xcrun --sdk macosx --show-sdk-path') + '-isysroot', ctx.env.MACOS_SDK ]) return res diff --git a/waftools/detections/compiler_swift.py b/waftools/detections/compiler_swift.py new file mode 100644 index 0000000000..36f4f0b432 --- /dev/null +++ b/waftools/detections/compiler_swift.py @@ -0,0 +1,71 @@ +from waflib import Utils + +def __run(cmd): + try: + cmd = Utils.subprocess.Popen(cmd, + stdout=Utils.subprocess.PIPE, + stderr=Utils.subprocess.PIPE, + shell=True) + output = cmd.stdout.read().strip() + return output + except Exception: + return "" + +def __add_swift_flags(ctx): + ctx.env.SWIFT_FLAGS = ('-frontend -c -sdk %s -enable-objc-interop -emit-objc-header' + ' -emit-module -parse-as-library') % (ctx.env.MACOS_SDK) + swift_version = __run(ctx.env.SWIFT + ' -version').split(' ')[3].split('.') + major, minor, sub = [int(n) for n in swift_version] + + # the -swift-version parameter is only supported on swift 3.1 and newer + if major >= 3 and minor >= 1 or major >= 4: + ctx.env.SWIFT_FLAGS += ' -swift-version 3' + + if ctx.is_optimization(): + ctx.env.SWIFT_FLAGS += ' -O' + +def __add_swift_library_linking_flags(ctx, swift_library): + ctx.env.append_value('LINKFLAGS', [ + '-L%s' % swift_library, + '-Xlinker', '-force_load_swift_libs', '-lc++', + ]) + +def __find_swift_library(ctx): + swift_library_paths = [ + 'Toolchains/XcodeDefault.xctoolchain/usr/lib/swift_static/macosx', + 'usr/lib/swift_static/macosx' + ] + dev_path = __run('xcode-select -p')[1:] + + ctx.start_msg('Checking for Swift Library') + for path in swift_library_paths: + swift_library = ctx.root.find_dir([dev_path, path]) + if swift_library is not None: + ctx.end_msg(swift_library) + __add_swift_library_linking_flags(ctx, swift_library) + return + ctx.end_msg(None, "RED") + +def __find_macos_sdk(ctx): + ctx.start_msg('Checking for macOS SDK') + sdk = __run('xcrun --sdk macosx --show-sdk-path') + if sdk != "": + ctx.end_msg(sdk) + ctx.env.MACOS_SDK = sdk + else: + ctx.end_msg(None, "RED") + +def __find_swift_compiler(ctx): + ctx.start_msg('Checking for swift (Swift compiler)') + swift = __run('xcrun -find swift') + if swift != "": + ctx.end_msg(swift) + ctx.env.SWIFT = swift + __add_swift_flags(ctx) + __find_swift_library(ctx) + else: + ctx.end_msg(None, "RED") + +def configure(ctx): + __find_macos_sdk(ctx) + __find_swift_compiler(ctx) diff --git a/waftools/generators/sources.py b/waftools/generators/sources.py index b0b423edda..b7766cc1b6 100644 --- a/waftools/generators/sources.py +++ b/waftools/generators/sources.py @@ -1,5 +1,5 @@ from waflib.Build import BuildContext -from waflib import TaskGen +from waflib import TaskGen, Utils from io import StringIO from TOOLS.matroska import generate_C_header, generate_C_definitions from TOOLS.file2string import file2string @@ -71,6 +71,15 @@ def __wayland_protocol_header__(ctx, **kwargs): **kwargs ) +@TaskGen.feature('cprogram') +@TaskGen.feature('apply_link') +def handle_add_object(tgen): + if getattr(tgen, 'add_object', None): + for input in Utils.to_list(tgen.add_object): + input_node = tgen.path.find_resource(input) + if input_node is not None: + tgen.link_task.inputs.append(input_node) + BuildContext.file2string = __file2string__ BuildContext.wayland_protocol_code = __wayland_protocol_code__ BuildContext.wayland_protocol_header = __wayland_protocol_header__ @@ -914,7 +914,12 @@ standalone_features = [ framework_name=['AppKit'], compile_filename='test-touchbar.m', linkflags='-fobjc-arc') - } + }, { + 'name': '--macos-cocoa-cb', + 'desc': 'macOS opengl-cb backend', + 'deps': 'cocoa', + 'func': check_true + } ] _INSTALL_DIRS_LIST = [ @@ -965,6 +970,10 @@ def options(opt): type = 'string', dest = 'LUA_VER', help = "select Lua package which should be autodetected. Choices: 51 51deb 51obsd 51fbsd 52 52deb 52arch 52fbsd luajit") + group.add_option('--swift-flags', + type = 'string', + dest = 'SWIFT_FLAGS', + help = "Optional Swift compiler flags") @conf def is_optimization(ctx): @@ -1000,6 +1009,7 @@ def configure(ctx): ctx.load('compiler_c') ctx.load('waf_customizations') ctx.load('dependencies') + ctx.load('detections.compiler_swift') ctx.load('detections.compiler') ctx.load('detections.devices') @@ -1022,6 +1032,9 @@ def configure(ctx): if ctx.options.LUA_VER: ctx.options.enable_lua = True + if ctx.options.SWIFT_FLAGS: + ctx.env.SWIFT_FLAGS += ' ' + ctx.options.SWIFT_FLAGS + ctx.parse_dependencies(standalone_features) ctx.load('generators.headers') diff --git a/wscript_build.py b/wscript_build.py index b2d61cf0be..78db131beb 100644 --- a/wscript_build.py +++ b/wscript_build.py @@ -139,6 +139,42 @@ def build(ctx): ctx(features = "ebml_header", target = "ebml_types.h") ctx(features = "ebml_definitions", target = "ebml_defs.c") + def swift(task): + src = ' '.join([x.abspath() for x in task.inputs]) + bridge = ctx.path.find_node("osdep/macOS_swift_bridge.h") + tgt = task.outputs[0].abspath() + header = task.outputs[1].abspath() + module = task.outputs[2].abspath() + + cmd = ('%s %s -module-name macOS_swift -emit-module-path %s ' + '-import-objc-header %s -emit-objc-header-path %s -o %s %s ' + '-I. -I..') % (ctx.env.SWIFT, ctx.env.SWIFT_FLAGS, module, + bridge, header, tgt, src) + return task.exec_command(cmd) + + if ctx.dependency_satisfied('macos-cocoa-cb'): + swift_source = [ + ( "video/out/cocoa_cb_common.swift" ), + ( "video/out/cocoa-cb/window.swift" ), + ( "video/out/cocoa-cb/events_view.swift" ), + ( "video/out/cocoa-cb/video_layer.swift" ), + ( "osdep/macOS_mpv_helper.swift" ) + ] + + ctx( + rule = swift, + source = ctx.filtered_sources(swift_source), + target = ('osdep/macOS_swift.o ' + 'osdep/macOS_swift.h ' + 'osdep/macOS_swift.swiftmodule'), + before = 'c', + ) + + ctx.env.append_value('LINKFLAGS', [ + '-Xlinker', '-add_ast_path', + '-Xlinker', '%s' % ctx.path.find_resource("osdep/macOS_swift.swiftmodule") + ]) + if ctx.dependency_satisfied('cplayer'): main_fn_c = ctx.pick_first_matching_dep([ ( "osdep/main-fn-cocoa.c", "cocoa" ), @@ -552,6 +588,7 @@ def build(ctx): target = "mpv", source = main_fn_c, use = ctx.dependencies_use() + ['objects'], + add_object = "osdep/macOS_swift.o", includes = _all_includes(ctx), features = "c cprogram" + (" syms" if syms else ""), export_symbols_def = "libmpv/mpv.def", # for syms=True |