ffmpeg播放器8

93 阅读8分钟

1背景

前一篇分析到了从媒体文件获取视频流,这一篇继续后续的分析。

2步骤

hffplayer open后续

2.1 获取视频源文件编码

    AVCodecParameters* codec_param = video_stream->codecpar;
    hlogi("codec_id=%d:%s", codec_param->codec_id, avcodec_get_name(codec_param->codec_id));

2.2 解码器

    AVCodec* codec = NULL;
    if (decode_mode != SOFTWARE_DECODE) {
try_hardware_decode:
        std::string decoder(avcodec_get_name(codec_param->codec_id));
        if (decode_mode == HARDWARE_DECODE_CUVID) {
            decoder += "_cuvid";
            real_decode_mode = HARDWARE_DECODE_CUVID;
        }
        else if (decode_mode == HARDWARE_DECODE_QSV) {
            decoder += "_qsv";
            real_decode_mode = HARDWARE_DECODE_QSV;
        }
        codec = avcodec_find_decoder_by_name(decoder.c_str());
        if (codec == NULL) {
            hlogi("Can not find decoder %s", decoder.c_str());
            // goto try_software_decode;
        }
        hlogi("decoder=%s", decoder.c_str());
    }

    if (codec == NULL) {
try_software_decode:
        codec = avcodec_find_decoder(codec_param->codec_id);
        if (codec == NULL) {
            hloge("Can not find decoder %s", avcodec_get_name(codec_param->codec_id));
            ret = -30;
            return ret;
        }
        real_decode_mode = SOFTWARE_DECODE;
    }

    hlogi("codec_name: %s=>%s", codec->name, codec->long_name);

上面使用了goto的标签,硬件解码失败用软件编码。。软件解码失败就没继续调回硬件解码,怕死循环吧

2.3 解码器上下文

    codec_ctx = avcodec_alloc_context3(codec);
    if (codec_ctx == NULL) {
        hloge("avcodec_alloc_context3");
        ret = -40;
        return ret;
    }
    defer (if (ret != 0 && codec_ctx) {avcodec_free_context(&codec_ctx); codec_ctx = NULL;})

实例化,实例化失败则释放对象

2.4 配置解码上下文的解码参数

    ret = avcodec_parameters_to_context(codec_ctx, codec_param);
    if (ret != 0) {
        hloge("avcodec_parameters_to_context error: %d", ret);
        return ret;
    }

2.5 配置引用计数

    if (codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO || codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) {
        av_dict_set(&codec_opts, "refcounted_frames", "1", 0);
    }

为视频或音频编解码器启用  AVFrame  的引用计数功能,通过共享数据缓冲区减少内存拷贝,提升性能。

优化场景

  • 减少内存拷贝:对于视频流(尤其是高分辨率、高帧率)或长音频流,避免重复拷贝数据缓冲区可以显著降低内存占用和 CPU 开销。

  • 高效数据共享:在滤镜处理(filter graph)、多路输出或帧缓存场景中,允许多个组件共享同一份数据,提升性能。

启用引用计数:当  refcounted_frames=1  时,解码器输出的  AVFrame  会启用引用计数(reference counting) 机制。

  • AVFrame  是 FFmpeg 中存储原始音视频数据的基本结构(如解码后的 YUV 图像或 PCM 音频数据)。

  • 引用计数允许多个  AVFrame  共享同一块数据缓冲区( data  字段),通过增减引用计数管理内存生命周期,避免重复拷贝。

默认行为对比

  • 未启用时( refcounted_frames=0 ):每次调用  avcodec_receive_frame()  返回的  AVFrame  会复制一份独立的数据缓冲区,即使数据内容相同,也会产生内存冗余。

  • 启用后( refcounted_frames=1 ):多个  AVFrame  可以共享同一块数据缓冲区,仅通过引用计数跟踪使用情况。当引用计数归零时,自动释放内存。

2.6 解码

    ret = avcodec_open2(codec_ctx, codec, &codec_opts);
    if (ret != 0) {
        if (real_decode_mode != SOFTWARE_DECODE) {
            hlogi("Can not open hardwrae codec error: %d, try software codec.", ret);
            goto try_software_decode;
        }
        hloge("Can not open software codec error: %d", ret);
        return ret;
    }

配置硬件解码,但是解码失败,继续软件解码

2.7 原始视频宽高以及像素格式

    int sw, sh, dw, dh;
    sw = codec_ctx->width;
    sh = codec_ctx->height;
    src_pix_fmt = codec_ctx->pix_fmt;
    hlogi("sw=%d sh=%d src_pix_fmt=%d:%s", sw, sh, src_pix_fmt, av_get_pix_fmt_name(src_pix_fmt));
    if (sw <= 0 || sh <= 0 || src_pix_fmt == AV_PIX_FMT_NONE) {
        hloge("Codec parameters wrong!");
        ret = -45;
        return ret;
    }
像素格式(Pixel Format):定义了视频帧中像素的存储方式,包括颜色空间(如 YUV、RGB)、色彩深度(如 8-bit、10-bit)、平面排列方式(如平面 YUV、打包 YUV)等。

常见示例:

 - AV_PIX_FMT_YUV420P :平面 YUV 4:2:0 格式(3 个独立平面存储 Y、U、V 分量)。

 - AV_PIX_FMT_NV12 :半平面 YUV 4:2:0 格式(Y 平面 + UV 交错平面)。

 - AV_PIX_FMT_RGB24 :打包的 RGB 格式(每个像素按 R、G、B 顺序存储)。

2.8 视频编码压缩

需要编解码的视频图像一般不使用RGB色彩空间,而是使用一种称为YUV的色彩空间。

YUV也是一种颜色编码方法,“Y”表示明亮度,“U”和“V”则分别表示色度和浓度。

不同于RGB图像一般按像素存储(如RGBRGBRGBRGB),YUV图像一般按平面存储,即将所有的Y放到一起,所有的U放到一起,所有的V放在一起 (YYYYUUUUVVVV),其中每一部分称为一个平面。

在这种编码算法下,如果编码后一帧的数据丢失,则会影响后面的解码,如果强行解码,就会出现花屏等现象(因为部分图像间的差异信息找不到了)。

在实际的编码器上,一般会对图像分组,分组后的图像称为GoP(Group of Pictures)。

这种不依赖前后图像、可单独编解码的图像一般被称为I帧,因此整个GoP序列的第1帧也被称为关键帧。

P帧(前向预测编码图像帧),它会参考前面的图像,仅对差异部分编码;

B帧(双向预测编码图像帧)它不仅参考前面的帧,还参考后面的帧,压缩率更高,可以节省更多带宽和存储空间,常用于视频文件的存储。由于B帧需要参考后面的帧,收到B帧后不能立即解码,在实时音视频应用中会带来延迟,因而在实时通信中一般不使用B帧。

2.9 目标视频宽高以及像素格式

    dw = sw >> 2 << 2; // align = 4
    dh = sh;
    dst_pix_fmt = AV_PIX_FMT_YUV420P;
    std::string str = g_confile->GetValue("dst_pix_fmt", "video");
    if (!str.empty()) {
        if (strcmp(str.c_str(), "YUV") == 0) {
            dst_pix_fmt = AV_PIX_FMT_YUV420P;
        }
        else if (strcmp(str.c_str(), "RGB") == 0) {
            dst_pix_fmt = AV_PIX_FMT_BGR24;
        }
    }
    hlogi("dw=%d dh=%d dst_pix_fmt=%d:%s", dw, dh, dst_pix_fmt, av_get_pix_fmt_name(dst_pix_fmt));

 dw = sw >> 2 << 2; // align = 4  这行代码的作用是:将  sw  的值向下取整到最近的 4 的倍数,并将结果赋值给  dw 。它本质是通过位运算实现的对齐操作。

  • 除2再乘2,没有按位操作快

 dw = sw & (~3);  这行代码的作用是 将  sw  的数值向下取整到最近的 4 的倍数,其核心原理是通过位运算清零最低两位,实现高效的数值对齐。    以下是详细解释:  将  sw  与  ~3  进行按位与运算时, sw  的最低两位会被强制清零,而高位保持不变。

0111 (7)  
&  
1100 (~3)  
-----  
0100 (4)  

dw  被对齐到 4 的倍数( dw = sw >> 2 << 2 ),这是为了满足 视频处理中对内存对齐的要求,尤其是在与 像素格式转换(YUV420P/RGB24)和编解码 相关的场景中需要遵循的对齐规则。以下是具体原因:

**** YUV420P 格式的存储特性

YUV420P (平面 YUV 4:2:0)是一种常见的视频像素格式,其存储特点如下:

  1. 分平面存储:Y(亮度)、U(色度)、V(色度)三个分量分别存储在独立的平面中。
  2. 分辨率对齐要求:每个平面的宽高通常是原始分辨率的一半(尤其是 U 和 V 分量),但这些分量的宽高必须是偶数,且可能要求对齐到 2/4/16 等值,以保证内存访问的效率和正确性。

编解码器和过滤器的硬性要求

某些编解码器(如 H.264/H.265)或图像滤波器(如  sws_scale )在内部可能要求输入的分辨率满足特定的对齐条件。例如:

  • FFmpeg 的  sws_scale  函数:用于图像缩放和格式转换,默认可能要求宽度对齐到 16 字节(但对齐到 4 是常见的最小要求)。

  • 硬件编码器(如 NVIDIA NVENC):可能强制要求分辨率对齐到偶数或 4 的倍数。

未对齐的分辨率可能导致以下问题:

  • 编码错误:编解码器拒绝处理非对齐的分辨率。
  • 数据损坏:U/V 分量地址计算错误,导致色度信息错位。

内存访问效率 现代 CPU/GPU 对内存的访问通常需要地址对齐(例如 16 字节对齐),尤其是多媒体指令集(如 SSE/AVX、NEON)在加速像素操作时,要求数据地址对齐到特定边界。对齐可避免非对齐访问带来的性能损失或错误。

为啥只处理了sw宽

  • 调用  sws_scale (格式转换/缩放)或编码器时,FFmpeg 可能自动修正高度对齐。
  • 原始高度已满足对齐要求

是否要处理高取决于像素格式和编解码器

  • YUV420P:宽度和高度均需对齐到 偶数(因为色度分量的分辨率是亮度的一半)。
  • RGB24:无强制对齐要求,但行对齐到 4 字节可提升性能。
  • 硬件编码器:可能要求高度为偶数或 4 的倍数(如 NVIDIA NVENC)。

2.10 图像缩放以及像素格式转换

    sws_ctx = sws_getContext(sw, sh, src_pix_fmt, dw, dh, dst_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL);
    if (sws_ctx == NULL) {
        hloge("sws_getContext");
        ret = -50;
        return ret;
    }

2.11 未完待续