同志們我懷著激動(dòng)而又沉重的心情寫這篇文章,這篇文章我主要是介紹我怎么一點(diǎn)點(diǎn)一步步把邊下邊播弄懂的芽世,而且我覺(jué)得難點(diǎn)還有重點(diǎn)我都會(huì)重點(diǎn)說(shuō)明挚赊,保證沒(méi)有弄過(guò)變下邊播的人看了這篇文章都能明白并且自己會(huì)做一些修改。
注意點(diǎn)或者建議:
1.先大致看一下我的文章济瓢,然后估計(jì)你在沒(méi)有看代碼的時(shí)候可能還是有些不明白荠割。然后去看代碼,代碼看的差不多了在來(lái)看我的文章你應(yīng)該就明白了
2.放心 代碼什么的都在git上給大家放好了葬荷,在文章結(jié)尾涨共,這樣做的目的是為了大家先看完文章在讀代碼如果先讀代碼可能效果不明顯纽帖。
3.文章中的圖片有可能會(huì)跟你們看到的其他文章相同,放心本人并不是抄襲他們举反,我只是覺(jué)得他們寫的對(duì)的會(huì)拿到我的文章中懊直,但是他們文章中沒(méi)有說(shuō)到的點(diǎn)或者他們比較模糊的點(diǎn)我都會(huì)說(shuō)明
4.看這個(gè)文章的時(shí)候希望大家不要浮躁,過(guò)于浮躁可能會(huì)使你只知道大概但是要問(wèn)你細(xì)節(jié)你可能就不明白了火鼻。
5.安裝緩存的音頻或者視頻的sdk的時(shí)候需要到入庫(kù)
封裝的結(jié)構(gòu)
解釋:其中我封裝的東西在這個(gè)四個(gè)文件中室囊。第一個(gè)文件中是非緩存的音頻,也就是說(shuō)不能緩存音樂(lè)的魁索,第二個(gè)文件是非緩存的視頻融撞,第三個(gè)是緩存的音頻(也支持非緩存的音頻),第四個(gè)大家應(yīng)該都知道了粗蔚。為什么我第三個(gè) 第四個(gè)文件都包括了所有的功能 我還要寫第一個(gè)文件和第二個(gè)文件尝偎。因?yàn)椋杭偃缒惴浅4_定你的app是不用緩存的并且產(chǎn)品經(jīng)理也非常的好那么干嘛寫那么多沒(méi)用的代碼。
-
拿一個(gè)視頻緩存的文件介紹
image.png
其中framework我已經(jīng)幫你打好了鹏控,但是他是debug致扯,因?yàn)槲胰绻o你打了release的framework一些斷言的信息什么的你都看不到了,這樣不利于你調(diào)試当辐。下面我來(lái)介紹一下怎么使用:
@protocol DGCacheVideoPlayerDelegate <NSObject>
/**
播放失敗了
@param error error
*/
- (void)DGCacheVideoPlayFailed:(NSError *)error;
/**
一首歌曲播放完成了抖僵,會(huì)把下一首需要播放的歌曲返回來(lái) 會(huì)自動(dòng)播放下一首,不要再這里播放下一首
@param nextModel 下一首歌曲的模型
*/
- (void)DGCacheVideoPlayFinish:(DGCacheVideoModel *)nextModel;
/**
播放狀態(tài)發(fā)生了改變
@param status 改變后的狀態(tài)
*/
- (void)DGCacheVideoPlayStatusChanged:(DGCacheVideoState)status;
/**
緩存的進(jìn)度 注意:當(dāng)需要緩存的時(shí)候是下載的進(jìn)度 不需要緩存的時(shí)候是監(jiān)聽(tīng)player loadedTimeRanges的進(jìn)度
@param cacheProgress 播放的緩存的進(jìn)度
*/
- (void)DGCacheVideoCacheProgress:(CGFloat )cacheProgress;
/**
當(dāng)前時(shí)間 總的時(shí)間 緩沖的進(jìn)度的播放代理回調(diào)
@param currentTime 當(dāng)前的時(shí)間
@param durationTime 總的時(shí)間
@param playProgress 播放的進(jìn)度 (0-1)之間
*/
- (void)DGCacheVideoPlayerCurrentTime:(CGFloat)currentTime
duration:(CGFloat)durationTime
playProgress:(CGFloat)playProgress;
@end
@interface DGCacheVideoPlayer : NSObject
#pragma mark - 初始化
@property (weak, nonatomic) id <DGCacheVideoPlayerDelegate> DGCacheVideoDelegate;
+(instancetype)shareInstance;
#pragma mark - 設(shè)置相關(guān)的方法
/**
設(shè)置播放列表沒(méi)有設(shè)置播放列表播放器沒(méi)有播放地址
@param playList 需要播放的模型數(shù)組
@param offset 偏移量
@param videoGravity 視頻的顯示類型
@param addViewLayer 需要添加的layer
@param cache 是否緩存 YES: 緩存 NO:不緩存
@param frame 視頻的frame
*/
- (void)setPlayList:(NSArray<DGCacheVideoModel *> *)playList
offset:(NSUInteger)offset
videoGravity:(AVLayerVideoGravity)videoGravity
addViewLayer:(CALayer *)addViewLayer
isCache:(BOOL)cache
layerFrame:(CGRect)frame;
/**
點(diǎn)擊下一個(gè)播放
*/
- (void)playNextVideo;
/**
點(diǎn)擊上一個(gè)播放
*/
- (void)playPreviousVideo;
/**
設(shè)置當(dāng)前的播放動(dòng)作
@param operate 動(dòng)作: 播放缘揪、暫停耍群、停止
停止:清空播放列表,如果在要播放需要重新設(shè)置播放列表
*/
- (void)playOperate:(DGCacheVideoOperate)operate;
/**
清空播放列表
@param isStopPlay YES:停止播放 NO:不停止播放
*/
- (void)clearPlayList:(BOOL)isStopPlay;
/**
刪除一個(gè)播放列表
@param deleteList 要?jiǎng)h除的播放列表
*/
- (void)deletePlayList:(NSArray<DGCacheVideoModel *>*)deleteList;
/**
添加一個(gè)新的歌單到播放列表
@param addList 新的歌曲的數(shù)組
*/
- (void)addPlayList:(NSArray<DGCacheVideoModel *>*)addList;
/**
快進(jìn)或者快退
@param time 要播放的那個(gè)時(shí)間點(diǎn)
*/
- (void)seekTime:(NSUInteger)time;
/**
設(shè)置播放器的音量 非系統(tǒng)的 (不是點(diǎn)擊手機(jī)音量加減鍵的音量)
@param value 【0-10】大于10 等于10 小于0 等于0
*/
- (void)setVolumeValue:(CGFloat)value;
#pragma mark - 可以獲得的方法
/**
當(dāng)前的播放狀態(tài)找筝,方便用戶隨時(shí)拿到
@return 對(duì)應(yīng)的播放狀態(tài)
*/
- (DGCacheVideoState)currentPlayeStatus;
/**
當(dāng)前的播放的模型
@return 當(dāng)前的播放模型
*/
- (DGCacheVideoModel *)currentMusicModel;
/**
當(dāng)前播放歌曲的下標(biāo)
@return 為了你更加省心 我給你提供出來(lái)
*/
- (NSUInteger)currentIndex;
/**
獲得播放列表
@return 播放列表
*/
- (NSArray<DGCacheVideoModel *> *)getPlayList;
/**
獲得當(dāng)前播放器的總時(shí)間
@return 時(shí)間
*/
- (CGFloat )durationTime;
/**
獲得播放器的音量
*/
- (CGFloat)getVolueValue;
解釋:我們要做的首先就是設(shè)置播放列表和代理其他的就看你自己的需求了蹈垢。然后播放器外部是看不到的。
比如我這樣使用
NSArray *temArray = @[@"https://weiboshipin.cmvideo.cn/depository_sp/fsv/trans/2018/11/25/649656072/25/5bfaa1eb6633d9b67e3369fe.mp4"];
NSMutableArray *infoArray = [NSMutableArray array];
for (NSInteger index = 0; index < temArray.count; index ++) {
DGVideoInfo *videoInfo = [[DGVideoInfo alloc] init];
videoInfo.videoId = [NSString stringWithFormat:@"%zd",index];
videoInfo.playUrl = temArray[index];
[infoArray addObject:videoInfo];
}
[DGVideoManager shareInstance].DGDelegate = self;
[[DGVideoManager shareInstance] setPlayList:infoArray offset:0 videoGravity:AVLayerVideoGravityResizeAspect addViewLayer:self.view.layer layerFrame:CGRectMake(0, 64, 375, 300)];
至于其他的我就不介紹怎么使用了呻征,要想看怎么使用直接看demo就行了
邊下邊播的流程
- 思路:
我們正常情況下是AVPlayer 調(diào)用AVPlayerItem 耘婚,item在調(diào)用url 就可以實(shí)現(xiàn)播放了,但是現(xiàn)在是邊下邊播陆赋,我們就需要自己實(shí)現(xiàn)底層的那一套代理的方法,自己下載緩存嚷闭,確定緩存好了的在告訴播放器進(jìn)行播放攒岛。 - 對(duì)AVPlayer的理解:
AVPlayer他的主要的功能是對(duì)視頻、音頻的解碼胞锰,他可以告訴播放速度灾锯、狀態(tài)等等。他管理AVPlayerItem. - AVPlayerItem的理解:
他的作用主要是承接AVPlyer和AVAsset的嗅榕,主要是用來(lái)統(tǒng)籌數(shù)據(jù)的顺饮,判斷各種狀態(tài)吵聪,監(jiān)聽(tīng)信息的(比如是否播放完成、當(dāng)前時(shí)間等等) -
AVAsset 的理解:
他的作用主要是處理一些流信息(比如我們的視頻包括:音頻流兼雄、視頻流等等)并且他還要和服務(wù)器打交道獲取一些流信息吟逝。我們一般不使用他。一般用它的子類
AVURLAsset.
他們?nèi)齻€(gè)的關(guān)系我用一張圖來(lái)表示:
image.png - AVPlayerLayer 理解:
這個(gè)東西在視頻播放的時(shí)候才會(huì)用到赦肋,他的作用主要是用來(lái)顯示畫面用的
疑問(wèn):但是我們?yōu)槭裁雌綍r(shí)寫非緩存的用不到AVURLAsset?
其實(shí)我們也是用到了块攒,比如我們平時(shí)可能這樣寫:
NSURL *url = [NSURL URLWithString:VideoInfo.playUrl];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:item];
self.playerItem = item;
self.player = player;
[self.layer removeFromSuperlayer];
self.layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.layer.frame = self.videoFrame;
self.layer.videoGravity = self.currentVideoGravity;
[self.needAddLayer addSublayer:self.layer];
[self.player play];
[self addMyObserver];
這也沒(méi)有用到啊,實(shí)際上這句話:
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
蘋果在內(nèi)部已經(jīng)給你處理好了佃乘,并且蘋果是閉元的所以我們感覺(jué)沒(méi)有用到罷了囱井。
AVPlayer 對(duì) 視頻、音頻處理方式
其實(shí)給我們一個(gè)url(我拿視頻做例子)趣避,其實(shí)他是一段一段進(jìn)行處理的庞呕。我們都知道一個(gè)視頻可以用ffmpeg對(duì)他進(jìn)行分割 我們可以分割成很多段,其實(shí)這個(gè)也是一樣的道理程帕,拿一個(gè)圖來(lái)舉例子:
解釋:假如我們分成8段(至于分成多少段住练、每段大小多大都有系統(tǒng)決定),開(kāi)始時(shí)候我們下載第一段骆捧,下載完了配置信息確定是已經(jīng)下載好了并且是完整的澎羞,就會(huì)交給播放器進(jìn)行播放,然后下載隊(duì)列還在下載其他的段敛苇。注意下載隊(duì)列和我們返回給播放器的每段并沒(méi)有直接的關(guān)系妆绞。他只是負(fù)責(zé)下載而我們會(huì)讀取數(shù)據(jù)確定是能夠播放的進(jìn)行播放。
一般邊下邊播的產(chǎn)品需求
1.支持正常播放器的一切功能枫攀,包括暫停括饶、播放和拖拽
2.如果視頻加載完成且完整,將視頻文件保存到本地cache来涨,下一次播放本地cache中的視頻图焰,不再請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)
3.如果視頻沒(méi)有加載完(半路關(guān)閉或者拖拽)就不用保存到本地cache,因?yàn)閿?shù)據(jù)不完整蹦掐,但是如果拖拽在緩存區(qū)的那么還是要緩存下來(lái)技羔。一句話話只要緩存連續(xù)就緩存下來(lái)。不連續(xù)不緩存
根據(jù)產(chǎn)品需求我們做出如下的判斷:
1.開(kāi)始的時(shí)候請(qǐng)求下載隊(duì)列正常下載卧抗,在這之前需要判斷時(shí)候有臨時(shí)的緩存文件如果有進(jìn)行刪除藤滥。是否有下載隊(duì)列如果有停止下載并且取消任務(wù)。如果有緩存文件不進(jìn)行下載直接本地播放音頻社裆、視頻拙绊。
2.開(kāi)啟一個(gè)新的請(qǐng)求任務(wù),并且往臨時(shí)文件中寫入數(shù)據(jù)。(因?yàn)槲覀冎挥写_認(rèn)是完成的數(shù)據(jù)才能將臨時(shí)文件中的數(shù)據(jù)拷貝到永久緩存文件中)
3.讀取臨時(shí)文件中的數(shù)據(jù)标沪,并且在在resourceloader代理中進(jìn)行判斷是否是完成的數(shù)據(jù)或者是否能夠把這個(gè)數(shù)據(jù)傳遞給播放器榄攀,ok的直接傳遞給播放器進(jìn)行播放。
4.如果出現(xiàn)拖拽金句,或前或后判斷拖拽的是否大于緩存區(qū)檩赢,如果大于則說(shuō)明緩存不能連續(xù)了,則繼續(xù)開(kāi)始一個(gè)新的下載隊(duì)列并且從當(dāng)前的請(qǐng)求位置開(kāi)始進(jìn)行請(qǐng)求下載趴梢。
5.如果緩存是完整的則對(duì)下載鏈接進(jìn)行md5 加密作為保存路徑從臨時(shí)文件中copy數(shù)據(jù)到永久緩存文件中漠畜。
實(shí)現(xiàn)方案
我們正常的情況下 這樣寫代碼
self.needAddLayer = addViewLayer;
self.videoFrame = frame;
self.currentVideoGravity = videoGravity;
self.resourceLoader = [[DGVideoResourceLoader alloc] init];
self.resourceLoader.loaderDelegate = self;
AVURLAsset *urlAset = [AVURLAsset URLAssetWithURL:self.currentModel.playUrl options:nil];
[urlAset.resourceLoader setDelegate:self.resourceLoader queue:dispatch_get_main_queue()];
self.playerItem = [AVPlayerItem playerItemWithAsset:urlAset];
self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
[self.layer removeFromSuperlayer];
self.layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.layer.frame = self.videoFrame;
self.layer.videoGravity = self.currentVideoGravity;
[self.needAddLayer addSublayer:self.layer];
[self.player play];
[self addMyObserver];
這樣的話是不會(huì)調(diào)用delegate的,必須我們將我們的下載鏈接的url的scheme改了 隨便換成一個(gè)(不能使http之類的)才會(huì)進(jìn)我們的代理坞靶。所以我們的方案是(這個(gè)圖是我從別的地方拿過(guò)來(lái)的因?yàn)樗f(shuō)的很對(duì)):
其中resourceLoader有兩個(gè)delegate使我們經(jīng)常使用的
第一個(gè):
/**
avasert 每次都會(huì)進(jìn)這個(gè)方法憔狞,他會(huì)返回每次的loadingRequest
@param resourceLoader resourceLoader
@param loadingRequest loadingRequest
@return 如果為YES:繼續(xù)返回 NO:終止返回不在返回loadingRequest
*/
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
這個(gè)方法就是我上面所說(shuō)的蘋果給我們返回來(lái)一段一段的流數(shù)據(jù),也就是每一個(gè)loadingRequest彰阴,他們?cè)谡麄€(gè)期間調(diào)用多次瘾敢。并且我們會(huì)在這個(gè)方法進(jìn)行判斷是否是已經(jīng)配置好的信息
第二個(gè):
/**
處理完成的請(qǐng)求取消
@param resourceLoader resourceLoader
@param loadingRequest loadingRequest
*/
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
這個(gè)方法就是我們配置完信息告訴完播放器進(jìn)行播放了之后,我們要把處理完的數(shù)據(jù)移除 就會(huì)調(diào)用這個(gè)方法尿这。這個(gè)放的觸發(fā)是在這個(gè)方法之后:
[loadingRequest finishLoading];
聯(lián)系起來(lái)代碼是這樣的:
#pragma mark - avassetResourceLoaderDelegate
/**
avasert 每次都會(huì)進(jìn)這個(gè)方法簇抵,他會(huì)返回每次的loadingRequest
@param resourceLoader resourceLoader
@param loadingRequest loadingRequest
@return 如果為YES:繼續(xù)返回 NO:終止返回不在返回loadingRequest
*/
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
[self handleLoadingRequest:loadingRequest];
return YES;
}
/**
處理完成的請(qǐng)求取消
@param resourceLoader resourceLoader
@param loadingRequest loadingRequest
*/
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
// 已經(jīng)取消的 從數(shù)組中移除
[self.requestList removeObject:loadingRequest];
}
#pragma mark - 自己事件的處理
/**
處理delegatee給的loadingRequest
@param loadingRequest loadingRequest
*/
- (void)handleLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
[self.requestList addObject:loadingRequest];
if (self.downloadManager) {
if (loadingRequest.dataRequest.requestedOffset >= self.downloadManager.requestOffset &&
loadingRequest.dataRequest.requestedOffset <= self.downloadManager.requestOffset + self.downloadManager.cacheLength) {
//數(shù)據(jù)已經(jīng)緩存,則直接完成
[self haveCacheProcessRequestList];
}else {
//數(shù)據(jù)還沒(méi)緩存射众,則等待數(shù)據(jù)下載碟摆;如果是Seek操作,則重新請(qǐng)求
if (self.isSeek) {
[self startNewLoadrequest:loadingRequest cache:NO];
}
}
}else {
[self startNewLoadrequest:loadingRequest cache:YES];
}
// 完事就要發(fā)送信號(hào)
dispatch_semaphore_signal(self.semaphore);
}
/**
處理緩存好的的請(qǐng)求
*/
- (void)haveCacheProcessRequestList{
NSMutableArray * finishRequestList = [NSMutableArray array];
for (AVAssetResourceLoadingRequest * loadingRequest in self.requestList) {
if ([self configFinishLoadingRequest:loadingRequest]) {
[finishRequestList addObject:loadingRequest];
}
}
if (finishRequestList.count) {
[self.requestList removeObjectsInArray:finishRequestList];
}
}
/**
配置相關(guān)的信息叨橱,并且判斷完成了沒(méi)
@param loadingRequest loadingRequest
@return 是否完成了
*/
- (BOOL)configFinishLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
NSString *mineType = [self.downloadManager getMyMimeType];
if (mineType.length == 0) {
mineType = @"video/mp4";
}
//填充信息
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mineType), NULL);
loadingRequest.contentInformationRequest.contentType = CFBridgingRelease(contentType);
loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
loadingRequest.contentInformationRequest.contentLength = self.downloadManager.fileLenth;
//讀文件典蜕,填充數(shù)據(jù)
NSUInteger cacheLength = self.downloadManager.cacheLength;
NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
if (loadingRequest.dataRequest.currentOffset != 0) {
requestedOffset = loadingRequest.dataRequest.currentOffset;
}
NSUInteger canReadLength = cacheLength - (requestedOffset - self.downloadManager.requestOffset);
NSUInteger respondLength = MIN(canReadLength, loadingRequest.dataRequest.requestedLength);
NSUInteger offset = requestedOffset - self.downloadManager.requestOffset;
if (requestedOffset < self.downloadManager.requestOffset) {
offset = 0;
}
[loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];
//如果完全響應(yīng)了所需要的數(shù)據(jù),則完成
NSUInteger nowendOffset = requestedOffset + canReadLength;
NSUInteger reqEndOffset = loadingRequest.dataRequest.requestedOffset + loadingRequest.dataRequest.requestedLength;
if (nowendOffset >= reqEndOffset) {
[loadingRequest finishLoading];
return YES;
}
return NO;
}
/**
開(kāi)始發(fā)送新的請(qǐng)求
@param loadingRequest loadingRequest
@param isCache isCache
*/
- (void)startNewLoadrequest:(AVAssetResourceLoadingRequest *)loadingRequest cache:(BOOL)isCache{
NSUInteger fileLength = 0;
if (self.downloadManager) {
fileLength = self.downloadManager.fileLenth;
self.downloadManager.cancel = YES;
}
self.downloadManager = [[DGVideoDownloadManager alloc] init];
self.downloadManager.requestURL = loadingRequest.request.URL;
self.downloadManager.requestOffset = loadingRequest.dataRequest.requestedOffset;
self.downloadManager.isCache = isCache;
if (fileLength > 0) {
self.downloadManager.fileLenth = fileLength;
}
self.downloadManager.downloadManagerDelegate = self;
[self.downloadManager startRequest];
self.isSeek = NO;
}
流程解釋:
1.開(kāi)始請(qǐng)求 并且創(chuàng)建一個(gè)下載隊(duì)列罗洗。
2.在delegate回調(diào)之后我們要判斷是否是已經(jīng)下載好的或者是否有拖拽的愉舔,如果是無(wú)拖拽的正常下載我們會(huì)對(duì)他配置信息并且交給播放器播放。
3.如果有拖拽并且大于緩存區(qū)了那么會(huì)重新開(kāi)始一個(gè)下載隊(duì)列伙菜,并且刪除之前的臨時(shí)緩存文件
4.配置完信息并且確定是加完的loadingRequest 從數(shù)組中移除轩缤。因?yàn)槲覀冮_(kāi)始會(huì)創(chuàng)建一個(gè)數(shù)組保存所有的loadingRequest。
5.回調(diào)信息等等會(huì)回調(diào)(進(jìn)度贩绕、當(dāng)前時(shí)間火的、總時(shí)間、播放狀態(tài)等等自己定義的delegate)
難于理解的點(diǎn)或者比較疏忽的點(diǎn)或者自己遇到的坑(個(gè)人認(rèn)為)
1.怎樣觸發(fā)resourceloader 那就更改他的scheme
我的代碼是這樣寫的:
NSURL *myUrl = [NSURL URLWithString:str];
NSString *schemeName = myUrl.scheme;
[[NSUserDefaults standardUserDefaults] setObject:schemeName forKey:DGVideoSchemeKey];
[[NSUserDefaults standardUserDefaults] synchronize];
NSURLComponents * components = [[NSURLComponents alloc] initWithURL:myUrl resolvingAgainstBaseURL:NO];
components.scheme = @"streaming";
return [components URL];
}
然后我獲取之前的scheme是這樣寫的:
NSString *schemeName = [[NSUserDefaults standardUserDefaults] objectForKey:DGVideoSchemeKey];
NSURLComponents * components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
components.scheme = schemeName.length > 0 ? schemeName: @"http";
return [components URL];
這樣寫的好處 就是之前是啥拿到之后就是之前那個(gè)淑倾,我看網(wǎng)上很多人直接返回返回 http的卫玖,那如果是https的不就不行了嗎。這是一個(gè)注意點(diǎn)
2.我們?cè)邳c(diǎn)擊下一首或者上一首等等的需要把之前的臨時(shí)文件刪除踊淳。如果不刪除那么緩存進(jìn)相當(dāng)于原來(lái)的數(shù)據(jù)相加 那樣肯定是有問(wèn)題的,比如我是這樣處理的
self.resourceLoader.downloadManager.cancel = YES;
self.resourceLoader = nil;
[DGVideoStrFileHandle deleleTempFile];
3.我們?cè)讷@取minetype的時(shí)候盡量用服務(wù)器返回給我們的那個(gè)
self.innerMyMimeType = response.MIMEType;
引用的地方這樣引用的,我拿視頻作為例子:
NSString *mineType = [self.downloadManager getMyMimeType];
if (mineType.length == 0) {
mineType = @"video/mp4";
}
我看網(wǎng)上的人直接寫死的 video/mp4迂尝,那一但不是呢脱茉?那不就出問(wèn)題了(因?yàn)楸救瞬恢酪曨l的mineType是不是都是video/mp4,個(gè)人理解不一定都是)
4.對(duì)下面這個(gè)方法詳細(xì)解釋:
if (loadingRequest.dataRequest.requestedOffset >= self.downloadManager.requestOffset &&
loadingRequest.dataRequest.requestedOffset <= self.downloadManager.requestOffset + self.downloadManager.cacheLength) {
//數(shù)據(jù)已經(jīng)緩存垄开,則直接完成
[self haveCacheProcessRequestList];
}else {
//數(shù)據(jù)還沒(méi)緩存琴许,則等待數(shù)據(jù)下載;如果是Seek操作溉躲,則重新請(qǐng)求
if (self.isSeek) {
[self startNewLoadrequest:loadingRequest cache:NO];
}
}
起初我當(dāng)時(shí)不明白的是為什么第一個(gè)判斷中&&后面的還要加上:self.downloadManager.requestOffset 就因?yàn)檫@個(gè)我糾結(jié)了好久問(wèn)了同事 說(shuō)實(shí)話他也是支支吾吾后來(lái)我們一起研究出來(lái)了榜田。他是這樣的:
因?yàn)檎G闆r下:self.downloadManager.requestOffset都是等于0的,因?yàn)樗窃谶@個(gè)方法中給他賦值的:
- (void)startNewLoadrequest:(AVAssetResourceLoadingRequest *)loadingRequest cache:(BOOL)isCache
剛開(kāi)始請(qǐng)求的時(shí)候這個(gè)肯定是0 后來(lái)正常的請(qǐng)求他們也不會(huì)開(kāi)始新的請(qǐng)求 所以就相當(dāng)于小于緩存文件的長(zhǎng)度這個(gè)判斷是正確的锻梳。假如現(xiàn)在出現(xiàn)了拖拽:1.在緩存文件范圍之內(nèi) 那么self.downloadManager.requestOffset還是0 所以還是對(duì)的箭券,2.假如不在緩存文件之內(nèi) 那么他會(huì)重新開(kāi)始一個(gè)新的請(qǐng)求 這時(shí)候self.downloadManager.requestOffset是上一個(gè)請(qǐng)求的的requestOffset,但是這時(shí)候的cacheLength也就不是之前的cacheLength 疑枯,個(gè)人理解就是當(dāng)前段的緩存 并不是之前段的那些緩存相加辩块。所以這個(gè)判斷還是對(duì)的。這個(gè)判斷寫的很神奇也很巧妙不知道我這樣解釋你懂了嗎荆永。
5.關(guān)于配置信息的這個(gè)方法
NSString *mineType = [self.downloadManager getMyMimeType];
if (mineType.length == 0) {
mineType = @"video/mp4";
}
//填充信息
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mineType), NULL);
loadingRequest.contentInformationRequest.contentType = CFBridgingRelease(contentType);
loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
loadingRequest.contentInformationRequest.contentLength = self.downloadManager.fileLenth;
//讀文件废亭,填充數(shù)據(jù)
NSUInteger cacheLength = self.downloadManager.cacheLength;
NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
if (loadingRequest.dataRequest.currentOffset != 0) {
requestedOffset = loadingRequest.dataRequest.currentOffset;
}
NSUInteger canReadLength = cacheLength - (requestedOffset - self.downloadManager.requestOffset);
NSUInteger respondLength = MIN(canReadLength, loadingRequest.dataRequest.requestedLength);
NSUInteger offset = requestedOffset - self.downloadManager.requestOffset;
if (requestedOffset < self.downloadManager.requestOffset) {
offset = 0;
}
[loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];
//如果完全響應(yīng)了所需要的數(shù)據(jù),則完成
NSUInteger nowendOffset = requestedOffset + canReadLength;
NSUInteger reqEndOffset = loadingRequest.dataRequest.requestedOffset + loadingRequest.dataRequest.requestedLength;
if (nowendOffset >= reqEndOffset) {
[loadingRequest finishLoading];
return YES;
}
return NO;
其他的我寫的注釋 很詳細(xì) 我主要是解釋這段代碼
為什么要
cacheLength - (requestedOffset - self.downloadManager.requestOffset);
還是和剛才差不多的self.downloadManager.requestOffset在這個(gè)方法里基本都是0具钥,相當(dāng)于cacheLength - requestedOffset 豆村,這不正好就是那一段的可以讀的requestedOffset,其中這個(gè)方法:
[loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];
就是告訴播放器流數(shù)據(jù)播放器好進(jìn)行播放骂删。當(dāng)調(diào)用了這個(gè)方法
[loadingRequest finishLoading];
也就是loadingRequest處理完了 就開(kāi)始調(diào)用刪除方法的delegate了掌动。
6.注意我們?cè)趧?chuàng)建臨時(shí)文件的時(shí)候 盡量不要?jiǎng)?chuàng)建文件夾。
什么意思 比如我的臨時(shí)文件路徑是:
#define DGMyTempPath [[NSHomeDirectory() stringByAppendingPathComponent:@"tmp"] stringByAppendingPathComponent:@"videoTemp.mp4"]
我的意思是盡量不要這樣寫:
#define DGMyTempPath [[NSHomeDirectory() stringByAppendingPathComponent:@"tmp/MediaCahce"] stringByAppendingPathComponent:@"videoTemp.mp4"]
在加一個(gè)文件夾因?yàn)檫@樣寫 創(chuàng)建時(shí)間有點(diǎn)稍微長(zhǎng)桃漾,如果你立刻寫入數(shù)據(jù)可能會(huì)寫入失敗坏匪。
7.關(guān)于AVPlayer 和 AVPlayeritem這個(gè)幾個(gè)觀察屬性的解釋
#define DGPlayerRateKey @"rate"
#define DGPlayerLoadTimeKey @"loadedTimeRanges"
#define DGPlayerBufferEmty @"playbackBufferEmpty"
#define DGPlayerLikelyToKeepUp @"playbackLikelyToKeepUp"
其中:
1.rate = 1.0 并不一定就是播放,怎么才是真正的播放我在代碼中有處理
2.loadedTimeRanges 進(jìn)度 當(dāng)進(jìn)度 = 1 之后不再調(diào)用了撬统,本地播放也不調(diào)用
3.playbackBufferEmpty 說(shuō)明緩存中呢适滓,但是下載緩存的視頻也不準(zhǔn),具體看我的代碼處理
4.playbackLikelyToKeepUp 一般沒(méi)有什么用恋追,我沒(méi)有處理
5.開(kāi)始播放的時(shí)候并不會(huì)進(jìn)rate的這個(gè)觀察者中 當(dāng)暫推炯#或者繼續(xù)才會(huì)調(diào)用。
8.關(guān)于下載隊(duì)列中某點(diǎn)說(shuō)明
其中下載隊(duì)列中的這段代碼:
if (self.requestOffset > 0) {
[request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld", self.requestOffset, self.fileLenth - 1] forHTTPHeaderField:@"Range"];
}
其實(shí)這個(gè)代碼在做斷點(diǎn)續(xù)傳的時(shí)候有用到 他的的意思是告訴請(qǐng)求頭從那塊開(kāi)始下載到哪塊結(jié)束
9.小小的注意點(diǎn)
AVPlayer 并沒(méi)有直接停止的方法 只有暫停的方法
代碼的存放位置
實(shí)話說(shuō) 我這樣給你講了 你可能有的點(diǎn)還是不明白 所以我開(kāi)始就是建議先大致讀一下然后讀代碼苦囱,讀完代碼在來(lái)讀我的文章嗅绸。我的代碼為在:https://github.com/liudiange/DGCachePlayer/tree/master
封裝的sdk的特點(diǎn)
1.外部是看不到avplayer 的,這樣寫的好處是外部不能隨便改播放器
2.功能齊全 撕彤,有如下的功能:
并且像音頻播放中的單曲循環(huán) 當(dāng)你點(diǎn)擊下一曲的時(shí)候我會(huì)通過(guò)你傳遞屬性來(lái)判斷是否是真正的要切換到下一曲
3.有緩存的時(shí)候 緩存進(jìn)度是下載的進(jìn)度鱼鸠,非緩存的情況下是監(jiān)聽(tīng)loadedTimeRanges的進(jìn)度
4.視頻有無(wú)緩存猛拴、音頻有無(wú)緩存以及單獨(dú)的sdk 我都給你封裝好了
5.會(huì)通過(guò)代理方式告訴當(dāng)前的狀態(tài)等等,條件不符合會(huì)斷言告訴你哪里錯(cuò)了蚀狰,判斷充足愉昆,比如我當(dāng)前的狀態(tài)判斷有這么多:
typedef NS_ENUM(NSUInteger,DGCacheVideoState) {
DGCacheVideoStatePlay = 1, // 播放
DGCacheVideoStatePause = 2, // 暫停
DGCacheVideoStateBuffer = 3, // 緩沖
DGCacheVideoStateStop = 4, // 停止
DGCacheVideoStateError = 5, // 錯(cuò)誤
};
6.當(dāng)前在播放音頻或者視頻的時(shí)候 當(dāng)你控制器引用完成之后如果你不讓他繼續(xù)播放需要做停止的操作,我會(huì)自動(dòng)幫你把所有無(wú)用的信息清空
7.添加歌曲列表或者刪除列表我會(huì)做去重判斷麻蹋,根據(jù)id來(lái)進(jìn)行判斷的跛溉。我每個(gè)sdk都有模型 包括歌曲或者視頻的基本信息,你如果要使用可以繼承我的model
8.假如發(fā)現(xiàn)bug或者不足的點(diǎn)可以在我的基礎(chǔ)上修改并且重新打包自己使用扮授。
9.沒(méi)有網(wǎng)絡(luò)什么的并沒(méi)有在sdk中寫芳室,因?yàn)橐话銇?lái)說(shuō)一個(gè)app都會(huì)有檢查網(wǎng)絡(luò)情況的方法
10.在播放緩存音樂(lè)的時(shí)候你會(huì)發(fā)現(xiàn)下載完了再播放 ,其實(shí)我仔細(xì)看了沒(méi)有問(wèn)題刹勃,系統(tǒng)的也是緩存完了才播放堪侯。并且那個(gè)delegate值回調(diào)一次 并不是多次。這個(gè)可能是根據(jù)音頻還有視頻有關(guān)系 或者跟當(dāng)前的音樂(lè)資源有關(guān)系深夯。
11.視頻中可能畫面有點(diǎn)歪抖格, 那個(gè)是約束還有你選的播放模式有關(guān)系 。不是sdk的問(wèn)題咕晋。
12.demo中我沒(méi)有對(duì)那些控件做約束雹拄,建議你在6s的手機(jī)或者模擬器中查看,我就是在此基礎(chǔ)上隨便做的掌呜,因?yàn)榻缑娌⒉皇俏覀兊闹攸c(diǎn)滓玖。
題外話
今天廢話有點(diǎn)多了,這個(gè)東西 耗費(fèi)我20多天的時(shí)間质蕉,當(dāng)然是工作之余势篡。我一步一步弄明白的,如果你覺(jué)得我說(shuō)的不對(duì)或者封裝的sdk不好 歡迎issues me