iOS-多媒体开发音视频iOS图像、音频开发

iOS视频播放详解2-封装边下边播的播放器

2017-01-02  本文已影响2807人  sxyxsp123

引言

最近项目中需要一个播放器,并且要对视频进行缓存,那么最好的方式就是边下边播,播完之后如果数据完整就把视频数据保存到硬盘(沙盒中 ),显然用这些方法就不能满足要求了(总不能播完之后再去下载吧,呵呵), 所以就深入研究了一下AVFoundation框架(其实AVFoundation就是一个对多媒体操作的库),这个框架中的东东还是很多很复杂的,因为是相对较为底层的一些东西嘛。下面首先来看看跟我们要实现的播放器有关系的AVFoundation框架中的一些类

AVAsset

AVAsset是一个抽象的基类。由多种AVAssetTrack(音频轨道、字幕轨道、视频轨道等)集合组成,描述了视频内容的静态内容,例如时长创建日期等,不能直接使用,一般我们会使用它的子类AVURLAsset,AVURLAsset实现了AVAsset中的一些方法可以创建对象,一般我们用url对它进行初始化。 在音视频播放中AVURLAsset主要负责链接相关的服务器,从URL中请求音视频数据,下面来看看AVAsset中的一些熟悉:

// 媒体的时长。
@property (nonatomic, readonly) CMTime duration;
// 媒体的默认播放速度。这个值绝大部分时间为1.0。
@property (nonatomic, readonly) float preferredRate;
// 媒体的默认播放音量。这个值绝大部分时间为1.0。
@property (nonatomic, readonly) float preferredVolume;
// 媒体的旋转,缩放,平移量。 The identity transform: [ 1 0 0 1 0 0 ]。
@property (nonatomic, readonly) CGAffineTransform preferredTransform;
// 此资源中包含的所有的AVAssetTrack , AVAsset 可以通过标识符,媒体类型或媒体特征等信息找到相应的track。
@property (nonatomic, readonly) NSArray<AVAssetTrack *> *tracks;
// 包含着当前视频常见格式类型的元数据。
@property (nonatomic, readonly) NSArray<AVMetadataItem *> *commonMetadata;
// 包含当前视频所有格式类型的元数据。
@property (nonatomic, readonly) NSArray<AVMetadataItem *> *metadata;
// 包含当前视频所有可用元数据的格式类型。
@property (nonatomic, readonly) NSArray<NSString *> *availableMetadataFormats;

AVPlayerItem

AVPlayerItem用于统筹数据,用于管理视频的动态内容和在播放资源的呈现状态(即:视频播放着的各种状态如:播放器是否准备好要去播放,数据请求是否失败,本地数据是否已经播放完了等,后面我们即将详细讲解)。用AVURLAsset对它进行初始化, 它就如同MVC中的model。
如果把AVPlayer,AVPlayerLayer,AVPlayerItem按照MVC架构划分的话,我认为,AVPlayer是C,AVPlayerLayer是V,AVPlayerItem是M
下面我们来看看它的一些属性。

//视频的播放状态
@property (nonatomic, readonly) AVPlayerItemStatus status;
//视频的播放时长
@property (nonatomic, readonly) CMTime duration;
//视频缓存区域大小
@property (nonatomic, readonly) NSArray<NSValue *> *loadedTimeRanges;
//callback,缓冲区有足够数据可以播放
@property (nonatomic, readonly, getter=isPlaybackLikelyToKeepUp) BOOL playbackLikelyToKeepUp;
//callback,缓冲区满了
@property (nonatomic, readonly, getter=isPlaybackBufferFull) BOOL playbackBufferFull;
//callback,缓冲区空了,需要等待数据
@property (nonatomic, readonly, getter=isPlaybackBufferEmpty) BOOL playbackBufferEmpty;

以上控制属性在视频播放中很常用,我们通常使用 KVO 来监控视频播放状态和缓冲区的状态

AVPlayer

AVPlayer 是一个不可见组件(不能显示视频画面),其实最主要的功能是对视频进行解码,并对视频播放进行控制,提供了play,pause以及跳动某个时间点开始播放等功能。

AVPlayerLayer

AVPlayerLayer 是构建于 Core Animation 框架之上(注1)的图层类型,扩展了 Core Animation 的 CALayer 类,为 iOS 的视频渲染提供支持,其实它在视频播放中的功能最主要是显示视频数据。

下面我们开始播放器的详解

首先我们的需求是:

根据需求我们的需要实现的功能是:

先看几张图:
                        正常使用AVPlayer`只播放视频`的流程是这样:
正常播放流程

所以我们的方案是

                    但是这样我们得不到播放中缓存的视频数据,所以我们需要这样做:
边下边播,播完将数据写的本地的播放流程
                我们的播放器代码流程是这样:
自定义播放器流程

即:

  1. 当开始播放视频时,通过视频url判断本地cache中是否已经缓存当前视频,如果有,则直接播放本地cache中视频

  2. 如果本地cache中没有视频,则视频播放器向代理请求数据

  3. 加载视频时展示正在加载的提示(菊花转)

  4. 如果可以正常播放视频,则去掉加载提示,播放视频,如果加载失败,去掉加载提示并显示失败提示

  5. 在播放过程中如果由于网络过慢或拖拽原因导致没有播放数据时,要展示加载提示,跳转到第4步

代理对象处理流程
  1. 当视频播放器向代理请求dataRequest时,判断代理是否已经向服务器发起了请求,如果没有,则发起下载整个视频文件的请求

  2. 如果代理已经和服务器建立链接,则判断当前的dataRequest请求的offset是否大于当前已经缓存的文件的offset,如果大于则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向后拖拽,并且超过了已缓存的数据时才会出现)

  3. 如果当前的dataRequest请求的offset小于已经缓存的文件的offset,同时大于代理向服务器请求的range的offset,说明有一部分已经缓存的数据可以传给播放器,则将这部分数据返回给播放器(此时应该是由于播放器向前拖拽,请求的数据已经缓存过才会出现)

  4. 如果当前的dataRequest请求的offset小于代理向服务器请求的range的offset,则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是由于播放器向前拖拽,并且超过了已缓存的数据时才会出现)

  5. 只要代理重新向服务器发起请求,就会导致缓存的数据不连续,则加载结束后不用将缓存的数据放入本地cache

  6. 如果代理和服务器的链接超时,重试一次,如果还是错误则通知播放器网络错误

  7. 如果服务器返回其他错误,则代理通知播放器网络错误

自此实现原理及其业务逻辑已经讲完,下面我们开始代码实现:

首先我们新建四个类如图:

播放器类

注:SPPlayer类主要处理一些界面上的东西,如播放,暂停,进度显示,监听播放器的各种状态。
SPPlayerProxyServer类就是我们上面所讲的播放器的代理类,它遵守AVAssetResourceLoaderDelegate协议,主要处理来自播放器的数据请求,并将已经请求到的数据实时传给播放器。
SPPlayerRequestTask类主要完成SPPlayerProxyServer类交给它的请求数据指令,完成数据请求,并实时将已经请求到的数据写入缓存并通知给SPPlayerProxyServer类,SPPlayerProxyServer类从缓存中读到数据后给SPPlayer。SPPlayerTool是个单例类主要完成创建文件目录,获取缓存数据文件目录等功能。

具体如下:
self.resouerLoader          = [[SPPlayerProxyServer alloc] init];
        NSURL *playUrl              = [_resouerLoader getSchemeVideoURL:[NSURL URLWithString:urlStr]];
        self.videoUrlAsset             = [AVURLAsset URLAssetWithURL:playUrl options:nil];
        [_videoUrlAsset.resourceLoader setDelegate:_resouerLoader queue:dispatch_get_main_queue()];
        self.playerItem          = [AVPlayerItem playerItemWithAsset:_videoUrlAsset];
        
        if (!self.player) {
            self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
        } else {
            [self.player replaceCurrentItemWithPlayerItem:self.playerItem];
        }
        
        //播放状态通知
    [self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    //监听播放器的下载进度
    [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];
    

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    AVPlayerItem *playerItem = (AVPlayerItem *)object;
    
    if ([keyPath isEqualToString:@"status"]) {
        if ([playerItem status] == AVPlayerStatusReadyToPlay) {
            [self showTime];
            [self.player play];
            [self progressTimer];
            
        } else if ([playerItem status] == AVPlayerStatusFailed || [playerItem status] == AVPlayerStatusUnknown) {
            self.toolView.alpha = 1;
            self.backButton.alpha = 1;
            [self removeShowTime];
            [self.player pause];
            [self removeProgressTimer];
        }
        
    } else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {  //监听播放器的下载进度
        
      //  [self calculateDownloadProgress:playerItem];
        
    } else if ([keyPath isEqualToString:@"playbackBufferEmpty"]) { //监听播放器在缓冲数据的状态
        NSLog(@"缓存数据已经播放完毕,开始下载数据");
        if (playerItem.isPlaybackBufferEmpty) {
            self.state = SPPlayerStateBuffering;
          //  [self bufferingSomeSecond];
        }
    }else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {  //监听播放器的下载进度 即将要播放
    
        
    }
}

- (NSURL *)getSchemeVideoURL:(NSURL *)url{
    // NSURLComponents用来替代NSMutableURL,可以readwrite修改URL
    // AVAssetResourceLoader通过你提供的委托对象去调节AVURLAsset所需要的加载资源。
    // 而很重要的一点是,AVAssetResourceLoader仅在AVURLAsset不知道如何去加载这个URL资源时才会被调用
    // 就是说你提供的委托对象在AVURLAsset不知道如何加载资源时才会得到调用。
    // 所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频URL地址的scheme替换为系统不能识别的scheme
    
    NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    components.scheme = @"systemCannotRecognition";
    
    return [components URL];
}
#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{
    
    [self.pendingRequests addObject:loadingRequest];
    
    [self dealWithLoadingRequest:loadingRequest];
    
    return YES;
}

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{

    [self.pendingRequests removeObject:loadingRequest];

}

//处理每一次的播放器数据请求
- (void)dealWithLoadingRequest:(AVAssetResourceLoadingRequest *) loadingRequest{

    NSURL *interceptedURL = [loadingRequest.request URL];
    NSRange range = NSMakeRange((NSUInteger)loadingRequest.dataRequest.currentOffset, NSUIntegerMax);
    
    if (self.task.downLoadingOffset > 0) {
        [self processPendingRequests];
    }
    
    if (!self.task) {
        self.task = [[SPPlayerRequestTask alloc] init];
        self.task.delegate = self;
        [self.task setUrl:interceptedURL offset:0];
    } else {
        // 如果新的rang的起始位置比当前缓存的位置还大300k,则重新按照range请求数据
        if (self.task.offset + self.task.downLoadingOffset + 1024 * 300 < range.location ||
            // 如果往回拖也重新请求
            range.location < self.task.offset) {
            [self.task setUrl:interceptedURL offset:range.location];
        }
    }
}

- (void)setUrl:(NSURL *)url offset:(NSUInteger)offset{

    _url = url;
    _offset = offset;
    [[SPPlayerTool sharedInstance] createFileCachePath];
    
    // 替代NSMutableURL, 可以动态修改scheme
    NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    actualURLComponents.scheme = @"http";
    
    // 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];
   
    _downLoadingOffset = 0;

    // fix offset of request(第二次及其以上发起请求时需要修改range哦)
    if (offset > 0 && self.videoLength > 0) {
        [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
    }
    
    // 重置(取消上次请求)
    [self.session invalidateAndCancel];
    
    // 创建Session,并设置代理
    self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    // 创建会话对象
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];
    
    // 开始下载
    [dataTask resume];

}

// ReceiveData
// 接收到服务器返回数据的时候调用,会调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    
    if (data.length>0) {
        _downLoadingOffset += data.length;
//        [self.outputStream write:data.bytes maxLength:data.length];
        [self.fileHandle seekToEndOfFile];
        [self.fileHandle writeData:data];
        
        // For Test
         NSLog(@"loading ... 正在下载");
        if ([self.delegate respondsToSelector:@selector(requestTask:didReceiveData:downloadOffset:tempFilePath:)]) {
            [self.delegate requestTask:self didReceiveData:data downloadOffset:_downLoadingOffset tempFilePath:[[SPPlayerTool sharedInstance] getFileCachePath]];
        }
    }
}

#pragma mark - SPPlayerRequestTaskDelegate
// 正在下载(传递获取到的数据和下载的偏移量以及临时文件存储路径)
-(void)requestTask:(SPPlayerRequestTask *)requesttask didReceiveData:(NSData *)data downloadOffset:(NSInteger)offset tempFilePath:(NSString *)filePath{

    [self processPendingRequests];

}

- (void)processPendingRequests{

    // 遍历所有的请求, 为每个请求加上请求的数据长度和文件类型等信息.
    // 在判断当前下载完的数据长度中有没有要请求的数据, 如果有,就把这段数据取出来,并且把这段数据填充给请求, 然后关闭这个请求
    // 如果没有, 继续等待下载完成.
    NSMutableArray *requestsCompleted = [NSMutableArray array];
    for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests) {
        
        [self fillInContentInformation:loadingRequest.contentInformationRequest];
        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest];
        
        if (didRespondCompletely) {
            [requestsCompleted addObject:loadingRequest];
            [loadingRequest finishLoading];
        }
    }
    [self.pendingRequests removeObjectsInArray:[requestsCompleted copy]];
}

//将此次下载到的数据传给此次的请求
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{
    
    long long startOffset = dataRequest.requestedOffset;
    if (dataRequest.currentOffset != 0)
        startOffset = dataRequest.currentOffset;
    
    NSData *fileData = [NSData dataWithContentsOfFile:[[SPPlayerTool sharedInstance] getFileCachePath] options:NSDataReadingMappedIfSafe error:nil];
    NSInteger unreadBytes = self.task.downLoadingOffset - self.task.offset - (NSInteger)startOffset;
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
    if (fileData.length != 0) {
        [dataRequest respondWithData:[fileData subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];
    }
    
    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;
    
    return didRespondFully;
}

//此时监听到playerItem的状态为AVPlayerStatusReadyToPlay,开始播放。
 if ([keyPath isEqualToString:@"status"]) {
        if ([playerItem status] == AVPlayerStatusReadyToPlay) {
            [self showTime];
            [self.player play];
            [self progressTimer];
            


-(void)downloadSuccessWithURLSession:(NSURLSession *)session task:(NSURLSessionTask *)task{
   
 //   If download success, then move the complete file from temporary path to cache path
 //   如果下载完成, 就把文件移到缓存文件夹
   if (self.taskArray.count < 2) { //注:如果中间有快进或者快退 则表示下载数据并不完整 就不保存
       NSFileManager *fileManager = [NSFileManager defaultManager];
       if ([fileManager fileExistsAtPath:[[SPPlayerTool sharedInstance] getFileSavePath]]) {
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
               
               [fileManager moveItemAtPath:[[SPPlayerTool sharedInstance] getFileCachePath] toPath:[[SPPlayerTool sharedInstance] getFileSavePath] error:nil];
               dispatch_async(dispatch_get_main_queue(), ^{
                   if ([self.delegate respondsToSelector:@selector(didFinishLoadingWithManager:fileSavePath:)]) {
                       [self.delegate didFinishLoadingWithManager:self fileSavePath:[[SPPlayerTool sharedInstance] getFileSavePath]];
                   }
               });
           });
       }

至此,主要代码已经完成,滑动进度条从某个时间点开始播放,也是通过3--5步骤完成数据请求的,注意还有一点,视频数据请求需要给request设置一个偏移量和长度,所以从第二次请求开始,这个range得计算好。

代码整理一下随后上传

本播放器参考了NewPan夜千寻墨两位大神的博客,在此表示敬意。

上一篇下一篇

猜你喜欢

热点阅读