VLCThumbnailsCache.m 11.4 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
12
13
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
14

15
#import "VLC for iOS-Prefix.pch"
16
#import "VLCThumbnailsCache.h"
17
#import <CommonCrypto/CommonDigest.h>
18
#import "UIImage+Blur.h"
19
#import "UIImage+Scaling.h"
20
21
#import <WatchKit/WatchKit.h>

22
23
24
25
26
27
28
@interface VLCThumbnailsCache() {
    NSInteger MaxCacheSize;
    NSCache *_thumbnailCache;
    NSCache *_thumbnailCacheMetadata;
    NSInteger _currentDeviceIdiom;
}
@end
29
30
31
32
33

@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
34
#define MAX_CACHE_SIZE_WATCH  15  // three times the number of items shown on 42mm Watch
35

36
- (instancetype)init
37
{
38
39
40
41
    self = [super init];
    if (self) {
        _currentDeviceIdiom = [[UIDevice currentDevice] userInterfaceIdiom];
        MaxCacheSize = 0;
42

43
44
45
46
47
48
49
        switch (_currentDeviceIdiom) {
            case UIUserInterfaceIdiomPad:
                MaxCacheSize = MAX_CACHE_SIZE_IPAD;
                break;
            case UIUserInterfaceIdiomPhone:
                MaxCacheSize = MAX_CACHE_SIZE_IPHONE;
                break;
50

51
52
53
54
            default:
                MaxCacheSize = MAX_CACHE_SIZE_WATCH;
                break;
        }
55

56
57
58
59
60
61
62
        _thumbnailCache = [[NSCache alloc] init];
        _thumbnailCacheMetadata = [[NSCache alloc] init];
        [_thumbnailCache setCountLimit: MaxCacheSize];
        [_thumbnailCacheMetadata setCountLimit: MaxCacheSize];
    }
    return self;
}
63

64
65
66
67
68
69
70
71
72
73
74
+ (instancetype)sharedThumbnailCache
{
    static dispatch_once_t onceToken;
    static VLCThumbnailsCache *sharedThumbnailCache;
    dispatch_once(&onceToken, ^{
        sharedThumbnailCache = [[VLCThumbnailsCache alloc] init];
    });

    return sharedThumbnailCache;
}

75
- (NSString *)_md5FromString:(NSString *)string
76
77
78
{
    const char *ptr = [string UTF8String];
    unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
79
    CC_MD5(ptr, (unsigned int)strlen(ptr), md5Buffer);
80
81
82
83
84
85
86
    NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
        [output appendFormat:@"%02x",md5Buffer[i]];

    return [NSString stringWithString:output];
}

87
+ (UIImage *)thumbnailForMediaItemWithTitle:(NSString *)title Artist:(NSString*)artist andAlbumName:(NSString*)albumname
88
{
89
    return [UIImage imageWithContentsOfFile:[[VLCThumbnailsCache sharedThumbnailCache] artworkPathForMediaItemWithTitle:title Artist:artist andAlbumName:albumname]];
90
}
91

92
- (NSString *)artworkPathForMediaItemWithTitle:(NSString *)title Artist:(NSString*)artist andAlbumName:(NSString*)albumname
93
94
{
    NSString *artworkURL;
95
96
97
98
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *cacheDir = searchPaths[0];
    cacheDir = [cacheDir stringByAppendingFormat:@"/%@", [[NSBundle mainBundle] bundleIdentifier]];

99
    if (artist.length == 0 || albumname.length == 0) {
100
101
102
103
        /* Use generated hash to find art */
        artworkURL = [cacheDir stringByAppendingFormat:@"/art/arturl/%@/art.jpg", [self _md5FromString:title]];
    } else {
        /* Otherwise, it was cached by artist and album */
104
        artworkURL = [cacheDir stringByAppendingFormat:@"/art/artistalbum/%@/%@/art.jpg", artist, albumname];
105
106
107
108
109
    }

    return artworkURL;
}

110
- (NSString *)_getArtworkPathFromMedia:(MLFile *)file
111
112
113
114
115
116
117
118
119
120
121
122
{
    NSString *artist, *album, *title;

    if (file.isAlbumTrack) {
        artist = file.albumTrack.artist;
        album = file.albumTrack.album.name;
    }
    title = file.title;

    return [self artworkPathForMediaItemWithTitle:title Artist:artist andAlbumName:album];
}

123
124
125
+ (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object
{
    UIImage *thumbnail;
126
    VLCThumbnailsCache *cache = [VLCThumbnailsCache sharedThumbnailCache];
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
    if ([object isKindOfClass:[MLShow class]]) {
        thumbnail = [cache thumbnailForShow:(MLShow *)object];
    } else if ([object isKindOfClass:[MLShowEpisode class]]) {
        MLFile *anyFileFromEpisode = [(MLShowEpisode *)object files].anyObject;
        thumbnail = [cache thumbnailForMediaFile:anyFileFromEpisode];
    } else if ([object isKindOfClass:[MLLabel class]]) {
        thumbnail = [cache thumbnailForLabel:(MLLabel *)object];
    } else if ([object isKindOfClass:[MLAlbum class]]) {
        MLFile *anyFileFromAnyTrack = [[(MLAlbum *)object tracks].anyObject files].anyObject;
        thumbnail = [cache thumbnailForMediaFile:anyFileFromAnyTrack];
    } else if ([object isKindOfClass:[MLAlbumTrack class]]) {
        MLFile *anyFileFromTrack = [(MLAlbumTrack *)object files].anyObject;
        thumbnail = [cache thumbnailForMediaFile:anyFileFromTrack];
    } else {
        thumbnail = [cache thumbnailForMediaFile:(MLFile *)object];
    }
    return thumbnail;
}

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
+ (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object toFitRect:(CGRect)rect shouldReplaceCache:(BOOL)replaceCache
{
    UIImage *rawThumbnail = [self thumbnailForManagedObject:object];
    CGSize rawSize = rawThumbnail.size;

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

    UIImage *scaledImage = [UIImage scaleImage:rawThumbnail toFitRect:rect];

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

    return scaledImage;
}

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

168
- (UIImage *)thumbnailForMediaFile:(MLFile *)mediaFile
169
170
171
172
173
{
    if (mediaFile == nil || mediaFile.objectID == nil)
        return nil;

    NSManagedObjectID *objID = mediaFile.objectID;
174
175
176
177
178
    UIImage *displayedImage = [_thumbnailCache objectForKey:objID];

    if (displayedImage)
        return displayedImage;

179
180
181
182
183
184
    if (mediaFile.isAlbumTrack || mediaFile.isShowEpisode)
        displayedImage = [UIImage imageWithContentsOfFile:[self _getArtworkPathFromMedia:mediaFile]];

    if (!displayedImage)
        displayedImage = mediaFile.computedThumbnail;

185
186
    if (displayedImage)
        [_thumbnailCache setObject:displayedImage forKey:objID];
187
188
189
190

    return displayedImage;
}

191
- (UIImage *)thumbnailForShow:(MLShow *)mediaShow
192
193
{
    NSManagedObjectID *objID = mediaShow.objectID;
194
    UIImage *displayedImage;
195
196
197
198
199
200
201
    BOOL forceRefresh = NO;

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

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

203
204
205
206
207
    if (!forceRefresh) {
        displayedImage = [_thumbnailCache objectForKey:objID];
        if (displayedImage)
            return displayedImage;
    }
208
209
210
211

    NSUInteger fileNumber = count > 3 ? 3 : count;
    NSArray *episodes = [mediaShow.episodes allObjects];
    NSMutableArray *files = [[NSMutableArray alloc] init];
212
213
214
215
216
217
    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];
    }
218

219
    displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:NO];
220
    if (displayedImage) {
221
        [_thumbnailCache setObject:displayedImage forKey:objID];
222
223
        [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
    }
224
225
226
227

    return displayedImage;
}

228
- (UIImage *)thumbnailForLabel:(MLLabel *)mediaLabel
229
230
{
    NSManagedObjectID *objID = mediaLabel.objectID;
231
    UIImage *displayedImage;
232
233
234
235
236
237
238
    BOOL forceRefresh = NO;

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

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

240
241
242
243
244
    if (!forceRefresh) {
        displayedImage = [_thumbnailCache objectForKey:objID];
        if (displayedImage)
            return displayedImage;
    }
245

246
    NSUInteger fileNumber = count > 3 ? 3 : count;
247
    NSArray *files = [mediaLabel.files allObjects];
248
249
250
251
    BOOL blur = NO;
    if (SYSTEM_RUNS_IOS7_OR_LATER)
        blur = YES;
    displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:blur];
252
    if (displayedImage) {
253
        [_thumbnailCache setObject:displayedImage forKey:objID];
254
255
        [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
    }
256
257
258

    return displayedImage;
}
259

260
- (UIImage *)clusterThumbFromFiles:(NSArray *)files andNumber:(NSUInteger)fileNumber blur:(BOOL)blurImage
261
262
{
    UIImage *clusterThumb;
263
    CGSize imageSize;
264
    if (_currentDeviceIdiom == UIUserInterfaceIdiomPad) {
265
        if ([UIScreen mainScreen].scale==2.0)
266
            imageSize = CGSizeMake(682., 384.);
267
        else
268
            imageSize = CGSizeMake(341., 192.);
269
    } else if (_currentDeviceIdiom == UIUserInterfaceIdiomPhone) {
270
271
272
273
274
275
        if (SYSTEM_RUNS_IOS7_OR_LATER) {
            if ([UIScreen mainScreen].scale==2.0)
                imageSize = CGSizeMake(480., 270.);
            else
                imageSize = CGSizeMake(720., 405.);
        } else {
276
            if ([UIScreen mainScreen].scale==2.0)
277
                imageSize = CGSizeMake(258., 145.);
278
            else
279
                imageSize = CGSizeMake(129., 73.);
280
        }
281
    } else {
282
283
284
285
286
287
        if (SYSTEM_RUNS_IOS82_OR_LATER) {
            if (WKInterfaceDevice.currentDevice != nil) {
                CGRect screenRect = WKInterfaceDevice.currentDevice.screenBounds;
                imageSize = CGSizeMake(screenRect.size.width * WKInterfaceDevice.currentDevice.screenScale, 120.);
            }
        }
288
    }
289
290

    UIGraphicsBeginImageContext(imageSize);
291
292
    NSUInteger iter = files.count < fileNumber ? files.count : fileNumber;
    for (NSUInteger i = 0; i < iter; i++) {
293
        MLFile *file =  [files objectAtIndex:i];
294
        clusterThumb = [self thumbnailForMediaFile:file];
295
        CGContextRef context = UIGraphicsGetCurrentContext();
296
        CGFloat imagePartWidth = (imageSize.width / iter);
297
298
299
300
301
302
303
304
        //the rect in which the image should be drawn
        CGRect clippingRect = CGRectMake(imagePartWidth * i, 0, imagePartWidth, imageSize.height);
        CGContextSaveGState(context);
        CGContextClipToRect(context, clippingRect);
        //take the center of the clippingRect and calculate the offset from the original center
        CGFloat centerOffset = (imagePartWidth * i + imagePartWidth / 2) - imageSize.width / 2;
        //shift the rect to draw the middle of the image in the clippingrect
        CGRect drawingRect = CGRectMake(centerOffset, 0, imageSize.width, imageSize.height);
305
        [clusterThumb drawInRect:drawingRect];
306
307
308
        //get rid of the old clippingRect
        CGContextRestoreGState(context);
    }
309
    clusterThumb = UIGraphicsGetImageFromCurrentImageContext();
310
311
    UIGraphicsEndImageContext();

312
313
314
315
    if (!blurImage)
        return clusterThumb;

    return [UIImage applyBlurOnImage:clusterThumb withRadius:0.1];
316
317
}

318
@end