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

  • 关于FFmpeg的资源网上有很多,但是在iOS平台的FFmpeg入门的资源却很少,刚开始学习的时候也是像闷头苍蝇,周旋了很久,所以很久之前就想出一个可以让新手也可以看懂的,基于iOS的FFmpeg教程了。

  • 看见了优秀的第三方库,总有一种想去探究其如何实现的冲动,半年前公司项目的需求,接触了基于FFmpeg库的Kxmovie这个音视频播放的第三方库,不得不说这个确实是一个很简单实用,代码有简单流畅的库。

  • 接下来,我们来一步一步的解析它,后三篇文章中的代码都是分离出比较精简的代码,所以有时候在逻辑上可能有一些漏洞,希望大家只可以指出我的错误。

  • 关于音视频的基础我就不再啰嗦了,建议去看雷神的博客

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

音视频解码的步骤

  1. 把文件分成视频和音频,初始化解码器
  2. 解码视频和音频
  3. 显示视频和播放音频

接下来我们一步一步的分开解析,今天的第一部分要做的就是:

  • 从文件中分离出视频流,然后初始化解码器

正式开始

以下内容都是基于Kxmovie写的,应该说是对这个第三方库的分解

初始化文件和解码器

- (void)start
{
    _path = [[NSBundle mainBundle] pathForResource:@"cuc_ieschool2" ofType:@"mp4"];
    __weak Aie1Controller * weakSelf = self;
    AieDecoder * decoder = [[AieDecoder alloc] init];
    decoder.delegate = self;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSError * error = nil;
        [decoder openFile:_path error:&error];
        
        __strong Aie1Controller * strongSelf = weakSelf;
        if (strongSelf) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [strongSelf setMovieDecoder:decoder];
            });
        }
    });
}

1. 初始化文件流并分离出视频流

  • 在初始化解码器之前,需要从视频文件中获取视频流,然后使用视频流中获信息去初始化解码器。

1.1 读取文件信息

- (BOOL)openInput:(NSString *)path
{
    AVFormatContext * formatCtx = NULL;
    formatCtx = avformat_alloc_context();
    if (!formatCtx)  {
        NSLog(@"打开文件失败");
        return NO;
    }
    
    if (avformat_open_input(&formatCtx, [path cStringUsingEncoding:NSUTF8StringEncoding], NULL, NULL) < 0)  {
        if (formatCtx) {
            avformat_free_context(formatCtx);
        }
        NSLog(@"打开文件失败");
        return NO;
    }
    
    if (avformat_find_stream_info(formatCtx, NULL) < 0) {
        avformat_close_input(&formatCtx);
        NSLog(@"无法获取流信息");
        return NO;
    }
    
    av_dump_format(formatCtx, 0, [path.lastPathComponent cStringUsingEncoding:NSUTF8StringEncoding], false);
    _formatCtx = formatCtx;
    return YES;
}
  • AVFormatContext 这个结构体很重要(FFmpeg里的结构体都很重要),jump进去,官方的解释是Format I/O context,是一个格式化输入输出的上下文。
  • AVFormatContext方法以及解释
    avformat_alloc_context() 是初始化方法。
    avformat_free_context() 是释放方法。
    avformat_open_input() 是打开文件方法。
    avformat_find_stream_info() 是从文件中获取流信息的方法。
    avformat_close_input() 是关闭输入,然后是释放。那么它和之前的avformat_free_context()有什么区别呢?细心的同学已经已经发现了,如果用打开文件avformat_open_input() 之后那就需要close,如果没有打开,那就直接free。
    av_dump_format() 其实就是一个打印输出输出的一个方法,可有可无。

1.2 打开视频流

// 打开视频流
- (BOOL)openVideoStream
{
    BOOL resual = YES;
    _videoStream = -1;
    _videoStreams = collectStreams(_formatCtx, AVMEDIA_TYPE_VIDEO);
    for (NSNumber * n in _videoStreams) {
        const NSUInteger iStream = n.integerValue;
        
        if (0 == (_formatCtx->streams[iStream]->disposition &
                  AV_DISPOSITION_ATTACHED_PIC)) {
            resual = [self openVideoStream:iStream];
            if (resual) {
                break;
            }
        }
    }
    return YES;
}
  • 这个过程并不重要,接下来看下一步,才是重头戏。

1.3 分离出视频裸流

static NSArray * collectStreams(AVFormatContext * formatCtx, enum AVMediaType codecType)
{
    NSMutableArray * ma = [NSMutableArray array];
    for (NSInteger i = 0; i < formatCtx->nb_streams; i++) {
        if (codecType == formatCtx->streams[i]->codec->codec_type) {
            [ma addObject:[NSNumber numberWithInteger:i]];
        }
    }
    return [ma copy];
}
  • 上上一步我们已经通过AVFormatContext打开了文件,现在这一步我们正式从文件中分离出真正的视频流。看上面这个函数,我们之前传入的第二个参数是AVMEDIA_TYPE_VIDEO,这个就是循环遍历文件中的所有的流,然后从文件流中提取出格式是AVMEDIA_TYPE_VIDEO的视频流,显而易见,音频流就是AVMEDIA_TYPE_AUDIO,其实这方面,音视频是共通的。

2. 初始化视频解码器并计算帧率

2.1 初始化视频解码器

  • 终于到了本文的小高潮了,这一步的参数是怎么来的呢?就是在上一步分离出来的视频流,循环遍历一帧有效的数据流,来初始化解码器,记住只要初始化一次就够了。
- (BOOL)openVideoStream:(NSInteger)videoStream
{
    AVCodecContext * codecCtx = _formatCtx->streams[videoStream]->codec;
    AVCodec * codec = avcodec_find_decoder(codecCtx->codec_id);
    if (!codec) {
        NSLog(@"无法找到解码器");
        return NO;
    }
    
    if (avcodec_open2(codecCtx, codec, NULL) < 0) {
        NSLog(@"打开解码器失败");
        return YES;
    }
    
    _videoFrame = av_frame_alloc();
    if (!_videoFrame) {
        avcodec_close(codecCtx);
        NSLog(@"创建视频帧失败");
        return NO;
    }
    _videoStream = videoStream;
    _videoCodecCtx = codecCtx;
    
    // 计算 fps 帧率
    AVStream * st = _formatCtx->streams[_videoStream];
    avStreamFPSTimeBase(st, 0.04, &_fps, &_videoTimeBase);
    return YES;
}
  • AVCodecContext 官方解释是main external API structure,主要外部API结构体,翻译起来怪怪的。但是雷神曾经说过:‘’AVCodecContext中很多的参数是编码的时候使用的,而不是解码的时候使用的。‘’,那我也可以理解成AVCodecContext结构体主要是存放编解码时候的的参数,到底有什么参数,我们到时候遇到再解释。
  • AVCodec 官方解释是,哦官方没有解释,他们的意思估计是这个不需要解释,你懂的。确实这个结构体里面的信息比其他的结构体少的多,综合的来说。AVCodec是一个储存编解码器信息的结构体,会用就好了。
  • AVFrame 官方解释是This structure describes decoded (raw) audio or video data,意思就是说这个是用来 存储解码的音视频数据(原始的音视频数据,就是YUV,PCM这些没有压缩过的,体积很大的数据) 的结构体。
  • AVStream 官方解释是Stream structure,流结构体,就是之前从文件中取出来的那个文件流,也是音视频流,记录这些流数据中的参数和信息。
  • AVCodecContext AVFrame方法以及解释
    avcodec_find_decoder() 通过对应匹配的编解码器ID找到已经注册的编解码器。
    avcodec_open2() 通过AVCodecContext打开解码器。
    avcodec_close() 通过AVCodecContext关闭解码器,注意了这里的close是释放AVCodecContext结构体的中的所有数据,不是释放AVCodecContext本身。
    av_frame_alloc() 初始化AVFrame
    av_frame_free() 释放AVFrame

2.2 计算FPS帧率

  • 终于到了这一段的结尾了
static void avStreamFPSTimeBase(AVStream *st, CGFloat defaultTimeBase, CGFloat *pFPS, CGFloat *pTimeBase)
{
    CGFloat fps, timebase;
    
    // ffmpeg提供了一个把AVRatioal结构转换成double的函数
    // 默认0.04 意思就是25帧
    if (st->time_base.den && st->time_base.num)
        timebase = av_q2d(st->time_base);
    else if(st->codec->time_base.den && st->codec->time_base.num)
        timebase = av_q2d(st->codec->time_base);
    else
        timebase = defaultTimeBase;
    
    if (st->codec->ticks_per_frame != 1) {  
    }
    
    // 平均帧率
    if (st->avg_frame_rate.den && st->avg_frame_rate.num)
        fps = av_q2d(st->avg_frame_rate);
    else if (st->r_frame_rate.den && st->r_frame_rate.num)
        fps = av_q2d(st->r_frame_rate);
    else
        fps = 1.0 / timebase;
    
    if (pFPS)
        *pFPS = fps;
    if (pTimeBase)
        *pTimeBase = timebase;
}
  • timebase的意思就是播放一帧需要的时间,默认是0.04,也可以说是0.04秒播放一帧,所以帧率就是1/0.04 = 25帧。
  • AVRationalAVCodecContextAVStream结构体中的一个结构里,里面只有两个参数,numden就是分子和分母的意思,两个参数同时存在的时候,才可以求出帧率。
  • av_q2d() 是把AVRatioal中的参数转换成double类型的一个函数,方便我们计算。

结尾

  • 代码会在整个播放器讲完之后再给出完整的。
  • 由于放了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

推荐阅读更多精彩内容