iOS软解码总结

文章目录

      • iOS 软解码总结
        • 读取数据
        • 解码数据
          • 初始化`AVCodecContext`
          • 寻找I 帧
          • 解码数据

iOS 软解码总结

在iOS 上,开发者可通过VideoToolbox实现硬解码,其具体的步骤可见iOS 硬解码总结。本文讲的是使用FFMpeg 进行视频流的软解码过程,因本demo中包含编译的iOS 下ffmpeg库,demo 较大,仓库已迁移到在gitee上可见。

为方便理解,我把软解码整个过程分为几个步骤,第一是读取,第二是解码,第三是渲染。

读取数据

读取数据我将其分为如下几个步骤:

  1. 注册编解码器
  2. 解析地址/文件,得到AVFormatContext
  3. 解析AVFormatContext,对其内部的AVstream进行赋值,得到数据信息
  4. 数据读取,获取待解码数据AVPacket

具体代码如下:

		// 注册编解码器
        av_register_all();
        AVDictionary     *opts          = NULL;
        av_dict_set(&opts, "timeout", "1000000", 0);
        // 解析地址或文件
        int ret = avformat_open_input(&pFormatContext, path.UTF8String, NULL, &opts);
        if (ret < 0) {
            av_log(NULL, AV_LOG_DEBUG, "avformat_open_input失败");
        }
        // 查找数据信息,按照雷神的说法,该方法其实已经完成了部分解码的过程
        if (avformat_find_stream_info(pFormatContext, &opts) < 0) {
            av_log(NULL, AV_LOG_DEBUG, "没找到数据流信息\n");
        }
        // 查找视频流数据下标. 一个文件或者数据流可能存在音频、视频、字幕流
        for (int i = 0; i < pFormatContext->nb_streams; i++) {
            if (pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                pVideoIndex = i;
                break;
            }
        }
        if (pVideoIndex == -1) {
            av_log(NULL, AV_LOG_ERROR, "数据中不存在视频流\n");
        }

读取指定流中的待解码数据:

    dispatch_async(readQueue, ^{
        while (!self->pStopParse) {
            if (!self->pFormatContext) {
                break;
            }
            AVPacket pkt;
            av_init_packet(&pkt);
            int ret = av_read_frame(self->pFormatContext, &pkt);
            if (ret < 0 || pkt.size < 0) {
                av_log(NULL, AV_LOG_ERROR, "读取数据失败\n");
                self->pStopParse = YES;
                break;
            }
            if (pkt.stream_index == self->pVideoIndex) {
                // 视频数据传递
                if (packet) {
                    packet(pkt);
                }
            }
            av_packet_unref(&pkt);
        }
//        avformat_close_input(&self->pFormatContext);
//        self->pFormatContext = NULL;
    });

解码数据

解码数据的步骤可分为:

  1. 初始化AVCodecContext 得到codec 上下文
  2. 找到第一个I帧
  3. 解码音视频数据
初始化AVCodecContext

较为复杂点是在获取CodecContext上,先上代码

pCodecContext = avcodec_alloc_context3(NULL);
AVCodecParameters *parameters = pFormatContext->streams[pVideoIndex]->codecpar;
ret = avcodec_parameters_to_context(pCodecContext, parameters);
AVCodec *codeC = avcodec_find_decoder(parameters->codec_id);
 if (codeC == NULL) {
            NSLog(@"没有对应的解码器");
  }
 if (avcodec_open2(pCodecContext, codeC, NULL) < 0 ) {
          NSLog(@"创建解码器上下文失败");
  }

上文创建AVCodecContext 分为几个步骤,初始化context,对context内部结构体进行赋值,查找到对应的解码器,使用avcodec_open2 解码器的内部配置。

完成了以上步骤,似乎就只剩下查找I帧和解码的格式操作了,但是有一个关键的点在于使用上面步骤解析出来的AVFrame 默认格式是yuv420p,在使用目前的OpenGLES 是无法完成pixelbuffer 的构建的,需要使用sws_getcontext才能完成图像构建。
所以回过头,我们需要对创建AVCodecContext时的一些参数进行配置,指定其pix_fmt格式。
我查找了资料,和参照了其他人的demo 发现,可以配置ffmpeg 的硬件解码来生成对应的数据格式。所以对以上代码进行配置增加后如下:

        const char *codecName = av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX);
        enum AVHWDeviceType type = av_hwdevice_find_type_by_name(codecName);
        int ret = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
        pCodecContext = avcodec_alloc_context3(NULL);
        ret = avcodec_parameters_to_context(pCodecContext, parameters);
        if (ret < 0) {
            NSLog(@"解码器配置失败");
        }
        // 硬件设备支持的赋值,sws_getcontext 这个类似,但sws——getcontext是成像
        ret = av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0);

通过该配置就保证了数据格式的匹配。

寻找I 帧

寻找i帧很简单,AVPacket 结构体内部的flag字段 为1 时,为keyFrame。其内部代码定义如下:

#define AV_PKT_FLAG_KEY     0x0001 ///< The packet contains a keyframe
#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
解码数据

解码AVPacket 使用的是avcodec_send_packetavcodec_receive_frame 将对应的未解码数据解码成AVFrame 对象。

渲染层面本人了解的还不够详细,就不写了,不然错的太多,引大家入了歧途不好,代码借用快手大神小东邪的渲染代码来完成。

本文参考资料:

雷神:FFMPEG 实现 YUV,RGB各种图像原始数据之间的转换
简书: FFmpeg 示例硬件解码hw_decode

你可能感兴趣的:(音视频开发)