扩展: 【iOS】文件管理NSFileManager、NSFileHandle
项目中集成其他人封装的第三方库,但对于怎么实现缺不清楚,这次趁着有时间自己梳理一遍,目标是自己也封装一个播放器。
文章总共分3篇
01-实现一个简单的播放器
02-实现一个能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 {
// 添加播放进度监控
[self addProgressObserver];
// 添加缓存监听
[self.playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
// 监听缓存不够,视频加载不出来
[self.playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
// 监听缓存足够播放状态
[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 {
// 该方法在卡顿的时候不会回调
__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(@"播放完成");
// 循环重复
[self.player pause];
[self.player seekToTime:kCMTimeZero];
[self.player play];
}
/// 暂停播放
- (void)playerPause {
[self.player pause];
}
/// 播放视频
- (void)playerPlay {
[self.player play];
}
#pragma mark - AVAssetResourceLoaderDelegate
// 一定要设置视频连接URL的scheme设置成自定义的,才会调用此方法
// 要求加载资源的代理方法,返回true表示该代理类现在可以处理该请求,我们需要在这里保存loadingRequest并开启下载数据的任务,下载回调中拿到响应数据后再对loadingRequest进行填充
// 如果返回NO,则表示当前代理下载数据,视频数据需要AVPlayer自己处理(但是之前视频URL的scheme被设置自定义的,所以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预加载状态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 缓存不足就会自动暂停,使用缓存充足了需要手动播放,才能继续播放");
[self.player play];
}
}
#pragma mark - 下载器
- (void)downVideoFileWithURL:(NSURL *)url {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalAndRemoteCacheData;
configuration.networkServiceType = NSURLNetworkServiceTypeVideo;
configuration.allowsCellularAccess = YES;
// cachePolicy 缓存策略
// NSURLRequestReloadIgnoringCacheData 每次都从网络加载
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];
// 设置请求体类型
// [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
// 设置请求方式
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(@"已下载数据 %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(@"视频内存大小:%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 是否请求完成 及 填充下载数据到dataRequest
/// @param loadingRequest loadingRequest
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
// 填充请求
// 将NSURLSession请求返回的Response中视频格式以及视频长度 塞给播放器
// 因为AVAssetResourceLoadingRequest在调用finishLoading的时候,会根据contentInformationRequest中信息去判断接下来要怎么处理,
// 比如获取的文件content-Type是系统不支持的类型,则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数据片段mediaData,判断有哪些AVAssetResourceLoadingRequest负责的小片段是包括在NSURLSession下载mediaData区域内,
如果是在mediaData区域内,则表示AVAssetResourceLoadingRequest请求已经下载完,调用finishLoading
*/
// 判断当前缓存数据量是否大于请求偏移量
NSData *dataUnwrapped = self.mediaData;
if (dataUnwrapped.length < startOffset) {
return NO;
}
// 计算还未装载到缓存数据
NSUInteger unreadBytes = dataUnwrapped.length - startOffset;
// 判断当前请求到的数据大小
NSUInteger numberOfBytesToResourceWidth = MIN(unreadBytes, requestLength);
// 将缓存数据的指定片段装载到视频加载请求中
[loadingRequest.dataRequest respondWithData:[dataUnwrapped subdataWithRange:NSMakeRange(startOffset, numberOfBytesToResourceWidth)]];
// 计算装载完毕后的数据偏移量
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详解系列(一)参数设置
2、 可能是目前最好的 AVPlayer 音视频缓存方案
3、 AVPlayer 边下边播与最佳实践
4、 iOS AVPlayer 视频缓存的设计与实现
5、 AVPlayer初体验之边下边播与视频缓存
6、 唱吧 iOS 音视频缓存处理框架
7、 基于AVPlayer封装的播放器细节
8、 iOS音频播放 (九):边播边缓存
网友评论