擴展: 【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音頻播放 (九):邊播邊緩存