iOS 音頻視頻播放器實(shí)現(xiàn)邊下載邊播放緩存視頻

前言

  • 本文主要介紹基于AVPlayer實(shí)現(xiàn)邊下邊播邊存處理,核心其實(shí)就是基于AVPlayerAVAssetResourceLoaderDelegate然后對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詳解系列(一)參數(shù)設(shè)置

邊下邊播方案

再介紹之前,我們再來了解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

文章關(guān)聯(lián)

關(guān)于播放器其他相關(guān)文章

后續(xù)該播放器殼子我會慢慢補(bǔ)充完善,老哥覺得好用還請幫我點(diǎn)個小星星傳送門

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容