/* * 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 . */ #include #include "config.h" #include "mpv_talloc.h" #include "common/msg.h" #include "input/input.h" #include "player/client.h" #include "options/m_config.h" #include "options/options.h" #import "osdep/macosx_application_objc.h" #import "osdep/macosx_events_objc.h" #include "osdep/threads.h" #include "osdep/main-fn.h" #if HAVE_MACOS_TOUCHBAR #import "osdep/macosx_touchbar.h" #endif #if HAVE_SWIFT #include "osdep/macOS_swift.h" #endif #define MPV_PROTOCOL @"mpv://" #define OPT_BASE_STRUCT struct macos_opts const struct m_sub_options macos_conf = { .opts = (const struct m_option[]) { {"macos-title-bar-appearance", OPT_CHOICE(macos_title_bar_appearance, {"auto", 0}, {"aqua", 1}, {"darkAqua", 2}, {"vibrantLight", 3}, {"vibrantDark", 4}, {"aquaHighContrast", 5}, {"darkAquaHighContrast", 6}, {"vibrantLightHighContrast", 7}, {"vibrantDarkHighContrast", 8})}, {"macos-title-bar-material", OPT_CHOICE(macos_title_bar_material, {"titlebar", 0}, {"selection", 1}, {"menu", 2}, {"popover", 3}, {"sidebar", 4}, {"headerView", 5}, {"sheet", 6}, {"windowBackground", 7}, {"hudWindow", 8}, {"fullScreen", 9}, {"toolTip", 10}, {"contentBackground", 11}, {"underWindowBackground", 12}, {"underPageBackground", 13}, {"dark", 14}, {"light", 15}, {"mediumLight", 16}, {"ultraDark", 17})}, {"macos-title-bar-color", OPT_COLOR(macos_title_bar_color)}, {"macos-fs-animation-duration", OPT_CHOICE(macos_fs_animation_duration, {"default", -1}), M_RANGE(0, 1000)}, {"macos-force-dedicated-gpu", OPT_BOOL(macos_force_dedicated_gpu)}, {"macos-app-activation-policy", OPT_CHOICE(macos_app_activation_policy, {"regular", 0}, {"accessory", 1}, {"prohibited", 2})}, {"macos-geometry-calculation", OPT_CHOICE(macos_geometry_calculation, {"visible", FRAME_VISIBLE}, {"whole", FRAME_WHOLE})}, {"macos-render-timer", OPT_CHOICE(macos_render_timer, {"callback", RENDER_TIMER_CALLBACK}, {"precise", RENDER_TIMER_PRECISE}, {"system", RENDER_TIMER_SYSTEM})}, {"cocoa-cb-sw-renderer", OPT_CHOICE(cocoa_cb_sw_renderer, {"auto", -1}, {"no", 0}, {"yes", 1})}, {"cocoa-cb-10bit-context", OPT_BOOL(cocoa_cb_10bit_context)}, {0} }, .size = sizeof(struct macos_opts), .defaults = &(const struct macos_opts){ .macos_title_bar_color = {0, 0, 0, 0}, .macos_fs_animation_duration = -1, .cocoa_cb_sw_renderer = -1, .cocoa_cb_10bit_context = true }, }; // Whether the NSApplication singleton was created. If this is false, we are // running in libmpv mode, and cocoa_main() was never called. static bool application_instantiated; static mp_thread playback_thread_id; @interface Application () { EventsResponder *_eventsResponder; } @end static Application *mpv_shared_app(void) { return (Application *)[Application sharedApplication]; } static void terminate_cocoa_application(void) { dispatch_async(dispatch_get_main_queue(), ^{ [NSApp hide:NSApp]; [NSApp terminate:NSApp]; }); } @implementation Application @synthesize menuBar = _menu_bar; @synthesize openCount = _open_count; @synthesize cocoaCB = _cocoa_cb; - (void)sendEvent:(NSEvent *)event { if ([self modalWindow] || ![_eventsResponder processKeyEvent:event]) [super sendEvent:event]; [_eventsResponder wakeup]; } - (id)init { if (self = [super init]) { _eventsResponder = [EventsResponder sharedInstance]; NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; [em setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } return self; } - (void)dealloc { NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; [em removeEventHandlerForEventClass:kInternetEventClass andEventID:kAEGetURL]; [em removeEventHandlerForEventClass:kCoreEventClass andEventID:kAEQuitApplication]; [super dealloc]; } static const char macosx_icon[] = #include "TOOLS/osxbundle/icon.icns.inc" ; - (NSImage *)getMPVIcon { // The C string contains a trailing null, so we strip it away NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon length:sizeof(macosx_icon) - 1 freeWhenDone:NO]; return [[NSImage alloc] initWithData:icon_data]; } #if HAVE_MACOS_TOUCHBAR - (NSTouchBar *)makeTouchBar { TouchBar *tBar = [[TouchBar alloc] init]; [tBar setApp:self]; tBar.delegate = tBar; tBar.customizationIdentifier = customID; tBar.defaultItemIdentifiers = @[play, previousItem, nextItem, seekBar]; tBar.customizationAllowedItemIdentifiers = @[play, seekBar, previousItem, nextItem, previousChapter, nextChapter, cycleAudio, cycleSubtitle, currentPosition, timeLeft]; return tBar; } #endif - (void)processEvent:(struct mpv_event *)event { #if HAVE_MACOS_TOUCHBAR [(TouchBar *)self.touchBar processEvent:event]; #endif if (_cocoa_cb) { [_cocoa_cb processEvent:event]; } } - (void)initCocoaCb:(struct mpv_handle *)ctx { #if HAVE_MACOS_COCOA_CB if (!_cocoa_cb) { [NSApp setCocoaCB:[[CocoaCB alloc] init:ctx]]; } #endif } + (const struct m_sub_options *)getMacOSConf { return &macos_conf; } + (const struct m_sub_options *)getVoSubConf { return &vo_sub_opts; } - (void)queueCommand:(char *)cmd { [_eventsResponder queueCommand:cmd]; } - (void)stopMPV:(char *)cmd { if (![_eventsResponder queueCommand:cmd]) terminate_cocoa_application(); } - (void)applicationWillFinishLaunching:(NSNotification *)notification { NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; [em setEventHandler:self andSelector:@selector(handleQuitEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEQuitApplication]; } - (void)handleQuitEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { [self stopMPV:"quit"]; } - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; url = [url stringByReplacingOccurrencesOfString:MPV_PROTOCOL withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, [MPV_PROTOCOL length])]; url = [url stringByRemovingPercentEncoding]; [_eventsResponder handleFilesArray:@[url]]; } - (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames { if (mpv_shared_app().openCount > 0) { mpv_shared_app().openCount--; return; } [self openFiles:filenames]; } - (void)openFiles:(NSArray *)filenames { SEL cmpsel = @selector(localizedStandardCompare:); NSArray *files = [filenames sortedArrayUsingSelector:cmpsel]; [_eventsResponder handleFilesArray:files]; } @end struct playback_thread_ctx { int *argc; char ***argv; }; static void cocoa_run_runloop(void) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; [NSApp run]; [pool drain]; } static MP_THREAD_VOID playback_thread(void *ctx_obj) { mp_thread_set_name("core/playback"); @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) [[NSApp menuBar] registerSelector:(SEL)action forKey:key]; } static void init_cocoa_application(bool regular) { NSApp = mpv_shared_app(); [NSApp setDelegate:NSApp]; [NSApp setMenuBar:[[MenuBar alloc] init]]; // 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 ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory]; atexit_b(^{ // Because activation policy has just been set to behave like a real // application, that policy must be reset on exit to prevent, among // other things, the menubar created here from remaining on screen. dispatch_async(dispatch_get_main_queue(), ^{ [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; }); }); } static bool bundle_started_from_finder() { NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"]; return [bundle isEqual:@"true"]; } static bool is_psn_argument(char *arg_to_check) { NSString *arg = [NSString stringWithUTF8String:arg_to_check]; return [arg hasPrefix:@"-psn_"]; } static void setup_bundle(int *argc, char *argv[]) { if (*argc > 1 && is_psn_argument(argv[1])) { *argc = 1; argv[1] = NULL; } NSDictionary *env = [[NSProcessInfo processInfo] environment]; NSString *path_bundle = [env objectForKey:@"PATH"]; NSString *path_new = [NSString stringWithFormat:@"%@:%@:%@:%@:%@", path_bundle, @"/usr/local/bin", @"/usr/local/sbin", @"/opt/local/bin", @"/opt/local/sbin"]; setenv("PATH", [path_new UTF8String], 1); } int cocoa_main(int argc, char *argv[]) { @autoreleasepool { application_instantiated = true; [[EventsResponder sharedInstance] setIsApplication:YES]; struct playback_thread_ctx ctx = {0}; ctx.argc = &argc; ctx.argv = &argv; if (bundle_started_from_finder()) { setup_bundle(&argc, argv); init_cocoa_application(true); } else { for (int i = 1; i < argc; i++) if (argv[i][0] != '-') mpv_shared_app().openCount++; init_cocoa_application(false); } mp_thread_create(&playback_thread_id, playback_thread, &ctx); [[EventsResponder sharedInstance] waitForInputContext]; cocoa_run_runloop(); // This should never be reached: cocoa_run_runloop blocks until the // process is quit fprintf(stderr, "There was either a problem " "initializing Cocoa or the Runloop was stopped unexpectedly. " "Please report this issues to a developer.\n"); mp_thread_join(playback_thread_id); return 1; } }