目前iOS端比較常見的視頻緩存的實現(xiàn)方式主要有兩種:
1英融、使用iOS自帶的AVURLAsset的AVAssetResourceLoader來實現(xiàn)但校。
2光稼、在客戶端搭建local服務器坤次,local服務器作為中間者步鉴,代替客戶端請求服務器數(shù)據(jù)揪胃,并將獲取到的數(shù)據(jù)緩存,再提供給客戶端唠叛。
我們項目里使用的是KTVHTTPCache來實現(xiàn)視頻緩存只嚣,KTVHTTPCache的實現(xiàn)方式就是第二種,項目地址:(https://github.com/ChangbaDevs/KTVHTTPCache)艺沼。
具體實現(xiàn):
KTVHTTPCache的使用比較簡單:
NSURL *proxyURL = [KTVHTTPCache proxyURLWithOriginalURL:originalURL];
AVPlayer *player = [AVPlayer playerWithURL:proxyURL];
可以看出册舞,它是將源視頻的URL替換成了自己定義格式的URL,這時我們其實請求的就是local服務器了障般。
核心的流程大概是這樣:
幾個核心類實現(xiàn):
1调鲸、KTVHCHTTPServer:
用來搭建local server的,內(nèi)部使用第三方庫HTTPServer實現(xiàn):
創(chuàng)建自己的Connection類繼承自HTTPConnection
@interface KTVHCHTTPConnection : HTTPConnection
重寫子類方法挽荡,返回相應的response類
- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
KTVHCLogHTTPConnection(@"%p, Receive request\nmethod : %@\npath : %@\nURL : %@", self, method, path, request.url);
NSDictionary<NSString *,NSString *> *parameters = [[KTVHCURLTool tool] parseQuery:request.url.query];
NSURL *URL = [NSURL URLWithString:[parameters objectForKey:@"url"]];
KTVHCDataRequest *dataRequest = [[KTVHCDataRequest alloc] initWithURL:URL headers:request.allHeaderFields];
KTVHCHTTPResponse *response = [[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest];
return response;
}
創(chuàng)建response作為Local Server數(shù)據(jù)返回體藐石,遵循HTTPResponse協(xié)議,實現(xiàn)協(xié)議方法
@interface KTVHCHTTPResponse : NSObject <HTTPResponse>
實現(xiàn)協(xié)議方法
#pragma mark - HTTPResponse
- (NSData *)readDataOfLength:(NSUInteger)length
{
"讀取數(shù)據(jù)最開始的入口"
NSData *data = [self.reader readDataOfLength:length];
KTVHCLogHTTPResponse(@"%p, Read data : %lld", self, (long long)data.length);
if (self.reader.isFinished) {
KTVHCLogHTTPResponse(@"%p, Read data did finished", self);
[self.reader close];
[self.connection responseDidAbort:self];
}
return data;
}
………………(省略定拟,節(jié)省篇幅)
這樣于微,當本地發(fā)生請求時,就會獲取KTVHCHTTPResponse內(nèi)部方法返回的數(shù)據(jù)青自。
2株依、KTVHCDataReader和KTVHCDataSourceManager
從服務器返回類可以看到,數(shù)據(jù)的入口是從KTVHCDataReader的readDataOfLength獲取的延窜。
#pragma mark - KTVHCDataReader
- (NSData *)readDataOfLength:(NSUInteger)length
{
[self lock];
if (self.isClosed) {
[self unlock];
return nil;
}
if (self.isFinished) {
[self unlock];
return nil;
}
if (self.error) {
[self unlock];
return nil;
}
NSData *data = [self.sourceManager readDataOfLength:length];
if (data.length > 0) {
self->_readedLength += data.length;
if (self.response.contentLength > 0) {
self->_progress = (double)self.readedLength / (double)self.response.contentLength;
}
}
KTVHCLogDataReader(@"%p, Read data : %lld", self, (long long)data.length);
if (self.sourceManager.isFinished) {
KTVHCLogDataReader(@"%p, Read data did finished", self);
self->_finished = YES;
[self close];
}
[self unlock];
return data;
}
從這個方法里我們可以看到恋腕,讀取數(shù)據(jù)又走到了KTVHCDataSourceManager中去。
#pragma mark - KTVHCDataReader
- (void)prepareSourceManager
{
"兩個數(shù)組保存兩種數(shù)據(jù)來源"
NSMutableArray<KTVHCDataFileSource *> *fileSources = [NSMutableArray array];
NSMutableArray<KTVHCDataNetworkSource *> *networkSources = [NSMutableArray array];
long long min = self.request.range.start;
long long max = self.request.range.end;
NSArray *unitItems = self.unit.unitItems;
for (KTVHCDataUnitItem *item in unitItems) {
long long itemMin = item.offset;
long long itemMax = item.offset + item.length - 1;
if (itemMax < min || itemMin > max) {
continue;
}
if (min > itemMin) {
itemMin = min;
}
if (max < itemMax) {
itemMax = max;
}
min = itemMax + 1;
KTVHCRange range = KTVHCMakeRange(item.offset, item.offset + item.length - 1);
KTVHCRange readRange = KTVHCMakeRange(itemMin - item.offset, itemMax - item.offset);
KTVHCDataFileSource *source = [[KTVHCDataFileSource alloc] initWithPath:item.absolutePath range:range readRange:readRange];
[fileSources addObject:source];
}
[fileSources sortUsingComparator:^NSComparisonResult(KTVHCDataFileSource *obj1, KTVHCDataFileSource *obj2) {
if (obj1.range.start < obj2.range.start) {
return NSOrderedAscending;
}
return NSOrderedDescending;
}];
"對比本地已緩存的數(shù)據(jù)和視頻數(shù)據(jù)量"
"除了本地的如果還有未獲取的數(shù)據(jù)逆瑞,就需要網(wǎng)絡請求獲取了"
long long offset = self.request.range.start;
long long length = KTVHCRangeIsFull(self.request.range) ? KTVHCRangeGetLength(self.request.range) : (self.request.range.end - offset + 1);
for (KTVHCDataFileSource *obj in fileSources) {
long long delta = obj.range.start + obj.readRange.start - offset;
if (delta > 0) {
KTVHCRange range = KTVHCMakeRange(offset, offset + delta - 1);
KTVHCDataRequest *request = [self.request newRequestWithRange:range];
KTVHCDataNetworkSource *source = [[KTVHCDataNetworkSource alloc] initWithRequest:request];
[networkSources addObject:source];
offset += delta;
length -= delta;
}
offset += KTVHCRangeGetLength(obj.readRange);
length -= KTVHCRangeGetLength(obj.readRange);
}
if (length > 0) {
KTVHCRange range = KTVHCMakeRange(offset, self.request.range.end);
KTVHCDataRequest *request = [self.request newRequestWithRange:range];
KTVHCDataNetworkSource *source = [[KTVHCDataNetworkSource alloc] initWithRequest:request];
[networkSources addObject:source];
}
NSMutableArray<id<KTVHCDataSource>> *sources = [NSMutableArray array];
[sources addObjectsFromArray:fileSources];
[sources addObjectsFromArray:networkSources];
self.sourceManager = [[KTVHCDataSourceManager alloc] initWithSources:sources delegate:self delegateQueue:self.internalDelegateQueue];
[self.sourceManager prepare];
}
看到KTVHCDataSourceManager的初始化過程荠藤, 可以看出其實正常獲取數(shù)據(jù)的是KTVHCDataFileSource和KTVHCDataNetworkSource兩個類伙单。
再看一下KTVHCDataSourceManager的readDataOfLength方法:
#pragma mark - KTVHCDataSourceManager
- (NSData *)readDataOfLength:(NSUInteger)length
{
[self lock];
if (self.isClosed) {
[self unlock];
return nil;
}
if (self.isFinished) {
[self unlock];
return nil;
}
if (self.error) {
[self unlock];
return nil;
}
"從Source里讀取數(shù)據(jù)"
NSData *data = [self.currentSource readDataOfLength:length];
self->_readedLength += data.length;
KTVHCLogDataSourceManager(@"%p, Read data : %lld", self, (long long)data.length);
if (self.currentSource.isFinished) {
"一個source讀完,切換到下一個Source"
self.currentSource = [self nextSource];
if (self.currentSource) {
KTVHCLogDataSourceManager(@"%p, Switch to next source, %@", self, self.currentSource);
if ([self.currentSource isKindOfClass:[KTVHCDataFileSource class]]) {
[self.currentSource prepare];
}
} else {
KTVHCLogDataSourceManager(@"%p, Read data did finished", self);
self->_finished = YES;
}
}
[self unlock];
return data;
}
**KTVHCDataNetworkSource和KTVHCDataFileSource
從名字就可以看出:這兩個類哈肖,一個是負責從直接從本地文件提供數(shù)據(jù)吻育,一個是負責從網(wǎng)絡讀取之后提供數(shù)據(jù)
KTVHCDataFileSource的readDataOfLength實現(xiàn)比較明顯,就是單純從文件里讀取數(shù)據(jù)牡彻。
看下KTVHCDataNetworkSource:
- (void)ktv_download:(KTVHCDownload *)download didReceiveResponse:(KTVHCDataResponse *)response
{
[self lock];
if (self.isClosed || self.error) {
[self unlock];
return;
}
self->_response = response;
NSString *path = [KTVHCPathTool filePathWithURL:self.request.URL offset:self.request.range.start];
self.unitItem = [[KTVHCDataUnitItem alloc] initWithPath:path offset:self.request.range.start];
KTVHCDataUnit *unit = [[KTVHCDataUnitPool pool] unitWithURL:self.request.URL];
[unit insertUnitItem:self.unitItem];
KTVHCLogDataNetworkSource(@"%p, Receive response\nResponse : %@\nUnit : %@\nUnitItem : %@", self, response, unit, self.unitItem);
[unit workingRelease];
"創(chuàng)建了兩個文件句柄扫沼,讀和寫。"
self.writingHandle = [NSFileHandle fileHandleForWritingAtPath:self.unitItem.absolutePath];
self.readingHandle = [NSFileHandle fileHandleForReadingAtPath:self.unitItem.absolutePath];
[self callbackForPrepared];
[self unlock];
}
- (void)ktv_download:(KTVHCDownload *)download didReceiveData:(NSData *)data
{
[self lock];
if (self.isClosed || self.error) {
[self unlock];
return;
}
@try {
"接收到數(shù)據(jù)之后庄吼,寫入文件缎除。"
[self.writingHandle writeData:data];
self.downloadLength += data.length;
[self.unitItem updateLength:self.downloadLength];
KTVHCLogDataNetworkSource(@"%p, Receive data : %lld, %lld, %lld", self, (long long)data.length, self.downloadLength, self.unitItem.length);
"有可用數(shù)據(jù)了,需要回調(diào)通知总寻。"
[self callbackForHasAvailableData];
} @catch (NSException *exception) {
NSError *error = [KTVHCError errorForException:exception];
KTVHCLogDataNetworkSource(@"%p, write exception\nError : %@", self, error);
[self callbackForFailed:error];
if (!self.downloadCalledComplete) {
KTVHCLogDataNetworkSource(@"%p, Cancel download task when write exception", self);
[self.downlaodTask cancel];
self.downlaodTask = nil;
}
}
[self unlock];
}
可以看出器罐,兩個source的實現(xiàn)比較類似,只不過KTVHCDataNetworkSource多了一個從網(wǎng)絡獲取數(shù)據(jù)寫入文件的步驟渐行,其實最終提供數(shù)據(jù)還是通過文件讀取的方式轰坊。
一旦有可用數(shù)據(jù),就通過delegate的方式一直回調(diào),通知response類有可用數(shù)據(jù)。
#pragma mark - KTVHCHTTPResponse
- (void)ktv_readerDidPrepare:(KTVHCDataReader *)reader
{
KTVHCLogHTTPResponse(@"%p, Prepared", self);
if (self.reader.isPrepared && self.waitingResponse == YES) {
KTVHCLogHTTPResponse(@"%p, Call connection did prepared", self);
[self.connection responseHasAvailableData:self];
}
}
"這個回調(diào)獲取有可用的數(shù)據(jù)的通知钙畔。"
- (void)ktv_readerHasAvailableData:(KTVHCDataReader *)reader
{
KTVHCLogHTTPResponse(@"%p, Has available data", self);
"這個方法就會觸發(fā)response的readDataOfLength"
[self.connection responseHasAvailableData:self];
}
- (void)ktv_reader:(KTVHCDataReader *)reader didFailWithError:(NSError *)error
{
KTVHCLogHTTPResponse(@"%p, Failed\nError : %@", self, error);
[self.reader close];
[self.connection responseDidAbort:self];
}