[vlc-commits] [Git][videolan/vlc][master] 5 commits: macosx/image cache: raise the limit and calculate a cost per image

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Thu Apr 16 21:56:54 UTC 2026



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
1fb2b4c6 by Felix Paul Kühne at 2026-04-16T22:57:46+02:00
macosx/image cache: raise the limit and calculate a cost per image

- - - - -
d1a59bd0 by Felix Paul Kühne at 2026-04-16T22:57:46+02:00
macosx/image cache: improve performance

- - - - -
c87778e0 by Felix Paul Kühne at 2026-04-16T22:57:46+02:00
macosx/image cache: downsample input item artwork on load instead of resizing display size

- - - - -
55de5801 by Felix Paul Kühne at 2026-04-16T22:57:46+02:00
macosx/image cache: remove unused thumbnailAtMrl method

- - - - -
230f5feb by Felix Paul Kühne at 2026-04-16T22:57:46+02:00
macosx/image cache: invalidate cached thumbnails

- - - - -


2 changed files:

- modules/gui/macosx/library/VLCLibraryImageCache.h
- modules/gui/macosx/library/VLCLibraryImageCache.m


Changes:

=====================================
modules/gui/macosx/library/VLCLibraryImageCache.h
=====================================
@@ -36,7 +36,6 @@ NS_ASSUME_NONNULL_BEGIN
                withCompletion:(void(^)(const NSImage *))completionHandler;
 + (void)thumbnailForPlayQueueItem:(VLCPlayQueueItem *)playlistItem
                    withCompletion:(void(^)(const NSImage *))completionHandler;
-+ (NSImage *)thumbnailAtMrl:(NSString *)smallArtworkMRL;
 
 @end
 


=====================================
modules/gui/macosx/library/VLCLibraryImageCache.m
=====================================
@@ -1,7 +1,7 @@
 /*****************************************************************************
 * VLCLibraryImageCache.m: MacOS X interface module
 *****************************************************************************
-* Copyright (C) 2020 VLC authors and VideoLAN
+* Copyright (C) 2020-2026 VLC authors and VideoLAN
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan -dot- org>
 *
@@ -26,12 +26,17 @@
 
 #import "library/VLCInputItem.h"
 #import "library/VLCLibraryDataTypes.h"
+#import "library/VLCLibraryModel.h"
 
 #import "main/VLCMain.h"
 
 #import "playqueue/VLCPlayQueueItem.h"
 
-NSUInteger kVLCMaximumLibraryImageCacheSize = 50;
+#import <ImageIO/ImageIO.h>
+
+NSUInteger kVLCMaximumLibraryImageCacheSize = 500;
+/* 256 MB cost limit based on estimated pixel data size per image */
+static const NSUInteger kVLCLibraryImageCacheCostLimit = 256 * 1024 * 1024;
 uint32_t kVLCDesiredThumbnailWidth = 512;
 uint32_t kVLCDesiredThumbnailHeight = 512;
 float kVLCDefaultThumbnailPosition = .15;
@@ -41,6 +46,7 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 @interface VLCLibraryImageCache()
 {
     NSCache *_imageCache;
+    NSImage *_noArtImage;
     vlc_medialibrary_t *_p_libraryInstance;
 }
 
@@ -48,16 +54,80 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 
 @implementation VLCLibraryImageCache
 
++ (NSImage *)downsampledImageFromURL:(NSURL *)url maxPixelSize:(uint32_t)maxPixelSize
+{
+    CGImageSourceRef const imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
+    if (!imageSource) {
+        return nil;
+    }
+
+    NSDictionary * const downsampleOptions = @{
+        (NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
+        (NSString *)kCGImageSourceCreateThumbnailWithTransform : @YES,
+        (NSString *)kCGImageSourceThumbnailMaxPixelSize : @(maxPixelSize),
+        (NSString *)kCGImageSourceShouldCacheImmediately : @YES,
+    };
+
+    CGImageRef const downsampledImage =
+        CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
+    CFRelease(imageSource);
+
+    if (!downsampledImage) {
+        return nil;
+    }
+
+    NSImage * const image = [[NSImage alloc] initWithCGImage:downsampledImage
+                                                        size:NSZeroSize];
+    CGImageRelease(downsampledImage);
+    return image;
+}
+
++ (NSUInteger)costForImage:(NSImage *)image
+{
+    NSBitmapImageRep *bitmapRep = nil;
+    for (NSImageRep *rep in image.representations) {
+        if ([rep isKindOfClass:[NSBitmapImageRep class]]) {
+            bitmapRep = (NSBitmapImageRep *)rep;
+            break;
+        }
+    }
+    if (bitmapRep) {
+        return bitmapRep.pixelsWide * bitmapRep.pixelsHigh * bitmapRep.bitsPerPixel / 8;
+    }
+    return (NSUInteger)(image.size.width * image.size.height * 4);
+}
+
 - (instancetype)init
 {
     self = [super init];
     if (self) {
         _imageCache = [[NSCache alloc] init];
         _imageCache.countLimit = kVLCMaximumLibraryImageCacheSize;
+        _imageCache.totalCostLimit = kVLCLibraryImageCacheCostLimit;
+        _noArtImage = [NSImage imageNamed:@"noart.png"];
+
+        NSNotificationCenter * const notificationCenter = [NSNotificationCenter defaultCenter];
+        [notificationCenter addObserver:self
+                               selector:@selector(mediaItemThumbnailGenerated:)
+                                   name:VLCLibraryModelMediaItemThumbnailGenerated
+                                 object:nil];
+        [notificationCenter addObserver:self
+                               selector:@selector(mediaItemUpdated:)
+                                   name:VLCLibraryModelAudioMediaItemUpdated
+                                 object:nil];
+        [notificationCenter addObserver:self
+                               selector:@selector(mediaItemUpdated:)
+                                   name:VLCLibraryModelVideoMediaItemUpdated
+                                 object:nil];
     }
     return self;
 }
 
+- (void)dealloc
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
 + (instancetype)sharedImageCache
 {
     static dispatch_once_t onceToken;
@@ -68,42 +138,61 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     return sharedImageCache;
 }
 
-+ (NSImage *)thumbnailForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
+- (void)mediaItemThumbnailGenerated:(NSNotification *)aNotification
 {
-    return [[VLCLibraryImageCache sharedImageCache] imageForLibraryItem:libraryItem];
+    VLCMediaLibraryMediaItem * const mediaItem = aNotification.object;
+    NSString * const artworkMRL = mediaItem.smallArtworkMRL;
+    if (mediaItem == nil || artworkMRL == nil) {
+        return;
+    }
+    [_imageCache removeObjectForKey:artworkMRL];
 }
 
-- (NSImage *)imageForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
+- (void)mediaItemUpdated:(NSNotification *)aNotification
 {
-    NSImage *cachedImage = [_imageCache objectForKey:libraryItem.smallArtworkMRL];
-    if (cachedImage) {
-        return cachedImage;
+    VLCMediaLibraryMediaItem * const mediaItem = aNotification.object;
+    NSString * const artworkMRL = mediaItem.smallArtworkMRL;
+    if (mediaItem == nil || artworkMRL == nil) {
+        return;
     }
-    return [self smallThumbnailForLibraryItem:libraryItem];
+    [_imageCache removeObjectForKey:artworkMRL];
 }
 
-- (NSImage *)smallThumbnailForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
+- (void)imageForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
+             withCompletion:(void(^)(const NSImage *))completionHandler
 {
-    NSImage *image;
     NSString * const artworkMRL = libraryItem.smallArtworkMRL;
+    if (artworkMRL) {
+        NSImage * const cachedImage = [_imageCache objectForKey:artworkMRL];
+        if (cachedImage) {
+            completionHandler(cachedImage);
+            return;
+        }
+    }
 
-    if (libraryItem.smallArtworkGenerated) {
-        image = [[NSImage alloc] initWithContentsOfURL:[NSURL URLWithString:artworkMRL]];
+    if (libraryItem.smallArtworkGenerated && artworkMRL) {
+        NSURL * const artworkURL = [NSURL URLWithString:artworkMRL];
+        dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
+            NSImage * const image =
+                [VLCLibraryImageCache downsampledImageFromURL:artworkURL
+                                                maxPixelSize:kVLCDesiredThumbnailWidth];
+            if (image) {
+                const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
+                [self->_imageCache setObject:image forKey:artworkMRL cost:cost];
+            }
+            dispatch_async(dispatch_get_main_queue(), ^{
+                completionHandler(image ?: self->_noArtImage);
+            });
+        });
     } else if ([libraryItem isKindOfClass:[VLCMediaLibraryMediaItem class]]) {
-        VLCMediaLibraryMediaItem * const mediaItem = (VLCMediaLibraryMediaItem*)libraryItem;
-        
+        VLCMediaLibraryMediaItem * const mediaItem = (VLCMediaLibraryMediaItem *)libraryItem;
         if (mediaItem.mediaType != VLC_ML_MEDIA_TYPE_AUDIO) {
             [self generateThumbnailForMediaItem:mediaItem.libraryID];
         }
+        completionHandler(_noArtImage);
+    } else {
+        completionHandler(_noArtImage);
     }
-
-    if (image) {
-        [_imageCache setObject:image forKey:artworkMRL];
-    } else { // If nothing so far worked, then fall back on default image
-        image = [NSImage imageNamed:@"noart.png"];
-    }
-
-    return image;
 }
 
 - (void)generateThumbnailForMediaItem:(int64_t)mediaID
@@ -136,34 +225,37 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     [self generateImageForInputItem:inputItem withCompletion:completionHandler];
 }
 
-- (void)generateImageForInputItem:(VLCInputItem *)inputItem 
+- (void)generateImageForInputItem:(VLCInputItem *)inputItem
                    withCompletion:(void(^)(const NSImage *))completionHandler
 {
     NSURL * const artworkURL = inputItem.artworkURL;
     const NSSize imageSize = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
-    
+
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
-        NSImage * image = [[NSImage alloc] initWithContentsOfURL:artworkURL];
-        
-        dispatch_async(dispatch_get_main_queue(), ^{
-            if (image) {
-                image.size = imageSize;
-                [self->_imageCache setObject:image forKey:inputItem.MRL];
+        NSImage * const image = artworkURL ?
+            [VLCLibraryImageCache downsampledImageFromURL:artworkURL
+                                            maxPixelSize:kVLCDesiredThumbnailWidth] : nil;
+
+        if (image) {
+            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
+            [self->_imageCache setObject:image forKey:inputItem.MRL cost:cost];
+            dispatch_async(dispatch_get_main_queue(), ^{
                 completionHandler(image);
+            });
+            return;
+        }
+
+        [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const thumbnail) {
+            if (thumbnail) {
+                const NSUInteger cost = [VLCLibraryImageCache costForImage:thumbnail];
+                [self->_imageCache setObject:thumbnail forKey:inputItem.MRL cost:cost];
             } else {
-                [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const image) {
-                    dispatch_async(dispatch_get_main_queue(), ^{
-                        if (image) {
-                            [self->_imageCache setObject:image forKey:inputItem.MRL];
-                            completionHandler(image);
-                        } else {
-                            NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
-                            completionHandler([NSImage imageNamed:@"noart.png"]);
-                        }
-                    });
-                }];
+                NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
             }
-        });
+            dispatch_async(dispatch_get_main_queue(), ^{
+                completionHandler(thumbnail ?: self->_noArtImage);
+            });
+        }];
     });
 }
 
@@ -177,47 +269,55 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 + (void)thumbnailForLibraryItem:(id<VLCMediaLibraryItemProtocol>)libraryItem
                  withCompletion:(void(^)(const NSImage *))completionHandler
 {
+    VLCLibraryImageCache * const cache = [VLCLibraryImageCache sharedImageCache];
+
     if (![libraryItem isKindOfClass:VLCMediaLibraryAlbum.class] &&
         ![libraryItem isKindOfClass:VLCMediaLibraryMediaItem.class]) {
 
         dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
-            NSMutableSet<NSImage *> * const itemImages = NSMutableSet.set;
+            NSMutableArray<NSImage *> * const itemImages = [NSMutableArray array];
+            dispatch_group_t const group = dispatch_group_create();
 
             [libraryItem iterateMediaItemsWithBlock:^(VLCMediaLibraryMediaItem * const item) {
-                NSImage * const itemImage = [VLCLibraryImageCache thumbnailForLibraryItem:item];
-                if (itemImage == nil || [itemImages containsObject:itemImage]) {
-                    return;
-                }
-                [itemImages addObject:itemImage];
+                dispatch_group_enter(group);
+                [cache imageForLibraryItem:item withCompletion:^(const NSImage * thumbnail) {
+                    NSImage * const mutableRef = (NSImage *)thumbnail;
+                    @synchronized (itemImages) {
+                        if (mutableRef && ![mutableRef isEqual:cache->_noArtImage]) {
+                            [itemImages addObject:mutableRef];
+                        }
+                    }
+                    dispatch_group_leave(group);
+                }];
             }];
 
+            dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
+
+            NSArray<NSImage *> *uniqueImages;
+            @synchronized (itemImages) {
+                uniqueImages = [NSOrderedSet orderedSetWithArray:itemImages].array;
+            }
+
+            if (uniqueImages.count == 0) {
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    completionHandler(cache->_noArtImage);
+                });
+                return;
+            }
+
             const NSSize size = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
-            NSArray<NSImage *> * const itemImagesArray = itemImages.allObjects;
             NSArray<NSValue *> * const frames =
-                [NSImage framesForCompositeImageSquareGridWithImages:itemImagesArray size:size gridItemCount:kVLCCompositeImageDefaultCompositedGridItemCount];
+                [NSImage framesForCompositeImageSquareGridWithImages:uniqueImages size:size gridItemCount:kVLCCompositeImageDefaultCompositedGridItemCount];
             NSImage * const compositeImage =
-                [NSImage compositeImageWithImages:itemImagesArray frames:frames size:size];
+                [NSImage compositeImageWithImages:uniqueImages frames:frames size:size];
 
             dispatch_async(dispatch_get_main_queue(), ^{
                 completionHandler(compositeImage);
             });
         });
     } else {
-        dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
-            NSImage * const image = [VLCLibraryImageCache thumbnailForLibraryItem:libraryItem];
-            dispatch_async(dispatch_get_main_queue(), ^{
-                completionHandler(image);
-            });
-        });
+        [cache imageForLibraryItem:libraryItem withCompletion:completionHandler];
     }
 }
 
-+ (NSImage *)thumbnailAtMrl:(NSString *)smallArtworkMRL
-{
-    NSImage * const cachedImage = 
-        [VLCLibraryImageCache.sharedImageCache->_imageCache objectForKey:smallArtworkMRL];
-    return cachedImage ?
-        cachedImage : [[NSImage alloc] initWithContentsOfURL:[NSURL URLWithString:smallArtworkMRL]];
-}
-
 @end



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/97a2d2ab8105f158679f3ab3676642b7a04c0aad...230f5febc38236293fd72f17ed352f867a8c8426

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/97a2d2ab8105f158679f3ab3676642b7a04c0aad...230f5febc38236293fd72f17ed352f867a8c8426
You're receiving this email because of your account on code.videolan.org.




More information about the vlc-commits mailing list