美文网首页AVPlayer 音视频学习iOS技术iOS开发
基于iOS平台的最简单的FFmpeg视频播放器(二)

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

作者: Aiewing | 来源:发表于2017-10-31 16:46 被阅读335次

    上一篇写了关于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会很大,下载的时候比较费时。
    • 谢谢阅读

    相关文章

      网友评论

        本文标题:基于iOS平台的最简单的FFmpeg视频播放器(二)

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