前言
- 本文主要介紹基于AVPlayer實(shí)現(xiàn)邊下邊播邊存處理棵红,核心其實(shí)就是基于AVPlayer的AVAssetResourceLoaderDelegate然后對(duì)FILE文件實(shí)現(xiàn)邊下邊播方案,
AVPlayer的基本知識(shí)
單純使用AVPlayer類是無(wú)法顯示視頻的卫枝,要將視頻層添加至AVPlayerLayer中掂榔,這樣才能將視頻顯示出來(lái)疼蛾,簡(jiǎn)單總結(jié)播放視頻就是這三者的使用枯途,AVPlayer教寂、AVPlayerLayer页慷、AVPlayerItem
- AVPlayer:負(fù)責(zé)控制播放器的播放憔足,暫停,播放速度等
- AVPlayerLayer:負(fù)責(zé)管理資源對(duì)象酒繁,提供播放數(shù)據(jù)源
- AVPlayerItem:負(fù)責(zé)顯示視頻滓彰,如果沒(méi)有添加該類,只有聲音沒(méi)有畫面
簡(jiǎn)單理解州袒,你可以把這三者理解為我們常用的MVC揭绑,AVPlayer就對(duì)應(yīng)C,AVPlayerLayer對(duì)應(yīng)V郎哭,AVPlayerItem對(duì)應(yīng)M
關(guān)于這些的介紹使用他匪,我就不介紹了弓叛,網(wǎng)上資料一大堆。本文主要介紹邊下邊播邊存方案
邊下邊播方案
再介紹之前诚纸,我們?cè)賮?lái)了解AVPlayer的一個(gè)類AVAsset
撰筷,該類主要用于獲取多媒體信息,再接著往下了解畦徘,AVURLAsset
該類是AVAsset
的子類毕籽,主要可以根據(jù)URL路徑創(chuàng)建包含媒體信息的AVURLAsset
對(duì)象,AVURLAsset
通過(guò)委托AVAssetResourceLoader
去加載所需文件井辆,同時(shí)可以進(jìn)行數(shù)據(jù)的緩存和讀取操作关筒,這樣就實(shí)現(xiàn)邊下邊播邊存的功能。
大致流程圖杯缺,
初始化AVURLAsset
// 判斷是否含有視頻軌道
NS_INLINE BOOL kPlayerHaveTracks(NSURL *videoURL, void(^assetblock)(AVURLAsset *), NSDictionary *requestHeader){
if (videoURL == nil) return NO;
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:videoURL options:requestHeader];
if (assetblock) assetblock(asset);
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
return [tracks count] > 0;
}
這里我們就得到AVURLAsset
蒸播,接下來(lái)就是設(shè)置委托引出主人公AVAssetResourceLoaderDelegate
,這個(gè)就是我們實(shí)現(xiàn)邊下邊播的中間橋梁
NSURL * URL = weakself.connection.kj_createSchemeURL(tempURL);
weakself.asset = [AVURLAsset URLAssetWithURL:URL options:weakself.requestHeader];
[weakself.asset.resourceLoader setDelegate:weakself.connection queue:dispatch_get_main_queue()];
AVAssetResourceLoaderDelegate實(shí)現(xiàn)
下面先來(lái)介紹AVAssetResourceLoaderDelegate
的委托方法萍肆,
/* 連接視頻播放和視頻斷點(diǎn)下載的橋梁
* 必須返回Yes袍榆,如果返回NO,則resourceLoader將會(huì)加載出現(xiàn)故障的數(shù)據(jù)
* 這里會(huì)出現(xiàn)很多個(gè)loadingRequest請(qǐng)求,需要為每一次請(qǐng)求作出處理
* 該接口會(huì)被調(diào)用多次塘揣,請(qǐng)求不同片段的視頻數(shù)據(jù)包雀,應(yīng)當(dāng)保存這些請(qǐng)求,在請(qǐng)求的數(shù)據(jù)全部響應(yīng)完畢才銷毀該請(qǐng)求
* @param resourceLoader 資源管理器
* @param loadingRequest 每一小塊數(shù)據(jù)的請(qǐng)求
*/
- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequest{
// TODO:在這里面開(kāi)始我們的網(wǎng)絡(luò)下載請(qǐng)求亲铡,也就是得到AVAssetResourceLoadingRequest對(duì)象
}
這里在提一下才写,由于會(huì)調(diào)用很多次,得到很多個(gè)分片信息奖蔓,所以我選擇用一個(gè)字典來(lái)將這些分片信息存儲(chǔ)起來(lái)赞草,然后逐一下載使用
NSString *key = kGetRequestKey(loadingRequest.request.URL);
if (key == nil) return NO;
KJResourceLoaderManager *manager = self.loaderMap[key];
if (manager == nil){
NSURL *resourceURL = loadingRequest.request.URL;
NSString *string = [resourceURL.absoluteString stringByReplacingOccurrencesOfString:kCustomVideoScheme withString:@""];
NSURL *videoURL = [NSURL URLWithString:string];
manager = [[KJResourceLoaderManager alloc] initWithVideoURL:videoURL];
manager.delegate = self;
self.loaderMap[key] = manager;
}
[manager kj_addRequest:loadingRequest];
/* 當(dāng)視頻播放器要取消請(qǐng)求時(shí),相應(yīng)的吆鹤,也應(yīng)該停止下載這部分?jǐn)?shù)據(jù)厨疙。
* 通常在拖拽視頻進(jìn)度時(shí)調(diào)這方法
* @param resourceLoader 資源管理器
* @param loadingRequest 每一小塊數(shù)據(jù)的請(qǐng)求
*/
- (void)resourceLoader:(AVAssetResourceLoader*)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest*)loadingRequest{
// TODO:停止下載請(qǐng)求
}
下面我們來(lái)一步一步講解得到AVAssetResourceLoadingRequest
之后,怎么去開(kāi)啟一個(gè)請(qǐng)求
第一步:獲取請(qǐng)求長(zhǎng)度檀头,文件類型等信息
這里開(kāi)啟一個(gè)小分片去獲取視頻數(shù)據(jù)信息轰异,然后配置正確的信息
/* 對(duì)請(qǐng)求加上長(zhǎng)度,文件類型等信息暑始,必須設(shè)置正確否則會(huì)報(bào)播放器Failed */
NS_INLINE void kSetDownloadConfiguration(KJDownloader *downloader, AVAssetResourceLoadingRequest *loadingRequest){
AVAssetResourceLoadingContentInformationRequest *request = loadingRequest.contentInformationRequest;
if (downloader.fileHandleManager.cacheInfo.contentType) {
request.contentType = downloader.fileHandleManager.cacheInfo.contentType;
}else{
CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(@"video/mp4"), NULL);
request.contentType = CFBridgingRelease(type);
}
request.byteRangeAccessSupported = YES;
request.contentLength = downloader.fileHandleManager.cacheInfo.contentLength;
}
第二步:將下載的NSData傳給播放器
總結(jié)其實(shí)就下面這一句代碼搭独,
[request.dataRequest respondWithData:data];
第三步:請(qǐng)求完成
取消并移除請(qǐng)求
if (error.code == KJPlayerCustomCodeCachedComplete) {
[weakself kj_cancelLoading];
}else if (error){
[request finishLoadingWithError:error];
}else{
[request finishLoading];
[weakself.requests removeObject:request];
}
到此拋開(kāi)下載器部分處理不說(shuō),簡(jiǎn)單的邊下邊播就已經(jīng)實(shí)現(xiàn)廊镜,下面我們就來(lái)說(shuō)說(shuō)下載器部分
下載器
下載器我這邊采用的是NSURLSession
牙肝,然后實(shí)現(xiàn)NSURLSessionDelegate
委托協(xié)議
主要就是這三個(gè)方法
- (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:(NSURLSessionDataTask*)task didCompleteWithError:(nullable NSError*)error{
}
這里關(guān)于下載就不做多余贅述,接著說(shuō)說(shuō)分片下載處理
NSUInteger fromOffset = fragment.range.location;
NSUInteger endOffset = fragment.range.location + fragment.range.length - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
self.startOffset = fragment.range.location;
self.task = [self.session dataTaskWithRequest:request];
[self.task resume];
到此分片下載我們也就實(shí)現(xiàn)完成,
文件管理
文件管理這邊配椭,我們聲明兩個(gè)NSFileHandle
虫溜,一個(gè)用來(lái)寫入分片資源,一個(gè)用來(lái)讀取已下載分片資源
寫入已下載分片文件
[self.writeHandle seekToFileOffset:range.location];
[self.writeHandle writeData:data];
[self.cacheInfo kj_continueCacheFragmentRange:range];
讀取已下載分片緩存數(shù)據(jù)
/* 讀取已下載分片緩存數(shù)據(jù) */
- (NSData*)kj_readCachedDataWithRange:(NSRange)range{
@synchronized(self.readHandle) {
[self.readHandle seekToFileOffset:range.location];
return [self.readHandle readDataOfLength:range.length];
}
}
這里還值得一提的就是股缸,我們有可能數(shù)據(jù)并沒(méi)有下載完成就就取消等等衡楞,這時(shí)候就選擇了歸檔的方式來(lái)存儲(chǔ)下載文件,然后下次進(jìn)入優(yōu)先讀取歸檔信息敦姻,接著繼續(xù)下載緩存這樣子
歸檔解檔處理
這里采用runtime結(jié)合kvc的方式獲取處理Ivar瘾境,快捷簡(jiǎn)便
#pragma mark - NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
KJFileHandleInfo *info = [[[self class] allocWithZone:zone] init];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++){
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
if ([value respondsToSelector:@selector(copyWithZone:)]) {
[info setValue:[value copy] forKey:key];
}else{
[info setValue:value forKey:key];
}
}
free(ivars);
return info;
}
/* 歸檔 */
- (void)encodeWithCoder:(NSCoder*)aCoder{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++){
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
}
/* 解檔 */
- (instancetype)initWithCoder:(NSCoder*)aDecoder{
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i<count; i++){
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
到此,其實(shí)我們的邊下邊播邊存就基本上完成
存入信息到Database
為了更方便更好的管理存儲(chǔ)數(shù)據(jù)镰惦,我還定義了一個(gè)數(shù)據(jù)庫(kù)迷守,然后我們將下載的信息存儲(chǔ)至數(shù)據(jù)庫(kù)當(dāng)中,
//存儲(chǔ)到本地?cái)?shù)據(jù)庫(kù)
- (BOOL)kj_saveDatabaseVideoIntact:(BOOL)videoIntact{
PLAYER_WEAKSELF;
NSError *__error;
[DBPlayerDataInfo kj_insertData:self.cacheInfo.fileName Data:^(DBPlayerData * data){
data.dbid = weakself.cacheInfo.fileName;
data.videoUrl = weakself.cacheInfo.videoURL.absoluteString;
data.videoFormat = weakself.cacheInfo.fileFormat;
data.sandboxPath = [weakself.cacheInfo.fileName stringByAppendingPathExtension:weakself.cacheInfo.fileFormat];
data.saveTime = NSDate.date.timeIntervalSince1970;
data.videoIntact = videoIntact;
data.videoContentLength = weakself.cacheInfo.contentLength;
} error:&__error];
if (__error) {
return YES;
}else if (videoIntact) {
kGCD_player_main(^{
weakself.playError = [DBPlayerDataInfo kj_errorSummarizing:KJPlayerCustomCodeSaveDatabase];
});
}
return NO;
}
緩存管理器
提供了文件的增刪改查等旺入,資源文件管理等等
#pragma mark - NSFileManager
/* 刪除指定文件 */
+ (BOOL)kj_removeFilePath:(NSString*)path;
/* 創(chuàng)建文件夾 */
+ (BOOL)kj_createFilePath:(NSString*)path;
/* 目錄下有用的文件路徑兑凿,排除臨時(shí)文件 */
+ (NSArray*)kj_videoFilePaths;
/* 目錄下的全部文件名,包含臨時(shí)文件 */
+ (NSArray*)kj_videoAllFileNames;
/* 刪除指定完整路徑數(shù)據(jù) */
+ (void)kj_removeAimPath:(NSString*)path,...;
/* 判斷文件是否存在茵瘾,存在拼接完整路徑 */
+ (BOOL)kj_haveFileSandboxPath:(NSString * _Nonnull __strong * _Nonnull)path;
/* 清除視頻緩存文件和數(shù)據(jù)庫(kù)數(shù)據(jù) */
+ (BOOL)kj_crearVideoCachedAndDatabase:(DBPlayerData*)data;
#pragma mark - Sandbox板塊
/* 判斷是否有緩存礼华,返回緩存鏈接 */
@property(nonatomic,copy,class,readonly)void(^kJudgeHaveCacheURL)(void(^)(BOOL locality), NSURL * _Nonnull __strong * _Nonnull);
/* 創(chuàng)建視頻緩存文件完整路徑 */
+ (NSString*)kj_createVideoCachedPath:(NSURL*)url;
/* 追加視頻臨時(shí)緩存路徑,用于播放器讀取 */
+ (NSString*)kj_appendingVideoTempPath:(NSURL*)url;
/* 獲取視頻緩存大小 */
+ (int64_t)kj_videoCachedSize;
/* 清除全部視頻緩存龄捡,暴露當(dāng)前正在下載數(shù)據(jù) */
+ (void)kj_clearAllVideoCache;
/* 清除指定視頻緩存 */
+ (BOOL)kj_clearVideoCacheWithURL:(NSURL*)url;
/* 存入視頻封面圖 */
+ (void)kj_saveVideoCoverImage:(UIImage*)image VideoURL:(NSURL*)url;
/* 讀取視頻封面圖 */
+ (UIImage*)kj_getVideoCoverImageWithURL:(NSURL*)url;
/* 清除視頻封面圖 */
+ (void)kj_clearVideoCoverImageWithURL:(NSURL*)url;
/* 清除全部封面緩存 */
+ (void)kj_clearAllVideoCoverImage;
關(guān)于seek處理
這里再說(shuō)說(shuō)卓嫂,關(guān)于我們seek的時(shí)候的處理,大致分3種情況聘殖,
第一種:seek處視頻已經(jīng)下載好
I Like 這種是最中規(guī)中矩的只需要直接讀取緩存播放即可
第二種:seek到視頻未下載部分
這時(shí)就需要先取消正在下載的數(shù)據(jù),然后從seek處開(kāi)始重新下載數(shù)據(jù)行瑞,只需要下載器支持分片指定位置下載即可實(shí)現(xiàn)該需求
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
第三種:seek來(lái)回多次數(shù)據(jù)就會(huì)包含已下載部分和未下載部分奸腺,斷斷續(xù)續(xù)
你咋這么煩呢?搞事情Q谩M徽铡!
這時(shí)候就需要對(duì)這段分片做個(gè)標(biāo)記氧吐,它到底屬于已下載分片讹蘑,還是未下載分片
1、如果為未下載分片數(shù)據(jù)筑舅,執(zhí)行分片下載
if (fragment.type){// 遠(yuǎn)端碎片座慰,即開(kāi)始下載
NSUInteger fromOffset = fragment.range.location;
NSUInteger endOffset = fragment.range.location + fragment.range.length - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
self.startOffset = fragment.range.location;
self.task = [self.session dataTaskWithRequest:request];
[self.task resume];
}
2、如果是已下載分片數(shù)據(jù)翠拣,則讀取分片數(shù)據(jù)
NSData *data = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range];
3版仔、如果讀取不成功,給一次機(jī)會(huì)再讀,好好珍惜 - -!
self.once = YES;
data = [self.fileHandleManager kj_readCachedDataWithRange:fragment.range];
4蛮粮、如果還是不成功益缎,則將此分片標(biāo)記為未下載分片,然后重新下載
if (data == nil) {
fragment.type = 1;
NSUInteger fromOffset = fragment.range.location;
NSUInteger endOffset = fragment.range.location + fragment.range.length - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.videoURL];
request.cachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
self.startOffset = fragment.range.location;
self.task = [self.session dataTaskWithRequest:request];
[self.task resume];
}
到此然想,關(guān)于邊下邊播邊存莺奔,并且斷點(diǎn)讀取播放繼續(xù)緩存處理也就介紹的差不多了,至于詳細(xì)信息变泄,我Dmeo里面寫的也很詳細(xì)弊仪,感興趣的朋友可以去下載 Demo地址:KJPlayerDemo