美文网首页
[iOS] AVPlayer边播边缓存-01

[iOS] AVPlayer边播边缓存-01

作者: MR_詹 | 来源:发表于2020-04-17 10:56 被阅读0次

扩展: 【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音频播放 (九):边播边缓存

相关文章

网友评论

      本文标题:[iOS] AVPlayer边播边缓存-01

      本文链接:https://www.haomeiwen.com/subject/qtwcmhtx.html