基于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会很大,下载的时候比较费时。
  • 谢谢阅读
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容