基于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会很大,下载的时候比较费时。
  • 谢谢阅读

你可能感兴趣的:(基于iOS平台的最简单的FFmpeg视频播放器(一))