iOS 视频缓存KTVHTTPCache原理和实现

目前iOS端比较常见的视频缓存的实现方式主要有两种:
1、使用iOS自带的AVURLAsset的AVAssetResourceLoader来实现。
2、在客户端搭建local服务器,local服务器作为中间者,代替客户端请求服务器数据,并将获取到的数据缓存,再提供给客户端。
我们项目里使用的是KTVHTTPCache来实现视频缓存,KTVHTTPCache的实现方式就是第二种,项目地址:(https://github.com/ChangbaDevs/KTVHTTPCache)。

具体实现:

KTVHTTPCache的使用比较简单:

NSURL *proxyURL = [KTVHTTPCache proxyURLWithOriginalURL:originalURL];
AVPlayer *player = [AVPlayer playerWithURL:proxyURL];

可以看出,它是将源视频的URL替换成了自己定义格式的URL,这时我们其实请求的就是local服务器了。
核心的流程大概是这样:


image.png

几个核心类实现:

1、KTVHCHTTPServer:
用来搭建local server的,内部使用第三方库HTTPServer实现:
创建自己的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;
}

创建response作为Local Server数据返回体,遵循HTTPResponse协议,实现协议方法

@interface KTVHCHTTPResponse : NSObject <HTTPResponse>

实现协议方法

#pragma mark - HTTPResponse
- (NSData *)readDataOfLength:(NSUInteger)length
{
  "读取数据最开始的入口"
   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;
}
………………(省略,节省篇幅)

这样,当本地发生请求时,就会获取KTVHCHTTPResponse内部方法返回的数据。
2、KTVHCDataReader和KTVHCDataSourceManager
从服务器返回类可以看到,数据的入口是从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;
}

从这个方法里我们可以看到,读取数据又走到了KTVHCDataSourceManager中去。

#pragma mark - KTVHCDataReader
- (void)prepareSourceManager
{
   "两个数组保存两种数据来源"
    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;
    }];
    "对比本地已缓存的数据和视频数据量"
   "除了本地的如果还有未获取的数据,就需要网络请求获取了"
    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的初始化过程, 可以看出其实正常获取数据的是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里读取数据"
    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
从名字就可以看出:这两个类,一个是负责从直接从本地文件提供数据,一个是负责从网络读取之后提供数据
KTVHCDataFileSource的readDataOfLength实现比较明显,就是单纯从文件里读取数据。
看下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];
    "创建了两个文件句柄,读和写。"
    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 {
        "接收到数据之后,写入文件。"
        [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);
       "有可用数据了,需要回调通知。"
        [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的实现比较类似,只不过KTVHCDataNetworkSource多了一个从网络获取数据写入文件的步骤,其实最终提供数据还是通过文件读取的方式。
一旦有可用数据,就通过delegate的方式一直回调,通知response类有可用数据。

#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];
    }
}
"这个回调获取有可用的数据的通知。"

- (void)ktv_readerHasAvailableData:(KTVHCDataReader *)reader
{
    KTVHCLogHTTPResponse(@"%p, Has available data", self);
    "这个方法就会触发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];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342