VLCThumbnailsCache.m 11.8 KB
Newer Older
1 2 3 4
/*****************************************************************************
 * VLCThumbnailsCache.m
 * VLC for iOS
 *****************************************************************************
5
 * Copyright (c) 2013-2015 VideoLAN. All rights reserved.
6 7 8 9
 * $Id$
 *
 * Authors: Gleb Pinigin <gpinigin # gmail.com>
 *          Felix Paul Kühne <fkuehne # videolan.org>
10
 *          Carola Nitz <caro # videolan.org>
11
 *          Tobias Conradi <videolan # tobias-conradi.de>
12 13 14
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
15 16

#import "VLCThumbnailsCache.h"
17
#import <CommonCrypto/CommonDigest.h>
18
#import "UIImage+Blur.h"
19
#import <WatchKit/WatchKit.h>
20 21
#import <CoreData/CoreData.h>
#import <MediaLibraryKit/MediaLibraryKit.h>
22
#import <MediaLibraryKit/UIImage+MLKit.h>
Felix Paul Kühne's avatar
Felix Paul Kühne committed
23 24 25
#if TARGET_OS_IOS
#import <UIKit/UIKit.h>
#endif
26

27
@interface VLCThumbnailsCache() {
28
    NSInteger MaxCacheSize;
29 30
    NSCache *_thumbnailCache;
    NSCache *_thumbnailCacheMetadata;
31
    NSInteger _currentDeviceIdiom;
32 33
}
@end
34 35 36 37 38

@implementation VLCThumbnailsCache

#define MAX_CACHE_SIZE_IPHONE 21  // three times the number of items shown on iPhone 5
#define MAX_CACHE_SIZE_IPAD   27  // three times the number of items shown on iPad
39
#define MAX_CACHE_SIZE_WATCH  15  // three times the number of items shown on 42mm Watch
40

41
- (instancetype)init
42
{
43 44
    self = [super init];
    if (self) {
45
// TODO: correct for watch
Felix Paul Kühne's avatar
Felix Paul Kühne committed
46
#if TARGET_OS_IOS
47 48
        _currentDeviceIdiom = [[UIDevice currentDevice] userInterfaceIdiom];
        MaxCacheSize = 0;
49

50
        switch (_currentDeviceIdiom) {
51
            case UIUserInterfaceIdiomPad:
52
                MaxCacheSize = MAX_CACHE_SIZE_IPAD;
53 54
                break;
            case UIUserInterfaceIdiomPhone:
55
                MaxCacheSize = MAX_CACHE_SIZE_IPHONE;
56
                break;
57

58
            default:
59
                MaxCacheSize = MAX_CACHE_SIZE_WATCH;
60 61
                break;
        }
62 63
#else
        MaxCacheSize = MAX_CACHE_SIZE_WATCH;
64
#endif
65 66
        _thumbnailCache = [[NSCache alloc] init];
        _thumbnailCacheMetadata = [[NSCache alloc] init];
67 68
        [_thumbnailCache setCountLimit: MaxCacheSize];
        [_thumbnailCacheMetadata setCountLimit: MaxCacheSize];
69 70 71
    }
    return self;
}
72

73 74 75 76 77 78 79 80 81 82 83
+ (instancetype)sharedThumbnailCache
{
    static dispatch_once_t onceToken;
    static VLCThumbnailsCache *sharedThumbnailCache;
    dispatch_once(&onceToken, ^{
        sharedThumbnailCache = [[VLCThumbnailsCache alloc] init];
    });

    return sharedThumbnailCache;
}

84
+ (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object
85 86 87 88 89 90
{
    return [self thumbnailForManagedObject:object refreshCache:NO];
}

+ (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object
                          refreshCache:(BOOL)refreshCache
91 92
{
    UIImage *thumbnail;
93
    VLCThumbnailsCache *cache = [VLCThumbnailsCache sharedThumbnailCache];
94
    if ([object isKindOfClass:[MLShow class]]) {
95
        thumbnail = [cache thumbnailForShow:(MLShow *)object refreshCache:refreshCache];
96 97
    } else if ([object isKindOfClass:[MLShowEpisode class]]) {
        MLFile *anyFileFromEpisode = [(MLShowEpisode *)object files].anyObject;
98
        thumbnail = [cache thumbnailForMediaFile:anyFileFromEpisode refreshCache:refreshCache];
99
    } else if ([object isKindOfClass:[MLLabel class]]) {
100
        thumbnail = [cache thumbnailForLabel:(MLLabel *)object refreshCache:refreshCache];
101
    } else if ([object isKindOfClass:[MLAlbum class]]) {
102
        thumbnail = [cache thumbnailForAlbum:(MLAlbum *)object refreshCache:refreshCache];
103
    } else if ([object isKindOfClass:[MLAlbumTrack class]]) {
104
        thumbnail = [cache thumbnailForAlbumTrack:(MLAlbumTrack *)object refreshCache:refreshCache];
105
    } else {
106
        thumbnail = [cache thumbnailForMediaFile:(MLFile *)object refreshCache:refreshCache];
107 108 109 110
    }
    return thumbnail;
}

111
+ (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object refreshCache:(BOOL)refreshCache toFitRect:(CGRect)rect scale:(CGFloat)scale shouldReplaceCache:(BOOL)replaceCache;
112
{
113
    UIImage *rawThumbnail = [self thumbnailForManagedObject:object refreshCache:refreshCache];
114
    CGSize rawSize = rawThumbnail.size;
115
    CGFloat rawScale = rawThumbnail.scale;
116 117

    /* scaling is potentially expensive, so we should avoid re-doing it for the same size over and over again */ 
118
    if (rawSize.width*rawScale <= rect.size.width*scale && rawSize.height*rawScale <= rect.size.height*scale)
119 120
        return rawThumbnail;

121
    UIImage *scaledImage = [UIImage scaleImage:rawThumbnail toFitRect:rect scale:scale];
122 123 124 125 126 127 128 129 130

    if (replaceCache)
        [[VLCThumbnailsCache sharedThumbnailCache] _setThumbnail:scaledImage forObjectId:object.objectID];

    return scaledImage;
}

- (void)_setThumbnail:(UIImage *)image forObjectId:(NSManagedObjectID *)objID
{
131 132
    if (image)
        [_thumbnailCache setObject:image forKey:objID];
133 134
}

135
- (UIImage *)thumbnailForMediaFile:(MLFile *)mediaFile refreshCache:(BOOL)refreshCache
136 137 138 139 140
{
    if (mediaFile == nil || mediaFile.objectID == nil)
        return nil;

    NSManagedObjectID *objID = mediaFile.objectID;
141
    UIImage *displayedImage;
142

143 144 145 146 147
    if (!refreshCache) {
        displayedImage = [_thumbnailCache objectForKey:objID];
        if (displayedImage)
            return displayedImage;
    }
148

149 150 151 152 153 154 155 156 157 158 159
    if (!displayedImage) {
        __block UIImage *computedImage = nil;
        void (^getThumbnailBlock)(void) = ^(){
            computedImage = mediaFile.computedThumbnail;
        };
        if ([NSThread isMainThread])
            getThumbnailBlock();
        else
            dispatch_sync(dispatch_get_main_queue(), getThumbnailBlock);
        displayedImage = computedImage;
    }
160

161 162 163 164 165 166 167 168 169 170
    if (!displayedImage) {
        if ([mediaFile isKindOfType:@"audio"]) {
            displayedImage = [UIImage imageNamed:@"no-artwork"];
        } else if ([mediaFile isKindOfType:@"movie"] ||
                   [mediaFile isKindOfType:@"tvShowEpisode"] ||
                   [mediaFile isKindOfType:@"clip"]) {
            displayedImage = [UIImage imageNamed:@"tvShow"];
        }
    }

171 172
    if (displayedImage)
        [_thumbnailCache setObject:displayedImage forKey:objID];
173 174 175 176

    return displayedImage;
}

177
- (UIImage *)thumbnailForShow:(MLShow *)mediaShow refreshCache:(BOOL)refreshCache
178 179
{
    NSManagedObjectID *objID = mediaShow.objectID;
180
    UIImage *displayedImage;
181 182 183 184 185 186 187
    BOOL forceRefresh = NO;

    NSUInteger count = [mediaShow.episodes count];
    NSNumber *previousCount = [_thumbnailCacheMetadata objectForKey:objID];

    if (previousCount.unsignedIntegerValue != count)
        forceRefresh = YES;
188

189 190 191
    if (refreshCache)
        forceRefresh = YES;

192 193 194 195 196
    if (!forceRefresh) {
        displayedImage = [_thumbnailCache objectForKey:objID];
        if (displayedImage)
            return displayedImage;
    }
197 198 199 200

    NSUInteger fileNumber = count > 3 ? 3 : count;
    NSArray *episodes = [mediaShow.episodes allObjects];
    NSMutableArray *files = [[NSMutableArray alloc] init];
201 202 203 204 205 206
    for (NSUInteger x = 0; x < count; x++) {
        /* this is a multi-threaded app, so the episode object might be there already,
         * but without an assigned file, so we need to check for its existance (#13128) */
        if ([episodes[x] files].anyObject != nil)
            [files addObject:[episodes[x] files].anyObject];
    }
207

208
    displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:NO];
209
    if (displayedImage) {
210
        [_thumbnailCache setObject:displayedImage forKey:objID];
211 212
        [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
    }
213 214 215 216

    return displayedImage;
}

217
- (UIImage *)thumbnailForLabel:(MLLabel *)mediaLabel refreshCache:(BOOL)refreshCache
218 219
{
    NSManagedObjectID *objID = mediaLabel.objectID;
220
    UIImage *displayedImage;
221 222 223 224 225 226 227
    BOOL forceRefresh = NO;

    NSUInteger count = [mediaLabel.files count];
    NSNumber *previousCount = [_thumbnailCacheMetadata objectForKey:objID];

    if (previousCount.unsignedIntegerValue != count)
        forceRefresh = YES;
228

229 230 231
    if (refreshCache)
        forceRefresh = YES;

232 233 234 235 236
    if (!forceRefresh) {
        displayedImage = [_thumbnailCache objectForKey:objID];
        if (displayedImage)
            return displayedImage;
    }
237

238
    NSUInteger fileNumber = count > 3 ? 3 : count;
239
    NSArray *files = [mediaLabel.files allObjects];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
240 241

    displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:YES];
242
    if (displayedImage) {
243
        [_thumbnailCache setObject:displayedImage forKey:objID];
244 245
        [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
    }
246 247 248

    return displayedImage;
}
249

250
- (UIImage *)thumbnailForAlbum:(MLAlbum *)album refreshCache:(BOOL)refreshCache
251 252 253 254 255 256 257 258 259 260
{
    __block MLAlbumTrack *track = nil;
    void (^getFileBlock)(void) = ^(){
        track = [album tracks].anyObject;
    };
    if ([NSThread isMainThread])
        getFileBlock();
    else
        dispatch_sync(dispatch_get_main_queue(), getFileBlock);

261
    return [self thumbnailForAlbumTrack:track refreshCache:refreshCache];
262 263
}

264
- (UIImage *)thumbnailForAlbumTrack:(MLAlbumTrack *)albumTrack refreshCache:(BOOL)refreshCache
265 266 267
{
    __block MLFile *anyFileFromAnyTrack = nil;
    void (^getFileBlock)(void) = ^(){
268
        anyFileFromAnyTrack = [albumTrack anyFileFromTrack];
269 270 271 272 273
    };
    if ([NSThread isMainThread])
        getFileBlock();
    else
        dispatch_sync(dispatch_get_main_queue(), getFileBlock);
274
    return [self thumbnailForMediaFile:anyFileFromAnyTrack refreshCache:refreshCache];
275 276
}

277
- (UIImage *)clusterThumbFromFiles:(NSArray *)files andNumber:(NSUInteger)fileNumber blur:(BOOL)blurImage
278 279
{
    UIImage *clusterThumb;
280
    CGSize imageSize = CGSizeZero;
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
    // TODO: correct for watch
#ifndef TARGET_OS_WATCH
    if (_currentDeviceIdiom == UIUserInterfaceIdiomPad) {
        if ([UIScreen mainScreen].scale==2.0)
            imageSize = CGSizeMake(682., 384.);
        else
            imageSize = CGSizeMake(341., 192.);
    } else if (_currentDeviceIdiom == UIUserInterfaceIdiomPhone) {
        if ([UIScreen mainScreen].scale==2.0)
            imageSize = CGSizeMake(480., 270.);
        else
            imageSize = CGSizeMake(720., 405.);
    } else
#endif
    {
296
        if (@available(iOS 8.2, *)) {
297 298 299 300 301 302
            if (WKInterfaceDevice.currentDevice != nil) {
                CGRect screenRect = WKInterfaceDevice.currentDevice.screenBounds;
                imageSize = CGSizeMake(screenRect.size.width * WKInterfaceDevice.currentDevice.screenScale, 120.);
            }
        }
    }
303

304
    UIGraphicsBeginImageContext(imageSize);
305 306
    NSUInteger iter = files.count < fileNumber ? files.count : fileNumber;
    for (NSUInteger i = 0; i < iter; i++) {
307
        MLFile *file =  [files objectAtIndex:i];
308
        clusterThumb = [self thumbnailForMediaFile:file refreshCache:NO];
309
        CGContextRef context = UIGraphicsGetCurrentContext();
310
        CGFloat imagePartWidth = (imageSize.width / iter);
311
        //the rect in which the image should be drawn
312
        CGRect clippingRect = CGRectMake(imagePartWidth * i, 0, imagePartWidth, imageSize.height);
313 314 315
        CGContextSaveGState(context);
        CGContextClipToRect(context, clippingRect);
        //take the center of the clippingRect and calculate the offset from the original center
316
        CGFloat centerOffset = (imagePartWidth * i + imagePartWidth / 2) - imageSize.width / 2;
317
        //shift the rect to draw the middle of the image in the clippingrect
318
        CGRect drawingRect = CGRectMake(centerOffset, 0, imageSize.width, imageSize.height);
Felix Paul Kühne's avatar
Felix Paul Kühne committed
319 320
        if (clusterThumb != nil)
            [clusterThumb drawInRect:drawingRect];
321 322 323
        //get rid of the old clippingRect
        CGContextRestoreGState(context);
    }
324
    clusterThumb = UIGraphicsGetImageFromCurrentImageContext();
325 326
    UIGraphicsEndImageContext();

327 328
    if (!blurImage)
        return clusterThumb;
329 330
// TODO: When we move to watch os 4.0 we can include the blurcategory and remove the if else block
#ifndef TARGET_OS_WATCH
331
    return [UIImage applyBlurOnImage:clusterThumb withRadius:0.1];
332 333 334
#else
    return clusterThumb;
#endif
335 336
}

337
@end