iOS 邊下載邊播放、支持多點(diǎn)下載播放器

一、MPMoviePlayerController 和 AVPlayer 簡(jiǎn)介

iOS 9 之前通常播放音頻、視頻都是基于MediaPlayer框架的MPMoviePlayerControler以及基于它的MPMoviePlayerViewController,后者提供了統(tǒng)一的控制界面泥栖;iOS 9后iOS推出了AVKit的播放器AVPlayer和AVPlayerViewController。AVKit提供的功能更加強(qiáng)大勋篓,所以我們基于AVKit里的AVPlayer進(jìn)行二次開(kāi)發(fā)吧享。

二、邊下邊播實(shí)現(xiàn)

關(guān)鍵字

  • AVPlayer和AVURLAsset
  • AVAssetResourceLoaderDelegate
  • NSURLSession

邊下邊播目前搜索到的幾篇博客差不多都是參考這篇英文文章進(jìn)行實(shí)現(xiàn)的譬嚣,這里大概復(fù)述一遍基本的原理:
首先钢颂,AVPlayer可以播放AVAsset資源文件這個(gè)不用多說(shuō),而AVURLAsset專門用于播放網(wǎng)絡(luò)音視頻資源拜银,基本的播放殊鞭、暫停、拖動(dòng)操作均能提供,AVURLAsset有個(gè)方法

- (void)setDelegate:(nullable id <AVAssetResourceLoaderDelegate>)delegate queue:(nullable dispatch_queue_t)delegateQueue;

兩個(gè)傳入?yún)?shù):AVAssetResourceLoaderDelegate 實(shí)現(xiàn)了該協(xié)議的資源加載器和處理該資源加載的隊(duì)列尼桶,就是給AVURLAsset添加一個(gè)資源加載的代理對(duì)象操灿,幫助AVURLAsset加載數(shù)據(jù)。
不過(guò)需要清楚一點(diǎn)泵督,當(dāng)提供的URL協(xié)議是AVURLAsset能識(shí)別處理的時(shí)候如(https://xxx.mp4)趾盐,該資源加載代理對(duì)象是不會(huì)收到來(lái)自AVURLAsset的數(shù)據(jù)加載請(qǐng)求的;
然而如果我們把URL改成如(sevenuncle://xxx.mp4)的時(shí)候小腊,由于AVURLAsset無(wú)法識(shí)別出該協(xié)議救鲤,就會(huì)轉(zhuǎn)向資源加載代理對(duì)象詢問(wèn)數(shù)據(jù)如何加載,此時(shí)就會(huì)調(diào)用AVAssetResourceLoaderDelegate 協(xié)議里的下述方法:

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest

AVURLAsset將數(shù)據(jù)加載請(qǐng)求轉(zhuǎn)發(fā)給了資源加載代理對(duì)象,AVURLAsset在播放一個(gè)音視頻文件的時(shí)候會(huì)按照當(dāng)前播放需要溢豆,發(fā)送一個(gè)AVAssetResourceLoadingRequest請(qǐng)求,這個(gè)請(qǐng)求包含了播放所需數(shù)據(jù)的請(qǐng)求封裝類AVAssetResourceLoadingDataRequest瘸羡,該類主要包含三個(gè)屬性:

@property (nonatomic, readonly) long long requestedOffset //請(qǐng)求數(shù)據(jù)的起點(diǎn)
@property (nonatomic, readonly) NSInteger requestedLength //請(qǐng)求長(zhǎng)度
@property (nonatomic, readonly) long long currentOffset;// 該請(qǐng)求當(dāng)前已填充的數(shù)據(jù)位置

所以AVURLAsset播放請(qǐng)求是以片段的方式發(fā)出請(qǐng)求的漩仙,而且是同時(shí)發(fā)送多個(gè)數(shù)據(jù)片段請(qǐng)求,所以在資源加載代理對(duì)象里需要把這些數(shù)據(jù)片段請(qǐng)求都保存起來(lái)。

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    //保存數(shù)據(jù)請(qǐng)求 
    [self addLoadingRequest:loadingRequest]; 
    return YES;
}
- (void)addLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
    [self.pendingRequests addObject:loadingRequest];
    //下載資源
    [self doDownloadSource:loadingRequest];
}

同時(shí)队他,資源加載代理對(duì)象這個(gè)時(shí)候就需要清楚資源文件真正下載方式(如恢復(fù)成Https://xxx.mp4)卷仑,資源加載代理對(duì)象自己去開(kāi)啟網(wǎng)絡(luò)請(qǐng)求去下載該資源,可以自己封裝一個(gè)資源文件下載類麸折,該類負(fù)責(zé)實(shí)際的數(shù)據(jù)下載锡凝,

NSMutableRequest *request = [NSMutableURLRequest  requestWithURL:effectiveURL];
NSString *value = [NSString stringWithFormat:@"bytes=%lld-%lld",requestOffset, requestLength]; //制定下載的范圍
[request addValue:value forHTTPHeaderField:@"Range"];

self.dataTask = [self.session dataTaskWithRequest:request];
[self.dataTask resume];

當(dāng)完成一段數(shù)據(jù)下載(HTTP本身已經(jīng)實(shí)現(xiàn)了資源文件請(qǐng)求時(shí)的分段傳輸,多點(diǎn)斷點(diǎn)下載等)的時(shí)候通知資源加載代理對(duì)象垢啼,就去遍歷前面保存的AVAssetResourceLoadingDataRequest窜锯,

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.temporyFilePath];
    [fileHandle seekToFileOffset:self.currentOffset];
    [fileHandle writeData:data];
   
    //通知數(shù)據(jù)更新
    if(self.freshDataCachedHandler) {
        self.freshDataCachedHandler();
    }
}

資源加載代理對(duì)象將已有數(shù)據(jù)一點(diǎn)一點(diǎn)的填充進(jìn)去,

[dataRequest respondWithData:data];

每次填充一段數(shù)據(jù)進(jìn)AVAssetResourceLoadingDataRequest,屬性currentOffset就會(huì)往后移動(dòng)到數(shù)據(jù)已經(jīng)填充到的位置芭析,當(dāng)一個(gè)AVAssetResourceLoadingDataRequest數(shù)據(jù)填充完畢之后(currentOffset+requestOffset >= requestLength)锚扎,就對(duì)其發(fā)送一個(gè)完成請(qǐng)求的消息,并將其移除隊(duì)列馁启,完成一個(gè)數(shù)據(jù)片段的請(qǐng)求驾孔。

  [loadingRequest finishLoading];

AVAssetResourceLoaderDelegate 的另外一個(gè)方法時(shí)當(dāng)seek等操作之后,AVURLAsset會(huì)取消之前發(fā)出去的一些數(shù)據(jù)請(qǐng)求:

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest

此時(shí)惯疙,我們可以把之前加入到等待隊(duì)列的AVAssetResourceLoadingRequest移除隊(duì)列翠勉。
這樣就達(dá)到了播放的同時(shí),自己實(shí)現(xiàn)了下載文件的需求霉颠;不過(guò)需要注意的一點(diǎn)对碌,這個(gè)過(guò)程有個(gè)缺點(diǎn),當(dāng)播放進(jìn)度拖動(dòng)了之后掉分,由于下載任務(wù)重新開(kāi)始俭缓,則拖動(dòng)之后下載的文件是從拖動(dòng)的位置開(kāi)始下載,導(dǎo)致下載的文件不完整酥郭。

三华坦、多點(diǎn)下載、拖動(dòng)播放保存實(shí)現(xiàn)

上述已經(jīng)闡述了邊下邊播的過(guò)程不从,但是有一個(gè)缺點(diǎn)惜姐;當(dāng)播放時(shí)拖動(dòng)了進(jìn)度之后,下載的文件不完整椿息,此時(shí)需要解決也簡(jiǎn)單歹袁,拖動(dòng)播放進(jìn)度之后不重新開(kāi)啟下載任務(wù),不刪除之前下載的數(shù)據(jù)寝优,所以解決方法:

  1. 用一個(gè)數(shù)據(jù)結(jié)構(gòu)保存間斷的數(shù)據(jù)分段范圍如(1-100条舔、300-2000);
  2. 分段請(qǐng)求數(shù)據(jù)(HTTP頭里面的Range字段可以指定數(shù)據(jù)的起始范圍)乏矾;
  3. 當(dāng)請(qǐng)求拖動(dòng)位置后面的數(shù)據(jù)下載完成后孟抗,重新回到前面下載之前未下載的數(shù)據(jù)片段迁杨。

很明顯,這個(gè)可以保存數(shù)據(jù)下載分片進(jìn)度的數(shù)據(jù)結(jié)構(gòu)是關(guān)鍵凄硼,作者按照NSRange的基本實(shí)現(xiàn)思路铅协,也實(shí)現(xiàn)了一個(gè)自己的SURange(SURangePointer),以鏈表的形式保存了各個(gè)分片進(jìn)度摊沉。

struct _SURange{
    unsigned long long      location;  //起點(diǎn)
    unsigned long long      length;    //長(zhǎng)度
    struct _SURange *next;             //下一個(gè)節(jié)點(diǎn)
};
typedef struct _SURange SURange;
typedef struct _SURange *SURangePointer;
  • 新增一個(gè)下載進(jìn)度
SURangePointer SUInsertNodeIntoRange(SURangePointer src, SURangePointer node1);

新增下載進(jìn)度函數(shù)狐史,舉個(gè)例子,src為[(0,100)(300,1000)]说墨,node1為(19,150)骏全,則結(jié)果為[(0,150)(300,1000)]

  • 獲取未下載進(jìn)度
SURangePointer SUGetGapRanges(SURangePointer links, SURangePointer node);

獲取未下載進(jìn)度,可以獲取分割的未下載片段婉刀,如links為[(100,300)(500,10000)(20000,50000)],node為(400,1000000),則范圍未下載的進(jìn)度為[(400,499)(10001,19999)(50001,100000)]吟温。
這樣保存數(shù)據(jù)分片進(jìn)度的數(shù)據(jù)結(jié)構(gòu)完成了。
剩下的就好辦了突颊,現(xiàn)在下載數(shù)據(jù)時(shí)鲁豪,申請(qǐng)一個(gè)所需大小的空文件存放數(shù)據(jù),每當(dāng)發(fā)出一個(gè)新帶seek位置的播放請(qǐng)求律秃,保存數(shù)據(jù)時(shí)先seek到制定的偏移位置爬橡,然后將數(shù)據(jù)保存進(jìn)臨時(shí)文件,然后更新進(jìn)度棒动。

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.temporyFilePath];
    //移動(dòng)到當(dāng)前請(qǐng)求的偏移位置糙申,保存數(shù)據(jù)
    [fileHandle seekToFileOffset:self.currentOffset];
    [fileHandle writeData:data];

    //插入下載進(jìn)度分片
    SURange *range;
    range = malloc(sizeof(SURange));
    range->location = self.currentOffset;
    range->length   = data.length;
    range->next     = NULL;
    _downloadRange = SUInsertNodeIntoRange(_downloadRange, range);
    free(range);
    
    //更新已經(jīng)下載數(shù)據(jù)偏移
    self.currentOffset += data.length;

    //通知數(shù)據(jù)更新
    if(self.freshDataCachedHandler) {
        self.freshDataCachedHandler();
    }
}

當(dāng)完成一個(gè)數(shù)據(jù)NSURLRequest之后,尋找未下載的片段范圍船惨,繼續(xù)下載柜裸,

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error {
    NSHTTPURLResponse *httpURLResponse = (NSHTTPURLResponse *)task.response;
    NSDictionary *dict = [httpURLResponse allHeaderFields];
    NSString     *content = [dict valueForKey:@"Content-Range"];
    NSString *offsetString = [NSString stringWithFormat:@"%lld", _currentOffset-1];
    
    //如果不是seek之后取消了一個(gè)下載請(qǐng)求,則尋找未下載的片段范圍
    if([content rangeOfString:offsetString].length>0) {
        if (self.currentOffset >= self.resourceLength) {
            [self seekToDownloadAtOffset:0 withURL:self.resourceURL];
        }else {
            [self seekToDownloadAtOffset:self.currentOffset withURL:self.resourceURL];
        }
    }
}

最后粱锐,等到所需進(jìn)度都下載完成疙挺,則將臨時(shí)文件保存持久化,同時(shí)添加進(jìn)緩存索引怜浅。與此同時(shí)利用SURange铐然,可以開(kāi)啟多個(gè)下載任務(wù)同時(shí)下載,共同維護(hù)一個(gè)下載進(jìn)度恶座,則多點(diǎn)下載也可以很快的實(shí)現(xiàn)搀暑。

demo地址:https://github.com/sevenuncler/SUAdvancePlayer

四、總結(jié)

  1. 變下邊播:使用自定義協(xié)議的URL傳入AVURLAsset,導(dǎo)致其將數(shù)據(jù)請(qǐng)求的任務(wù)交給實(shí)現(xiàn)了AVAssetResourceLoaderDelegate協(xié)議的數(shù)據(jù)加載代理對(duì)象跨琳;
  2. 數(shù)據(jù)加載代理自己負(fù)責(zé)數(shù)據(jù)的下載自点,然后一點(diǎn)點(diǎn)將下載的數(shù)據(jù)返回給AVURLAsset;
  3. 多點(diǎn)下載和Seek之后文件完整保存:需要實(shí)現(xiàn)一個(gè)能夠保存分片不連續(xù)進(jìn)度的數(shù)據(jù)結(jié)構(gòu)脉让,當(dāng)前請(qǐng)求的進(jìn)度下載完成之后尋找下一個(gè)未下載的數(shù)據(jù)進(jìn)度繼續(xù)下載桂敛。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冈绊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子埠啃,更是在濱河造成了極大的恐慌,老刑警劉巖伟恶,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碴开,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡博秫,警方通過(guò)查閱死者的電腦和手機(jī)潦牛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挡育,“玉大人巴碗,你說(shuō)我怎么就攤上這事〖春” “怎么了橡淆?”我有些...
    開(kāi)封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)母赵。 經(jīng)常有香客問(wèn)我逸爵,道長(zhǎng),這世上最難降的妖魔是什么凹嘲? 我笑而不...
    開(kāi)封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任师倔,我火速辦了婚禮,結(jié)果婚禮上周蹭,老公的妹妹穿的比我還像新娘趋艘。我一直安慰自己,他們只是感情好凶朗,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布瓷胧。 她就那樣靜靜地躺著,像睡著了一般俱尼。 火紅的嫁衣襯著肌膚如雪抖单。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天遇八,我揣著相機(jī)與錄音矛绘,去河邊找鬼。 笑死刃永,一個(gè)胖子當(dāng)著我的面吹牛货矮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斯够,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼囚玫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼喧锦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起抓督,我...
    開(kāi)封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤燃少,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后铃在,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體阵具,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年定铜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阳液。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡揣炕,死狀恐怖帘皿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情畸陡,我是刑警寧澤鹰溜,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站丁恭,受9級(jí)特大地震影響奉狈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涩惑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一仁期、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竭恬,春花似錦跛蛋、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至岔绸,卻和暖如春理逊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盒揉。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工晋被, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刚盈。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓羡洛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親藕漱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子欲侮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容