美文网首页iOS 新技术Android进阶iOS
基于iOS平台的最简单的FFmpeg视频播放器(一)

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

作者: Aiewing | 来源:发表于2017-10-31 11:23 被阅读375次
    • 关于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会很大,下载的时候比较费时。
    • 谢谢阅读

    相关文章

      网友评论

      • 钱湛:大佬,您好!真不错,虽然没看懂!
      • 牛巴粉带走:你好,对于FFmpeg这一块我是个新手,我想问一下,用命令行操作跟像你这样使用c++代码有什么区别?
        Aiewing:其实本质上都是一样的,都可以达到相同的效果,但是代码的适应性更加强一点,命令行更加方便快捷一点,但是有局限性,你可以去看看这个简书,http://www.jianshu.com/p/17c9cc72e09c,里面的Demo也是可用的,或许可以对你有所帮助
      • LLVKS:大佬,你好
        Aiewing:以后多多交流
      • 58fe5465d15f:Mark! 沙发
        Aiewing:谢谢

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

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