前言
- 本文主要介紹基于AVPlayer實(shí)現(xiàn)邊下邊播邊存處理,核心其實(shí)就是基于AVPlayer的AVAssetResourceLoaderDelegate然后對FILE文件實(shí)現(xiàn)邊下邊播方案,
AVPlayer的基本知識
單純使用AVPlayer類是無法顯示視頻的,要將視頻層添加至AVPlayerLayer中,這樣才能將視頻顯示出來,簡單總結(jié)播放視頻就是這三者的使用,AVPlayer、AVPlayerLayer、AVPlayerItem
- AVPlayer:負(fù)責(zé)控制播放器的播放,暫停,播放速度等
- AVPlayerLayer:負(fù)責(zé)管理資源對象,提供播放數(shù)據(jù)源
- AVPlayerItem:負(fù)責(zé)顯示視頻,如果沒有添加該類,只有聲音沒有畫面
簡單理解,你可以把這三者理解為我們常用的MVC,AVPlayer就對應(yīng)C,AVPlayerLayer對應(yīng)V,AVPlayerItem對應(yīng)M
關(guān)于這些的介紹使用,我就不介紹了,網(wǎng)上資料一大堆。本文主要介紹邊下邊播邊存方案
邊下邊播方案
再介紹之前,我們再來了解AVPlayer的一個類AVAsset,該類主要用于獲取多媒體信息,再接著往下了解,AVURLAsset該類是AVAsset的子類,主要可以根據(jù)URL路徑創(chuàng)建包含媒體信息的AVURLAsset對象,AVURLAsset通過委托AVAssetResourceLoader去加載所需文件,同時可以進(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,接下來就是設(shè)置委托引出主人公AVAssetResourceLoaderDelegate,這個就是我們實(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)
下面先來介紹AVAssetResourceLoaderDelegate的委托方法,
/* 連接視頻播放和視頻斷點(diǎn)下載的橋梁
* 必須返回Yes,如果返回NO,則resourceLoader將會加載出現(xiàn)故障的數(shù)據(jù)
* 這里會出現(xiàn)很多個loadingRequest請求,需要為每一次請求作出處理
* 該接口會被調(diào)用多次,請求不同片段的視頻數(shù)據(jù),應(yīng)當(dāng)保存這些請求,在請求的數(shù)據(jù)全部響應(yīng)完畢才銷毀該請求
* @param resourceLoader 資源管理器
* @param loadingRequest 每一小塊數(shù)據(jù)的請求
*/
- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequest{
// TODO:在這里面開始我們的網(wǎng)絡(luò)下載請求,也就是得到AVAssetResourceLoadingRequest對象
}
這里在提一下,由于會調(diào)用很多次,得到很多個分片信息,所以我選擇用一個字典來將這些分片信息存儲起來,然后逐一下載使用
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)視頻播放器要取消請求時,相應(yīng)的,也應(yīng)該停止下載這部分?jǐn)?shù)據(jù)。
* 通常在拖拽視頻進(jìn)度時調(diào)這方法
* @param resourceLoader 資源管理器
* @param loadingRequest 每一小塊數(shù)據(jù)的請求
*/
- (void)resourceLoader:(AVAssetResourceLoader*)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest*)loadingRequest{
// TODO:停止下載請求
}
下面我們來一步一步講解得到AVAssetResourceLoadingRequest之后,怎么去開啟一個請求
第一步:獲取請求長度,文件類型等信息
這里開啟一個小分片去獲取視頻數(shù)據(jù)信息,然后配置正確的信息
/* 對請求加上長度,文件類型等信息,必須設(shè)置正確否則會報播放器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];
第三步:請求完成
取消并移除請求
if (error.code == KJPlayerCustomCodeCachedComplete) {
[weakself kj_cancelLoading];
}else if (error){
[request finishLoadingWithError:error];
}else{
[request finishLoading];
[weakself.requests removeObject:request];
}
到此拋開下載器部分處理不說,簡單的邊下邊播就已經(jīng)實(shí)現(xiàn),下面我們就來說說下載器部分
下載器
下載器我這邊采用的是NSURLSession,然后實(shí)現(xiàn)NSURLSessionDelegate委托協(xié)議
主要就是這三個方法
- (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)于下載就不做多余贅述,接著說說分片下載處理
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)完成,
文件管理
文件管理這邊,我們聲明兩個NSFileHandle,一個用來寫入分片資源,一個用來讀取已下載分片資源
寫入已下載分片文件
[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ù)并沒有下載完成就就取消等等,這時候就選擇了歸檔的方式來存儲下載文件,然后下次進(jìn)入優(yōu)先讀取歸檔信息,接著繼續(xù)下載緩存這樣子
歸檔解檔處理
這里采用runtime結(jié)合kvc的方式獲取處理Ivar,快捷簡便
#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
為了更方便更好的管理存儲數(shù)據(jù),我還定義了一個數(shù)據(jù)庫,然后我們將下載的信息存儲至數(shù)據(jù)庫當(dāng)中,
//存儲到本地數(shù)據(jù)庫
- (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;
/* 目錄下有用的文件路徑,排除臨時文件 */
+ (NSArray*)kj_videoFilePaths;
/* 目錄下的全部文件名,包含臨時文件 */
+ (NSArray*)kj_videoAllFileNames;
/* 刪除指定完整路徑數(shù)據(jù) */
+ (void)kj_removeAimPath:(NSString*)path,...;
/* 判斷文件是否存在,存在拼接完整路徑 */
+ (BOOL)kj_haveFileSandboxPath:(NSString * _Nonnull __strong * _Nonnull)path;
/* 清除視頻緩存文件和數(shù)據(jù)庫數(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;
/* 追加視頻臨時緩存路徑,用于播放器讀取 */
+ (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處理
這里再說說,關(guān)于我們seek的時候的處理,大致分3種情況,
第一種:seek處視頻已經(jīng)下載好
I Like 這種是最中規(guī)中矩的只需要直接讀取緩存播放即可
第二種:seek到視頻未下載部分
這時就需要先取消正在下載的數(shù)據(jù),然后從seek處開始重新下載數(shù)據(jù),只需要下載器支持分片指定位置下載即可實(shí)現(xiàn)該需求
NSString *range = [NSString stringWithFormat:@"bytes=%lu-%lu", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
第三種:seek來回多次數(shù)據(jù)就會包含已下載部分和未下載部分,斷斷續(xù)續(xù)
你咋這么煩呢?搞事情?。?!
這時候就需要對這段分片做個標(biāo)記,它到底屬于已下載分片,還是未下載分片
1、如果為未下載分片數(shù)據(jù),執(zhí)行分片下載
if (fragment.type){// 遠(yuǎn)端碎片,即開始下載
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ī)會再讀,好好珍惜 - -!
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