Commit 66cdaaaf authored by Marvin Scholz's avatar Marvin Scholz

osx_notifications: remove Growl support, refactor

As per the discussion on the ML, Growl support is removed for VLC 4.0.

With this refactoring the module is ARC compatible too, simplifying
memory management.

https://mailman.videolan.org/pipermail/vlc-devel/2018-February/117924.html
parent 086a2838
......@@ -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:
--------------------------------
......
......@@ -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
......
......@@ -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
......
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)
......
/*****************************************************************************
* 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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment