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()