diff --git a/NEWS b/NEWS index c585bbe900a4197047b9f5a30e18f07b71fe240d..6ad7c760fd33d6fff7fe53498e5b43320857b5d1 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,9 @@ Video output: * Remove evas plugin * Remove omxil_vout plugin +macOS: + * Remove Growl notification support + Changes between 2.2.8 and 3.0.0: -------------------------------- diff --git a/configure.ac b/configure.ac index 734a2e228150084d4bd755eb7e0901d33d10cd63..67a883ea9de4b74fd845b958803631a634079a2f 100644 --- a/configure.ac +++ b/configure.ac @@ -4103,15 +4103,11 @@ dnl dnl OS X notification plugin dnl AC_ARG_ENABLE(osx_notifications, - [ --enable-osx-notifications osx notification plugin (default disabled)],, + [AS_HELP_STRING([--enable-osx-notifications], + [macOS notification plugin (default disabled)])],, [enable_osx_notifications=no]) AS_IF([test "${enable_osx_notifications}" != "no"], [ - if test -d ${CONTRIB_DIR}/Growl.framework -o -d ${CONTRIB_DIR}/Frameworks/Growl.framework - then - VLC_ADD_PLUGIN([osx_notifications]) - VLC_ADD_LIBS([osx_notifications], [-Wl,-framework,Growl,-framework,Foundation]) - VLC_ADD_OBJCFLAGS([osx_notifications], [-fobjc-exceptions] ) - fi + VLC_ADD_PLUGIN([osx_notifications]) ]) dnl diff --git a/modules/MODULES_LIST b/modules/MODULES_LIST index 2c0cce560f659e36188737e79066a008099bcb55..0cc554651c93c2f70e759e1b28f8b2df9318cf9b 100644 --- a/modules/MODULES_LIST +++ b/modules/MODULES_LIST @@ -281,7 +281,7 @@ $Id$ * opus: a opus audio decoder/packetizer/encoder using the libopus library * os2drive: service discovery for OS/2 drives * oss: audio output module using the OSS /dev/dsp interface - * osx_notifications: announce currently playing stream to OS X/Growl + * osx_notifications: announce currently playing stream to OS X * packetizer_a52: A/52 basic parser/packetizer * packetizer_avparser: libavcodec packetizer * packetizer_copy: Simple copy packetizer diff --git a/modules/notify/Makefile.am b/modules/notify/Makefile.am index 60045aab80c2910d50f55b8af24ddbd6ca6dda87..e97bac63e3743348d889d6bc2464bd111bf76fb2 100644 --- a/modules/notify/Makefile.am +++ b/modules/notify/Makefile.am @@ -1,9 +1,8 @@ notifydir = $(pluginsdir)/notify libosx_notifications_plugin_la_SOURCES = notify/osx_notifications.m -libosx_notifications_plugin_la_OBJCFLAGS = $(AM_OBJCFLAGS) $(OBJCFLAGS_osx_notifications) -libosx_notifications_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(notifydir)' -Wl,-framework,AppKit -libosx_notifications_plugin_la_LIBADD = $(LIBS_osx_notifications) +libosx_notifications_plugin_la_OBJCFLAGS = $(AM_OBJCFLAGS) -fobjc-arc -fobjc-exceptions +libosx_notifications_plugin_la_LDFLAGS = $(AM_LDFLAGS) -rpath '$(notifydir)' -Wl,-framework,AppKit,-framework,Foundation libnotify_plugin_la_SOURCES = notify/notify.c libnotify_plugin_la_CFLAGS = $(AM_CFLAGS) $(NOTIFY_CFLAGS) diff --git a/modules/notify/osx_notifications.m b/modules/notify/osx_notifications.m index b987d38a5ca01671ece8df2f32ede2dd36a01c50..8ac215e851fd0ecf6af54dfe456d979aecabb671 100644 --- a/modules/notify/osx_notifications.m +++ b/modules/notify/osx_notifications.m @@ -1,9 +1,10 @@ /***************************************************************************** - * osx_notifications.m : OS X notification plugin - ***************************************************************************** - * VLC specific code: + * osx_notifications.m : macOS notification plugin * - * Copyright © 2008,2011,2012,2015 the VideoLAN team + * This plugin provides support for macOS notifications on current playlist + * item changes. + ***************************************************************************** + * Copyright © 2008, 2011, 2012, 2015, 2018 the VideoLAN team * $Id$ * * Authors: Rafaël Carré <funman@videolanorg> @@ -23,42 +24,9 @@ * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. - * - * --- - * - * Growl specific code, ripped from growlnotify: - * - * Copyright (c) The Growl Project, 2004-2005 - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Growl nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS - * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - *****************************************************************************/ + */ -/***************************************************************************** - * Preamble - *****************************************************************************/ +#define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS #ifdef HAVE_CONFIG_H # include "config.h" @@ -66,9 +34,7 @@ #import <Foundation/Foundation.h> #import <Cocoa/Cocoa.h> -#import <Growl/Growl.h> -#define VLC_MODULE_LICENSE VLC_LICENSE_GPL_2_PLUS #include <vlc_common.h> #include <vlc_plugin.h> #include <vlc_playlist.h> @@ -77,170 +43,158 @@ #include <vlc_interface.h> #include <vlc_url.h> -/***************************************************************************** - * intf_sys_t, VLCGrowlDelegate - *****************************************************************************/ -@interface VLCGrowlDelegate : NSObject <GrowlApplicationBridgeDelegate> +#pragma mark - +#pragma mark Class interfaces +@interface VLCNotificationDelegate : NSObject <NSUserNotificationCenterDelegate> { - NSString *applicationName; - NSString *notificationType; - NSMutableDictionary *registrationDictionary; - id lastNotification; - bool isInForeground; - intf_thread_t *interfaceThread; + /** Interface thread, required for skipping to the next item */ + intf_thread_t * _Nonnull interfaceThread; + + /** Holds the last notification so it can be cleared when the next one is delivered */ + NSUserNotification * _Nullable lastNotification; + + /** Indicates if VLC is in foreground */ + BOOL isInForeground; } -- (id)initWithInterfaceThread:(intf_thread_t *)thread; -- (void)registerToGrowl; -- (void)notifyWithTitle:(const char *)title - artist:(const char *)artist - album:(const char *)album - andArtUrl:(const char *)url; +/** + * Initializes a new VLCNotification Delegate with a given intf_thread_t + */ +- (instancetype)initWithInterfaceThread:(intf_thread_t * _Nonnull)intf_thread; + +/** + * Delegate method called when the current input changed + */ +- (void)currentInputDidChanged:(input_thread_t * _Nonnull)input; + @end + +#pragma mark - +#pragma mark Local prototypes struct intf_sys_t { - VLCGrowlDelegate *o_growl_delegate; + void *vlcNotificationDelegate; }; -/***************************************************************************** - * Local prototypes - *****************************************************************************/ -static int Open ( vlc_object_t * ); -static void Close ( vlc_object_t * ); - -static int InputCurrent( vlc_object_t *, const char *, - vlc_value_t, vlc_value_t, void * ); +static int InputCurrent(vlc_object_t *, const char *, + vlc_value_t, vlc_value_t, void *); -/***************************************************************************** - * Module descriptor - ****************************************************************************/ -vlc_module_begin () -set_category( CAT_INTERFACE ) -set_subcategory( SUBCAT_INTERFACE_CONTROL ) -set_shortname( "OSX-Notifications" ) -add_shortcut( "growl" ) -set_description( N_("OS X Notification Plugin") ) -set_capability( "interface", 0 ) -set_callbacks( Open, Close ) -vlc_module_end () -/***************************************************************************** - * Open: initialize and create stuff - *****************************************************************************/ -static int Open( vlc_object_t *p_this ) +#pragma mark - +#pragma mark C module functions +/* + * Open: Initialization of the module + */ +static int Open(vlc_object_t *p_this) { intf_thread_t *p_intf = (intf_thread_t *)p_this; - playlist_t *p_playlist = pl_Get( p_intf ); - intf_sys_t *p_sys = p_intf->p_sys = calloc( 1, sizeof(intf_sys_t) ); + playlist_t *p_playlist = pl_Get(p_intf); + intf_sys_t *p_sys = p_intf->p_sys = calloc(1, sizeof(intf_sys_t)); - if( !p_sys ) + if (!p_sys) return VLC_ENOMEM; - p_sys->o_growl_delegate = [[VLCGrowlDelegate alloc] initWithInterfaceThread:p_intf]; - if( !p_sys->o_growl_delegate ) - { - free( p_sys ); - return VLC_ENOMEM; + @autoreleasepool { + VLCNotificationDelegate *notificationDelegate = + [[VLCNotificationDelegate alloc] initWithInterfaceThread:p_intf]; + + if (notificationDelegate == nil) { + free(p_sys); + return VLC_ENOMEM; + } + + p_sys->vlcNotificationDelegate = (__bridge_retained void*)notificationDelegate; } - var_AddCallback( p_playlist, "input-current", InputCurrent, p_intf ); + var_AddCallback(p_playlist, "input-current", InputCurrent, p_intf); - [p_sys->o_growl_delegate registerToGrowl]; return VLC_SUCCESS; } -/***************************************************************************** - * Close: destroy interface stuff - *****************************************************************************/ -static void Close( vlc_object_t *p_this ) +/* + * Close: Destruction of the module + */ +static void Close(vlc_object_t *p_this) { intf_thread_t *p_intf = (intf_thread_t *)p_this; - playlist_t *p_playlist = pl_Get( p_intf ); + playlist_t *p_playlist = pl_Get(p_intf); intf_sys_t *p_sys = p_intf->p_sys; + + // Remove the callback, this must be done here, before deallocating the + // notification delegate object + var_DelCallback(p_playlist, "input-current", InputCurrent, p_intf); - var_DelCallback( p_playlist, "input-current", InputCurrent, p_intf ); + @autoreleasepool { + // Transfer ownership of notification delegate object back to ARC + VLCNotificationDelegate *notificationDelegate = + (__bridge_transfer VLCNotificationDelegate*)p_sys->vlcNotificationDelegate; + + // Ensure the object is deallocated + notificationDelegate = nil; + } - [GrowlApplicationBridge setGrowlDelegate:nil]; - [p_sys->o_growl_delegate release]; - free( p_sys ); + free(p_sys); } -/***************************************************************************** - * InputCurrent: Current playlist item changed callback - *****************************************************************************/ -static int InputCurrent( vlc_object_t *p_this, const char *psz_var, - vlc_value_t oldval, vlc_value_t newval, void *param ) +/* + * Callback invoked on playlist item change + */ +static int InputCurrent(vlc_object_t *p_this, const char *psz_var, + vlc_value_t oldval, vlc_value_t newval, void *param) { - VLC_UNUSED(oldval); - intf_thread_t *p_intf = (intf_thread_t *)param; intf_sys_t *p_sys = p_intf->p_sys; input_thread_t *p_input = newval.p_address; - char *psz_title = NULL; - char *psz_artist = NULL; - char *psz_album = NULL; - char *psz_arturl = NULL; - - if( !p_input ) - return VLC_SUCCESS; - - input_item_t *p_item = input_GetItem( p_input ); - if( !p_item ) - return VLC_SUCCESS; - - /* Get title */ - psz_title = input_item_GetNowPlayingFb( p_item ); - if( !psz_title ) - psz_title = input_item_GetTitleFbName( p_item ); - - if( EMPTY_STR( psz_title ) ) - { - free( psz_title ); - return VLC_SUCCESS; - } + VLC_UNUSED(oldval); - /* Get Artist name */ - psz_artist = input_item_GetArtist( p_item ); - if( EMPTY_STR( psz_artist ) ) - FREENULL( psz_artist ); - - /* Get Album name */ - psz_album = input_item_GetAlbum( p_item ) ; - if( EMPTY_STR( psz_album ) ) - FREENULL( psz_album ); - - /* Get Art path */ - psz_arturl = input_item_GetArtURL( p_item ); - if( psz_arturl ) - { - char *psz = vlc_uri2path( psz_arturl ); - free( psz_arturl ); - psz_arturl = psz; + @autoreleasepool { + VLCNotificationDelegate *notificationDelegate = + (__bridge VLCNotificationDelegate*)p_sys->vlcNotificationDelegate; + + [notificationDelegate currentInputDidChanged:(input_thread_t *)p_input]; } - [p_sys->o_growl_delegate notifyWithTitle:psz_title - artist:psz_artist - album:psz_album - andArtUrl:psz_arturl]; - - free( psz_title ); - free( psz_artist ); - free( psz_album ); - free( psz_arturl ); return VLC_SUCCESS; } -/***************************************************************************** - * VLCGrowlDelegate - *****************************************************************************/ -@implementation VLCGrowlDelegate - -- (id)initWithInterfaceThread:(intf_thread_t *)thread { - if( !( self = [super init] ) ) +/** + * Transfers a null-terminated UTF-8 C "string" to a NSString + * in a way that the NSString takes ownership of it. + * + * \warning After calling this function, passed cStr must not be used anymore! + * + * \param cStr Pointer to a zero-terminated UTF-8 encoded char array + * + * \return An NSString instance that uses cStr as internal data storage and + * frees it when done. On error, nil is returned and cStr is freed. + */ +static inline NSString* CharsToNSString(char * _Nullable cStr) +{ + if (!cStr) return nil; - @autoreleasepool { + NSString *resString = [[NSString alloc] initWithBytesNoCopy:cStr + length:strlen(cStr) + encoding:NSUTF8StringEncoding + freeWhenDone:YES]; + if (unlikely(resString == nil)) + free(cStr); + + return resString; +} + +#pragma mark - +#pragma mark Class implementation +@implementation VLCNotificationDelegate + +- (id)initWithInterfaceThread:(intf_thread_t *)intf_thread +{ + self = [super init]; + + if (self) { + interfaceThread = intf_thread; + // Subscribe to notifications to determine if VLC is in foreground or not [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationActiveChange:) @@ -251,159 +205,64 @@ static int InputCurrent( vlc_object_t *p_this, const char *psz_var, selector:@selector(applicationActiveChange:) name:NSApplicationDidResignActiveNotification object:nil]; - } - // Start in background - isInForeground = NO; - - lastNotification = nil; - applicationName = nil; - notificationType = nil; - registrationDictionary = nil; - interfaceThread = thread; - - return self; -} -- (void)dealloc -{ - // Clear the remaining lastNotification in Notification Center, if any - @autoreleasepool { - if (lastNotification) { - [NSUserNotificationCenter.defaultUserNotificationCenter - removeDeliveredNotification:(NSUserNotification *)lastNotification]; - [lastNotification release]; - } - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self]; } - - // Release everything - [applicationName release]; - [notificationType release]; - [registrationDictionary release]; - [super dealloc]; + + return self; } -- (void)registerToGrowl +- (void)currentInputDidChanged:(input_thread_t *)input { - @autoreleasepool { - applicationName = [[NSString alloc] initWithUTF8String:_( "VLC media player" )]; - notificationType = [[NSString alloc] initWithUTF8String:_( "New input playing" )]; - - NSArray *defaultAndAllNotifications = [NSArray arrayWithObject: notificationType]; - registrationDictionary = [[NSMutableDictionary alloc] init]; - [registrationDictionary setObject:defaultAndAllNotifications - forKey:GROWL_NOTIFICATIONS_ALL]; - [registrationDictionary setObject:defaultAndAllNotifications - forKey: GROWL_NOTIFICATIONS_DEFAULT]; - - [GrowlApplicationBridge setGrowlDelegate:self]; - - [[NSUserNotificationCenter defaultUserNotificationCenter] - setDelegate:(id<NSUserNotificationCenterDelegate>)self]; + if (!input) + return; + + input_item_t *item = input_GetItem(input); + if (!item) + return; + + // Get title, first try now playing + NSString *title = CharsToNSString(input_item_GetNowPlayingFb(item)); + + // Fallback to item title or name + if ([title length] == 0) + title = CharsToNSString(input_item_GetTitleFbName(item)); + + // If there is still not title, do not notify + if (unlikely([title length] == 0)) + return; + + // Get artist name + NSString *artist = CharsToNSString(input_item_GetArtist(item)); + + // Get album name + NSString *album = CharsToNSString(input_item_GetAlbum(item)); + + // Get coverart path + NSString *artPath = nil; + + char *psz_arturl = input_item_GetArtURL(item); + if (psz_arturl) { + artPath = CharsToNSString(vlc_uri2path(psz_arturl)); + free(psz_arturl); } -} -- (void)notifyWithTitle:(const char *)title - artist:(const char *)artist - album:(const char *)album - andArtUrl:(const char *)url -{ - @autoreleasepool { - // Do not notify if in foreground - if (isInForeground) - return; - - // Init Cover - NSData *coverImageData = nil; - NSImage *coverImage = nil; - - if (url) { - coverImageData = [NSData dataWithContentsOfFile:[NSString stringWithUTF8String:url]]; - coverImage = [[NSImage alloc] initWithData:coverImageData]; - } - - // Init Track info - NSString *titleStr = nil; - NSString *artistStr = nil; - NSString *albumStr = nil; - - if (title) { - titleStr = [NSString stringWithUTF8String:title]; - } else { - // Without title, notification makes no sense, so return here - // title should never be empty, but better check than crash. - [coverImage release]; - return; - } - if (artist) - artistStr = [NSString stringWithUTF8String:artist]; - if (album) - albumStr = [NSString stringWithUTF8String:album]; - - // Notification stuff - if ([GrowlApplicationBridge isGrowlRunning]) { - // Make the Growl notification string - NSString *desc = nil; - - if (artistStr && albumStr) { - desc = [NSString stringWithFormat:@"%@\n%@ [%@]", titleStr, artistStr, albumStr]; - } else if (artistStr) { - desc = [NSString stringWithFormat:@"%@\n%@", titleStr, artistStr]; - } else { - desc = titleStr; - } - - // Send notification - [GrowlApplicationBridge notifyWithTitle:[NSString stringWithUTF8String:_("Now playing")] - description:desc - notificationName:notificationType - iconData:coverImageData - priority:0 - isSticky:NO - clickContext:nil - identifier:@"VLCNowPlayingNotification"]; - } else { - // Make the OS X notification and string - NSUserNotification *notification = [NSUserNotification new]; - NSString *desc = nil; - - if (artistStr && albumStr) { - desc = [NSString stringWithFormat:@"%@ – %@", artistStr, albumStr]; - } else if (artistStr) { - desc = artistStr; - } - - notification.title = titleStr; - notification.subtitle = desc; - notification.hasActionButton = YES; - notification.actionButtonTitle = [NSString stringWithUTF8String:_("Skip")]; - - // Private APIs to set cover image, see rdar://23148801 - // and show action button, see rdar://23148733 - [notification setValue:coverImage forKey:@"_identityImage"]; - [notification setValue:@(YES) forKey:@"_showsButtons"]; - [NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:notification]; - [notification release]; - } + // Construct final description string + NSString *desc = nil; - // Release stuff - [coverImage release]; + if (artist && album) { + desc = [NSString stringWithFormat:@"%@ – %@", artist, album]; + } else if (artist) { + desc = artist; } + + // Notify! + [self notifyWithTitle:title description:desc imagePath:artPath]; } -/***************************************************************************** - * Delegate methods - *****************************************************************************/ -- (NSDictionary *)registrationDictionaryForGrowl -{ - return registrationDictionary; -} - -- (NSString *)applicationNameForGrowl -{ - return applicationName; -} - +/* + * Called when the applications activity status changes + */ - (void)applicationActiveChange:(NSNotification *)n { if (n.name == NSApplicationDidBecomeActiveNotification) isInForeground = YES; @@ -411,24 +270,99 @@ static int InputCurrent( vlc_object_t *p_this, const char *psz_var, isInForeground = NO; } +/* + * Called when the user interacts with a notification + */ - (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification { - // Skip to next song + // Check if notification button ("Skip") was clicked if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) { + // Skip to next song playlist_Next(pl_Get(interfaceThread)); } } +/* + * Called when a new notification was delivered + */ - (void)userNotificationCenter:(NSUserNotificationCenter *)center didDeliverNotification:(NSUserNotification *)notification { // Only keep the most recent notification in the Notification Center + if (lastNotification) + [center removeDeliveredNotification:lastNotification]; + + lastNotification = notification; +} + +/* + * Send a notification to the default user notification center + */ +- (void)notifyWithTitle:(NSString * _Nonnull)titleText + description:(NSString * _Nullable)descriptionText + imagePath:(NSString * _Nullable)imagePath +{ + NSImage *image = nil; + + // Load image if any + if (imagePath) { + image = [[NSImage alloc] initWithContentsOfFile:imagePath]; + } + + // Create notification + NSUserNotification *notification = [NSUserNotification new]; + + notification.title = titleText; + notification.subtitle = descriptionText; + notification.hasActionButton = YES; + notification.actionButtonTitle = [NSString stringWithUTF8String:_("Skip")]; + + // Try to set private properties + @try { + // Private API to set cover image, see rdar://23148801 + [notification setValue:image forKey:@"_identityImage"]; + // Private API to show action button, see rdar://23148733 + [notification setValue:@(YES) forKey:@"_showsButtons"]; + } @catch (NSException *exception) { + if (exception.name == NSUndefinedKeyException) + NSLog(@"VLC macOS notifcations plugin failed to set private notification values."); + else + @throw exception; + } + + // Send notification + [[NSUserNotificationCenter defaultUserNotificationCenter] + deliverNotification:notification]; +} + +/* + * Cleanup + */ +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + // Clear a remaining lastNotification in Notification Center, if any if (lastNotification) { - [center removeDeliveredNotification: (NSUserNotification *)lastNotification]; - [lastNotification release]; + [[NSUserNotificationCenter defaultUserNotificationCenter] + removeDeliveredNotification:lastNotification]; + lastNotification = nil; } - [notification retain]; - lastNotification = notification; } + @end + + +#pragma mark - +#pragma mark VLC Module descriptor + +vlc_module_begin() + set_shortname("OSX-Notifications") + set_description(N_("macOS notifications plugin")) + add_shortcut("growl") // Kept for backwards compatibility + set_category(CAT_INTERFACE) + set_subcategory(SUBCAT_INTERFACE_CONTROL) + set_capability("interface", 0) + set_callbacks(Open, Close) +vlc_module_end()