[vlc-commits] [Git][videolan/vlc][master] 10 commits: macosx: Set up thumbnailer preparser

Steve Lhomme (@robUx4) gitlab at videolan.org
Wed Aug 5 20:00:29 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
d880015b by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Set up thumbnailer preparser

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
f53c2726 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Add VLCThumbnailRequest

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
9b9274c2 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Add convenience method to convert picture_t to NSImage

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
272c652e by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Use preparser to generate file thumbnails

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
aaa36b47 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Use workspaceImage in VLCInputItem

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
f91ac5ea by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Only use preparser for thumbnails if the item is playable

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
688f5fe4 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Cache generate thumbnails to disk

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
c23e8398 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Clean up outdated thumbnails in image cache

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
736eb055 by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Fix no-art on quicklook items

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -
6661dbda by Claudio Cambra at 2026-08-05T19:16:48+00:00
macosx: Prune stored thumbnails after 30 days

Signed-off-by: Claudio Cambra <developer at claudiocambra.com>

- - - - -


10 changed files:

- extras/package/macosx/VLC.xcodeproj/project.pbxproj
- modules/gui/macosx/Makefile.am
- modules/gui/macosx/extensions/NSImage+VLCAdditions.h
- modules/gui/macosx/extensions/NSImage+VLCAdditions.m
- modules/gui/macosx/library/VLCInputItem.m
- modules/gui/macosx/library/VLCLibraryImageCache.m
- + modules/gui/macosx/library/VLCThumbnailRequest.h
- + modules/gui/macosx/library/VLCThumbnailRequest.m
- modules/gui/macosx/main/VLCMain.h
- modules/gui/macosx/main/VLCMain.m


Changes:

=====================================
extras/package/macosx/VLC.xcodeproj/project.pbxproj
=====================================
@@ -240,6 +240,7 @@
 		7D903EAD2243928800917358 /* libvlc.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D903EAC2243928800917358 /* libvlc.dylib */; };
 		7D903EB6224394BE00917358 /* specific.c in Sources */ = {isa = PBXBuildFile; fileRef = 7D903EB5224394BE00917358 /* specific.c */; };
 		7D92AF2123DDCA8D00D81EA3 /* VLCLibraryImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D92AF2023DDCA8D00D81EA3 /* VLCLibraryImageCache.m */; };
+		7D92AF2223DDCA8D00D81EA3 /* VLCThumbnailRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D92AF2423DDCA8D00D81EA3 /* VLCThumbnailRequest.m */; };
 		7D93D8FC2316C2DC001C0063 /* VLCCustomCropArWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D93D8FB2316C2DC001C0063 /* VLCCustomCropArWindowController.m */; };
 		7DB40D2A20CBCEB500F63173 /* VLCMainMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DB40D2920CBCEB500F63173 /* VLCMainMenu.m */; };
 		7DB40D2D20CBCEC200F63173 /* VLCStatusBarIcon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DB40D2B20CBCEC200F63173 /* VLCStatusBarIcon.m */; };
@@ -882,6 +883,8 @@
 		7D903EB5224394BE00917358 /* specific.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = specific.c; path = ../../../src/darwin/specific.c; sourceTree = "<group>"; };
 		7D92AF1F23DDCA8D00D81EA3 /* VLCLibraryImageCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCLibraryImageCache.h; sourceTree = "<group>"; };
 		7D92AF2023DDCA8D00D81EA3 /* VLCLibraryImageCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibraryImageCache.m; sourceTree = "<group>"; };
+		7D92AF2323DDCA8D00D81EA3 /* VLCThumbnailRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCThumbnailRequest.h; sourceTree = "<group>"; };
+		7D92AF2423DDCA8D00D81EA3 /* VLCThumbnailRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCThumbnailRequest.m; sourceTree = "<group>"; };
 		7D93D8F92316C142001C0063 /* VLCCustomCropARPanel.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VLCCustomCropARPanel.xib; sourceTree = "<group>"; };
 		7D93D8FA2316C2DC001C0063 /* VLCCustomCropArWindowController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCCustomCropArWindowController.h; sourceTree = "<group>"; };
 		7D93D8FB2316C2DC001C0063 /* VLCCustomCropArWindowController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCCustomCropArWindowController.m; sourceTree = "<group>"; };
@@ -1513,6 +1516,8 @@
 				53B40FD62AA878E400C814E4 /* VLCLibraryHeroView.m */,
 				7D92AF1F23DDCA8D00D81EA3 /* VLCLibraryImageCache.h */,
 				7D92AF2023DDCA8D00D81EA3 /* VLCLibraryImageCache.m */,
+				7D92AF2323DDCA8D00D81EA3 /* VLCThumbnailRequest.h */,
+				7D92AF2423DDCA8D00D81EA3 /* VLCThumbnailRequest.m */,
 				536283DC291146BC00640C15 /* VLCLibraryItemInternalMediaItemsDataSource.h */,
 				536283DF291146BC00640C15 /* VLCLibraryItemInternalMediaItemsDataSource.m */,
 				5360E99F2C5BE01F0046BA8B /* VLCLibraryMasterDetailViewTableViewDataSource.h */,
@@ -2497,6 +2502,7 @@
 				1C3113941E508C6900D4DD76 /* VLCAddonsWindowController.m in Sources */,
 				53CBE30B2C159A4D006BF2E3 /* VLCNoResultsLabel.m in Sources */,
 				7D92AF2123DDCA8D00D81EA3 /* VLCLibraryImageCache.m in Sources */,
+				7D92AF2223DDCA8D00D81EA3 /* VLCThumbnailRequest.m in Sources */,
 				7D2E0EDE20CD206F0033A221 /* VLCVideoWindowCommon.m in Sources */,
 				1C3113961E508C6900D4DD76 /* applescript.m in Sources */,
 				5325C57D29302E6800B2B63A /* VLCLibraryAudioViewController.m in Sources */,


=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -192,6 +192,8 @@ libmacosx_plugin_la_SOURCES = \
 	gui/macosx/library/VLCLibraryHeroView.m \
 	gui/macosx/library/VLCLibraryImageCache.h \
 	gui/macosx/library/VLCLibraryImageCache.m \
+	gui/macosx/library/VLCThumbnailRequest.h \
+	gui/macosx/library/VLCThumbnailRequest.m \
 	gui/macosx/library/VLCLibraryItemInternalMediaItemsDataSource.h \
 	gui/macosx/library/VLCLibraryItemInternalMediaItemsDataSource.m \
 	gui/macosx/library/VLCLibraryItemPresentingCapable.h \


=====================================
modules/gui/macosx/extensions/NSImage+VLCAdditions.h
=====================================
@@ -24,6 +24,9 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
+struct picture_t;
+struct vlc_object_t;
+
 @interface NSImage (VLCAdditions)
 
 // App images
@@ -76,6 +79,9 @@ NS_ASSUME_NONNULL_BEGIN
                   completionHandler:(void (^)(NSImage * _Nullable))completionHandler;
 + (instancetype)quickLookPreviewForLocalPath:(NSString*)path withSize:(NSSize)size;
 + (instancetype)quickLookPreviewForLocalURL:(NSURL*)url withSize:(NSSize)size;
++ (nullable NSImage *)imageFromVLCPicture:(struct picture_t *)picture
+                                vlcObject:(struct vlc_object_t *)object
+                                     size:(NSSize)size;
 + (instancetype)compositeImageWithImages:(NSArray<NSImage *> * const)images
                                   frames:(NSArray<NSValue *> * const)frames
                                     size:(const NSSize)size;


=====================================
modules/gui/macosx/extensions/NSImage+VLCAdditions.m
=====================================
@@ -26,6 +26,8 @@
 
 #import <QuickLook/QuickLook.h>
 #import <QuickLookThumbnailing/QuickLookThumbnailing.h>
+#import <vlc_block.h>
+#import <vlc_picture.h>
 
 static NSImage *ImageFromEmoji(NSString *emoji, NSSize size)
 {
@@ -346,6 +348,61 @@ static NSImage *ImageFromEmoji(NSString *emoji, NSSize size)
     return image;
 }
 
++ (NSImage *)imageFromVLCPicture:(picture_t *)picture
+                       vlcObject:(vlc_object_t *)object
+                            size:(NSSize)size
+{
+    if (picture == NULL || object == NULL) {
+        return nil;
+    }
+
+    block_t *block = NULL;
+    video_format_t format;
+    const int exportStatus = picture_Export(object,
+                                            &block,
+                                            &format,
+                                            picture,
+                                            VLC_CODEC_ARGB,
+                                            (int)size.width,
+                                            (int)size.height,
+                                            true);
+    if (exportStatus != VLC_SUCCESS || block == NULL) {
+        return nil;
+    }
+
+    const NSUInteger bytesPerRow = (NSUInteger)format.i_width * 4;
+    const NSUInteger expectedSize = bytesPerRow * (NSUInteger)format.i_height;
+    if (block->i_buffer < expectedSize) {
+        block_Release(block);
+        return nil;
+    }
+
+    NSBitmapImageRep * const bitmap = [[NSBitmapImageRep alloc]
+        initWithBitmapDataPlanes:NULL
+                      pixelsWide:format.i_width
+                      pixelsHigh:format.i_height
+                   bitsPerSample:8
+                 samplesPerPixel:4
+                        hasAlpha:YES
+                        isPlanar:NO
+                  colorSpaceName:NSDeviceRGBColorSpace
+                    bitmapFormat:NSBitmapFormatThirtyTwoBitLittleEndian
+                     bytesPerRow:bytesPerRow
+                    bitsPerPixel:32];
+    if (bitmap == nil) {
+        block_Release(block);
+        return nil;
+    }
+
+    memcpy(bitmap.bitmapData, block->p_buffer, expectedSize);
+    block_Release(block);
+
+    NSImage * const image = [[NSImage alloc]
+        initWithSize:NSMakeSize(format.i_width, format.i_height)];
+    [image addRepresentation:bitmap];
+    return image;
+}
+
 + (instancetype)compositeImageWithImages:(NSArray<NSImage *> * const)images
                                   frames:(NSArray<NSValue *> * const)frames
                                     size:(const NSSize)size


=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -530,8 +530,8 @@ static const struct input_item_parser_cbs_t parserCallbacks =
 
         NSImage * const workspaceImage = [NSWorkspace.sharedWorkspace iconForFile:path];
         if (workspaceImage) {
-            image.size = size;
-            completionHandler(image);
+            workspaceImage.size = size;
+            completionHandler(workspaceImage);
             return;
         }
 


=====================================
modules/gui/macosx/library/VLCLibraryImageCache.m
=====================================
@@ -27,6 +27,7 @@
 #import "library/VLCInputItem.h"
 #import "library/VLCLibraryDataTypes.h"
 #import "library/VLCLibraryModel.h"
+#import "library/VLCThumbnailRequest.h"
 
 #import "main/VLCMain.h"
 
@@ -34,9 +35,15 @@
 
 #import <ImageIO/ImageIO.h>
 
+#import <vlc_configuration.h>
+#import <vlc_hash.h>
+#import <vlc_preparser.h>
+#import <vlc_strings.h>
+
 NSUInteger kVLCMaximumLibraryImageCacheSize = 500;
 /* 256 MB cost limit based on estimated pixel data size per image */
 static const NSUInteger kVLCLibraryImageCacheCostLimit = 256 * 1024 * 1024;
+static const NSTimeInterval kVLCThumbnailCacheMaximumAge = 30 * 24 * 60 * 60;
 uint32_t kVLCDesiredThumbnailWidth = 512;
 uint32_t kVLCDesiredThumbnailHeight = 512;
 float kVLCDefaultThumbnailPosition = .15;
@@ -47,10 +54,42 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     NSCache *_imageCache;
     NSImage *_noArtImage;
     vlc_medialibrary_t *_p_libraryInstance;
+    NSString *_thumbnailCacheDirectory;
+    NSMutableDictionary<NSString *, VLCThumbnailRequest *> *_pendingThumbnailRequests;
 }
 
+- (void)thumbnailRequest:(VLCThumbnailRequest *)request
+     didFinishWithStatus:(int)status
+                 results:(const bool *)results
+             resultCount:(size_t)resultCount;
+
 @end
 
+static void vlcThumbnailerToFilesOnEnded(vlc_preparser_req *req,
+                                         int status,
+                                         const bool *results,
+                                         size_t resultCount,
+                                         void *data)
+{
+    VLCThumbnailRequest * const request = (__bridge VLCThumbnailRequest *)data;
+    [request.imageCache thumbnailRequest:request
+                     didFinishWithStatus:status
+                                 results:results
+                             resultCount:resultCount];
+    vlc_preparser_req_Release(req);
+}
+
+static NSString *thumbnailHashForString(NSString *string)
+{
+    NSData * const data = [string dataUsingEncoding:NSUTF8StringEncoding];
+    char hash[VLC_HASH_MD5_DIGEST_HEX_SIZE];
+    vlc_hash_md5_t md5;
+    vlc_hash_md5_Init(&md5);
+    vlc_hash_md5_Update(&md5, data.bytes, data.length);
+    vlc_hash_FinishHex(&md5, hash);
+    return [NSString stringWithUTF8String:hash];
+}
+
 @implementation VLCLibraryImageCache
 
 + (NSImage *)downsampledImageFromURL:(NSURL *)url maxPixelSize:(uint32_t)maxPixelSize
@@ -104,6 +143,28 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
         _imageCache.countLimit = kVLCMaximumLibraryImageCacheSize;
         _imageCache.totalCostLimit = kVLCLibraryImageCacheCostLimit;
         _noArtImage = NSImage.VLCNoArtImage;
+        _pendingThumbnailRequests = NSMutableDictionary.dictionary;
+
+        char * const cacheDirectory = config_GetUserDir(VLC_CACHE_DIR);
+        if (cacheDirectory != NULL) {
+            NSString * const thumbnailCacheDirectory =
+                [NSString stringWithFormat:@"%s/macosx-thumbnails", cacheDirectory];
+            free(cacheDirectory);
+
+            NSError *error = nil;
+            if ([NSFileManager.defaultManager createDirectoryAtPath:thumbnailCacheDirectory
+                                        withIntermediateDirectories:YES
+                                                         attributes:@{NSFilePosixPermissions: @0700}
+                                                              error:&error]) {
+                _thumbnailCacheDirectory = [thumbnailCacheDirectory copy];
+                dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
+                    [self pruneStoredThumbnailCache];
+                });
+            } else {
+                NSLog(@"Failed to create thumbnail cache directory %@: %@",
+                      thumbnailCacheDirectory, error);
+            }
+        }
 
         NSNotificationCenter * const notificationCenter = [NSNotificationCenter defaultCenter];
         [notificationCenter addObserver:self
@@ -127,6 +188,106 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     [[NSNotificationCenter defaultCenter] removeObserver:self];
 }
 
+- (void)pruneStoredThumbnailCache
+{
+    if (_thumbnailCacheDirectory == nil) {
+        return;
+    }
+
+    @synchronized (self) {
+        NSArray<NSString *> * const cachedFiles =
+            [NSFileManager.defaultManager contentsOfDirectoryAtPath:_thumbnailCacheDirectory
+                                                              error:NULL];
+        NSDate * const now = NSDate.date;
+
+        for (NSString * const cachedFile in cachedFiles) {
+            if (![cachedFile.pathExtension isEqualToString:@"jpg"]) {
+                continue;
+            }
+
+            NSString * const cachedPath =
+                [_thumbnailCacheDirectory stringByAppendingPathComponent:cachedFile];
+            NSDictionary * const attributes =
+                [NSFileManager.defaultManager attributesOfItemAtPath:cachedPath error:NULL];
+            NSDate * const modificationDate = attributes[NSFileModificationDate];
+            if (modificationDate != nil &&
+                [now timeIntervalSinceDate:modificationDate] > kVLCThumbnailCacheMaximumAge) {
+                [NSFileManager.defaultManager removeItemAtPath:cachedPath error:NULL];
+            }
+        }
+    }
+}
+
+- (nullable NSString *)thumbnailPathForInputItem:(VLCInputItem *)inputItem
+                                        cacheKey:(NSString *)cacheKey
+{
+    if (_thumbnailCacheDirectory == nil || inputItem.path.length == 0 || cacheKey.length == 0) {
+        return nil;
+    }
+
+    NSString * const sourceKey = inputItem.path;
+    NSString * const sourceHash = thumbnailHashForString(sourceKey);
+    NSDictionary * const sourceAttributes =
+        [NSFileManager.defaultManager attributesOfItemAtPath:inputItem.path error:NULL];
+    NSDate * const modificationDate = sourceAttributes[NSFileModificationDate];
+    NSNumber * const sourceSize = sourceAttributes[NSFileSize];
+    NSString * const identifier = [NSString stringWithFormat:@"%@-%@-%llu-%.6f-%ux%u-%.6f",
+                                   sourceKey,
+                                   cacheKey,
+                                   sourceSize.unsignedLongLongValue,
+                                   modificationDate.timeIntervalSince1970,
+                                   kVLCDesiredThumbnailWidth,
+                                   kVLCDesiredThumbnailHeight,
+                                   kVLCDefaultThumbnailPosition];
+    NSString * const thumbnailHash = thumbnailHashForString(identifier);
+
+    return [_thumbnailCacheDirectory stringByAppendingPathComponent:
+            [NSString stringWithFormat:@"%@-%@.jpg", sourceHash, thumbnailHash]];
+}
+
+- (nullable NSImage *)thumbnailFromCacheForInputItem:(VLCInputItem *)inputItem
+                                            cacheKey:(NSString *)cacheKey
+{
+    NSString * const thumbnailPath = [self thumbnailPathForInputItem:inputItem
+                                                            cacheKey:cacheKey];
+    if (thumbnailPath == nil) {
+        return nil;
+    }
+
+    [self removeStaleThumbnailFilesForInputItem:inputItem
+                                       cacheKey:cacheKey
+                                    keepingPath:thumbnailPath];
+    NSImage * const image = [[NSImage alloc] initWithContentsOfFile:thumbnailPath];
+    if (image == nil && [NSFileManager.defaultManager fileExistsAtPath:thumbnailPath]) {
+        [NSFileManager.defaultManager removeItemAtPath:thumbnailPath error:NULL];
+    }
+    return image;
+}
+
+- (void)removeStaleThumbnailFilesForInputItem:(VLCInputItem *)inputItem
+                                     cacheKey:(NSString *)cacheKey
+                                  keepingPath:(NSString *)keepingPath
+{
+    if (_thumbnailCacheDirectory == nil || inputItem.path.length == 0 || cacheKey.length == 0) {
+        return;
+    }
+
+    NSString * const sourceKey = inputItem.path;
+    NSString * const sourcePrefix = [thumbnailHashForString(sourceKey) stringByAppendingString:@"-"];
+    NSArray<NSString *> * const cachedFiles =
+        [NSFileManager.defaultManager contentsOfDirectoryAtPath:_thumbnailCacheDirectory error:NULL];
+    for (NSString * const cachedFile in cachedFiles) {
+        if (![cachedFile hasPrefix:sourcePrefix] || ![cachedFile.pathExtension isEqualToString:@"jpg"]) {
+            continue;
+        }
+
+        NSString * const cachedPath = [_thumbnailCacheDirectory stringByAppendingPathComponent:cachedFile];
+        if (![cachedPath isEqualToString:keepingPath]) {
+            [NSFileManager.defaultManager removeItemAtPath:cachedPath error:NULL];
+        }
+    }
+}
+
 + (instancetype)sharedImageCache
 {
     static dispatch_once_t onceToken;
@@ -174,7 +335,7 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
         dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
             NSImage * const image =
                 [VLCLibraryImageCache downsampledImageFromURL:artworkURL
-                                                maxPixelSize:kVLCDesiredThumbnailWidth];
+                                                 maxPixelSize:kVLCDesiredThumbnailWidth];
             if (image) {
                 const NSUInteger cost = [VLCLibraryImageCache costForImage:image];
                 [self->_imageCache setObject:image forKey:artworkMRL cost:cost];
@@ -242,7 +403,8 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 - (void)imageForInputItem:(VLCInputItem *)inputItem 
            withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
 {
-    NSImage * const cachedImage = [_imageCache objectForKey:inputItem.MRL];
+    NSString * const memoryCacheKey = inputItem.MRL;
+    NSImage * const cachedImage = [_imageCache objectForKey:memoryCacheKey];
     if (cachedImage) {
         completionHandler(cachedImage);
         return;
@@ -255,30 +417,81 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
 {
     NSURL * const artworkURL = inputItem.artworkURL;
     const NSSize imageSize = NSMakeSize(kVLCDesiredThumbnailWidth, kVLCDesiredThumbnailHeight);
+    NSString * const memoryCacheKey = inputItem.MRL;
 
     dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
-        NSImage * const image = artworkURL ?
-            [VLCLibraryImageCache downsampledImageFromURL:artworkURL
-                                            maxPixelSize:kVLCDesiredThumbnailWidth] : nil;
+        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];
+            [self->_imageCache setObject:image forKey:memoryCacheKey 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];
+        if (inputItem.inputType == ITEM_TYPE_FILE &&
+            !inputItem.isStream &&
+            input_item_Playable(inputItem.path.UTF8String)) {
+            NSImage * const cachedThumbnail =
+                [self thumbnailFromCacheForInputItem:inputItem
+                                            cacheKey:memoryCacheKey];
+            if (cachedThumbnail != nil) {
+                const NSUInteger cost = [VLCLibraryImageCache costForImage:cachedThumbnail];
+                [self->_imageCache setObject:cachedThumbnail forKey:memoryCacheKey cost:cost];
+                dispatch_async(dispatch_get_main_queue(), ^{
+                    completionHandler(cachedThumbnail);
+                });
+                return;
+            }
+
+            [self generateVLCThumbnailForInputItem:inputItem
+                                          cacheKey:inputItem.MRL
+                                        completion:^(NSImage * const thumbnail) {
+                if (thumbnail != nil) {
+                    const NSUInteger cost = [VLCLibraryImageCache costForImage:thumbnail];
+                    [self->_imageCache setObject:thumbnail forKey:memoryCacheKey cost:cost];
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        completionHandler(thumbnail);
+                    });
+                    return;
+                }
+
+                [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const quickLookThumbnail) {
+                    if (quickLookThumbnail) {
+                        const NSUInteger cost = [VLCLibraryImageCache costForImage:quickLookThumbnail];
+                        [self->_imageCache setObject:quickLookThumbnail forKey:memoryCacheKey cost:cost];
+                    } else {
+                        NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
+                    }
+                    dispatch_async(dispatch_get_main_queue(), ^{
+                        completionHandler(quickLookThumbnail ?: self->_noArtImage);
+                    });
+                }];
+            }];
+            return;
+        }
+
+        if (inputItem.isStream || inputItem.path.length == 0) {
+            dispatch_async(dispatch_get_main_queue(), ^{
+                completionHandler(self->_noArtImage);
+            });
+            return;
+        }
+
+        [inputItem thumbnailWithSize:imageSize completionHandler:^(NSImage * const quickLookThumbnail) {
+            if (quickLookThumbnail) {
+                const NSUInteger cost = [VLCLibraryImageCache costForImage:quickLookThumbnail];
+                [self->_imageCache setObject:quickLookThumbnail forKey:memoryCacheKey cost:cost];
             } else {
                 NSLog(@"Failed to generate thumbnail for input item %@", inputItem.MRL);
             }
             dispatch_async(dispatch_get_main_queue(), ^{
-                completionHandler(thumbnail ?: self->_noArtImage);
+                completionHandler(quickLookThumbnail ?: self->_noArtImage);
             });
         }];
     });
@@ -303,8 +516,106 @@ const NSUInteger kVLCCompositeImageDefaultCompositedGridItemCount = 4;
     }];
 }
 
+- (void)generateVLCThumbnailForInputItem:(VLCInputItem *)inputItem
+                                cacheKey:(NSString *)cacheKey
+                              completion:(VLCThumbnailCompletion)completion
+{
+    VLCThumbnailRequest *request;
+    @synchronized (self) {
+        request = _pendingThumbnailRequests[cacheKey];
+        if (request != nil) {
+            [request.completionHandlers addObject:[completion copy]];
+            return;
+        }
+
+        request = [[VLCThumbnailRequest alloc] init];
+        request.imageCache = self;
+        request.cacheKey = cacheKey;
+        request.thumbnailPath = [self thumbnailPathForInputItem:inputItem
+                                                       cacheKey:cacheKey];
+        request.completionHandlers = [NSMutableArray arrayWithObject:[completion copy]];
+        _pendingThumbnailRequests[cacheKey] = request;
+    }
+
+    vlc_preparser_t * const thumbnailer = getThumbnailer();
+    if (thumbnailer == NULL || request.thumbnailPath == nil) {
+        [self thumbnailRequest:request
+           didFinishWithStatus:VLC_EGENERIC
+                       results:NULL
+                   resultCount:0];
+        return;
+    }
+
+    struct vlc_thumbnailer_arg thumbnailerArgument = {
+        .seek = {
+            .type = VLC_THUMBNAILER_SEEK_POS,
+            .pos = kVLCDefaultThumbnailPosition,
+            .speed = VLC_THUMBNAILER_SEEK_FAST,
+        },
+        .hw_dec = false,
+    };
+    const struct vlc_thumbnailer_output output = {
+        .format = VLC_THUMBNAILER_FORMAT_JPEG,
+        .width = kVLCDesiredThumbnailWidth,
+        .height = kVLCDesiredThumbnailHeight,
+        .crop = true,
+        .file_path = request.thumbnailPath.fileSystemRepresentation,
+        .creat_mode = 0600,
+    };
+    static const struct vlc_thumbnailer_to_files_cbs callbacks = {
+        .on_ended = vlcThumbnailerToFilesOnEnded,
+    };
+
+    vlc_preparser_req * const requestHandle = vlc_preparser_GenerateThumbnailToFiles(
+        thumbnailer,
+        inputItem.vlcInputItem,
+        &thumbnailerArgument,
+        &output,
+        1,
+        &callbacks,
+        (__bridge void *)request);
+    if (requestHandle == NULL) {
+        [self thumbnailRequest:request
+           didFinishWithStatus:VLC_EGENERIC
+                       results:NULL
+                   resultCount:0];
+    }
+}
+
+- (void)thumbnailRequest:(VLCThumbnailRequest *)request
+     didFinishWithStatus:(int)status
+                 results:(const bool *)results
+             resultCount:(size_t)resultCount
+{
+    NSArray<VLCThumbnailCompletion> *completionHandlers;
+    @synchronized (self) {
+        [_pendingThumbnailRequests removeObjectForKey:request.cacheKey];
+        completionHandlers = [request.completionHandlers copy];
+    }
+
+    if (status == -EINTR) {
+        for (VLCThumbnailCompletion completion in completionHandlers) {
+            completion(nil);
+        }
+        return;
+    }
+
+    NSImage *image = nil;
+    if (status == VLC_SUCCESS && resultCount == 1 && results != NULL && results[0]) {
+        image = [[NSImage alloc] initWithContentsOfFile:request.thumbnailPath];
+    }
+
+    if (image == nil && request.thumbnailPath != nil) {
+        [NSFileManager.defaultManager removeItemAtPath:request.thumbnailPath error:NULL];
+    }
+
+    for (VLCThumbnailCompletion completion in completionHandlers) {
+        completion(image);
+    }
+}
+
 + (void)thumbnailForPlayQueueItem:(VLCPlayQueueItem *)playQueueItem
-                  withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
+                   withCompletion:(nonnull void (^)(const NSImage * _Nonnull))completionHandler
 {
     return [VLCLibraryImageCache.sharedImageCache imageForInputItem:playQueueItem.inputItem
                                                      withCompletion:completionHandler];


=====================================
modules/gui/macosx/library/VLCThumbnailRequest.h
=====================================
@@ -0,0 +1,40 @@
+/*****************************************************************************
+* VLCThumbnailRequest.h: MacOS X interface module
+*****************************************************************************
+* Copyright (C) 2026 VLC authors and VideoLAN
+*
+* Authors: Claudio Cambra <developer at claudiocambra.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* 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.
+*****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at class VLCLibraryImageCache;
+
+typedef void (^VLCThumbnailCompletion)(NSImage * _Nullable image);
+
+ at interface VLCThumbnailRequest : NSObject
+
+ at property (nonatomic, weak, nullable) VLCLibraryImageCache *imageCache;
+ at property (nonatomic, copy) NSString *cacheKey;
+ at property (nonatomic, copy, nullable) NSString *thumbnailPath;
+ at property (nonatomic, strong) NSMutableArray<VLCThumbnailCompletion> *completionHandlers;
+
+ at end
+
+NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/VLCThumbnailRequest.m
=====================================
@@ -0,0 +1,26 @@
+/*****************************************************************************
+* VLCThumbnailRequest.m: MacOS X interface module
+*****************************************************************************
+* Copyright (C) 2026 VLC authors and VideoLAN
+*
+* Authors: Claudio Cambra <developer at claudiocambra.com>
+*
+* This program is free software; you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation; either version 2 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* 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.
+*****************************************************************************/
+
+#import "VLCThumbnailRequest.h"
+
+ at implementation VLCThumbnailRequest
+ at end


=====================================
modules/gui/macosx/main/VLCMain.h
=====================================
@@ -41,6 +41,7 @@ typedef struct vlc_preparser_t vlc_preparser_t;
 
 intf_thread_t *getIntf(void);
 vlc_preparser_t *getNetworkPreparser();
+vlc_preparser_t *getThumbnailer();
 
 extern NSString *VLCConfigurationChangedNotification;
 extern NSString * const kVLCPreferencesVersion;


=====================================
modules/gui/macosx/main/VLCMain.m
=====================================
@@ -143,6 +143,7 @@ NSString * const kVLCPreferencesVersion = @"VLCPreferencesVersion";
 
 static intf_thread_t *p_interface_thread;
 static vlc_preparser_t *p_network_preparser;
+static vlc_preparser_t *p_thumbnailer;
 vlc_sem_t g_wait_quit;
 
 intf_thread_t *getIntf()
@@ -155,6 +156,11 @@ vlc_preparser_t *getNetworkPreparser()
     return p_network_preparser;
 }
 
+vlc_preparser_t *getThumbnailer()
+{
+    return p_thumbnailer;
+}
+
 int OpenIntf (vlc_object_t *p_this)
 {
     __block int retcode = VLC_SUCCESS;
@@ -177,6 +183,17 @@ int OpenIntf (vlc_object_t *p_this)
                 dispatch_semaphore_signal(sem);
                 return;
             }
+
+            const struct vlc_preparser_cfg thumbnailer_cfg = {
+                .types = VLC_PREPARSER_TYPE_THUMBNAIL_TO_FILES,
+                .max_thumbnailer_threads = 1,
+                .timeout = VLC_TICK_FROM_SEC(3),
+                .external_process = false,
+            };
+            p_thumbnailer = vlc_preparser_New(p_this, &thumbnailer_cfg);
+            if (p_thumbnailer == NULL) {
+                msg_Warn(p_intf, "Could not initialize the thumbnailer");
+            }
             msg_Dbg(p_intf, "Starting macosx interface");
 
             @try {
@@ -212,6 +229,11 @@ void CloseIntf (vlc_object_t *p_this)
         @autoreleasepool {
             msg_Dbg(p_this, "Closing macosx interface");
             [VLCMain.sharedInstance applicationWillTerminate:nil];
+            if (p_thumbnailer != NULL) {
+                vlc_preparser_Cancel(p_thumbnailer, NULL);
+                vlc_preparser_Delete(p_thumbnailer);
+                p_thumbnailer = NULL;
+            }
             [VLCMain killInstance];
         }
         vlc_preparser_Delete(p_network_preparser);



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/26e02eae8da67914d1a65f91a8fa19c67617d378...6661dbda26b71cf24a8a663e86fc5ea0f955c6f1

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/26e02eae8da67914d1a65f91a8fa19c67617d378...6661dbda26b71cf24a8a663e86fc5ea0f955c6f1
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