[vlc-commits] [Git][videolan/vlc][master] macosx: library: fetch remote artwork asynchronously via NSURLSession

Steve Lhomme (@robUx4) gitlab at videolan.org
Fri Sep 4 12:22:16 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
6261405e by Serhii Bykov at 2026-09-04T12:02:45+00:00
macosx: library: fetch remote artwork asynchronously via NSURLSession

- - - - -


1 changed file:

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


Changes:

=====================================
modules/gui/macosx/library/VLCLibraryImageCache.m
=====================================
@@ -40,6 +40,10 @@
 #import <vlc_preparser.h>
 #import <vlc_strings.h>
 
+static const NSTimeInterval kVLCRemoteArtworkRequestTimeout = 15;
+static const NSTimeInterval kVLCRemoteArtworkResourceTimeout = 30;
+static const NSInteger kVLCRemoteArtworkMaximumConnectionsPerHost = 4;
+
 NSUInteger kVLCMaximumLibraryImageCacheSize = 500;
 /* 256 MB cost limit based on estimated pixel data size per image */
 static const NSUInteger kVLCLibraryImageCacheCostLimit = 256 * 1024 * 1024;
@@ -56,6 +60,8 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     vlc_medialibrary_t *_p_libraryInstance;
     NSString *_thumbnailCacheDirectory;
     NSMutableDictionary<NSString *, VLCThumbnailRequest *> *_pendingThumbnailRequests;
+    NSMutableDictionary<NSString *, NSMutableArray<void (^)(const NSImage *)> *> *_pendingRemoteArtworkRequests;
+    NSURLSession *_remoteArtworkSession;
 }
 
 - (void)thumbnailRequest:(VLCThumbnailRequest *)request
@@ -94,13 +100,9 @@ static NSString *thumbnailHashForString(NSString *string)
 
 @implementation VLCLibraryImageCache
 
-+ (NSImage *)downsampledImageFromURL:(NSURL *)url maxPixelSize:(uint32_t)maxPixelSize
++ (NSImage *)downsampledImageFromSource:(CGImageSourceRef)imageSource
+                           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,
@@ -110,8 +112,6 @@ static NSString *thumbnailHashForString(NSString *string)
 
     CGImageRef const downsampledImage =
         CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)downsampleOptions);
-    CFRelease(imageSource);
-
     if (!downsampledImage) {
         return nil;
     }
@@ -122,6 +122,42 @@ static NSString *thumbnailHashForString(NSString *string)
     return image;
 }
 
++ (NSImage *)downsampledImageFromURL:(NSURL *)url maxPixelSize:(uint32_t)maxPixelSize
+{
+    CGImageSourceRef const imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
+    if (!imageSource) {
+        return nil;
+    }
+    NSImage * const image = [self downsampledImageFromSource:imageSource maxPixelSize:maxPixelSize];
+    CFRelease(imageSource);
+    return image;
+}
+
++ (BOOL)artworkURLRequiresRemoteFetch:(NSURL *)url
+{
+    return [url.scheme isEqualToString:@"http"] ||
+           [url.scheme isEqualToString:@"https"];
+}
+
++ (BOOL)remoteArtworkResponseCanContainImage:(NSURLResponse *)response
+{
+    if ([response isKindOfClass:NSHTTPURLResponse.class]) {
+        NSHTTPURLResponse * const httpResponse = (NSHTTPURLResponse *)response;
+        if (httpResponse.statusCode < 200 || httpResponse.statusCode >= 300) {
+            return NO;
+        }
+    }
+
+    NSString * const MIMEType = response.MIMEType.lowercaseString;
+    if (MIMEType.length > 0 &&
+        ![MIMEType hasPrefix:@"image/"] &&
+        ![MIMEType isEqualToString:@"application/octet-stream"]) {
+        return NO;
+    }
+
+    return YES;
+}
+
 + (NSUInteger)costForImage:(NSImage *)image
 {
     NSBitmapImageRep *bitmapRep = nil;
@@ -146,6 +182,20 @@ static NSString *thumbnailHashForString(NSString *string)
         _imageCache.totalCostLimit = kVLCLibraryImageCacheCostLimit;
         _noArtImage = NSImage.VLCNoArtImage;
         _pendingThumbnailRequests = NSMutableDictionary.dictionary;
+        _pendingRemoteArtworkRequests = NSMutableDictionary.dictionary;
+
+        NSURLSessionConfiguration * const remoteArtworkSessionConfiguration =
+            NSURLSessionConfiguration.ephemeralSessionConfiguration;
+        remoteArtworkSessionConfiguration.timeoutIntervalForRequest =
+            kVLCRemoteArtworkRequestTimeout;
+        remoteArtworkSessionConfiguration.timeoutIntervalForResource =
+            kVLCRemoteArtworkResourceTimeout;
+        remoteArtworkSessionConfiguration.requestCachePolicy =
+            NSURLRequestReloadIgnoringLocalCacheData;
+        remoteArtworkSessionConfiguration.HTTPMaximumConnectionsPerHost =
+            kVLCRemoteArtworkMaximumConnectionsPerHost;
+        _remoteArtworkSession =
+            [NSURLSession sessionWithConfiguration:remoteArtworkSessionConfiguration];
 
         char * const cacheDirectory = config_GetUserDir(VLC_CACHE_DIR);
         if (cacheDirectory != NULL) {
@@ -187,6 +237,7 @@ static NSString *thumbnailHashForString(NSString *string)
 
 - (void)dealloc
 {
+    [_remoteArtworkSession invalidateAndCancel];
     [[NSNotificationCenter defaultCenter] removeObserver:self];
 }
 
@@ -402,11 +453,15 @@ static NSString *thumbnailHashForString(NSString *string)
     });
 }
 
-- (void)imageForInputItem:(VLCInputItem *)inputItem 
+- (void)imageForInputItem:(VLCInputItem *)inputItem
            withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
 {
-    NSString * const memoryCacheKey = inputItem.MRL;
-    NSImage * const cachedImage = [_imageCache objectForKey:memoryCacheKey];
+    NSURL * const artworkURL = inputItem.artworkURL;
+    NSString * const cacheKey = [VLCLibraryImageCache artworkURLRequiresRemoteFetch:artworkURL]
+        ? artworkURL.absoluteString
+        : inputItem.MRL;
+
+    NSImage * const cachedImage = [_imageCache objectForKey:cacheKey];
     if (cachedImage) {
         completionHandler(cachedImage);
         return;
@@ -414,6 +469,56 @@ static NSString *thumbnailHashForString(NSString *string)
     [self generateImageForInputItem:inputItem withCompletion:completionHandler];
 }
 
+- (void)fetchRemoteArtwork:(NSURL *)url
+               forCacheKey:(NSString *)cacheKey
+            withCompletion:(void(^)(const NSImage *))completionHandler
+{
+    @synchronized (self) {
+        NSMutableArray<void (^)(const NSImage *)> * const pendingCompletions =
+            _pendingRemoteArtworkRequests[cacheKey];
+        if (pendingCompletions != nil) {
+            [pendingCompletions addObject:[completionHandler copy]];
+            return;
+        }
+
+        _pendingRemoteArtworkRequests[cacheKey] =
+            [NSMutableArray arrayWithObject:[completionHandler copy]];
+    }
+
+    [[_remoteArtworkSession dataTaskWithURL:url
+                          completionHandler:^(NSData *data,
+                                              NSURLResponse *response,
+                                              NSError *error) {
+        NSImage *image = nil;
+
+        if (!error && [VLCLibraryImageCache remoteArtworkResponseCanContainImage:response] && data) {
+            CGImageSourceRef const source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+            if (source) {
+                image = [VLCLibraryImageCache downsampledImageFromSource:source
+                                                            maxPixelSize:kVLCDesiredThumbnailWidth];
+                CFRelease(source);
+            }
+        }
+        if (image) {
+            const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
+            [self->_imageCache setObject:image forKey:cacheKey cost:cost];
+        }
+
+        NSArray<void (^)(const NSImage *)> *completionHandlers;
+        @synchronized (self) {
+            completionHandlers = [self->_pendingRemoteArtworkRequests[cacheKey] copy];
+            [self->_pendingRemoteArtworkRequests removeObjectForKey:cacheKey];
+        }
+
+        dispatch_async(dispatch_get_main_queue(), ^{
+            const NSImage * const artwork = image ?: self->_noArtImage;
+            for (void (^handler)(const NSImage *) in completionHandlers) {
+                handler(artwork);
+            }
+        });
+    }] resume];
+}
+
 - (void)generateArtworkForInputItem:(VLCInputItem *)inputItem
                      withCompletion:(void(^)(const NSImage *))completionHandler
 {
@@ -421,6 +526,13 @@ static NSString *thumbnailHashForString(NSString *string)
     const NSSize imageSize = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
     NSString * const memoryCacheKey = inputItem.MRL;
 
+    if ([VLCLibraryImageCache artworkURLRequiresRemoteFetch:artworkURL]) {
+        [self fetchRemoteArtwork:artworkURL
+                     forCacheKey:artworkURL.absoluteString
+                  withCompletion:completionHandler];
+        return;
+    }
+
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
         NSImage * const image = artworkURL
             ? [VLCLibraryImageCache downsampledImageFromURL:artworkURL



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/6261405e66281d5170df2f793d4adfcf68046bea

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/6261405e66281d5170df2f793d4adfcf68046bea
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list