基于iOS平台的最简单的FFmpeg视频播放器(二)

上一篇写了关于FFmpeg的对文件的处理以及初始化解码器,算是为本片做下了很重要的基础,需要看基础的同学还是推荐看雷神的博客

基于iOS平台的最简单的FFmpeg视频播放器(一)
基于iOS平台的最简单的FFmpeg视频播放器(二)
基于iOS平台的最简单的FFmpeg视频播放器(三)
废话不多说,让我们

正式开始

  • 粗略的来概括一下今天的内容,分为两步:
    1.使用上一篇文章初始化的解码器,将原始数据进行解码。
    2.保存解码后的数据到一个数组中。

1.1 热身运动

  • 这是在解码视频之前的热身运动
- (void)setMovieDecoder:(AieDecoder *)decoder
{
    if (decoder) {
        _decoder = decoder;
        _dispatchQueue = dispatch_queue_create("AieMovie", DISPATCH_QUEUE_SERIAL);
        _videoFrames = [NSMutableArray array];
    }
    
    _minBufferedDuration = LOCAL_MIN_BUFFERED_DURATION;
    _maxBufferedDuration = LOCAL_MAX_BUFFERED_DURATION;
    
    if (self.isViewLoaded) {
        [self setupPresentView];
    }
}
  • _dispatchQueue 手动创建的一个串行队列,用于之后解码的线程。
  • _videoFrames 这个是一个用来存储解码后的数据的可变数组。
  • _minBufferedDuration_maxBufferedDuration这两个函数是用来做什么的,我现在来简单解释一下,到后面有相关的代码就了解了。其实就是用来控制是否开始解码的两个参数,当小于_minBufferedDuration的时候,就开始解码,当大于_maxBufferedDuration的时候,就停止解码,当处于两者之间,那就一直解码,不要停。

1.2 再次热身(很重要)

  • 引用马老师的话来说,这段代码真的是,李时珍的皮
- (void)asyncDecodeFrames
{
   __weak Aie1Controller * weakSelf = self;
   __weak AieDecoder * weakDecoder = _decoder;
   
   dispatch_async(_dispatchQueue, ^{

       // 当已经解码的视频总时间大于_maxBufferedDuration 停止解码
       BOOL good = YES;
       while (good) {
           good = NO;
           
           @autoreleasepool {
               __strong AieDecoder * strongDecoder = weakDecoder;
               
               if (strongDecoder) {
                   NSArray * frames = [strongDecoder decodeFrames:0.1];
                   
                   if (frames.count) {
                       __strong Aie1Controller * strongSelf = weakSelf;
                       
                       if (strongSelf) {
                           good = [strongSelf addFrames:frames];
                       }
                   }
               }
           }
       }
   });
}
  • 很多人看这段代码的时候,可能看见__weak , __strong, dispatch_async, while , @autoreleasepool,组合在一起的时候就已经蒙圈了,那现在我们一句句来解释。
  • 一开始我们定义了一个GCD_dispatchQueue,现在就用到了,正因为用到了block,所以我们需要__weak来防止循环引用,__strong是相对应的,因为在block中是一个延时的,持续的操作,所以如果不使用__strong的话,会导致block中的对象被弱引用,而提早释放,所以需要__strong再次对block中的对象强引用。
  • while循环是为了让解码器可以持续的去解码(如果不出现异常情况下),就算跳出了循环,还会有其他的地方调用asyncDecodeFrames,再次进入循环,所以可以一直不停解码。
  • @autoreleasepool自动释放池,有些人会问,iOS项目main函数已经有@autoreleasepool,为什么还要加一个呢,是不是画蛇添足?当然不是,我们看@autoreleasepool中的代码,每一次的解码都会产生一个数组,所以如果不及时释放的话,内存就会一直变大(视频帧的数据量可不是开玩笑的),所以需要在这里加一个@autoreleasepool

2.1 开始解码

  • 终于等到你,本系列文章最重要的篇章
- (NSArray *)decodeFrames:(CGFloat)minDuration
{
    if (_videoStream == -1) {
        return nil;
    }
    
    NSMutableArray * result = [NSMutableArray array];
    AVPacket packet;
    CGFloat decodedDuration = 0;
    BOOL finished = NO;
    
    while (!finished) {
        if (av_read_frame(_formatCtx, &packet) < 0) {
            NSLog(@"读取Frame失败");
            break;
        }
        
        if (packet.stream_index == _videoStream) {

            int pktSize = packet.size;
            while (pktSize > 0) {
                int gotFrame = 0;
                int len = avcodec_decode_video2(_videoCodecCtx, _videoFrame, &gotFrame, &packet);
                if (len < 0) {
                    NSLog(@"解码失败");
                    break;
                }
                
                if (gotFrame) {
                    AieVideoFrame * frame = [self handleVideoFrame];
                    frame.type = AieFrameTypeVideo;
                    NSLog(@"当前帧的时间戳:%f, 当前帧的持续时间:%f", frame.position, frame.duration);
                    
                    if (frame) {
                        [result addObject:frame];
                        
                        _position = frame.position;
                        decodedDuration += frame.duration;
                        if (decodedDuration > minDuration) {
                            finished = YES;
                        }
                    }
                }
                
                if (0 == len) {
                    break;
                }
                pktSize -= len;
            }
        }
        av_free_packet(&packet);
    }
    return result;
}
  • 上面的代码相对来说比较多一些,所以为了可以容易看一点,所以我们再把代码细分一下。
  • 在解析之前我们先要弄清楚几件事情。
    1.从哪里来?
    2.怎么来?
    3.来干嘛?
    4.到哪里去?
    5.怎么去?

2.1.1 从哪里来?

AVPacket packet;
  • 数据就存在AVPacket里面,是解码前的数据,压缩过的数据。
  • AVPacket官方解释是This structure stores compressed data. It is typically exported by demuxers and then passed as input to decoders, or received as output from encoders and then passed to muxers.,这次的解释相对来说比较长,说明这个是一个很重要的机构体。大致意思就是说这是一个用来存储压缩数据以及相关信息的机构体,是一个把数据导入到解码器的分配器,也是用来接收编码后的数据的结构体。

2.1.2 怎么来?

if (av_read_frame(_formatCtx, &packet) < 0) {
   NSLog(@"读取Frame失败");
   break;
}
  • av_read_frame()作用是读取一帧视频帧或者是多帧音频帧。AVPacket中的数据会一直有效,除非读取到下一帧或者是AVFormatContext中的数据被彻底清空(调用avformat_close_input())。

2.1.3 来干嘛?(这就是最重要的解码)

if (packet.stream_index == _videoStream) {

            int pktSize = packet.size;
            while (pktSize > 0) {
                int gotFrame = 0;
                int len = avcodec_decode_video2(_videoCodecCtx, _videoFrame, &gotFrame, &packet);
                
                if (len < 0) {
                    NSLog(@"解码失败");
                    break;
                }
                
                if (gotFrame) {
                }
                
                if (0 == len) {
                    break;
                }
                pktSize -= len;
                
            }
        }
  • 如果读出的AVPacket中的流的位置和当前这一帧的流的位置相同,那就开始解码。
  • 接下来就开始解码AVPacket中存储的所有的数据,如果解码成功一次就pktSize -= len;减去已经解码过的长度,直到解码完AVPacket中的所有数据,就结束循环。
  • avcodec_decode_video2 ()就是把AVPacket中的视频流数据解码成图片,解码后的数据就存储在之前定义的AVFrame中,返回的是已经被解码的数据的大小(不是解码后的数据大小)。
  • 有兴趣的朋友们可以去看看avcodec_decode_video2 ()的源码,也是很简单易懂的,以后有空可以单独拿出来讲讲。

2.1.4 怎么去?

  • 到现在为止所有的解码都结束了
AieVideoFrame * frame = [self handleVideoFrame];
frame.type = AieFrameTypeVideo;
  • AieVideoFrame是自定义的一个存放解码后数据的类,里面的操作就是把解码后的数据AVFrame按照一定的格式存入自定义的frame,然后再标明它的类型。

2.1.5 去哪里?

if (frame) {
     [result addObject:frame];
     _position = frame.position;
     decodedDuration += frame.duration;
     if (decodedDuration > minDuration) {
         finished = YES;
     }
}
  • 把数据存到之前定义的数组中,然后返回这个数组。
  • 最后几句代码我来解释一下,decodedDuration存的是当前这个一帧中解码后数据的时间的总和,如果这个总和大于minDuration,那就停止解码。在项目中我传入的是0.1,意思就是我这一帧的最大时长是0.1秒,如果帧长度很大,我也只解码0.1秒的数据。

结尾

  • 解码相关的都已经讲完了,AieVideoFrame是关于显示的类,里面的具体操作,到时候一起说。
  • 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
  • 谢谢阅读

推荐阅读更多精彩内容