[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