[iOS] AVPlayer邊播邊緩存-01

擴展: 【iOS】文件管理NSFileManager、NSFileHandle

項目中集成其他人封裝的第三方庫秩命,但對于怎么實現(xiàn)缺不清楚弃锐,這次趁著有時間自己梳理一遍,目標是自己也封裝一個播放器剧蚣。
文章總共分3篇
01-實現(xiàn)一個簡單的播放器
02-實現(xiàn)一個能seek的播放器
03-將播放器封裝

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
#import <CoreServices/CoreServices.h>

@interface ViewController ()<AVAssetResourceLoaderDelegate,NSURLSessionDataDelegate>
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) AVPlayerLayer *playerLayer;
@property (nonatomic, strong) AVURLAsset *urlAsset;
@property (nonatomic, strong) AVPlayerItem *playerItem;


@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSURLResponse *response;

@property (nonatomic, copy  ) NSString *mimeType;   // 資源格式
@property (nonatomic, assign) long  long expectedContentLength; // 資源大小
@property (nonatomic, copy  ) NSString *sourceScheme;   // 視頻路徑scheme
@property (nonatomic, strong) NSMutableArray <AVAssetResourceLoadingRequest *> *requestsArray;
@property (nonatomic, strong) NSMutableData *mediaData;
@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor blackColor];
    
    _requestsArray = [NSMutableArray array];
    _mediaData = [NSMutableData data];
    
    
    NSURL *videoUrl = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2019/03/18/mp4/190318231014076505.mp4"];
    NSURLComponents *components = [[NSURLComponents alloc]initWithURL:videoUrl resolvingAgainstBaseURL:NO];
    self.sourceScheme = components.scheme;
    components.scheme = @"scheme";
    
    _urlAsset = [AVURLAsset URLAssetWithURL:components.URL options:nil];
    [_urlAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];

    _playerItem = [AVPlayerItem playerItemWithAsset:self.urlAsset];
    [_playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

    _player = [[AVPlayer alloc]initWithPlayerItem:self.playerItem];
    _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
    _playerLayer.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
    [self.view.layer addSublayer:_playerLayer];
    
    [self addObserver];
}


- (void)addObserver {
    // 添加播放進度監(jiān)控
    [self addProgressObserver];
    // 添加緩存監(jiān)聽
    [self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    // 監(jiān)聽緩存不夠,視頻加載不出來
    [self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    // 監(jiān)聽緩存足夠播放狀態(tài)
    [self.playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
    
    /*
     //聲音被打斷的通知(電話打來)
     AVAudioSessionInterruptionNotification
     //耳機插入和拔出的通知
     AVAudioSessionRouteChangeNotification
     
     //播放完成
     AVPlayerItemDidPlayToEndTimeNotification
     //播放失敗
     AVPlayerItemFailedToPlayToEndTimeNotification
     //異常中斷
     AVPlayerItemPlaybackStalledNotification
     */
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerFinish) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    
    /*
     //進入后臺
     UIApplicationWillResignActiveNotification
     //返回前臺
     UIApplicationDidBecomeActiveNotification
     */
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerPlay) name:UIApplicationDidBecomeActiveNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerPause) name:UIApplicationWillResignActiveNotification object:nil];
}


- (void)addProgressObserver {
    // 該方法在卡頓的時候不會回調(diào)
    __weak __typeof(self) wself = self;
    [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        if (wself.playerItem.status == AVPlayerItemStatusReadyToPlay) {
            AVPlayerItem *currentItem = wself.player.currentItem;
            // 當前播放時間
            float currentTime = currentItem.currentTime.value/currentItem.currentTime.timescale;
            // 視頻總長
            float totalTime = CMTimeGetSeconds(currentItem.asset.duration);
            
            NSLog(@"%f ===== %f",totalTime,currentTime);
        }
    }];
}

/// 播放完成
- (void)playerFinish {
    NSLog(@"播放完成");
    // 循環(huán)重復
    [self.player pause];
    [self.player seekToTime:kCMTimeZero];
    [self.player play];
}

/// 暫停播放
- (void)playerPause {
    [self.player pause];
}

/// 播放視頻
- (void)playerPlay {
    [self.player play];
}


#pragma mark - AVAssetResourceLoaderDelegate
// 一定要設(shè)置視頻連接URL的scheme設(shè)置成自定義的,才會調(diào)用此方法
// 要求加載資源的代理方法扎运,返回true表示該代理類現(xiàn)在可以處理該請求,我們需要在這里保存loadingRequest并開啟下載數(shù)據(jù)的任務(wù)测蹲,下載回調(diào)中拿到響應(yīng)數(shù)據(jù)后再對loadingRequest進行填充
// 如果返回NO扣甲,則表示當前代理下載數(shù)據(jù)齿椅,視頻數(shù)據(jù)需要AVPlayer自己處理(但是之前視頻URL的scheme被設(shè)置自定義的,所以AVPlayer不能識別示辈,最后導致 AVPlayerItemStatusFailed)
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    static int i=0;
    if (self.sourceScheme && i==0) {
        NSURLComponents *components = [[NSURLComponents alloc]initWithURL:[NSURL URLWithString:loadingRequest.request.URL.absoluteString] resolvingAgainstBaseURL:NO];
        components.scheme = self.sourceScheme;
        [self downVideoFileWithURL:components.URL];
    }
    
    [_requestsArray addObject:loadingRequest];
    
    NSLog(@"======== %@",loadingRequest.request.URL);
    
    i++;
    return YES;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
    NSLog(@"didCancelLoadingRequest");
    [_requestsArray removeObject:loadingRequest];
}


#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"status"]) {
        switch (self.playerItem.status) {
            case AVPlayerItemStatusUnknown: {
                NSLog(@"AVPlayerItemStatusUnknown");
            }
                break;
            case AVPlayerItemStatusReadyToPlay: {
                // 此方法可以在視頻未播放的時候矾麻,獲取視頻的總時長(備注:一定要在AVPlayer預加載狀態(tài)status是AVPlayerItemStatusReadyToPlay才能獲取)
                // NSLog(@"total %f",CMTimeGetSeconds(self.playerItem.asset.duration));
                [self.player play];
                NSLog(@"AVPlayerItemStatusReadyToPlay");
            }
                break;
            case AVPlayerItemStatusFailed: {
                NSLog(@"AVPlayerItemStatusFailed");
            }
                break;
            default:
                break;
        }
    }
    else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        NSArray *array = self.playerItem.loadedTimeRanges;
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
        float startSeconds = CMTimeGetSeconds(timeRange.start);
        float durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval totalBuffer = startSeconds + durationSeconds;
        NSLog(@"當前緩沖時間:%f",totalBuffer);
    }
    else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) {
//        NSLog(@"緩存不夠险耀,視頻加載未能播放");
    }
    else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
//        NSLog(@"由于 AVPlayer 緩存不足就會自動暫停玖喘,使用緩存充足了需要手動播放累奈,才能繼續(xù)播放");
        [self.player play];
    }
}


#pragma mark - 下載器
- (void)downVideoFileWithURL:(NSURL *)url {
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
    configuration.networkServiceType = NSURLNetworkServiceTypeVideo;
    configuration.allowsCellularAccess = YES;

    // cachePolicy 緩存策略
    // NSURLRequestReloadIgnoringCacheData 每次都從網(wǎng)絡(luò)加載
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];
    // 設(shè)置請求體類型
//    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    // 設(shè)置請求方式
    request.HTTPMethod = @"GET";
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
    [dataTask resume];
    self.dataTask = dataTask;
}



#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.mediaData appendData:data];
    [self processPendingRequests];
    NSLog(@"已下載數(shù)據(jù) %f M    當前下載 %f M",self.mediaData.length/1024.0f/1024.0f,data.length/1024.0f/1024.0f);
}


- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    completionHandler(NSURLSessionResponseAllow);
    self.mimeType = response.MIMEType;
    self.expectedContentLength = response.expectedContentLength;
    NSLog(@"視頻內(nèi)存大信烀健:%f M",response.expectedContentLength/1024.0f/1024.0f);
}



- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {

}



- (void)processPendingRequests {
    NSMutableArray *requestCompleted = [NSMutableArray array];
    [self.requestsArray enumerateObjectsUsingBlock:^(AVAssetResourceLoadingRequest * _Nonnull loadingRequest, NSUInteger idx, BOOL * _Nonnull stop) {
        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest];
        if (didRespondCompletely) {
            [requestCompleted addObject:loadingRequest];
            [loadingRequest finishLoading];
        }
        
    }];
    // 移除所有已完成 AVAssetResourceLoadingRequest
    [self.requestsArray removeObjectsInArray:requestCompleted];
}




/// 判斷 AVAssetResourceLoadingRequest 是否請求完成 及 填充下載數(shù)據(jù)到dataRequest
/// @param loadingRequest loadingRequest
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
    // 填充請求
    // 將NSURLSession請求返回的Response中視頻格式以及視頻長度 塞給播放器
    // 因為AVAssetResourceLoadingRequest在調(diào)用finishLoading的時候戒努,會根據(jù)contentInformationRequest中信息去判斷接下來要怎么處理,
    // 比如獲取的文件content-Type是系統(tǒng)不支持的類型冬三,則AVURLAsset將會無法正常播放
    loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;    // 是否支持分片請求
    loadingRequest.contentInformationRequest.contentType = self.mimeType;
    loadingRequest.contentInformationRequest.contentLength = self.expectedContentLength;
    
    
    NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
    NSUInteger requestLength = loadingRequest.dataRequest.requestedLength;
    NSUInteger currentOffset = loadingRequest.dataRequest.currentOffset;
    
    // AVAssetResourceLoadingRequest請求偏移量
    long long startOffset = requestedOffset;
    if (currentOffset != 0) {
        startOffset = currentOffset;
    }
    
    /**
        解析:
        AVPlayer是”分片“下載策略勾笆,也就是一個視頻是通過若多個AVAssetResourceLoadingRequest下載桥滨,
        每一個AVAssetResourceLoadingRequest負責下載小片段的視頻
        而通過對比我們自定義的下載器NSURLSession數(shù)據(jù)片段mediaData弛车,判斷有哪些AVAssetResourceLoadingRequest負責的小片段是包括在NSURLSession下載mediaData區(qū)域內(nèi)蒲每,
        如果是在mediaData區(qū)域內(nèi),則表示AVAssetResourceLoadingRequest請求已經(jīng)下載完贫奠,調(diào)用finishLoading
     */
    
    
    // 判斷當前緩存數(shù)據(jù)量是否大于請求偏移量
    NSData *dataUnwrapped = self.mediaData;
    if (dataUnwrapped.length < startOffset) {
        return NO;
    }
    
    // 計算還未裝載到緩存數(shù)據(jù)
    NSUInteger unreadBytes = dataUnwrapped.length - startOffset;
    // 判斷當前請求到的數(shù)據(jù)大小
    NSUInteger numberOfBytesToResourceWidth = MIN(unreadBytes, requestLength);
    // 將緩存數(shù)據(jù)的指定片段裝載到視頻加載請求中
    [loadingRequest.dataRequest respondWithData:[dataUnwrapped subdataWithRange:NSMakeRange(startOffset, numberOfBytesToResourceWidth)]];
    // 計算裝載完畢后的數(shù)據(jù)偏移量
    long long endOffset = startOffset + loadingRequest.dataRequest.requestedLength;
    // 判斷請求是完成
    BOOL didRespondFully = dataUnwrapped.length >= endOffset;
    
    return didRespondFully;
}



- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
    [self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    [self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
}

@end

[DEMO](鏈接:https://pan.baidu.com/s/10yFGRjzqyBsuO1SYx6Z3JA 密碼:bkig)

參考文章:
1唤崭、 AVPlayer詳解系列(一)參數(shù)設(shè)置
2脖律、 可能是目前最好的 AVPlayer 音視頻緩存方案
3小泉、 AVPlayer 邊下邊播與最佳實踐
4、 iOS AVPlayer 視頻緩存的設(shè)計與實現(xiàn)
5眯分、 AVPlayer初體驗之邊下邊播與視頻緩存
6柒桑、 唱吧 iOS 音視頻緩存處理框架
7、 基于AVPlayer封裝的播放器細節(jié)
8飘诗、 iOS音頻播放 (九):邊播邊緩存

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末界逛,一起剝皮案震驚了整個濱河市息拜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌喳瓣,老刑警劉巖赞别,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異惠毁,居然都是意外死亡,警方通過查閱死者的電腦和手機腰埂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門屿笼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丈挟,“玉大人志电,你說我怎么就攤上這事±欤” “怎么了鱼蝉?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渔隶。 經(jīng)常有香客問我洁奈,道長利术,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任被冒,我火速辦了婚禮轮蜕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘幔戏。我一直安慰自己税课,他們只是感情好痊剖,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布陆馁。 她就那樣靜靜地躺著合愈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪益老。 梳的紋絲不亂的頭發(fā)上寸莫,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天膘茎,我揣著相機與錄音,去河邊找鬼披坏。 笑死棒拂,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的帚屉。 我是一名探鬼主播涮阔,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼掰邢!你這毒婦竟也來了伟阔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤怀估,失蹤者是張志新(化名)和其女友劉穎多搀,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體康铭,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡从藤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了懊蒸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悯搔。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡鳖孤,死狀恐怖抡笼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情平匈,我是刑警寧澤藏古,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布拧晕,位于F島的核電站,受9級特大地震影響厂捞,放射性物質(zhì)發(fā)生泄漏靡馁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一赔嚎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侠畔,春花似錦袄膏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至兽狭,卻和暖如春鹿蜀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颠焦。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工往枣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人圾另。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓集乔,卻偏偏與公主長得像坡椒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子幼衰,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

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

  • 關(guān)于邊下邊播功能目前流傳的版本大體相同渡嚣,本篇文章主要介紹另一種可行的實現(xiàn)方式。 關(guān)于AVPlayer在這里我們不做...
    seej閱讀 3,862評論 0 7
  • 本片為轉(zhuǎn)載內(nèi)容绝葡,主要是以后自己看起來方便一些原文地址: iOS音視頻實現(xiàn)邊下載邊播放其實音視頻本地緩存的思想都差不...
    白極翁閱讀 5,319評論 1 19
  • 早在放暑假前藏畅,防溺水工作就緊鑼密鼓的進行著功咒,學校三令五申,老師苦口婆心力奋,可是酷暑難耐景殷,孩子們還是架不住誘...
    打碎的水閱讀 194評論 0 0
  • 個人專欄丨湖南詩人王忠平 荷影婆娑(組詩) 荷池蛙鳴 從藕的根部開始 把冬眠的荷搖醒 穿越蓮葉的雨滴掀起點點漣漪 ...
    糖點什么閱讀 365評論 1 1
  • 前言 2016猿挚,是多事的一年。好多事情蜂擁而至绩蜻,讓我措手不及。新的工作踏兜,組建家庭八秃,兒子出生肉盹,在一開始,一切看起來都...
    Eutopia405閱讀 306評論 0 0