zhongbaojian 5 lat temu
commit
89f70d36d1
41 zmienionych plików z 3408 dodań i 0 usunięć
  1. 20 0
      LICENSE
  2. 40 0
      README.md
  3. 14 0
      VIMediaCache.podspec
  4. 13 0
      VIMediaCache/Cache/NSString+VIMD5.h
  5. 27 0
      VIMediaCache/Cache/NSString+VIMD5.m
  6. 23 0
      VIMediaCache/Cache/VICacheAction.h
  7. 42 0
      VIMediaCache/Cache/VICacheAction.m
  8. 47 0
      VIMediaCache/Cache/VICacheConfiguration.h
  9. 248 0
      VIMediaCache/Cache/VICacheConfiguration.m
  10. 58 0
      VIMediaCache/Cache/VICacheManager.h
  11. 170 0
      VIMediaCache/Cache/VICacheManager.m
  12. 17 0
      VIMediaCache/Cache/VICacheSessionManager.h
  13. 39 0
      VIMediaCache/Cache/VICacheSessionManager.m
  14. 32 0
      VIMediaCache/Cache/VIMediaCacheWorker.h
  15. 229 0
      VIMediaCache/Cache/VIMediaCacheWorker.m
  16. 18 0
      VIMediaCache/ResourceLoader/VIContentInfo.h
  17. 37 0
      VIMediaCache/ResourceLoader/VIContentInfo.m
  18. 54 0
      VIMediaCache/ResourceLoader/VIMediaDownloader.h
  19. 491 0
      VIMediaCache/ResourceLoader/VIMediaDownloader.m
  20. 31 0
      VIMediaCache/ResourceLoader/VIResourceLoader.h
  21. 121 0
      VIMediaCache/ResourceLoader/VIResourceLoader.m
  22. 42 0
      VIMediaCache/ResourceLoader/VIResourceLoaderManager.h
  23. 118 0
      VIMediaCache/ResourceLoader/VIResourceLoaderManager.m
  24. 32 0
      VIMediaCache/ResourceLoader/VIResourceLoadingRequestWorker.h
  25. 107 0
      VIMediaCache/ResourceLoader/VIResourceLoadingRequestWorker.m
  26. 17 0
      VIMediaCache/VIMediaCache.h
  27. 534 0
      VIMediaCacheDemo.xcodeproj/project.pbxproj
  28. 7 0
      VIMediaCacheDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  29. 17 0
      VIMediaCacheDemo/AppDelegate.h
  30. 45 0
      VIMediaCacheDemo/AppDelegate.m
  31. 38 0
      VIMediaCacheDemo/Assets.xcassets/AppIcon.appiconset/Contents.json
  32. 27 0
      VIMediaCacheDemo/Base.lproj/LaunchScreen.storyboard
  33. 145 0
      VIMediaCacheDemo/Base.lproj/Main.storyboard
  34. 45 0
      VIMediaCacheDemo/Info.plist
  35. 18 0
      VIMediaCacheDemo/PlayerView.h
  36. 30 0
      VIMediaCacheDemo/PlayerView.m
  37. 15 0
      VIMediaCacheDemo/ViewController.h
  38. 222 0
      VIMediaCacheDemo/ViewController.m
  39. 16 0
      VIMediaCacheDemo/main.m
  40. 24 0
      VIMediaCacheDemoTests/Info.plist
  41. 138 0
      VIMediaCacheDemoTests/VIMediaCacheDemoTests.m

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+Copyright © 2016 Vito Zhang <vvitozhang@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 40 - 0
README.md

@@ -0,0 +1,40 @@
+# VIMediaCache
+
+[中文说明](https://mp.weixin.qq.com/s/v1sw_Sb8oKeZ8sWyjBUXGA)
+
+Cache media file while play media using AVPlayerr.
+
+VIMediaCache use AVAssetResourceLoader to control AVPlayer download media data.
+
+### CocoaPods
+
+`pod 'VIMediaCache'`
+
+### Usage
+
+**Objective C**
+
+```Objc
+NSURL *url = [NSURL URLWithString:@"https://mvvideo5.meitudata.com/571090934cea5517.mp4"];
+VIResourceLoaderManager *resourceLoaderManager = [VIResourceLoaderManager new];
+self.resourceLoaderManager = resourceLoaderManager;
+AVPlayerItem *playerItem = [resourceLoaderManager playerItemWithURL:url];
+AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
+```
+
+**Swift**
+
+```Swift
+let url = URL(string: "https://mvvideo5.meitudata.com/571090934cea5517.mp4")
+let resourceLoaderManager = VIResourceLoaderManager()
+let playerItem = resourceLoaderManager.playerItem(with: url)
+let player = AVPlayer(playerItem: playerItem)
+```
+
+### Contact
+
+vvitozhang@gmail.com
+
+### License
+
+MIT

+ 14 - 0
VIMediaCache.podspec

@@ -0,0 +1,14 @@
+Pod::Spec.new do |s|
+    s.name = 'VIMediaCache'
+    s.version = '0.1.0'
+    s.license = 'MIT'
+    s.summary = 'VIMediaCache is a tool to cache media file while play media using AVPlayer'
+    s.homepage = 'https://github.com/vitoziv/VIMediaCache'
+    s.author = { 'Vito' => 'vvitozhang@gmail.com' }
+    s.source = { :git => 'http://svn.ouj.com:3000/duowan_iOS/VideoCache.git', :tag => s.version.to_s }
+    s.platform = :ios, '9.0'
+    s.source_files = 'VIMediaCache/*.{h,m}', 'VIMediaCache/**/*.{h,m}'
+    s.frameworks = 'MobileCoreServices', 'AVFoundation'
+    s.requires_arc = true
+end
+

+ 13 - 0
VIMediaCache/Cache/NSString+VIMD5.h

@@ -0,0 +1,13 @@
+//
+//  NSString+VIMD5.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 21/11/2017.
+//  Copyright © 2017 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface NSString (VIMD5)
+- (NSString *)vi_md5;
+@end

+ 27 - 0
VIMediaCache/Cache/NSString+VIMD5.m

@@ -0,0 +1,27 @@
+//
+//  NSString+VIMD5.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 21/11/2017.
+//  Copyright © 2017 Vito. All rights reserved.
+//
+
+#import "NSString+VIMD5.h"
+#import <CommonCrypto/CommonDigest.h>
+
+@implementation NSString (VIMD5)
+
+- (NSString *)vi_md5 {
+    const char* str = [self UTF8String];
+    unsigned char result[CC_MD5_DIGEST_LENGTH];
+    CC_MD5(str, (CC_LONG)strlen(str), result);
+    
+    NSMutableString *ret = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH*2];
+    for(int i = 0; i<CC_MD5_DIGEST_LENGTH; i++) {
+        [ret appendFormat:@"%02x",result[i]];
+    }
+    return ret;
+}
+
+@end
+

+ 23 - 0
VIMediaCache/Cache/VICacheAction.h

@@ -0,0 +1,23 @@
+//
+//  VICacheAction.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSUInteger, VICacheAtionType) {
+    VICacheAtionTypeLocal = 0,
+    VICacheAtionTypeRemote
+};
+
+@interface VICacheAction : NSObject
+
+- (instancetype)initWithActionType:(VICacheAtionType)actionType range:(NSRange)range;
+
+@property (nonatomic) VICacheAtionType actionType;
+@property (nonatomic) NSRange range;
+
+@end

+ 42 - 0
VIMediaCache/Cache/VICacheAction.m

@@ -0,0 +1,42 @@
+//
+//  VICacheAction.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VICacheAction.h"
+
+@implementation VICacheAction
+
+- (instancetype)initWithActionType:(VICacheAtionType)actionType range:(NSRange)range {
+    self = [super init];
+    if (self) {
+        _actionType = actionType;
+        _range = range;
+    }
+    return self;
+}
+
+- (BOOL)isEqual:(VICacheAction *)object {
+    if (!NSEqualRanges(object.range, self.range)) {
+        return NO;
+    }
+    
+    if (object.actionType != self.actionType) {
+        return NO;
+    }
+    
+    return YES;
+}
+
+- (NSUInteger)hash {
+    return [[NSString stringWithFormat:@"%@%@", NSStringFromRange(self.range), @(self.actionType)] hash];
+}
+
+- (NSString *)description {
+    return [NSString stringWithFormat:@"actionType %@, range: %@", @(self.actionType), NSStringFromRange(self.range)];
+}
+
+@end

+ 47 - 0
VIMediaCache/Cache/VICacheConfiguration.h

@@ -0,0 +1,47 @@
+//
+//  VICacheConfiguration.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "VIContentInfo.h"
+
+@interface VICacheConfiguration : NSObject <NSCopying>
+
++ (NSString *)configurationFilePathForFilePath:(NSString *)filePath;
+
++ (instancetype)configurationWithFilePath:(NSString *)filePath;
+
+@property (nonatomic, copy, readonly) NSString *filePath;
+@property (nonatomic, strong) VIContentInfo *contentInfo;
+@property (nonatomic, strong) NSURL *url;
+
+- (NSArray<NSValue *> *)cacheFragments;
+
+/**
+ *  cached progress
+ */
+@property (nonatomic, readonly) float progress;
+@property (nonatomic, readonly) long long downloadedBytes;
+@property (nonatomic, readonly) float downloadSpeed; // kb/s
+
+#pragma mark - update API
+
+- (void)save;
+- (void)addCacheFragment:(NSRange)fragment;
+
+/**
+ *  Record the download speed
+ */
+- (void)addDownloadedBytes:(long long)bytes spent:(NSTimeInterval)time;
+
+@end
+
+@interface VICacheConfiguration (VIConvenient)
+
++ (BOOL)createAndSaveDownloadedConfigurationForURL:(NSURL *)url error:(NSError **)error;
+
+@end

+ 248 - 0
VIMediaCache/Cache/VICacheConfiguration.m

@@ -0,0 +1,248 @@
+//
+//  VICacheConfiguration.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VICacheConfiguration.h"
+#import "VICacheManager.h"
+#import <MobileCoreServices/MobileCoreServices.h>
+
+static NSString *kFileNameKey = @"kFileNameKey";
+static NSString *kCacheFragmentsKey = @"kCacheFragmentsKey";
+static NSString *kDownloadInfoKey = @"kDownloadInfoKey";
+static NSString *kContentInfoKey = @"kContentInfoKey";
+static NSString *kURLKey = @"kURLKey";
+
+@interface VICacheConfiguration () <NSCoding>
+
+@property (nonatomic, copy) NSString *filePath;
+@property (nonatomic, copy) NSString *fileName;
+@property (nonatomic, copy) NSArray<NSValue *> *internalCacheFragments;
+@property (nonatomic, copy) NSArray *downloadInfo;
+
+@end
+
+@implementation VICacheConfiguration
+
++ (instancetype)configurationWithFilePath:(NSString *)filePath {
+    filePath = [self configurationFilePathForFilePath:filePath];
+    VICacheConfiguration *configuration = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
+    
+    if (!configuration) {
+        configuration = [[VICacheConfiguration alloc] init];
+        configuration.fileName = [filePath lastPathComponent];
+    }
+    configuration.filePath = filePath;
+    
+    return configuration;
+}
+
++ (NSString *)configurationFilePathForFilePath:(NSString *)filePath {
+    return [filePath stringByAppendingPathExtension:@"mt_cfg"];
+}
+
+- (NSArray<NSValue *> *)internalCacheFragments {
+    if (!_internalCacheFragments) {
+        _internalCacheFragments = [NSArray array];
+    }
+    return _internalCacheFragments;
+}
+
+- (NSArray *)downloadInfo {
+    if (!_downloadInfo) {
+        _downloadInfo = [NSArray array];
+    }
+    return _downloadInfo;
+}
+
+- (NSArray<NSValue *> *)cacheFragments {
+    return [_internalCacheFragments copy];
+}
+
+- (float)progress {
+    float progress = self.downloadedBytes / (float)self.contentInfo.contentLength;
+    return progress;
+}
+
+- (long long)downloadedBytes {
+    float bytes = 0;
+    @synchronized (self.internalCacheFragments) {
+        for (NSValue *range in self.internalCacheFragments) {
+            bytes += range.rangeValue.length;
+        }
+    }
+    return bytes;
+}
+
+- (float)downloadSpeed {
+    long long bytes = 0;
+    NSTimeInterval time = 0;
+    @synchronized (self.downloadInfo) {
+        for (NSArray *a in self.downloadInfo) {
+            bytes += [[a firstObject] longLongValue];
+            time += [[a lastObject] doubleValue];
+        }
+    }
+    return bytes / 1024.0 / time;
+}
+
+#pragma mark - NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:self.fileName forKey:kFileNameKey];
+    [aCoder encodeObject:self.internalCacheFragments forKey:kCacheFragmentsKey];
+    [aCoder encodeObject:self.downloadInfo forKey:kDownloadInfoKey];
+    [aCoder encodeObject:self.contentInfo forKey:kContentInfoKey];
+    [aCoder encodeObject:self.url forKey:kURLKey];
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    if (self) {
+        _fileName = [aDecoder decodeObjectForKey:kFileNameKey];
+        _internalCacheFragments = [[aDecoder decodeObjectForKey:kCacheFragmentsKey] mutableCopy];
+        if (!_internalCacheFragments) {
+            _internalCacheFragments = [NSArray array];
+        }
+        _downloadInfo = [aDecoder decodeObjectForKey:kDownloadInfoKey];
+        _contentInfo = [aDecoder decodeObjectForKey:kContentInfoKey];
+        _url = [aDecoder decodeObjectForKey:kURLKey];
+    }
+    return self;
+}
+
+#pragma mark - NSCopying
+
+- (id)copyWithZone:(nullable NSZone *)zone {
+    VICacheConfiguration *configuration = [[VICacheConfiguration allocWithZone:zone] init];
+    configuration.fileName = self.fileName;
+    configuration.filePath = self.filePath;
+    configuration.internalCacheFragments = self.internalCacheFragments;
+    configuration.downloadInfo = self.downloadInfo;
+    configuration.url = self.url;
+    configuration.contentInfo = self.contentInfo;
+    
+    return configuration;
+}
+
+#pragma mark - Update
+
+- (void)save {
+    @synchronized (self.internalCacheFragments) {
+        [NSKeyedArchiver archiveRootObject:self toFile:self.filePath];
+    }
+}
+
+- (void)addCacheFragment:(NSRange)fragment {
+    if (fragment.location == NSNotFound || fragment.length == 0) {
+        return;
+    }
+    
+    @synchronized (self.internalCacheFragments) {
+        NSMutableArray *internalCacheFragments = [self.internalCacheFragments mutableCopy];
+        
+        NSValue *fragmentValue = [NSValue valueWithRange:fragment];
+        NSInteger count = self.internalCacheFragments.count;
+        if (count == 0) {
+            [internalCacheFragments addObject:fragmentValue];
+        } else {
+            NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
+            [internalCacheFragments enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+                NSRange range = obj.rangeValue;
+                if ((fragment.location + fragment.length) <= range.location) {
+                    if (indexSet.count == 0) {
+                        [indexSet addIndex:idx];
+                    }
+                    *stop = YES;
+                } else if (fragment.location <= (range.location + range.length) && (fragment.location + fragment.length) > range.location) {
+                    [indexSet addIndex:idx];
+                } else if (fragment.location >= range.location + range.length) {
+                    if (idx == count - 1) { // Append to last index
+                        [indexSet addIndex:idx];
+                    }
+                }
+            }];
+            
+            if (indexSet.count > 1) {
+                NSRange firstRange = self.internalCacheFragments[indexSet.firstIndex].rangeValue;
+                NSRange lastRange = self.internalCacheFragments[indexSet.lastIndex].rangeValue;
+                NSInteger location = MIN(firstRange.location, fragment.location);
+                NSInteger endOffset = MAX(lastRange.location + lastRange.length, fragment.location + fragment.length);
+                NSRange combineRange = NSMakeRange(location, endOffset - location);
+                [internalCacheFragments removeObjectsAtIndexes:indexSet];
+                [internalCacheFragments insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex];
+            } else if (indexSet.count == 1) {
+                NSRange firstRange = self.internalCacheFragments[indexSet.firstIndex].rangeValue;
+                
+                NSRange expandFirstRange = NSMakeRange(firstRange.location, firstRange.length + 1);
+                NSRange expandFragmentRange = NSMakeRange(fragment.location, fragment.length + 1);
+                NSRange intersectionRange = NSIntersectionRange(expandFirstRange, expandFragmentRange);
+                if (intersectionRange.length > 0) { // Should combine
+                    NSInteger location = MIN(firstRange.location, fragment.location);
+                    NSInteger endOffset = MAX(firstRange.location + firstRange.length, fragment.location + fragment.length);
+                    NSRange combineRange = NSMakeRange(location, endOffset - location);
+                    [internalCacheFragments removeObjectAtIndex:indexSet.firstIndex];
+                    [internalCacheFragments insertObject:[NSValue valueWithRange:combineRange] atIndex:indexSet.firstIndex];
+                } else {
+                    if (firstRange.location > fragment.location) {
+                        [internalCacheFragments insertObject:fragmentValue atIndex:[indexSet lastIndex]];
+                    } else {
+                        [internalCacheFragments insertObject:fragmentValue atIndex:[indexSet lastIndex] + 1];
+                    }
+                }
+            }
+        }
+        
+        self.internalCacheFragments = [internalCacheFragments copy];
+    }
+}
+
+- (void)addDownloadedBytes:(long long)bytes spent:(NSTimeInterval)time {
+    @synchronized (self.downloadInfo) {
+        self.downloadInfo = [self.downloadInfo arrayByAddingObject:@[@(bytes), @(time)]];
+    }
+}
+
+@end
+
+@implementation VICacheConfiguration (VIConvenient)
+
++ (BOOL)createAndSaveDownloadedConfigurationForURL:(NSURL *)url error:(NSError **)error {
+    NSString *filePath = [VICacheManager cachedFilePathForURL:url];
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSDictionary<NSFileAttributeKey, id> *attributes = [fileManager attributesOfItemAtPath:filePath error:error];
+    if (!attributes) {
+        return NO;
+    }
+    
+    NSUInteger fileSize = (NSUInteger)attributes.fileSize;
+    NSRange range = NSMakeRange(0, fileSize);
+    
+    VICacheConfiguration *configuration = [VICacheConfiguration configurationWithFilePath:filePath];
+    configuration.url = url;
+    
+    VIContentInfo *contentInfo = [VIContentInfo new];
+    
+    NSString *fileExtension = [url pathExtension];
+    NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL);
+    NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
+    if (!contentType) {
+        contentType = @"application/octet-stream";
+    }
+    contentInfo.contentType = contentType;
+    
+    contentInfo.contentLength = fileSize;
+    contentInfo.byteRangeAccessSupported = YES;
+    contentInfo.downloadedContentLength = fileSize;
+    configuration.contentInfo = contentInfo;
+    
+    [configuration addCacheFragment:range];
+    [configuration save];
+    
+    return YES;
+}
+
+@end

+ 58 - 0
VIMediaCache/Cache/VICacheManager.h

@@ -0,0 +1,58 @@
+//
+//  VICacheManager.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "VICacheConfiguration.h"
+
+extern NSString *VICacheManagerDidUpdateCacheNotification;
+extern NSString *VICacheManagerDidFinishCacheNotification;
+
+extern NSString *VICacheConfigurationKey;
+extern NSString *VICacheFinishedErrorKey;
+
+@interface VICacheManager : NSObject
+
++ (void)setCacheDirectory:(NSString *)cacheDirectory;
++ (NSString *)cacheDirectory;
+
+
+/**
+ How often trigger `VICacheManagerDidUpdateCacheNotification` notification
+
+ @param interval Minimum interval
+ */
++ (void)setCacheUpdateNotifyInterval:(NSTimeInterval)interval;
++ (NSTimeInterval)cacheUpdateNotifyInterval;
+
++ (NSString *)cachedFilePathForURL:(NSURL *)url;
++ (VICacheConfiguration *)cacheConfigurationForURL:(NSURL *)url;
+
++ (void)setFileNameRules:(NSString *(^)(NSURL *url))rules;
+
+
+/**
+ Calculate cached files size
+
+ @param error If error not empty, calculate failed
+ @return files size, respresent by `byte`, if error occurs, return -1
+ */
++ (unsigned long long)calculateCachedSizeWithError:(NSError **)error;
++ (void)cleanAllCacheWithError:(NSError **)error;
++ (void)cleanCacheForURL:(NSURL *)url error:(NSError **)error;
+
+
+/**
+ Useful when you upload a local file to the server
+
+ @param filePath local file path
+ @param url remote resource url
+ @param error On input, a pointer to an error object. If an error occurs, this pointer is set to an actual error object containing the error information.
+ */
++ (BOOL)addCacheFile:(NSString *)filePath forURL:(NSURL *)url error:(NSError **)error;
+
+@end

+ 170 - 0
VIMediaCache/Cache/VICacheManager.m

@@ -0,0 +1,170 @@
+//
+//  VICacheManager.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VICacheManager.h"
+#import "VIMediaDownloader.h"
+#import "NSString+VIMD5.h"
+
+NSString *VICacheManagerDidUpdateCacheNotification = @"VICacheManagerDidUpdateCacheNotification";
+NSString *VICacheManagerDidFinishCacheNotification = @"VICacheManagerDidFinishCacheNotification";
+
+NSString *VICacheConfigurationKey = @"VICacheConfigurationKey";
+NSString *VICacheFinishedErrorKey = @"VICacheFinishedErrorKey";
+
+static NSString *kMCMediaCacheDirectory;
+static NSTimeInterval kMCMediaCacheNotifyInterval;
+static NSString *(^kMCFileNameRules)(NSURL *url);
+
+@implementation VICacheManager
+
++ (void)load {
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        [self setCacheDirectory:[NSTemporaryDirectory() stringByAppendingPathComponent:@"vimedia"]];
+        [self setCacheUpdateNotifyInterval:0.1];
+    });
+}
+
++ (void)setCacheDirectory:(NSString *)cacheDirectory {
+    kMCMediaCacheDirectory = cacheDirectory;
+}
+
++ (NSString *)cacheDirectory {
+    return kMCMediaCacheDirectory;
+}
+
++ (void)setCacheUpdateNotifyInterval:(NSTimeInterval)interval {
+    kMCMediaCacheNotifyInterval = interval;
+}
+
++ (NSTimeInterval)cacheUpdateNotifyInterval {
+    return kMCMediaCacheNotifyInterval;
+}
+
++ (void)setFileNameRules:(NSString *(^)(NSURL *url))rules {
+    kMCFileNameRules = rules;
+}
+
++ (NSString *)cachedFilePathForURL:(NSURL *)url {
+    NSString *pathComponent = nil;
+    if (kMCFileNameRules) {
+        pathComponent = kMCFileNameRules(url);
+    } else {
+        pathComponent = [url.absoluteString vi_md5];
+        pathComponent = [pathComponent stringByAppendingPathExtension:url.pathExtension];
+    }
+    return [[self cacheDirectory] stringByAppendingPathComponent:pathComponent];
+}
+
++ (VICacheConfiguration *)cacheConfigurationForURL:(NSURL *)url {
+    NSString *filePath = [self cachedFilePathForURL:url];
+    VICacheConfiguration *configuration = [VICacheConfiguration configurationWithFilePath:filePath];
+    return configuration;
+}
+
++ (unsigned long long)calculateCachedSizeWithError:(NSError **)error {
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSString *cacheDirectory = [self cacheDirectory];
+    NSArray *files = [fileManager contentsOfDirectoryAtPath:cacheDirectory error:error];
+    unsigned long long size = 0;
+    if (files) {
+        for (NSString *path in files) {
+            NSString *filePath = [cacheDirectory stringByAppendingPathComponent:path];
+            NSDictionary<NSFileAttributeKey, id> *attribute = [fileManager attributesOfItemAtPath:filePath error:error];
+            if (!attribute) {
+                size = -1;
+                break;
+            }
+            
+            size += [attribute fileSize];
+        }
+    }
+    return size;
+}
+
++ (void)cleanAllCacheWithError:(NSError **)error {
+    // Find downloaing file
+    NSMutableSet *downloadingFiles = [NSMutableSet set];
+    [[[VIMediaDownloaderStatus shared] urls] enumerateObjectsUsingBlock:^(NSURL * _Nonnull obj, BOOL * _Nonnull stop) {
+        NSString *file = [self cachedFilePathForURL:obj];
+        [downloadingFiles addObject:file];
+        NSString *configurationPath = [VICacheConfiguration configurationFilePathForFilePath:file];
+        [downloadingFiles addObject:configurationPath];
+    }];
+    
+    // Remove files
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSString *cacheDirectory = [self cacheDirectory];
+    
+    NSArray *files = [fileManager contentsOfDirectoryAtPath:cacheDirectory error:error];
+    if (files) {
+        for (NSString *path in files) {
+            NSString *filePath = [cacheDirectory stringByAppendingPathComponent:path];
+            if ([downloadingFiles containsObject:filePath]) {
+                continue;
+            }
+            if (![fileManager removeItemAtPath:filePath error:error]) {
+                break;
+            }
+        }
+    }
+}
+
++ (void)cleanCacheForURL:(NSURL *)url error:(NSError **)error {
+    if ([[VIMediaDownloaderStatus shared] containsURL:url]) {
+        NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Clean cache for url `%@` can't be done, because it's downloading", nil), url];
+        if (error) {
+            *error = [NSError errorWithDomain:@"com.mediadownload" code:2 userInfo:@{NSLocalizedDescriptionKey: description}];
+        }
+        return;
+    }
+    
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    NSString *filePath = [self cachedFilePathForURL:url];
+    
+    if ([fileManager fileExistsAtPath:filePath]) {
+        if (![fileManager removeItemAtPath:filePath error:error]) {
+            return;
+        }
+    }
+    
+    NSString *configurationPath = [VICacheConfiguration configurationFilePathForFilePath:filePath];
+    if ([fileManager fileExistsAtPath:configurationPath]) {
+        if (![fileManager removeItemAtPath:configurationPath error:error]) {
+            return;
+        }
+    }
+}
+
++ (BOOL)addCacheFile:(NSString *)filePath forURL:(NSURL *)url error:(NSError **)error {
+    NSFileManager *fileManager = [NSFileManager defaultManager];
+    
+    NSString *cachePath = [VICacheManager cachedFilePathForURL:url];
+    NSString *cacheFolder = [cachePath stringByDeletingLastPathComponent];
+    if (![fileManager fileExistsAtPath:cacheFolder]) {
+        if (![fileManager createDirectoryAtPath:cacheFolder
+                    withIntermediateDirectories:YES
+                                     attributes:nil
+                                          error:error]) {
+            return NO;
+        }
+    }
+    
+    if (![fileManager copyItemAtPath:filePath toPath:cachePath error:error]) {
+        return NO;
+    }
+    
+    if (![VICacheConfiguration createAndSaveDownloadedConfigurationForURL:url error:error]) {
+        [fileManager removeItemAtPath:cachePath error:nil]; // if remove failed, there is nothing we can do.
+        return NO;
+    }
+    
+    return YES;
+}
+
+@end

+ 17 - 0
VIMediaCache/Cache/VICacheSessionManager.h

@@ -0,0 +1,17 @@
+//
+//  VICacheSessionManager.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface VICacheSessionManager : NSObject
+
+@property (nonatomic, strong, readonly) NSOperationQueue *downloadQueue;
+
++ (instancetype)shared;
+
+@end

+ 39 - 0
VIMediaCache/Cache/VICacheSessionManager.m

@@ -0,0 +1,39 @@
+//
+//  VICacheSessionManager.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VICacheSessionManager.h"
+
+@interface VICacheSessionManager ()
+
+@property (nonatomic, strong) NSOperationQueue *downloadQueue;
+
+@end
+
+@implementation VICacheSessionManager
+
++ (instancetype)shared {
+    static id instance = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        instance = [[self alloc] init];
+    });
+    
+    return instance;
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
+        queue.name = @"com.vimediacache.download";
+        _downloadQueue = queue;
+    }
+    return self;
+}
+
+@end

+ 32 - 0
VIMediaCache/Cache/VIMediaCacheWorker.h

@@ -0,0 +1,32 @@
+//
+//  VIMediaCacheWorker.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import "VICacheConfiguration.h"
+
+@class VICacheAction;
+
+@interface VIMediaCacheWorker : NSObject
+
+- (instancetype)initWithURL:(NSURL *)url;
+
+@property (nonatomic, strong, readonly) VICacheConfiguration *cacheConfiguration;
+@property (nonatomic, strong, readonly) NSError *setupError; // Create fileHandler error, can't save/use cache
+
+- (void)cacheData:(NSData *)data forRange:(NSRange)range error:(NSError **)error;
+- (NSArray<VICacheAction *> *)cachedDataActionsForRange:(NSRange)range;
+- (NSData *)cachedDataForRange:(NSRange)range error:(NSError **)error;
+
+- (void)setContentInfo:(VIContentInfo *)contentInfo error:(NSError **)error;
+
+- (void)save;
+
+- (void)startWritting;
+- (void)finishWritting;
+
+@end

+ 229 - 0
VIMediaCache/Cache/VIMediaCacheWorker.m

@@ -0,0 +1,229 @@
+//
+//  VIMediaCacheWorker.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIMediaCacheWorker.h"
+#import "VICacheAction.h"
+#import "VICacheManager.h"
+
+@import UIKit;
+
+static NSInteger const kPackageLength = 204800; // 200kb per package
+static NSString *kMCMediaCacheResponseKey = @"kMCMediaCacheResponseKey";
+static NSString *VIMediaCacheErrorDoamin = @"com.vimediacache";
+
+@interface VIMediaCacheWorker ()
+
+@property (nonatomic, strong) NSFileHandle *readFileHandle;
+@property (nonatomic, strong) NSFileHandle *writeFileHandle;
+@property (nonatomic, strong, readwrite) NSError *setupError;
+@property (nonatomic, copy) NSString *filePath;
+@property (nonatomic, strong) VICacheConfiguration *internalCacheConfiguration;
+
+@property (nonatomic) long long currentOffset;
+
+@property (nonatomic, strong) NSDate *startWriteDate;
+@property (nonatomic) float writeBytes;
+@property (nonatomic) BOOL writting;
+
+@end
+
+@implementation VIMediaCacheWorker
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+    [self save];
+    [_readFileHandle closeFile];
+    [_writeFileHandle closeFile];
+}
+
+- (instancetype)initWithURL:(NSURL *)url {
+    self = [super init];
+    if (self) {
+        NSString *path = [VICacheManager cachedFilePathForURL:url];
+        NSFileManager *fileManager = [NSFileManager defaultManager];
+        _filePath = path;
+        NSError *error;
+        NSString *cacheFolder = [path stringByDeletingLastPathComponent];
+        if (![fileManager fileExistsAtPath:cacheFolder]) {
+            [fileManager createDirectoryAtPath:cacheFolder
+                   withIntermediateDirectories:YES
+                                    attributes:nil
+                                         error:&error];
+        }
+        
+        if (!error) {
+            if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
+                [[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil];
+            }
+            NSURL *fileURL = [NSURL fileURLWithPath:path];
+            _readFileHandle = [NSFileHandle fileHandleForReadingFromURL:fileURL error:&error];
+            if (!error) {
+                _writeFileHandle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&error];
+                _internalCacheConfiguration = [VICacheConfiguration configurationWithFilePath:path];
+                _internalCacheConfiguration.url = url;
+            }
+        }
+        
+        _setupError = error;
+    }
+    return self;
+}
+
+- (VICacheConfiguration *)cacheConfiguration {
+    return self.internalCacheConfiguration;
+}
+
+- (void)cacheData:(NSData *)data forRange:(NSRange)range error:(NSError **)error {
+    @synchronized(self.writeFileHandle) {
+        @try {
+            [self.writeFileHandle seekToFileOffset:range.location];
+            [self.writeFileHandle writeData:data];
+            self.writeBytes += data.length;
+            [self.internalCacheConfiguration addCacheFragment:range];
+        } @catch (NSException *exception) {
+            NSLog(@"write to file error");
+            *error = [NSError errorWithDomain:exception.name code:123 userInfo:@{NSLocalizedDescriptionKey: exception.reason, @"exception": exception}];
+        }
+    }
+}
+
+- (NSData *)cachedDataForRange:(NSRange)range error:(NSError **)error {
+    @synchronized(self.readFileHandle) {
+        @try {
+            [self.readFileHandle seekToFileOffset:range.location];
+            NSData *data = [self.readFileHandle readDataOfLength:range.length]; // 空数据也会返回,所以如果 range 错误,会导致播放失效
+            return data;
+        } @catch (NSException *exception) {
+            NSLog(@"read cached data error %@",exception);
+            *error = [NSError errorWithDomain:exception.name code:123 userInfo:@{NSLocalizedDescriptionKey: exception.reason, @"exception": exception}];
+        }
+    }
+    return nil;
+}
+
+- (NSArray<VICacheAction *> *)cachedDataActionsForRange:(NSRange)range {
+    NSArray *cachedFragments = [self.internalCacheConfiguration cacheFragments];
+    NSMutableArray *actions = [NSMutableArray array];
+    
+    if (range.location == NSNotFound) {
+        return [actions copy];
+    }
+    NSInteger endOffset = range.location + range.length;
+    // Delete header and footer not in range
+    [cachedFragments enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+        NSRange fragmentRange = obj.rangeValue;
+        NSRange intersectionRange = NSIntersectionRange(range, fragmentRange);
+        if (intersectionRange.length > 0) {
+            NSInteger package = intersectionRange.length / kPackageLength;
+            for (NSInteger i = 0; i <= package; i++) {
+                VICacheAction *action = [VICacheAction new];
+                action.actionType = VICacheAtionTypeLocal;
+                
+                NSInteger offset = i * kPackageLength;
+                NSInteger offsetLocation = intersectionRange.location + offset;
+                NSInteger maxLocation = intersectionRange.location + intersectionRange.length;
+                NSInteger length = (offsetLocation + kPackageLength) > maxLocation ? (maxLocation - offsetLocation) : kPackageLength;
+                action.range = NSMakeRange(offsetLocation, length);
+                
+                [actions addObject:action];
+            }
+        } else if (fragmentRange.location >= endOffset) {
+            *stop = YES;
+        }
+    }];
+    
+    if (actions.count == 0) {
+        VICacheAction *action = [VICacheAction new];
+        action.actionType = VICacheAtionTypeRemote;
+        action.range = range;
+        [actions addObject:action];
+    } else {
+        // Add remote fragments
+        NSMutableArray *localRemoteActions = [NSMutableArray array];
+        [actions enumerateObjectsUsingBlock:^(VICacheAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+            NSRange actionRange = obj.range;
+            if (idx == 0) {
+                if (range.location < actionRange.location) {
+                    VICacheAction *action = [VICacheAction new];
+                    action.actionType = VICacheAtionTypeRemote;
+                    action.range = NSMakeRange(range.location, actionRange.location - range.location);
+                    [localRemoteActions addObject:action];
+                }
+                [localRemoteActions addObject:obj];
+            } else {
+                VICacheAction *lastAction = [localRemoteActions lastObject];
+                NSInteger lastOffset = lastAction.range.location + lastAction.range.length;
+                if (actionRange.location > lastOffset) {
+                    VICacheAction *action = [VICacheAction new];
+                    action.actionType = VICacheAtionTypeRemote;
+                    action.range = NSMakeRange(lastOffset, actionRange.location - lastOffset);
+                    [localRemoteActions addObject:action];
+                }
+                [localRemoteActions addObject:obj];
+            }
+            
+            if (idx == actions.count - 1) {
+                NSInteger localEndOffset = actionRange.location + actionRange.length;
+                if (endOffset > localEndOffset) {
+                    VICacheAction *action = [VICacheAction new];
+                    action.actionType = VICacheAtionTypeRemote;
+                    action.range = NSMakeRange(localEndOffset, endOffset - localEndOffset);
+                    [localRemoteActions addObject:action];
+                }
+            }
+        }];
+        
+        actions = localRemoteActions;
+    }
+    
+    return [actions copy];
+}
+
+- (void)setContentInfo:(VIContentInfo *)contentInfo error:(NSError **)error {
+    self.internalCacheConfiguration.contentInfo = contentInfo;
+    @try {
+        [self.writeFileHandle truncateFileAtOffset:contentInfo.contentLength];
+        [self.writeFileHandle synchronizeFile];
+    } @catch (NSException *exception) {
+        NSLog(@"read cached data error %@", exception);
+        *error = [NSError errorWithDomain:exception.name code:123 userInfo:@{NSLocalizedDescriptionKey: exception.reason, @"exception": exception}];
+    }
+}
+
+- (void)save {
+    @synchronized (self.writeFileHandle) {
+        [self.writeFileHandle synchronizeFile];
+        [self.internalCacheConfiguration save];
+    }
+}
+
+- (void)startWritting {
+    if (!self.writting) {
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
+    }
+    self.writting = YES;
+    self.startWriteDate = [NSDate date];
+    self.writeBytes = 0;
+}
+
+- (void)finishWritting {
+    if (self.writting) {
+        self.writting = NO;
+        [[NSNotificationCenter defaultCenter] removeObserver:self];
+        NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.startWriteDate];
+        [self.internalCacheConfiguration addDownloadedBytes:self.writeBytes spent:time];
+    }
+}
+
+#pragma mark - Notification
+
+- (void)applicationDidEnterBackground:(NSNotification *)notification {
+    [self save];
+}
+
+@end

+ 18 - 0
VIMediaCache/ResourceLoader/VIContentInfo.h

@@ -0,0 +1,18 @@
+//
+//  VIContentInfo.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface VIContentInfo : NSObject <NSCoding>
+
+@property (nonatomic, copy) NSString *contentType;
+@property (nonatomic, assign) BOOL byteRangeAccessSupported;
+@property (nonatomic, assign) unsigned long long contentLength;
+@property (nonatomic) unsigned long long downloadedContentLength;
+
+@end

+ 37 - 0
VIMediaCache/ResourceLoader/VIContentInfo.m

@@ -0,0 +1,37 @@
+//
+//  VIContentInfo.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIContentInfo.h"
+
+static NSString *kContentLengthKey = @"kContentLengthKey";
+static NSString *kContentTypeKey = @"kContentTypeKey";
+static NSString *kByteRangeAccessSupported = @"kByteRangeAccessSupported";
+
+@implementation VIContentInfo
+
+- (NSString *)debugDescription {
+    return [NSString stringWithFormat:@"%@\ncontentLength: %lld\ncontentType: %@\nbyteRangeAccessSupported:%@", NSStringFromClass([self class]), self.contentLength, self.contentType, @(self.byteRangeAccessSupported)];
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [aCoder encodeObject:@(self.contentLength) forKey:kContentLengthKey];
+    [aCoder encodeObject:self.contentType forKey:kContentTypeKey];
+    [aCoder encodeObject:@(self.byteRangeAccessSupported) forKey:kByteRangeAccessSupported];
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super init];
+    if (self) {
+        _contentLength = [[aDecoder decodeObjectForKey:kContentLengthKey] longLongValue];
+        _contentType = [aDecoder decodeObjectForKey:kContentTypeKey];
+        _byteRangeAccessSupported = [[aDecoder decodeObjectForKey:kByteRangeAccessSupported] boolValue];
+    }
+    return self;
+}
+
+@end

+ 54 - 0
VIMediaCache/ResourceLoader/VIMediaDownloader.h

@@ -0,0 +1,54 @@
+//
+//  VIMediaDownloader.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@protocol VIMediaDownloaderDelegate;
+@class VIContentInfo;
+@class VIMediaCacheWorker;
+
+@interface VIMediaDownloaderStatus : NSObject
+
++ (instancetype)shared;
+
+- (void)addURL:(NSURL *)url;
+- (void)removeURL:(NSURL *)url;
+
+/**
+ return YES if downloading the url source
+ */
+- (BOOL)containsURL:(NSURL *)url;
+- (NSSet *)urls;
+
+@end
+
+@interface VIMediaDownloader : NSObject
+
+- (instancetype)initWithURL:(NSURL *)url cacheWorker:(VIMediaCacheWorker *)cacheWorker;
+@property (nonatomic, strong, readonly) NSURL *url;
+@property (nonatomic, weak) id<VIMediaDownloaderDelegate> delegate;
+@property (nonatomic, strong) VIContentInfo *info;
+@property (nonatomic, assign) BOOL saveToCache;
+
+- (void)downloadTaskFromOffset:(unsigned long long)fromOffset
+                        length:(NSUInteger)length
+                         toEnd:(BOOL)toEnd;
+- (void)downloadFromStartToEnd;
+
+- (void)cancel;
+
+@end
+
+@protocol VIMediaDownloaderDelegate <NSObject>
+
+@optional
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didReceiveResponse:(NSURLResponse *)response;
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didReceiveData:(NSData *)data;
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didFinishedWithError:(NSError *)error;
+
+@end

+ 491 - 0
VIMediaCache/ResourceLoader/VIMediaDownloader.m

@@ -0,0 +1,491 @@
+//
+//  VIMediaDownloader.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIMediaDownloader.h"
+#import "VIContentInfo.h"
+#import <MobileCoreServices/MobileCoreServices.h>
+#import "VICacheSessionManager.h"
+
+#import "VIMediaCacheWorker.h"
+#import "VICacheManager.h"
+#import "VICacheAction.h"
+
+#pragma mark - Class: VIURLSessionDelegateObject
+
+@protocol  VIURLSessionDelegateObjectDelegate <NSObject>
+
+- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler;
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error;
+
+@end
+
+static NSInteger kBufferSize = 10 * 1024;
+
+@interface VIURLSessionDelegateObject : NSObject <NSURLSessionDelegate>
+
+- (instancetype)initWithDelegate:(id<VIURLSessionDelegateObjectDelegate>)delegate;
+
+@property (nonatomic, weak) id<VIURLSessionDelegateObjectDelegate> delegate;
+@property (nonatomic, strong) NSMutableData *bufferData;
+
+@end
+
+@implementation VIURLSessionDelegateObject
+
+- (instancetype)initWithDelegate:(id<VIURLSessionDelegateObjectDelegate>)delegate {
+    self = [super init];
+    if (self) {
+        _delegate = delegate;
+        _bufferData = [NSMutableData data];
+    }
+    return self;
+}
+
+#pragma mark - NSURLSessionDataDelegate
+- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler{
+    [self.delegate URLSession:session didReceiveChallenge:challenge completionHandler:completionHandler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+          dataTask:(NSURLSessionDataTask *)dataTask
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
+    [self.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
+}
+
+- (void)URLSession:(NSURLSession *)session
+          dataTask:(NSURLSessionDataTask *)dataTask
+    didReceiveData:(NSData *)data {
+    @synchronized (self.bufferData) {
+        [self.bufferData appendData:data];
+        if (self.bufferData.length > kBufferSize) {
+            NSRange chunkRange = NSMakeRange(0, self.bufferData.length);
+            NSData *chunkData = [self.bufferData subdataWithRange:chunkRange];
+            [self.bufferData replaceBytesInRange:chunkRange withBytes:NULL length:0];
+            [self.delegate URLSession:session dataTask:dataTask didReceiveData:chunkData];
+        }
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session
+              task:(NSURLSessionDataTask *)task
+didCompleteWithError:(nullable NSError *)error {
+    @synchronized (self.bufferData) {
+        if (self.bufferData.length > 0 && !error) {
+            NSRange chunkRange = NSMakeRange(0, self.bufferData.length);
+            NSData *chunkData = [self.bufferData subdataWithRange:chunkRange];
+            [self.bufferData replaceBytesInRange:chunkRange withBytes:NULL length:0];
+            [self.delegate URLSession:session dataTask:task didReceiveData:chunkData];
+        }
+    }
+    [self.delegate URLSession:session task:task didCompleteWithError:error];
+}
+
+@end
+
+#pragma mark - Class: VIActionWorker
+
+@class VIActionWorker;
+
+@protocol VIActionWorkerDelegate <NSObject>
+
+- (void)actionWorker:(VIActionWorker *)actionWorker didReceiveResponse:(NSURLResponse *)response;
+- (void)actionWorker:(VIActionWorker *)actionWorker didReceiveData:(NSData *)data isLocal:(BOOL)isLocal;
+- (void)actionWorker:(VIActionWorker *)actionWorker didFinishWithError:(NSError *)error;
+
+@end
+
+@interface VIActionWorker : NSObject <VIURLSessionDelegateObjectDelegate>
+
+@property (nonatomic, strong) NSMutableArray<VICacheAction *> *actions;
+- (instancetype)initWithActions:(NSArray<VICacheAction *> *)actions url:(NSURL *)url cacheWorker:(VIMediaCacheWorker *)cacheWorker;
+
+@property (nonatomic, assign) BOOL canSaveToCache;
+@property (nonatomic, weak) id<VIActionWorkerDelegate> delegate;
+
+- (void)start;
+- (void)cancel;
+
+
+@property (nonatomic, getter=isCancelled) BOOL cancelled;
+
+@property (nonatomic, strong) VIMediaCacheWorker *cacheWorker;
+@property (nonatomic, strong) NSURL *url;
+
+@property (nonatomic, strong) NSURLSession *session;
+@property (nonatomic, strong) VIURLSessionDelegateObject *sessionDelegateObject;
+@property (nonatomic, strong) NSURLSessionDataTask *task;
+@property (nonatomic) NSInteger startOffset;
+
+@end
+
+@interface VIActionWorker ()
+
+@property (nonatomic) NSTimeInterval notifyTime;
+
+@end
+
+@implementation VIActionWorker
+
+- (void)dealloc {
+    [self cancel];
+}
+
+- (instancetype)initWithActions:(NSArray<VICacheAction *> *)actions url:(NSURL *)url cacheWorker:(VIMediaCacheWorker *)cacheWorker {
+    self = [super init];
+    if (self) {
+        _canSaveToCache = YES;
+        _actions = [actions mutableCopy];
+        _cacheWorker = cacheWorker;
+        _url = url;
+    }
+    return self;
+}
+
+- (void)start {
+    [self processActions];
+}
+
+- (void)cancel {
+    if (_session) {
+        [self.session invalidateAndCancel];
+    }
+    self.cancelled = YES;
+}
+
+- (VIURLSessionDelegateObject *)sessionDelegateObject {
+    if (!_sessionDelegateObject) {
+        _sessionDelegateObject = [[VIURLSessionDelegateObject alloc] initWithDelegate:self];
+    }
+    
+    return _sessionDelegateObject;
+}
+
+- (NSURLSession *)session {
+    if (!_session) {
+        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
+        NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self.sessionDelegateObject delegateQueue:[VICacheSessionManager shared].downloadQueue];
+        _session = session;
+    }
+    return _session;
+}
+
+- (void)processActions {
+    if (self.isCancelled) {
+        return;
+    }
+    
+    VICacheAction *action = [self.actions firstObject];
+    if (!action) {
+        if ([self.delegate respondsToSelector:@selector(actionWorker:didFinishWithError:)]) {
+            [self.delegate actionWorker:self didFinishWithError:nil];
+        }
+        return;
+    }
+    [self.actions removeObjectAtIndex:0];
+    
+    if (action.actionType == VICacheAtionTypeLocal) {
+        NSError *error;
+        NSData *data = [self.cacheWorker cachedDataForRange:action.range error:&error];
+        if (error) {
+            if ([self.delegate respondsToSelector:@selector(actionWorker:didFinishWithError:)]) {
+                [self.delegate actionWorker:self didFinishWithError:error];
+            }
+        } else {
+            if ([self.delegate respondsToSelector:@selector(actionWorker:didReceiveData:isLocal:)]) {
+                [self.delegate actionWorker:self didReceiveData:data isLocal:YES];
+            }
+            [self processActions];
+        }
+    } else {
+        long long fromOffset = action.range.location;
+        long long endOffset = action.range.location + action.range.length - 1;
+        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.url];
+        request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
+        NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, endOffset];
+        [request setValue:range forHTTPHeaderField:@"Range"];
+        self.startOffset = action.range.location;
+        self.task = [self.session dataTaskWithRequest:request];
+        [self.task resume];
+    }
+}
+
+- (void)notifyDownloadProgressWithFlush:(BOOL)flush finished:(BOOL)finished {
+    double currentTime = CFAbsoluteTimeGetCurrent();
+    double interval = [VICacheManager cacheUpdateNotifyInterval];
+    if ((self.notifyTime < currentTime - interval) || flush) {
+        self.notifyTime = currentTime;
+        VICacheConfiguration *configuration = [self.cacheWorker.cacheConfiguration copy];
+        [[NSNotificationCenter defaultCenter] postNotificationName:VICacheManagerDidUpdateCacheNotification
+                                                            object:self
+                                                          userInfo:@{
+                                                                     VICacheConfigurationKey: configuration,
+                                                                     }];
+            
+        if (finished && configuration.progress >= 1.0) {
+            [self notifyDownloadFinishedWithError:nil];
+        }
+    }
+}
+
+- (void)notifyDownloadFinishedWithError:(NSError *)error {
+    VICacheConfiguration *configuration = [self.cacheWorker.cacheConfiguration copy];
+    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+    [userInfo setValue:configuration forKey:VICacheConfigurationKey];
+    [userInfo setValue:error forKey:VICacheFinishedErrorKey];
+    
+    [[NSNotificationCenter defaultCenter] postNotificationName:VICacheManagerDidFinishCacheNotification
+                                                        object:self
+                                                      userInfo:userInfo];
+}
+
+#pragma mark - VIURLSessionDelegateObjectDelegate
+
+- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
+    NSURLCredential *card = [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust];
+    completionHandler(NSURLSessionAuthChallengeUseCredential,card);
+}
+
+- (void)URLSession:(NSURLSession *)session
+          dataTask:(NSURLSessionDataTask *)dataTask
+didReceiveResponse:(NSURLResponse *)response
+ completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
+    NSString *mimeType = response.MIMEType;
+    // Only download video/audio data
+    if ([mimeType rangeOfString:@"video/"].location == NSNotFound &&
+        [mimeType rangeOfString:@"audio/"].location == NSNotFound &&
+        [mimeType rangeOfString:@"application"].location == NSNotFound) {
+        completionHandler(NSURLSessionResponseCancel);
+    } else {
+        if ([self.delegate respondsToSelector:@selector(actionWorker:didReceiveResponse:)]) {
+            [self.delegate actionWorker:self didReceiveResponse:response];
+        }
+        if (self.canSaveToCache) {
+            [self.cacheWorker startWritting];
+        }
+        completionHandler(NSURLSessionResponseAllow);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session
+          dataTask:(NSURLSessionDataTask *)dataTask
+    didReceiveData:(NSData *)data {
+    if (self.isCancelled) {
+        return;
+    }
+    
+    if (self.canSaveToCache) {
+        NSRange range = NSMakeRange(self.startOffset, data.length);
+        NSError *error;
+        [self.cacheWorker cacheData:data forRange:range error:&error];
+        if (error) {
+            if ([self.delegate respondsToSelector:@selector(actionWorker:didFinishWithError:)]) {
+                [self.delegate actionWorker:self didFinishWithError:error];
+            }
+            return;
+        }
+        [self.cacheWorker save];
+    }
+    
+    self.startOffset += data.length;
+    if ([self.delegate respondsToSelector:@selector(actionWorker:didReceiveData:isLocal:)]) {
+        [self.delegate actionWorker:self didReceiveData:data isLocal:NO];
+    }
+    
+    [self notifyDownloadProgressWithFlush:NO finished:NO];
+}
+
+- (void)URLSession:(NSURLSession *)session
+              task:(NSURLSessionTask *)task
+didCompleteWithError:(nullable NSError *)error {
+    if (self.canSaveToCache) {
+        [self.cacheWorker finishWritting];
+        [self.cacheWorker save];
+    }
+    if (error) {
+        if ([self.delegate respondsToSelector:@selector(actionWorker:didFinishWithError:)]) {
+            [self.delegate actionWorker:self didFinishWithError:error];
+        }
+        [self notifyDownloadFinishedWithError:error];
+    } else {
+        [self notifyDownloadProgressWithFlush:YES finished:YES];
+        [self processActions];
+    }
+}
+
+@end
+
+#pragma mark - Class: VIMediaDownloaderStatus
+
+
+@interface VIMediaDownloaderStatus ()
+
+@property (nonatomic, strong) NSMutableSet *downloadingURLS;
+
+@end
+
+@implementation VIMediaDownloaderStatus
+
++ (instancetype)shared {
+    static VIMediaDownloaderStatus *instance = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        instance = [[self alloc] init];
+        instance.downloadingURLS = [NSMutableSet set];
+    });
+    
+    return instance;
+}
+
+- (void)addURL:(NSURL *)url {
+    @synchronized (self.downloadingURLS) {
+        [self.downloadingURLS addObject:url];
+    }
+}
+
+- (void)removeURL:(NSURL *)url {
+    @synchronized (self.downloadingURLS) {
+        [self.downloadingURLS removeObject:url];
+    }
+}
+
+- (BOOL)containsURL:(NSURL *)url {
+    @synchronized (self.downloadingURLS) {
+        return [self.downloadingURLS containsObject:url];
+    }
+}
+
+- (NSSet *)urls {
+    return [self.downloadingURLS copy];
+}
+
+@end
+
+#pragma mark - Class: VIMediaDownloader
+
+@interface VIMediaDownloader () <VIActionWorkerDelegate>
+
+@property (nonatomic, strong) NSURL *url;
+@property (nonatomic, strong) NSURLSessionDataTask *task;
+
+@property (nonatomic, strong) VIMediaCacheWorker *cacheWorker;
+@property (nonatomic, strong) VIActionWorker *actionWorker;
+
+@property (nonatomic) BOOL downloadToEnd;
+
+@end
+
+@implementation VIMediaDownloader
+
+- (void)dealloc {
+    [[VIMediaDownloaderStatus shared] removeURL:self.url];
+}
+
+- (instancetype)initWithURL:(NSURL *)url cacheWorker:(VIMediaCacheWorker *)cacheWorker {
+    self = [super init];
+    if (self) {
+        _saveToCache = YES;
+        _url = url;
+        _cacheWorker = cacheWorker;
+        _info = _cacheWorker.cacheConfiguration.contentInfo;
+        [[VIMediaDownloaderStatus shared] addURL:self.url];
+    }
+    return self;
+}
+
+- (void)downloadTaskFromOffset:(unsigned long long)fromOffset
+                        length:(NSUInteger)length
+                         toEnd:(BOOL)toEnd {
+    // ---
+    NSRange range = NSMakeRange((NSUInteger)fromOffset, length);
+    
+    if (toEnd) {
+        range.length = (NSUInteger)self.cacheWorker.cacheConfiguration.contentInfo.contentLength - range.location;
+    }
+    
+    NSArray *actions = [self.cacheWorker cachedDataActionsForRange:range];
+
+    self.actionWorker = [[VIActionWorker alloc] initWithActions:actions url:self.url cacheWorker:self.cacheWorker];
+    self.actionWorker.canSaveToCache = self.saveToCache;
+    self.actionWorker.delegate = self;
+    [self.actionWorker start];
+}
+
+- (void)downloadFromStartToEnd {
+    // ---
+    self.downloadToEnd = YES;
+    NSRange range = NSMakeRange(0, 2);
+    NSArray *actions = [self.cacheWorker cachedDataActionsForRange:range];
+
+    self.actionWorker = [[VIActionWorker alloc] initWithActions:actions url:self.url cacheWorker:self.cacheWorker];
+    self.actionWorker.canSaveToCache = self.saveToCache;
+    self.actionWorker.delegate = self;
+    [self.actionWorker start];
+}
+
+- (void)cancel {
+    self.actionWorker.delegate = nil;
+    [[VIMediaDownloaderStatus shared] removeURL:self.url];
+    [self.actionWorker cancel];
+    self.actionWorker = nil;
+}
+
+#pragma mark - VIActionWorkerDelegate
+
+- (void)actionWorker:(VIActionWorker *)actionWorker didReceiveResponse:(NSURLResponse *)response {
+    if (!self.info) {
+        VIContentInfo *info = [VIContentInfo new];
+        
+        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+            NSHTTPURLResponse *HTTPURLResponse = (NSHTTPURLResponse *)response;
+            NSString *acceptRange = HTTPURLResponse.allHeaderFields[@"Accept-Ranges"];
+            info.byteRangeAccessSupported = [acceptRange isEqualToString:@"bytes"];
+            info.contentLength = [[[HTTPURLResponse.allHeaderFields[@"Content-Range"] componentsSeparatedByString:@"/"] lastObject] longLongValue];
+        }
+        NSString *mimeType = response.MIMEType;
+        CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
+        info.contentType = CFBridgingRelease(contentType);
+        self.info = info;
+        
+        NSError *error;
+        [self.cacheWorker setContentInfo:info error:&error];
+        if (error) {
+            if ([self.delegate respondsToSelector:@selector(mediaDownloader:didFinishedWithError:)]) {
+                [self.delegate mediaDownloader:self didFinishedWithError:error];
+            }
+            return;
+        }
+    }
+    
+    if ([self.delegate respondsToSelector:@selector(mediaDownloader:didReceiveResponse:)]) {
+        [self.delegate mediaDownloader:self didReceiveResponse:response];
+    }
+}
+
+- (void)actionWorker:(VIActionWorker *)actionWorker didReceiveData:(NSData *)data isLocal:(BOOL)isLocal {
+    if ([self.delegate respondsToSelector:@selector(mediaDownloader:didReceiveData:)]) {
+        [self.delegate mediaDownloader:self didReceiveData:data];
+    }
+}
+
+- (void)actionWorker:(VIActionWorker *)actionWorker didFinishWithError:(NSError *)error {
+    [[VIMediaDownloaderStatus shared] removeURL:self.url];
+    
+    if (!error && self.downloadToEnd) {
+        self.downloadToEnd = NO;
+        [self downloadTaskFromOffset:2 length:(NSUInteger)(self.cacheWorker.cacheConfiguration.contentInfo.contentLength - 2) toEnd:YES];
+    } else {
+        if ([self.delegate respondsToSelector:@selector(mediaDownloader:didFinishedWithError:)]) {
+            [self.delegate mediaDownloader:self didFinishedWithError:error];
+        }
+    }
+}
+
+@end

+ 31 - 0
VIMediaCache/ResourceLoader/VIResourceLoader.h

@@ -0,0 +1,31 @@
+//
+//  VIResoureLoader.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+@import AVFoundation;
+@protocol VIResourceLoaderDelegate;
+
+@interface VIResourceLoader : NSObject
+
+@property (nonatomic, strong, readonly) NSURL *url;
+@property (nonatomic, weak) id<VIResourceLoaderDelegate> delegate;
+
+- (instancetype)initWithURL:(NSURL *)url;
+
+- (void)addRequest:(AVAssetResourceLoadingRequest *)request;
+- (void)removeRequest:(AVAssetResourceLoadingRequest *)request;
+
+- (void)cancel;
+
+@end
+
+@protocol VIResourceLoaderDelegate <NSObject>
+
+- (void)resourceLoader:(VIResourceLoader *)resourceLoader didFailWithError:(NSError *)error;
+
+@end

+ 121 - 0
VIMediaCache/ResourceLoader/VIResourceLoader.m

@@ -0,0 +1,121 @@
+//
+//  VIResoureLoader.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIResourceLoader.h"
+#import "VIMediaDownloader.h"
+#import "VIResourceLoadingRequestWorker.h"
+#import "VIContentInfo.h"
+#import "VIMediaCacheWorker.h"
+
+NSString * const MCResourceLoaderErrorDomain = @"LSFilePlayerResourceLoaderErrorDomain";
+
+@interface VIResourceLoader () <VIResourceLoadingRequestWorkerDelegate>
+
+@property (nonatomic, strong, readwrite) NSURL *url;
+@property (nonatomic, strong) VIMediaCacheWorker *cacheWorker;
+@property (nonatomic, strong) VIMediaDownloader *mediaDownloader;
+@property (nonatomic, strong) NSMutableArray<VIResourceLoadingRequestWorker *> *pendingRequestWorkers;
+
+@property (nonatomic, getter=isCancelled) BOOL cancelled;
+
+@end
+
+@implementation VIResourceLoader
+
+
+- (void)dealloc {
+    [_mediaDownloader cancel];
+}
+
+- (instancetype)initWithURL:(NSURL *)url {
+    self = [super init];
+    if (self) {
+        _url = url;
+        _cacheWorker = [[VIMediaCacheWorker alloc] initWithURL:url];
+        _mediaDownloader = [[VIMediaDownloader alloc] initWithURL:url cacheWorker:_cacheWorker];
+        _pendingRequestWorkers = [NSMutableArray array];
+    }
+    return self;
+}
+
+- (instancetype)init {
+    NSAssert(NO, @"Use - initWithURL: instead");
+    return nil;
+}
+
+- (void)addRequest:(AVAssetResourceLoadingRequest *)request {
+    if (self.pendingRequestWorkers.count > 0) {
+        [self startNoCacheWorkerWithRequest:request];
+    } else {
+        [self startWorkerWithRequest:request];
+    }
+}
+
+- (void)removeRequest:(AVAssetResourceLoadingRequest *)request {
+    __block VIResourceLoadingRequestWorker *requestWorker = nil;
+    [self.pendingRequestWorkers enumerateObjectsUsingBlock:^(VIResourceLoadingRequestWorker *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+        if (obj.request == request) {
+            requestWorker = obj;
+            *stop = YES;
+        }
+    }];
+    if (requestWorker) {
+        [requestWorker finish];
+        [self.pendingRequestWorkers removeObject:requestWorker];
+    }
+}
+
+- (void)cancel {
+    [self.mediaDownloader cancel];
+    [self.pendingRequestWorkers removeAllObjects];
+    
+    [[VIMediaDownloaderStatus shared] removeURL:self.url];
+}
+
+#pragma mark - VIResourceLoadingRequestWorkerDelegate
+
+- (void)resourceLoadingRequestWorker:(VIResourceLoadingRequestWorker *)requestWorker didCompleteWithError:(NSError *)error {
+    [self removeRequest:requestWorker.request];
+    if (error && [self.delegate respondsToSelector:@selector(resourceLoader:didFailWithError:)]) {
+        [self.delegate resourceLoader:self didFailWithError:error];
+    }
+    if (self.pendingRequestWorkers.count == 0) {
+        [[VIMediaDownloaderStatus shared] removeURL:self.url];
+    }
+}
+
+#pragma mark - Helper
+
+- (void)startNoCacheWorkerWithRequest:(AVAssetResourceLoadingRequest *)request {
+    [[VIMediaDownloaderStatus shared] addURL:self.url];
+    VIMediaDownloader *mediaDownloader = [[VIMediaDownloader alloc] initWithURL:self.url cacheWorker:self.cacheWorker];
+    VIResourceLoadingRequestWorker *requestWorker = [[VIResourceLoadingRequestWorker alloc] initWithMediaDownloader:mediaDownloader
+                                                                                             resourceLoadingRequest:request];
+    [self.pendingRequestWorkers addObject:requestWorker];
+    requestWorker.delegate = self;
+    [requestWorker startWork];
+}
+
+- (void)startWorkerWithRequest:(AVAssetResourceLoadingRequest *)request {
+    [[VIMediaDownloaderStatus shared] addURL:self.url];
+    VIResourceLoadingRequestWorker *requestWorker = [[VIResourceLoadingRequestWorker alloc] initWithMediaDownloader:self.mediaDownloader
+                                                                                             resourceLoadingRequest:request];
+    [self.pendingRequestWorkers addObject:requestWorker];
+    requestWorker.delegate = self;
+    [requestWorker startWork];
+    
+}
+
+- (NSError *)loaderCancelledError {
+    NSError *error = [[NSError alloc] initWithDomain:MCResourceLoaderErrorDomain
+                                                code:-3
+                                            userInfo:@{NSLocalizedDescriptionKey:@"Resource loader cancelled"}];
+    return error;
+}
+
+@end

+ 42 - 0
VIMediaCache/ResourceLoader/VIResourceLoaderManager.h

@@ -0,0 +1,42 @@
+//
+//  VIResourceLoaderManager.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+@import AVFoundation;
+@protocol VIResourceLoaderManagerDelegate;
+
+@interface VIResourceLoaderManager : NSObject <AVAssetResourceLoaderDelegate>
+
+
+@property (nonatomic, weak) id<VIResourceLoaderManagerDelegate> delegate;
+
+/**
+ Normally you no need to call this method to clean cache. Cache cleaned after AVPlayer delloc.
+ If you have a singleton AVPlayer then you need call this method to clean cache at suitable time.
+ */
+- (void)cleanCache;
+
+/**
+ Cancel all downloading loaders.
+ */
+- (void)cancelLoaders;
+
+@end
+
+@protocol VIResourceLoaderManagerDelegate <NSObject>
+
+- (void)resourceLoaderManagerLoadURL:(NSURL *)url didFailWithError:(NSError *)error;
+
+@end
+
+@interface VIResourceLoaderManager (Convenient)
+
++ (NSURL *)assetURLWithURL:(NSURL *)url;
+- (AVPlayerItem *)playerItemWithURL:(NSURL *)url;
+
+@end

+ 118 - 0
VIMediaCache/ResourceLoader/VIResourceLoaderManager.m

@@ -0,0 +1,118 @@
+//
+//  VIResourceLoaderManager.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIResourceLoaderManager.h"
+#import "VIResourceLoader.h"
+
+static NSString *kCacheScheme = @"__VIMediaCache___:";
+
+@interface VIResourceLoaderManager () <VIResourceLoaderDelegate>
+
+@property (nonatomic, strong) NSMutableDictionary<id<NSCoding>, VIResourceLoader *> *loaders;
+
+@end
+
+@implementation VIResourceLoaderManager
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        _loaders = [NSMutableDictionary dictionary];
+    }
+    return self;
+}
+
+- (void)cleanCache {
+    [self.loaders removeAllObjects];
+}
+
+- (void)cancelLoaders {
+    [self.loaders enumerateKeysAndObjectsUsingBlock:^(id<NSCoding>  _Nonnull key, VIResourceLoader * _Nonnull obj, BOOL * _Nonnull stop) {
+        [obj cancel];
+    }];
+    [self.loaders removeAllObjects];
+}
+
+#pragma mark - AVAssetResourceLoaderDelegate
+
+- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest  {
+    NSURL *resourceURL = [loadingRequest.request URL];
+    if ([resourceURL.absoluteString hasPrefix:kCacheScheme]) {
+        VIResourceLoader *loader = [self loaderForRequest:loadingRequest];
+        if (!loader) {
+            NSURL *originURL = nil;
+            NSString *originStr = [resourceURL absoluteString];
+            originStr = [originStr stringByReplacingOccurrencesOfString:kCacheScheme withString:@""];
+            originURL = [NSURL URLWithString:originStr];
+            loader = [[VIResourceLoader alloc] initWithURL:originURL];
+            loader.delegate = self;
+            NSString *key = [self keyForResourceLoaderWithURL:resourceURL];
+            self.loaders[key] = loader;
+        }
+        [loader addRequest:loadingRequest];
+        return YES;
+    }
+    
+    return NO;
+}
+
+- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
+    VIResourceLoader *loader = [self loaderForRequest:loadingRequest];
+    [loader removeRequest:loadingRequest];
+}
+
+#pragma mark - VIResourceLoaderDelegate
+
+- (void)resourceLoader:(VIResourceLoader *)resourceLoader didFailWithError:(NSError *)error {
+    [resourceLoader cancel];
+    if ([self.delegate respondsToSelector:@selector(resourceLoaderManagerLoadURL:didFailWithError:)]) {
+        [self.delegate resourceLoaderManagerLoadURL:resourceLoader.url didFailWithError:error];
+    }
+}
+
+#pragma mark - Helper
+
+- (NSString *)keyForResourceLoaderWithURL:(NSURL *)requestURL {
+    if([[requestURL absoluteString] hasPrefix:kCacheScheme]){
+        NSString *s = requestURL.absoluteString;
+        return s;
+    }
+    return nil;
+}
+
+- (VIResourceLoader *)loaderForRequest:(AVAssetResourceLoadingRequest *)request {
+    NSString *requestKey = [self keyForResourceLoaderWithURL:request.request.URL];
+    VIResourceLoader *loader = self.loaders[requestKey];
+    return loader;
+}
+
+@end
+
+@implementation VIResourceLoaderManager (Convenient)
+
++ (NSURL *)assetURLWithURL:(NSURL *)url {
+    if (!url) {
+        return nil;
+    }
+
+    NSURL *assetURL = [NSURL URLWithString:[kCacheScheme stringByAppendingString:[url absoluteString]]];
+    return assetURL;
+}
+
+- (AVPlayerItem *)playerItemWithURL:(NSURL *)url {
+    NSURL *assetURL = [VIResourceLoaderManager assetURLWithURL:url];
+    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:assetURL options:nil];
+    [urlAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
+    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:urlAsset];
+    if ([playerItem respondsToSelector:@selector(setCanUseNetworkResourcesForLiveStreamingWhilePaused:)]) {
+        playerItem.canUseNetworkResourcesForLiveStreamingWhilePaused = YES;
+    }
+    return playerItem;
+}
+
+@end

+ 32 - 0
VIMediaCache/ResourceLoader/VIResourceLoadingRequestWorker.h

@@ -0,0 +1,32 @@
+//
+//  VIResourceLoadingRequestWorker.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@class VIMediaDownloader, AVAssetResourceLoadingRequest;
+@protocol VIResourceLoadingRequestWorkerDelegate;
+
+@interface VIResourceLoadingRequestWorker : NSObject
+
+- (instancetype)initWithMediaDownloader:(VIMediaDownloader *)mediaDownloader resourceLoadingRequest:(AVAssetResourceLoadingRequest *)request;
+
+@property (nonatomic, weak) id<VIResourceLoadingRequestWorkerDelegate> delegate;
+
+@property (nonatomic, strong, readonly) AVAssetResourceLoadingRequest *request;
+
+- (void)startWork;
+- (void)cancel;
+- (void)finish;
+
+@end
+
+@protocol VIResourceLoadingRequestWorkerDelegate <NSObject>
+
+- (void)resourceLoadingRequestWorker:(VIResourceLoadingRequestWorker *)requestWorker didCompleteWithError:(NSError *)error;
+
+@end

+ 107 - 0
VIMediaCache/ResourceLoader/VIResourceLoadingRequestWorker.m

@@ -0,0 +1,107 @@
+//
+//  VIResourceLoadingRequestWorker.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/21/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "VIResourceLoadingRequestWorker.h"
+#import "VIMediaDownloader.h"
+#import "VIContentInfo.h"
+
+@import MobileCoreServices;
+@import AVFoundation;
+@import UIKit;
+
+@interface VIResourceLoadingRequestWorker () <VIMediaDownloaderDelegate>
+
+@property (nonatomic, strong, readwrite) AVAssetResourceLoadingRequest *request;
+@property (nonatomic, strong) VIMediaDownloader *mediaDownloader;
+
+@end
+
+@implementation VIResourceLoadingRequestWorker
+
+- (instancetype)initWithMediaDownloader:(VIMediaDownloader *)mediaDownloader resourceLoadingRequest:(AVAssetResourceLoadingRequest *)request {
+    self = [super init];
+    if (self) {
+        _mediaDownloader = mediaDownloader;
+        _mediaDownloader.delegate = self;
+        _request = request;
+        
+        [self fullfillContentInfo];
+    }
+    return self;
+}
+
+- (void)startWork {
+    AVAssetResourceLoadingDataRequest *dataRequest = self.request.dataRequest;
+    
+    long long offset = dataRequest.requestedOffset;
+    NSInteger length = dataRequest.requestedLength;
+    if (dataRequest.currentOffset != 0) {
+        offset = dataRequest.currentOffset;
+    }
+    
+    BOOL toEnd = NO;
+    if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) {
+        if (dataRequest.requestsAllDataToEndOfResource) {
+            toEnd = YES;
+        }
+    }
+    [self.mediaDownloader downloadTaskFromOffset:offset length:length toEnd:toEnd];
+}
+
+- (void)cancel {
+    [self.mediaDownloader cancel];
+}
+
+- (void)finish {
+    if (!self.request.isFinished) {
+        [self.request finishLoadingWithError:[self loaderCancelledError]];
+    }
+}
+
+- (NSError *)loaderCancelledError{
+    NSError *error = [[NSError alloc] initWithDomain:@"com.resourceloader"
+                                                code:-3
+                                            userInfo:@{NSLocalizedDescriptionKey:@"Resource loader cancelled"}];
+    return error;
+}
+
+- (void)fullfillContentInfo {
+    AVAssetResourceLoadingContentInformationRequest *contentInformationRequest = self.request.contentInformationRequest;
+    if (self.mediaDownloader.info && !contentInformationRequest.contentType) {
+        // Fullfill content information
+        contentInformationRequest.contentType = self.mediaDownloader.info.contentType;
+        contentInformationRequest.contentLength = self.mediaDownloader.info.contentLength;
+        contentInformationRequest.byteRangeAccessSupported = self.mediaDownloader.info.byteRangeAccessSupported;
+    }
+}
+
+#pragma mark - VIMediaDownloaderDelegate
+
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didReceiveResponse:(NSURLResponse *)response {
+    [self fullfillContentInfo];
+}
+
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didReceiveData:(NSData *)data {
+    [self.request.dataRequest respondWithData:data];
+}
+
+- (void)mediaDownloader:(VIMediaDownloader *)downloader didFinishedWithError:(NSError *)error {
+    if (error.code == NSURLErrorCancelled) {
+        return;
+    }
+    
+    if (!error) {
+        [self.request finishLoading];
+    } else {
+        [self.request finishLoadingWithError:error];
+    }
+    
+    [self.delegate resourceLoadingRequestWorker:self didCompleteWithError:error];
+}
+
+@end

+ 17 - 0
VIMediaCache/VIMediaCache.h

@@ -0,0 +1,17 @@
+//
+//  VIMediaCache.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 4/22/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#ifndef VIMediaCache_h
+#define VIMediaCache_h
+
+#import "VIResourceLoaderManager.h"
+#import "VICacheManager.h"
+#import "VIMediaDownloader.h"
+#import "VIContentInfo.h"
+
+#endif /* VIMediaCache_h */

+ 534 - 0
VIMediaCacheDemo.xcodeproj/project.pbxproj

@@ -0,0 +1,534 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		5F2836BD1D96548E000910CA /* VICacheAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2836B41D96548E000910CA /* VICacheAction.m */; };
+		5F2836BE1D96548E000910CA /* VICacheConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2836B61D96548E000910CA /* VICacheConfiguration.m */; };
+		5F2836BF1D96548E000910CA /* VICacheManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2836B81D96548E000910CA /* VICacheManager.m */; };
+		5F2836C01D96548E000910CA /* VICacheSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2836BA1D96548E000910CA /* VICacheSessionManager.m */; };
+		5F2836C11D96548E000910CA /* VIMediaCacheWorker.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2836BC1D96548E000910CA /* VIMediaCacheWorker.m */; };
+		5F5FA6101CEA2BE2004439D3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5FA60F1CEA2BE2004439D3 /* main.m */; };
+		5F5FA6131CEA2BE2004439D3 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5FA6121CEA2BE2004439D3 /* AppDelegate.m */; };
+		5F5FA6161CEA2BE2004439D3 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5FA6151CEA2BE2004439D3 /* ViewController.m */; };
+		5F5FA6191CEA2BE2004439D3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F5FA6171CEA2BE2004439D3 /* Main.storyboard */; };
+		5F5FA61B1CEA2BE2004439D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F5FA61A1CEA2BE2004439D3 /* Assets.xcassets */; };
+		5F5FA61E1CEA2BE2004439D3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F5FA61C1CEA2BE2004439D3 /* LaunchScreen.storyboard */; };
+		5F5FA6291CEA2BE2004439D3 /* VIMediaCacheDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5FA6281CEA2BE2004439D3 /* VIMediaCacheDemoTests.m */; };
+		5F5FA67F1CEA2D95004439D3 /* PlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F5FA67E1CEA2D95004439D3 /* PlayerView.m */; };
+		5FC756B41FC47A4C00608495 /* NSString+VIMD5.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FC756B31FC47A4C00608495 /* NSString+VIMD5.m */; };
+		5FDCF2AE1CEBF26800188E0C /* VIContentInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCF2971CEBF26800188E0C /* VIContentInfo.m */; };
+		5FDCF2AF1CEBF26800188E0C /* VIMediaDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCF2991CEBF26800188E0C /* VIMediaDownloader.m */; };
+		5FDCF2B01CEBF26800188E0C /* VIResourceLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCF29B1CEBF26800188E0C /* VIResourceLoader.m */; };
+		5FDCF2B11CEBF26800188E0C /* VIResourceLoaderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCF29D1CEBF26800188E0C /* VIResourceLoaderManager.m */; };
+		5FDCF2B21CEBF26800188E0C /* VIResourceLoadingRequestWorker.m in Sources */ = {isa = PBXBuildFile; fileRef = 5FDCF29F1CEBF26800188E0C /* VIResourceLoadingRequestWorker.m */; };
+		5FDCF2BA1CEBF74600188E0C /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FDCF2B91CEBF74600188E0C /* MobileCoreServices.framework */; };
+		5FDCF2BC1CEBF76200188E0C /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FDCF2BB1CEBF76200188E0C /* AVFoundation.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		5F5FA6251CEA2BE2004439D3 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 5F5FA6031CEA2BE2004439D3 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 5F5FA60A1CEA2BE2004439D3;
+			remoteInfo = VIMediaCacheDemo;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		5F2836B31D96548E000910CA /* VICacheAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VICacheAction.h; sourceTree = "<group>"; };
+		5F2836B41D96548E000910CA /* VICacheAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VICacheAction.m; sourceTree = "<group>"; };
+		5F2836B51D96548E000910CA /* VICacheConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VICacheConfiguration.h; sourceTree = "<group>"; };
+		5F2836B61D96548E000910CA /* VICacheConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VICacheConfiguration.m; sourceTree = "<group>"; };
+		5F2836B71D96548E000910CA /* VICacheManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VICacheManager.h; sourceTree = "<group>"; };
+		5F2836B81D96548E000910CA /* VICacheManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VICacheManager.m; sourceTree = "<group>"; };
+		5F2836B91D96548E000910CA /* VICacheSessionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VICacheSessionManager.h; sourceTree = "<group>"; };
+		5F2836BA1D96548E000910CA /* VICacheSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VICacheSessionManager.m; sourceTree = "<group>"; };
+		5F2836BB1D96548E000910CA /* VIMediaCacheWorker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIMediaCacheWorker.h; sourceTree = "<group>"; };
+		5F2836BC1D96548E000910CA /* VIMediaCacheWorker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIMediaCacheWorker.m; sourceTree = "<group>"; };
+		5F5FA60B1CEA2BE2004439D3 /* VIMediaCacheDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VIMediaCacheDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		5F5FA60F1CEA2BE2004439D3 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		5F5FA6111CEA2BE2004439D3 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		5F5FA6121CEA2BE2004439D3 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		5F5FA6141CEA2BE2004439D3 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
+		5F5FA6151CEA2BE2004439D3 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
+		5F5FA6181CEA2BE2004439D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		5F5FA61A1CEA2BE2004439D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		5F5FA61D1CEA2BE2004439D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		5F5FA61F1CEA2BE2004439D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		5F5FA6241CEA2BE2004439D3 /* VIMediaCacheDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VIMediaCacheDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		5F5FA6281CEA2BE2004439D3 /* VIMediaCacheDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VIMediaCacheDemoTests.m; sourceTree = "<group>"; };
+		5F5FA62A1CEA2BE2004439D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		5F5FA67D1CEA2D95004439D3 /* PlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerView.h; sourceTree = "<group>"; };
+		5F5FA67E1CEA2D95004439D3 /* PlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerView.m; sourceTree = "<group>"; };
+		5FC756B21FC47A4C00608495 /* NSString+VIMD5.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+VIMD5.h"; sourceTree = "<group>"; };
+		5FC756B31FC47A4C00608495 /* NSString+VIMD5.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+VIMD5.m"; sourceTree = "<group>"; };
+		5FDCF2961CEBF26800188E0C /* VIContentInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIContentInfo.h; sourceTree = "<group>"; };
+		5FDCF2971CEBF26800188E0C /* VIContentInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIContentInfo.m; sourceTree = "<group>"; };
+		5FDCF2981CEBF26800188E0C /* VIMediaDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIMediaDownloader.h; sourceTree = "<group>"; };
+		5FDCF2991CEBF26800188E0C /* VIMediaDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIMediaDownloader.m; sourceTree = "<group>"; };
+		5FDCF29A1CEBF26800188E0C /* VIResourceLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIResourceLoader.h; sourceTree = "<group>"; };
+		5FDCF29B1CEBF26800188E0C /* VIResourceLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIResourceLoader.m; sourceTree = "<group>"; };
+		5FDCF29C1CEBF26800188E0C /* VIResourceLoaderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIResourceLoaderManager.h; sourceTree = "<group>"; };
+		5FDCF29D1CEBF26800188E0C /* VIResourceLoaderManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIResourceLoaderManager.m; sourceTree = "<group>"; };
+		5FDCF29E1CEBF26800188E0C /* VIResourceLoadingRequestWorker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIResourceLoadingRequestWorker.h; sourceTree = "<group>"; };
+		5FDCF29F1CEBF26800188E0C /* VIResourceLoadingRequestWorker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIResourceLoadingRequestWorker.m; sourceTree = "<group>"; };
+		5FDCF2AD1CEBF26800188E0C /* VIMediaCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIMediaCache.h; sourceTree = "<group>"; };
+		5FDCF2B91CEBF74600188E0C /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
+		5FDCF2BB1CEBF76200188E0C /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		5F5FA6081CEA2BE2004439D3 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5FDCF2BC1CEBF76200188E0C /* AVFoundation.framework in Frameworks */,
+				5FDCF2BA1CEBF74600188E0C /* MobileCoreServices.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5F5FA6211CEA2BE2004439D3 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		5F2836B21D96548E000910CA /* Cache */ = {
+			isa = PBXGroup;
+			children = (
+				5F2836B31D96548E000910CA /* VICacheAction.h */,
+				5F2836B41D96548E000910CA /* VICacheAction.m */,
+				5F2836B51D96548E000910CA /* VICacheConfiguration.h */,
+				5F2836B61D96548E000910CA /* VICacheConfiguration.m */,
+				5F2836B71D96548E000910CA /* VICacheManager.h */,
+				5F2836B81D96548E000910CA /* VICacheManager.m */,
+				5F2836B91D96548E000910CA /* VICacheSessionManager.h */,
+				5F2836BA1D96548E000910CA /* VICacheSessionManager.m */,
+				5F2836BB1D96548E000910CA /* VIMediaCacheWorker.h */,
+				5F2836BC1D96548E000910CA /* VIMediaCacheWorker.m */,
+				5FC756B21FC47A4C00608495 /* NSString+VIMD5.h */,
+				5FC756B31FC47A4C00608495 /* NSString+VIMD5.m */,
+			);
+			path = Cache;
+			sourceTree = "<group>";
+		};
+		5F5FA6021CEA2BE2004439D3 = {
+			isa = PBXGroup;
+			children = (
+				5F5FA60D1CEA2BE2004439D3 /* VIMediaCacheDemo */,
+				5F5FA6271CEA2BE2004439D3 /* VIMediaCacheDemoTests */,
+				5F5FA60C1CEA2BE2004439D3 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		5F5FA60C1CEA2BE2004439D3 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				5F5FA60B1CEA2BE2004439D3 /* VIMediaCacheDemo.app */,
+				5F5FA6241CEA2BE2004439D3 /* VIMediaCacheDemoTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		5F5FA60D1CEA2BE2004439D3 /* VIMediaCacheDemo */ = {
+			isa = PBXGroup;
+			children = (
+				5FDCF2941CEBF26800188E0C /* VIMediaCache */,
+				5F5FA6111CEA2BE2004439D3 /* AppDelegate.h */,
+				5F5FA6121CEA2BE2004439D3 /* AppDelegate.m */,
+				5F5FA6141CEA2BE2004439D3 /* ViewController.h */,
+				5F5FA6151CEA2BE2004439D3 /* ViewController.m */,
+				5F5FA6171CEA2BE2004439D3 /* Main.storyboard */,
+				5F5FA61A1CEA2BE2004439D3 /* Assets.xcassets */,
+				5F5FA61C1CEA2BE2004439D3 /* LaunchScreen.storyboard */,
+				5F5FA61F1CEA2BE2004439D3 /* Info.plist */,
+				5F5FA60E1CEA2BE2004439D3 /* Supporting Files */,
+				5F5FA67D1CEA2D95004439D3 /* PlayerView.h */,
+				5F5FA67E1CEA2D95004439D3 /* PlayerView.m */,
+			);
+			path = VIMediaCacheDemo;
+			sourceTree = "<group>";
+		};
+		5F5FA60E1CEA2BE2004439D3 /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				5FDCF2BB1CEBF76200188E0C /* AVFoundation.framework */,
+				5FDCF2B91CEBF74600188E0C /* MobileCoreServices.framework */,
+				5F5FA60F1CEA2BE2004439D3 /* main.m */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		5F5FA6271CEA2BE2004439D3 /* VIMediaCacheDemoTests */ = {
+			isa = PBXGroup;
+			children = (
+				5F5FA6281CEA2BE2004439D3 /* VIMediaCacheDemoTests.m */,
+				5F5FA62A1CEA2BE2004439D3 /* Info.plist */,
+			);
+			path = VIMediaCacheDemoTests;
+			sourceTree = "<group>";
+		};
+		5FDCF2941CEBF26800188E0C /* VIMediaCache */ = {
+			isa = PBXGroup;
+			children = (
+				5F2836B21D96548E000910CA /* Cache */,
+				5FDCF2951CEBF26800188E0C /* ResourceLoader */,
+				5FDCF2AD1CEBF26800188E0C /* VIMediaCache.h */,
+			);
+			path = VIMediaCache;
+			sourceTree = SOURCE_ROOT;
+		};
+		5FDCF2951CEBF26800188E0C /* ResourceLoader */ = {
+			isa = PBXGroup;
+			children = (
+				5FDCF2961CEBF26800188E0C /* VIContentInfo.h */,
+				5FDCF2971CEBF26800188E0C /* VIContentInfo.m */,
+				5FDCF2981CEBF26800188E0C /* VIMediaDownloader.h */,
+				5FDCF2991CEBF26800188E0C /* VIMediaDownloader.m */,
+				5FDCF29A1CEBF26800188E0C /* VIResourceLoader.h */,
+				5FDCF29B1CEBF26800188E0C /* VIResourceLoader.m */,
+				5FDCF29C1CEBF26800188E0C /* VIResourceLoaderManager.h */,
+				5FDCF29D1CEBF26800188E0C /* VIResourceLoaderManager.m */,
+				5FDCF29E1CEBF26800188E0C /* VIResourceLoadingRequestWorker.h */,
+				5FDCF29F1CEBF26800188E0C /* VIResourceLoadingRequestWorker.m */,
+			);
+			path = ResourceLoader;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		5F5FA60A1CEA2BE2004439D3 /* VIMediaCacheDemo */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5F5FA62D1CEA2BE2004439D3 /* Build configuration list for PBXNativeTarget "VIMediaCacheDemo" */;
+			buildPhases = (
+				5F5FA6071CEA2BE2004439D3 /* Sources */,
+				5F5FA6081CEA2BE2004439D3 /* Frameworks */,
+				5F5FA6091CEA2BE2004439D3 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = VIMediaCacheDemo;
+			productName = VIMediaCacheDemo;
+			productReference = 5F5FA60B1CEA2BE2004439D3 /* VIMediaCacheDemo.app */;
+			productType = "com.apple.product-type.application";
+		};
+		5F5FA6231CEA2BE2004439D3 /* VIMediaCacheDemoTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 5F5FA6301CEA2BE2004439D3 /* Build configuration list for PBXNativeTarget "VIMediaCacheDemoTests" */;
+			buildPhases = (
+				5F5FA6201CEA2BE2004439D3 /* Sources */,
+				5F5FA6211CEA2BE2004439D3 /* Frameworks */,
+				5F5FA6221CEA2BE2004439D3 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				5F5FA6261CEA2BE2004439D3 /* PBXTargetDependency */,
+			);
+			name = VIMediaCacheDemoTests;
+			productName = VIMediaCacheDemoTests;
+			productReference = 5F5FA6241CEA2BE2004439D3 /* VIMediaCacheDemoTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		5F5FA6031CEA2BE2004439D3 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0800;
+				ORGANIZATIONNAME = Vito;
+				TargetAttributes = {
+					5F5FA60A1CEA2BE2004439D3 = {
+						CreatedOnToolsVersion = 7.3.1;
+						DevelopmentTeam = ZZX7396L9W;
+					};
+					5F5FA6231CEA2BE2004439D3 = {
+						CreatedOnToolsVersion = 7.3.1;
+						TestTargetID = 5F5FA60A1CEA2BE2004439D3;
+					};
+				};
+			};
+			buildConfigurationList = 5F5FA6061CEA2BE2004439D3 /* Build configuration list for PBXProject "VIMediaCacheDemo" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 5F5FA6021CEA2BE2004439D3;
+			productRefGroup = 5F5FA60C1CEA2BE2004439D3 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				5F5FA60A1CEA2BE2004439D3 /* VIMediaCacheDemo */,
+				5F5FA6231CEA2BE2004439D3 /* VIMediaCacheDemoTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		5F5FA6091CEA2BE2004439D3 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F5FA61E1CEA2BE2004439D3 /* LaunchScreen.storyboard in Resources */,
+				5F5FA61B1CEA2BE2004439D3 /* Assets.xcassets in Resources */,
+				5F5FA6191CEA2BE2004439D3 /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5F5FA6221CEA2BE2004439D3 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		5F5FA6071CEA2BE2004439D3 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5FDCF2AF1CEBF26800188E0C /* VIMediaDownloader.m in Sources */,
+				5FDCF2B21CEBF26800188E0C /* VIResourceLoadingRequestWorker.m in Sources */,
+				5F2836BE1D96548E000910CA /* VICacheConfiguration.m in Sources */,
+				5F5FA67F1CEA2D95004439D3 /* PlayerView.m in Sources */,
+				5F5FA6161CEA2BE2004439D3 /* ViewController.m in Sources */,
+				5F2836C11D96548E000910CA /* VIMediaCacheWorker.m in Sources */,
+				5F5FA6131CEA2BE2004439D3 /* AppDelegate.m in Sources */,
+				5FDCF2B01CEBF26800188E0C /* VIResourceLoader.m in Sources */,
+				5FDCF2B11CEBF26800188E0C /* VIResourceLoaderManager.m in Sources */,
+				5F2836C01D96548E000910CA /* VICacheSessionManager.m in Sources */,
+				5F5FA6101CEA2BE2004439D3 /* main.m in Sources */,
+				5FC756B41FC47A4C00608495 /* NSString+VIMD5.m in Sources */,
+				5F2836BD1D96548E000910CA /* VICacheAction.m in Sources */,
+				5F2836BF1D96548E000910CA /* VICacheManager.m in Sources */,
+				5FDCF2AE1CEBF26800188E0C /* VIContentInfo.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		5F5FA6201CEA2BE2004439D3 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				5F5FA6291CEA2BE2004439D3 /* VIMediaCacheDemoTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		5F5FA6261CEA2BE2004439D3 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 5F5FA60A1CEA2BE2004439D3 /* VIMediaCacheDemo */;
+			targetProxy = 5F5FA6251CEA2BE2004439D3 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		5F5FA6171CEA2BE2004439D3 /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5F5FA6181CEA2BE2004439D3 /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		5F5FA61C1CEA2BE2004439D3 /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				5F5FA61D1CEA2BE2004439D3 /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		5F5FA62B1CEA2BE2004439D3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+			};
+			name = Debug;
+		};
+		5F5FA62C1CEA2BE2004439D3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		5F5FA62E1CEA2BE2004439D3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = ZZX7396L9W;
+				INFOPLIST_FILE = VIMediaCacheDemo/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIMediaCacheDemo;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		5F5FA62F1CEA2BE2004439D3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEVELOPMENT_TEAM = ZZX7396L9W;
+				INFOPLIST_FILE = VIMediaCacheDemo/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIMediaCacheDemo;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
+		5F5FA6311CEA2BE2004439D3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				INFOPLIST_FILE = VIMediaCacheDemoTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIMediaCacheDemoTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VIMediaCacheDemo.app/VIMediaCacheDemo";
+			};
+			name = Debug;
+		};
+		5F5FA6321CEA2BE2004439D3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				INFOPLIST_FILE = VIMediaCacheDemoTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.vito.VIMediaCacheDemoTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VIMediaCacheDemo.app/VIMediaCacheDemo";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		5F5FA6061CEA2BE2004439D3 /* Build configuration list for PBXProject "VIMediaCacheDemo" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5F5FA62B1CEA2BE2004439D3 /* Debug */,
+				5F5FA62C1CEA2BE2004439D3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5F5FA62D1CEA2BE2004439D3 /* Build configuration list for PBXNativeTarget "VIMediaCacheDemo" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5F5FA62E1CEA2BE2004439D3 /* Debug */,
+				5F5FA62F1CEA2BE2004439D3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		5F5FA6301CEA2BE2004439D3 /* Build configuration list for PBXNativeTarget "VIMediaCacheDemoTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				5F5FA6311CEA2BE2004439D3 /* Debug */,
+				5F5FA6321CEA2BE2004439D3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 5F5FA6031CEA2BE2004439D3 /* Project object */;
+}

+ 7 - 0
VIMediaCacheDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:VIMediaCacheDemo.xcodeproj">
+   </FileRef>
+</Workspace>

+ 17 - 0
VIMediaCacheDemo/AppDelegate.h

@@ -0,0 +1,17 @@
+//
+//  AppDelegate.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+
+@end
+

+ 45 - 0
VIMediaCacheDemo/AppDelegate.m

@@ -0,0 +1,45 @@
+//
+//  AppDelegate.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "AppDelegate.h"
+
+@interface AppDelegate ()
+
+@end
+
+@implementation AppDelegate
+
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+    // Override point for customization after application launch.
+    return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+}
+
+@end

+ 38 - 0
VIMediaCacheDemo/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 27 - 0
VIMediaCacheDemo/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15E65" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 145 - 0
VIMediaCacheDemo/Base.lproj/Main.storyboard

@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12118" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="guh-7O-aMj">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12086"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--Table View Controller-->
+        <scene sceneID="bc8-Qy-GL7">
+            <objects>
+                <tableViewController id="amF-rs-8QD" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="lWb-bE-fjV">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <sections>
+                            <tableViewSection id="c3M-Gp-Nwj">
+                                <cells>
+                                    <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" textLabel="ejz-TK-cHo" style="IBUITableViewCellStyleDefault" id="IIr-lM-73b">
+                                        <rect key="frame" x="0.0" y="35" width="375" height="44"/>
+                                        <autoresizingMask key="autoresizingMask"/>
+                                        <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="IIr-lM-73b" id="L8k-E5-tQT">
+                                            <rect key="frame" x="0.0" y="0.0" width="342" height="44"/>
+                                            <autoresizingMask key="autoresizingMask"/>
+                                            <subviews>
+                                                <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Play video" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ejz-TK-cHo">
+                                                    <rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
+                                                    <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                                                    <fontDescription key="fontDescription" type="system" pointSize="16"/>
+                                                    <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                                    <nil key="highlightedColor"/>
+                                                </label>
+                                            </subviews>
+                                        </tableViewCellContentView>
+                                        <connections>
+                                            <segue destination="EfO-db-3YO" kind="show" id="Eho-lw-nZG"/>
+                                        </connections>
+                                    </tableViewCell>
+                                </cells>
+                            </tableViewSection>
+                        </sections>
+                        <connections>
+                            <outlet property="dataSource" destination="amF-rs-8QD" id="TC5-n4-9oE"/>
+                            <outlet property="delegate" destination="amF-rs-8QD" id="GOe-JW-H5g"/>
+                        </connections>
+                    </tableView>
+                    <navigationItem key="navigationItem" id="Paq-Ot-At9"/>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="mDU-HB-BlF" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="926" y="330"/>
+        </scene>
+        <!--View Controller-->
+        <scene sceneID="1xZ-i6-N1V">
+            <objects>
+                <viewController id="EfO-db-3YO" customClass="ViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="zkD-BA-2ra"/>
+                        <viewControllerLayoutGuide type="bottom" id="iJb-1M-zMl"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="WD1-aN-dww">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="U2l-bI-vhJ" customClass="PlayerView">
+                                <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                                <color key="backgroundColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                            </view>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="YIi-pp-uLf">
+                                <rect key="frame" x="20" y="621" width="17" height="16"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bbi-NS-p8Q">
+                                <rect key="frame" x="338" y="621" width="17" height="16"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                <color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="Hnz-ft-KOF">
+                                <rect key="frame" x="18" y="637" width="339" height="31"/>
+                                <connections>
+                                    <action selector="sliderAction:" destination="EfO-db-3YO" eventType="touchUpInside" id="fEj-M7-r8m"/>
+                                    <action selector="touchSliderAction:" destination="EfO-db-3YO" eventType="touchDown" id="nkJ-oM-K4t"/>
+                                </connections>
+                            </slider>
+                        </subviews>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                        <constraints>
+                            <constraint firstItem="iJb-1M-zMl" firstAttribute="top" secondItem="Hnz-ft-KOF" secondAttribute="bottom" id="2EP-hd-bYW"/>
+                            <constraint firstItem="Hnz-ft-KOF" firstAttribute="leading" secondItem="WD1-aN-dww" secondAttribute="leading" constant="20" id="5vq-Ib-cOe"/>
+                            <constraint firstItem="Hnz-ft-KOF" firstAttribute="leading" secondItem="YIi-pp-uLf" secondAttribute="leading" id="Bhc-W9-DPb"/>
+                            <constraint firstItem="U2l-bI-vhJ" firstAttribute="leading" secondItem="WD1-aN-dww" secondAttribute="leading" id="NFG-qS-Xp6"/>
+                            <constraint firstAttribute="trailing" secondItem="Hnz-ft-KOF" secondAttribute="trailing" constant="20" id="PbO-FL-oPX"/>
+                            <constraint firstAttribute="trailing" secondItem="U2l-bI-vhJ" secondAttribute="trailing" id="TW2-Ck-FS4"/>
+                            <constraint firstItem="U2l-bI-vhJ" firstAttribute="top" secondItem="WD1-aN-dww" secondAttribute="top" id="Xh7-iZ-A4o"/>
+                            <constraint firstItem="Hnz-ft-KOF" firstAttribute="top" secondItem="YIi-pp-uLf" secondAttribute="bottom" id="Zen-Ji-Lfh"/>
+                            <constraint firstItem="iJb-1M-zMl" firstAttribute="top" secondItem="U2l-bI-vhJ" secondAttribute="bottom" id="ekK-Lg-a7m"/>
+                            <constraint firstItem="Hnz-ft-KOF" firstAttribute="top" secondItem="bbi-NS-p8Q" secondAttribute="bottom" id="emf-ph-rqm"/>
+                            <constraint firstItem="Hnz-ft-KOF" firstAttribute="trailing" secondItem="bbi-NS-p8Q" secondAttribute="trailing" id="zDX-pB-CJ3"/>
+                        </constraints>
+                    </view>
+                    <navigationItem key="navigationItem" id="TLm-eT-nEB">
+                        <barButtonItem key="rightBarButtonItem" title="toggle" id="aw6-kj-xSR">
+                            <connections>
+                                <action selector="toggleAction:" destination="EfO-db-3YO" id="pXb-2Q-P2v"/>
+                            </connections>
+                        </barButtonItem>
+                    </navigationItem>
+                    <connections>
+                        <outlet property="currentTimeLabel" destination="YIi-pp-uLf" id="LUh-yw-3oO"/>
+                        <outlet property="playerView" destination="U2l-bI-vhJ" id="Ga8-S6-hyQ"/>
+                        <outlet property="slider" destination="Hnz-ft-KOF" id="eXK-TL-QHI"/>
+                        <outlet property="totalTimeLabel" destination="bbi-NS-p8Q" id="ImM-6r-teU"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="fTe-1X-K8x" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1595" y="330"/>
+        </scene>
+        <!--Navigation Controller-->
+        <scene sceneID="sBK-qS-hCr">
+            <objects>
+                <navigationController automaticallyAdjustsScrollViewInsets="NO" id="guh-7O-aMj" sceneMemberID="viewController">
+                    <toolbarItems/>
+                    <navigationBar key="navigationBar" contentMode="scaleToFill" id="v5P-CJ-TG3">
+                        <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                    </navigationBar>
+                    <nil name="viewControllers"/>
+                    <connections>
+                        <segue destination="amF-rs-8QD" kind="relationship" relationship="rootViewController" id="UUK-u5-dGL"/>
+                    </connections>
+                </navigationController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="Fd7-Se-RBW" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="170" y="330"/>
+        </scene>
+    </scenes>
+</document>

+ 45 - 0
VIMediaCacheDemo/Info.plist

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 18 - 0
VIMediaCacheDemo/PlayerView.h

@@ -0,0 +1,18 @@
+//
+//  PlayerView.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+@import AVFoundation;
+
+@interface PlayerView : UIView
+
+- (AVPlayerLayer *)playerLayer;
+
+- (void)setPlayer:(AVPlayer *)player;
+
+@end

+ 30 - 0
VIMediaCacheDemo/PlayerView.m

@@ -0,0 +1,30 @@
+//
+//  PlayerView.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "PlayerView.h"
+
+@implementation PlayerView
+
++ (Class)layerClass {
+    return [AVPlayerLayer class];
+}
+
+- (AVPlayer*)player {
+    return [(AVPlayerLayer *)[self layer] player];
+}
+
+- (void)setPlayer:(AVPlayer *)player {
+    [(AVPlayerLayer *)[self layer] setPlayer:player];
+}
+
+- (AVPlayerLayer *)playerLayer
+{
+    return (AVPlayerLayer *)self.layer;
+}
+
+@end

+ 15 - 0
VIMediaCacheDemo/ViewController.h

@@ -0,0 +1,15 @@
+//
+//  ViewController.h
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface ViewController : UIViewController
+
+
+@end
+

+ 222 - 0
VIMediaCacheDemo/ViewController.m

@@ -0,0 +1,222 @@
+//
+//  ViewController.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import "ViewController.h"
+#import "VIMediaCache.h"
+#import "PlayerView.h"
+
+@interface ViewController ()
+
+@property (nonatomic, strong) VIResourceLoaderManager *resourceLoaderManager;
+@property (nonatomic, strong) AVPlayer *player;
+@property (nonatomic, strong) AVPlayerItem *playerItem;
+@property (nonatomic, strong) id timeObserver;
+@property (nonatomic) CMTime duration;
+
+@property (weak, nonatomic) IBOutlet PlayerView *playerView;
+@property (weak, nonatomic) IBOutlet UISlider *slider;
+@property (weak, nonatomic) IBOutlet UILabel *totalTimeLabel;
+@property (weak, nonatomic) IBOutlet UILabel *currentTimeLabel;
+
+@property (nonatomic, strong) VIMediaDownloader *downloader;
+
+@end
+
+@implementation ViewController
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+    [self.player removeTimeObserver:self.timeObserver];
+    self.timeObserver = nil;
+    [self.playerItem removeObserver:self forKeyPath:@"status"];
+    [self.player removeObserver:self forKeyPath:@"timeControlStatus"];
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    
+    [self cleanCache];
+    
+    
+//    NSURL *url = [NSURL URLWithString:@"https://mvvideo5.meitudata.com/56ea0e90d6cb2653.mp4"];
+//    VIMediaDownloader *downloader = [[VIMediaDownloader alloc] initWithURL:url];
+//    [downloader downloadFromStartToEnd];
+//    self.downloader = downloader;
+    
+    [self setupPlayer];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mediaCacheDidChanged:) name:VICacheManagerDidUpdateCacheNotification object:nil];
+}
+
+- (void)viewDidAppear:(BOOL)animated {
+    [super viewDidAppear:animated];
+    [self.player play];
+}
+
+- (void)cleanCache {
+    unsigned long long fileSize = [VICacheManager calculateCachedSizeWithError:nil];
+    NSLog(@"file cache size: %@", @(fileSize));
+    NSError *error;
+    [VICacheManager cleanAllCacheWithError:&error];
+    if (error) {
+        NSLog(@"clean cache failure: %@", error);
+    }
+    
+    [VICacheManager cleanAllCacheWithError:&error];
+}
+
+- (IBAction)touchSliderAction:(UISlider *)sender {
+    sender.tag = -1;
+}
+
+- (IBAction)sliderAction:(UISlider *)sender {
+    CMTime duration = self.player.currentItem.asset.duration;
+    CMTime seekTo = CMTimeMake((NSInteger)(duration.value * sender.value), duration.timescale);
+    NSLog(@"seetTo %ld", (long)(duration.value * sender.value) / duration.timescale);
+    __weak typeof(self)weakSelf = self;
+    [self.player pause];
+    [self.player seekToTime:seekTo completionHandler:^(BOOL finished) {
+        sender.tag = 0;
+        [weakSelf.player play];
+    }];
+}
+
+- (IBAction)toggleAction:(id)sender {
+    [self cleanCache];
+    
+    [self.playerItem removeObserver:self forKeyPath:@"status"];
+    [self.player removeObserver:self forKeyPath:@"timeControlStatus"];
+    
+    [self.resourceLoaderManager cancelLoaders];
+    
+    NSURL *url = [NSURL URLWithString:@"https://mvvideo5.meitudata.com/56ea0e90d6cb2653.mp4"];
+    AVPlayerItem *playerItem = [self.resourceLoaderManager playerItemWithURL:url];
+    self.playerItem = playerItem;
+    
+    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
+    [self.player addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew context:nil];
+    [self.player replaceCurrentItemWithPlayerItem:playerItem];
+}
+
+#pragma mark - Setup
+
+- (void)setupPlayer {
+
+//        NSURL *url = [NSURL URLWithString:@"http://gedftnj8mkvfefuaefm.exp.bcevod.com/mda-hc2s2difdjz6c5y9/hd/mda-hc2s2difdjz6c5y9.mp4?playlist%3D%5B%22hd%22%5D&auth_key=1500559192-0-0-dcb501bf19beb0bd4e0f7ad30c380763&bcevod_channel=searchbox_feed&srchid=3ed366b1b0bf70e0&channel_id=2&d_t=2&b_v=9.1.0.0"];
+//        NSURL *url = [NSURL URLWithString:@"https://mvvideo5.meitudata.com/56a9e1389b9706520.mp4"];
+        NSURL *url = [NSURL URLWithString:@"https://mvvideo5.meitudata.com/56ea0e90d6cb2653.mp4"];
+
+    VIResourceLoaderManager *resourceLoaderManager = [VIResourceLoaderManager new];
+    self.resourceLoaderManager = resourceLoaderManager;
+    
+    AVPlayerItem *playerItem = [resourceLoaderManager playerItemWithURL:url];
+    self.playerItem = playerItem;
+    
+    VICacheConfiguration *configuration = [VICacheManager cacheConfigurationForURL:url];
+    if (configuration.progress >= 1.0) {
+        NSLog(@"cache completed");
+    }
+
+    AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
+//    AVPlayer *player = [AVPlayer playerWithURL:url];
+    player.automaticallyWaitsToMinimizeStalling = NO;
+    self.player = player;
+    [self.playerView setPlayer:player];
+    
+    
+    __weak typeof(self)weakSelf = self;
+    self.timeObserver =
+    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 10)
+                                              queue:dispatch_queue_create("player.time.queue", NULL)
+                                         usingBlock:^(CMTime time) {
+                                             dispatch_async(dispatch_get_main_queue(), ^(void) {
+                                                 if (weakSelf.slider.tag == 0) {
+                                                     CGFloat duration = CMTimeGetSeconds(weakSelf.player.currentItem.duration);
+                                                     weakSelf.totalTimeLabel.text = [NSString stringWithFormat:@"%.f", duration];
+                                                     CGFloat currentDuration = CMTimeGetSeconds(time);
+                                                     weakSelf.currentTimeLabel.text = [NSString stringWithFormat:@"%.f", currentDuration];
+                                                     weakSelf.slider.value = currentDuration / duration;
+                                                 }
+                                             });
+                                         }];
+    
+    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
+    [self.player addObserver:self forKeyPath:@"timeControlStatus" options:NSKeyValueObservingOptionNew context:nil];
+    
+    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapPlayerViewAction:)];
+    [self.playerView addGestureRecognizer:tap];
+}
+
+- (void)tapPlayerViewAction:(UITapGestureRecognizer *)gesture {
+    if (gesture.state == UIGestureRecognizerStateEnded) {
+        if (self.player.rate > 0.0) {
+            [self.player pause];
+        } else {
+            [self.player play];
+        }
+    }
+}
+
+#pragma mark - KVO
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
+                        change:(NSDictionary *)change context:(void *)context {
+    if (object == self.playerItem && [keyPath isEqualToString:@"status"]) {
+        NSLog(@"player status %@, rate %@, error: %@", @(self.playerItem.status), @(self.player.rate), self.playerItem.error);
+        if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
+            dispatch_async(dispatch_get_main_queue(), ^(void) {
+                CGFloat duration = CMTimeGetSeconds(self.playerItem.duration);
+                self.totalTimeLabel.text = [NSString stringWithFormat:@"%.f", duration];
+            });
+        } else if (self.playerItem.status == AVPlayerItemStatusFailed) {
+            // something went wrong. player.error should contain some information
+            NSLog(@"player error %@", self.playerItem.error);
+        }
+    } else if (object == self.player && [keyPath isEqualToString:@"timeControlStatus"]) {
+        NSLog(@"timeControlStatus: %@, reason: %@, rate: %@", @(self.player.timeControlStatus), self.player.reasonForWaitingToPlay, @(self.player.rate));
+    }
+}
+
+#pragma mark - notification
+
+- (void)mediaCacheDidChanged:(NSNotification *)notification {
+    NSDictionary *userInfo = notification.userInfo;
+    VICacheConfiguration *configuration = userInfo[VICacheConfigurationKey];
+    NSArray<NSValue *> *cachedFragments = configuration.cacheFragments;
+    long long contentLength = configuration.contentInfo.contentLength;
+    
+    NSInteger number = 100;
+    NSMutableString *progressStr = [NSMutableString string];
+    
+    [cachedFragments enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
+        NSRange range = obj.rangeValue;
+        
+        NSInteger location = roundf((range.location / (double)contentLength) * number);
+        
+        NSInteger progressCount = progressStr.length;
+        [self string:progressStr appendString:@"0" muti:location - progressCount];
+        
+        NSInteger length = roundf((range.length / (double)contentLength) * number);
+        [self string:progressStr appendString:@"1" muti:length];
+        
+        
+        if (idx == cachedFragments.count - 1 && (location + length) <= number + 1) {
+            [self string:progressStr appendString:@"0" muti:number - (length + location)];
+        }
+    }];
+    
+    NSLog(@"%@", progressStr);
+}
+
+- (void)string:(NSMutableString *)string appendString:(NSString *)appendString muti:(NSInteger)muti {
+    for (NSInteger i = 0; i < muti; i++) {
+        [string appendString:appendString];
+    }
+}
+
+@end

+ 16 - 0
VIMediaCacheDemo/main.m

@@ -0,0 +1,16 @@
+//
+//  main.m
+//  VIMediaCacheDemo
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char * argv[]) {
+    @autoreleasepool {
+        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+    }
+}

+ 24 - 0
VIMediaCacheDemoTests/Info.plist

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 138 - 0
VIMediaCacheDemoTests/VIMediaCacheDemoTests.m

@@ -0,0 +1,138 @@
+//
+//  VIMediaCacheDemoTests.m
+//  VIMediaCacheDemoTests
+//
+//  Created by Vito on 5/17/16.
+//  Copyright © 2016 Vito. All rights reserved.
+//
+
+#import <XCTest/XCTest.h>
+#import "VICacheConfiguration.h"
+#import "VIMediaCacheWorker.h"
+#import "VICacheAction.h"
+
+@interface VIMediaCacheDemoTests : XCTestCase
+
+@end
+
+@implementation VIMediaCacheDemoTests
+
+- (void)setUp {
+    [super setUp];
+    // Put setup code here. This method is called before the invocation of each test method in the class.
+}
+
+- (void)tearDown {
+    // Put teardown code here. This method is called after the invocation of each test method in the class.
+    [super tearDown];
+}
+- (void)testConfiguration1 {
+    VICacheConfiguration *configuration = [[VICacheConfiguration alloc] init];
+    NSRange range1 = NSMakeRange(10, 10);
+    [configuration addCacheFragment:range1];
+    NSArray *fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 1 && NSEqualRanges([fragments[0] rangeValue], range1) , @"add (10, 10) to [], should equal [(10, 10)]");
+    
+    [configuration addCacheFragment:range1];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 1 && NSEqualRanges([fragments[0] rangeValue], range1) , @"add (10, 10) to [(10, 10)], should equal [(10, 10)]");
+    
+    NSRange range0 = NSMakeRange(5, 1);
+    [configuration addCacheFragment:range0];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 2 && NSEqualRanges([fragments[0] rangeValue], range0) && NSEqualRanges([fragments[1] rangeValue], range1), @"add (5, 1) to [(10, 10)], should equal [(5, 1), (10, 10)]");
+    
+    NSRange range3 = NSMakeRange(1, 1);
+    [configuration addCacheFragment:range3];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 3 &&
+              NSEqualRanges([fragments[0] rangeValue], range3) &&
+              NSEqualRanges([fragments[1] rangeValue], range0) &&
+              NSEqualRanges([fragments[2] rangeValue], range1),
+              @"add (1, 1) to [(5, 1), (10, 10)], should equal [(1, 1), (5, 1), (10, 10)]");
+    
+    NSRange range4 = NSMakeRange(0, 9);
+    [configuration addCacheFragment:range4];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 2 &&
+              NSEqualRanges([fragments[0] rangeValue], NSMakeRange(0, 9)) &&
+              NSEqualRanges([fragments[1] rangeValue], range1),
+              @"add (0, 9) to [(1, 1), (5, 1), (10, 10)], should equal [(0, 9), (10, 10)]");
+}
+
+- (void)testConfiguration2 {
+    VICacheConfiguration *configuration = [[VICacheConfiguration alloc] init];
+    NSRange range1 = NSMakeRange(10, 10);
+    [configuration addCacheFragment:range1];
+    NSArray *fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 1 && NSEqualRanges([fragments[0] rangeValue], range1) , @"add (10, 10) to [], should equal [(10, 10)]");
+    
+    NSRange range2 = NSMakeRange(30, 10);
+    [configuration addCacheFragment:range2];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 2 && NSEqualRanges([fragments[0] rangeValue], range1) && NSEqualRanges([fragments[1] rangeValue], range2), @"add (30, 10) to [(10, 10)] should equal [(10, 10), (30, 10)]");
+    
+    NSRange range3 = NSMakeRange(50, 10);
+    [configuration addCacheFragment:range3];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 3 &&
+              NSEqualRanges([fragments[0] rangeValue], range1) &&
+              NSEqualRanges([fragments[1] rangeValue], range2) &&
+              NSEqualRanges([fragments[2] rangeValue], range3),
+              @"add (50, 10) to [(10, 10), (30, 10)] should equal [(10, 10), (30, 10), (50, 10)]");
+    
+    NSRange range4 = NSMakeRange(25, 26);
+    [configuration addCacheFragment:range4];
+    fragments = [configuration cacheFragments];
+    XCTAssert(fragments.count == 2 &&
+              NSEqualRanges([fragments[0] rangeValue], range1) &&
+              NSEqualRanges([fragments[1] rangeValue], NSMakeRange(25, 35)),
+              @"add (25, 26) to [(10, 10), (30, 10), (50, 10)] should equal [(10, 10), (25, 35)]");
+}
+
+- (void)testCacheWorker {
+    VIMediaCacheWorker *cacheWorker = [[VIMediaCacheWorker alloc] initWithCacheName:@"test.mp4"];
+    
+    NSArray *startOffsets = @[@(50), @(80), @(200), @(708), @(1024), @(1500)];
+    [cacheWorker setCacheResponse:nil];
+    
+    if (!cacheWorker.cachedResponse) {
+        NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"testUrl"]
+                                                                    MIMEType:@"mime"
+                                                       expectedContentLength:2048
+                                                            textEncodingName:nil];
+        [cacheWorker setCacheResponse:response];
+        
+        
+        for (NSNumber *offset in startOffsets) {
+            NSString *str = @"ddddddddddddddddddddddddddddddddddddddddd"; // 42
+            const char *utfStr = [str UTF8String];
+            NSData *data = [NSData dataWithBytes:utfStr length:strlen(utfStr) + 1];
+            [cacheWorker cacheData:data forRange:NSMakeRange(offset.integerValue, data.length)];
+            [cacheWorker save];
+        }
+    }
+    
+    NSRange range = NSMakeRange(0, 50);
+    NSArray *cacheDataActions1 = [cacheWorker cachedDataActionsForRange:range];
+    NSArray *expectActions1 = @[
+                                [[VICacheAction alloc] initWithActionType:VICacheAtionTypeRemote range:range]
+                                ];
+    XCTAssert([cacheDataActions1 isEqualToArray:expectActions1], @"cacheDataActions1 count should equal to %@", expectActions1);
+    
+    
+    NSRange range2 = NSMakeRange(51, 204);
+    NSArray *cacheDataActions2 = [cacheWorker cachedDataActionsForRange:range2];
+    XCTAssert(cacheDataActions2.count == 4, @"actions count should equal startoffsets's count");
+    
+    NSRange range3 = NSMakeRange(1300, 300);
+    NSArray *cacheDataActions3 = [cacheWorker cachedDataActionsForRange:range3];
+    NSArray *expectActions3 = @[
+                                [[VICacheAction alloc] initWithActionType:VICacheAtionTypeRemote range:NSMakeRange(1300, 200)],
+                                [[VICacheAction alloc] initWithActionType:VICacheAtionTypeLocal range:NSMakeRange(1500, 42)],
+                                [[VICacheAction alloc] initWithActionType:VICacheAtionTypeRemote range:NSMakeRange(1542, 58)]
+                                ];
+    XCTAssert([cacheDataActions3 isEqualToArray:expectActions3], @"actions count should equal");
+}
+
+@end