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)是一种常见的视频像素格式,其存储特点如下:
- 分平面存储:Y(亮度)、U(色度)、V(色度)三个分量分别存储在独立的平面中。
- 分辨率对齐要求:每个平面的宽高通常是原始分辨率的一半(尤其是 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;
}