我们在尝试瑞芯微的mppencoder时,遇到了麻烦,输出的视频会有一条边。咨询了瑞芯微的工作人员,它们回复说,本质的原因是:
the mpp requires 1920x1080 with 16-aligned strides.(for example 1920x1088)
事实上,前期引入硬件解码器,来企图解决乱码或者绿边时,我们确实是按照这个思路走的。并且在瑞芯微自己的解码测试例程里,我们确实看到了与最终问题解决相关的这一组孪生变量:
width = mpp_frame_get_width(frame); height = mpp_frame_get_height(frame); h_stride = mpp_frame_get_hor_stride(frame); v_stride = mpp_frame_get_ver_stride(frame);
在编码时,我们的目标是要组装正确的符合h_stride, v_stride要求的数据帧,并且要能够把正确的width, height, h_stride, v_stride传入。
然后拿到了瑞芯微自己的编解码示例代码,在这里:
git clone https://github.com/rockchip-linux/mpp.git
对应的mpp_enc的示例代码(mpp_enc_test.c) 中,我们可以看一下这里的参数传递和相关的函数调用过程:
// get paramter from cmd p->width = cmd->width; p->height = cmd->height; p->hor_stride = (cmd->hor_stride) ? (cmd->hor_stride) : (MPP_ALIGN(cmd->width, 16)); p->ver_stride = (cmd->ver_stride) ? (cmd->ver_stride) : (MPP_ALIGN(cmd->height, 16));
这里有一个自动的stride计算过程,如果hor_stride是没有配置,是缺省的0,那么,有一个用来生成正确的,stride 的计算宏,就是在这里,1920x1080转变为1920*1088。
接下来的各个层级的调用中:
p->mdinfo_size = (MPP_ALIGN(p->hor_stride, 64) >> 6) * (MPP_ALIGN(p->ver_stride, 64) >> 6) * 32; p->cam_ctx = camera_source_init(cmd->file_input, 4, p->width, p->height, p->fmt); switch (p->fmt & MPP_FRAME_FMT_MASK) { case MPP_FMT_YUV420SP: case MPP_FMT_YUV420P: { p->frame_size = MPP_ALIGN(p->hor_stride, 64) * MPP_ALIGN(p->ver_stride, 64) * 3 / 2; } break; case MPP_FMT_RGBA8888 : { p->frame_size = MPP_ALIGN(p->hor_stride, 64) * MPP_ALIGN(p->ver_stride, 64); } break; //这段代码是错的。default: { p->frame_size = MPP_ALIGN(p->hor_stride, 64) * MPP_ALIGN(p->ver_stride, 64) * 4; } break; if (MPP_FRAME_FMT_IS_FBC(p->fmt)) { if ((p->fmt & MPP_FRAME_FBC_MASK) == MPP_FRAME_FBC_AFBC_V1) p->header_size = MPP_ALIGN(MPP_ALIGN(p->width, 16) * MPP_ALIGN(p->height, 16) / 16, SZ_4K); framer里可能包含一个header. mpp_enc_cfg_set_s32(cfg, "prep:width", p->width); mpp_enc_cfg_set_s32(cfg, "prep:height", p->height); mpp_enc_cfg_set_s32(cfg, "prep:hor_stride", p->hor_stride); mpp_enc_cfg_set_s32(cfg, "prep:ver_stride", p->ver_stride);- 视频编码, 组装frame的过程:
void *buf = mpp_buffer_get_ptr(p->frm_buf); ret = read_image(buf, p->fp_input, p->width, p->height, p->hor_stride, p->ver_stride, p->fmt); ret = fill_image(buf, p->width, p->height, p->hor_stride, p->ver_stride, p->fmt, p->frame_count); ret = mpp_frame_init(&frame); mpp_frame_set_width(frame, p->width); mpp_frame_set_height(frame, p->height); mpp_frame_set_hor_stride(frame, p->hor_stride); mpp_frame_set_ver_stride(frame, p->ver_stride); mpp_frame_set_fmt(frame, p->fmt); if (p->fp_input && feof(p->fp_input)) mpp_frame_set_buffer(frame, NULL); else if (cam_buf) mpp_frame_set_buffer(frame, cam_buf); else mpp_frame_set_buffer(frame, p->frm_buf)
从上面代码,可以看到mppencording,手搓C代码时,需要传入width, height,h_stride, v_stride,然后用这些参数来初始化与decode对应的frame结构。
接下来,因为瑞芯微在与gstreamer集成时,也必须在gstreamer相应接口中,传入这些变量,我们先看看瑞芯微的提供的gstreamer的encoding接口规约:
>gst-inspect-1.0 | grep encoder
可以打印系统所有的编码器然后使用:
>gst_inspect-1.0 mpph264enc
可以打印h264硬件编码器的信息,我们可以在打印的信息里看到它实现的gstreamer接口:
下一步我们到GstMppH264Enc的地方,看看有没有相应的参数传递接口。然后看看如何在gstreamer配置里添加这些我们已知的配置(width, height, h_stride, v_stride)
查看git的代码,可以看到:stride是计算来的:
#define GST_MPP_VIDEO_INFO_HSTRIDE(i) GST_VIDEO_INFO_PLANE_STRIDE(i, 0)
#define GST_MPP_VIDEO_INFO_VSTRIDE(i) \
(GST_VIDEO_INFO_N_PLANES(i) == 1 ? GST_VIDEO_INFO_HEIGHT(i) : \
(gint) (GST_VIDEO_INFO_PLANE_OFFSET(i, 1) / GST_MPP_VIDEO_INFO_HSTRIDE(i)))
现在的逻辑归结到:
GST_VIDEO_INFO_PLANE_STRIDE
这个宏隶属gstreamer,它是直接取了GstVideoInfo的一个变量,这个定义在这里:
它只是简单的取了(GstVideoInfo*)指向对象的stride对象的值。并且,HStride是直接取了之前设置的值,所以,设置的位置还要靠前。继续按照stride来搜索:
前一个的源头,明显还是同样的源头,后一个源码中展开:
mpp_enc_cfg_set_s32 (self->mpp_cfg, "prep:width", width); mpp_enc_cfg_set_s32 (self->mpp_cfg, "prep:height", height); mpp_enc_cfg_set_s32 (self->mpp_cfg, "prep:hor_stride", GST_MPP_VIDEO_INFO_HSTRIDE (info)); mpp_enc_cfg_set_s32 (self->mpp_cfg, "prep:ver_stride", GST_MPP_VIDEO_INFO_VSTRIDE (info));
仍然是同一个源头。注意目前为止,我们所搜索的数据结构都是源自GstVideoInfo* info。
我们需要定位到它的初始化的部分。这次代码搜索GstVideoInfo*
我们需要在这组搜索结果中,遍历找到那个赋值的位置。
暂时的结论:
GstVideoInfo * stride设置的位置居然不在这里。所以问题的答案在gstreamer的在线文档里,因为GstVideoInfo* info是由前级的缓冲区设置预设好,传过来的。
=>查找GstVideoInfo* info的修改接口。只要能改掉hstride就能达到目的。然后一个便捷的做法,就是直接改mpph264enc,但是不到万不得已,我不会这么做。
查询chatgpt,问是否可以在进入encorder前修改GstVideoInfo*,答案是可以的:
》》》我可以定义一个自定义插件,得到GstVideoInfo对象,然后修改它,然后重新向后传输?
是的,你可以通过自定义 GStreamer 插件来获取和修改
GstVideoInfo
对象,然后将修改后的信息传递到管道的下游。要实现这一点,你可以继承自Gst.BaseTransform
或Gst.BaseFilter
,并在插件中处理视频数据。以下是如何在 Python 中实现这一点的步骤和示例代码。自定义插件的步骤
创建插件类: 创建一个自定义插件类,继承自
Gst.BaseTransform
或Gst.BaseFilter
,并实现必要的方法。获取和修改
GstVideoInfo
: 在transform_frame
方法中,获取帧的GstVideoInfo
对象,修改其属性。重新传输修改后的帧: 将修改后的帧传输到下游元素。
示例代码
下面是一个 Python 示例,展示了如何创建一个自定义插件来获取、修改
GstVideoInfo
对象,并将修改后的帧传递到下游。import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject Gst.init(None) class VideoInfoTransform(Gst.BaseTransform): def __init__(self): super(VideoInfoTransform, self).__init__() def transform_frame(self, frame): # 获取视频信息 video_info = Gst.Video.VideoInfo() if not video_info.from_caps(frame.get_caps()): return Gst.FlowReturn.ERROR # 修改视频信息(例如调整分辨率) video_info.width = 640 video_info.height = 480 # 更新帧的属性 frame.set_caps(Gst.Caps.from_string(video_info.to_caps_string())) # 继续处理其他帧数据 return Gst.FlowReturn.OK # 创建 GStreamer 管道 pipeline = Gst.parse_launch('videotestsrc ! videoconvert ! video/x-raw ! videoinfo_transform ! x264enc ! rtspclientsink location=rtsp://localhost:8554/test') # 获取管道中的自定义插件元素 videoinfo_transform = pipeline.get_by_name('videoinfo_transform') if not videoinfo_transform: videoinfo_transform = VideoInfoTransform() pipeline.add(videoinfo_transform) # 启动管道 pipeline.set_state(Gst.State.PLAYING) # 运行主循环 loop = GObject.MainLoop() loop.run()
解释
插件创建:
VideoInfoTransform
继承自Gst.BaseTransform
。在transform_frame
方法中,你可以获取和修改GstVideoInfo
对象。修改视频信息: 你可以使用
Gst.Video.VideoInfo()
来获取视频帧的详细信息,并根据需要修改属性。然后,使用Gst.Caps
将修改后的信息应用到帧上。管道配置: 使用
Gst.parse_launch
创建 GStreamer 管道,并将自定义插件添加到管道中。运行管道: 设置管道状态为
Gst.State.PLAYING
并运行主循环。注意事项
调试和测试: 自定义插件可能需要调试和测试,以确保它在实际数据流中按预期工作。
性能考虑: 修改视频帧可能会影响性能,尤其是在高分辨率或高帧率的情况下。需要确保插件的实现效率。
通过这种方式,你可以在 GStreamer 管道中插入自定义处理步骤,调整
GstVideoInfo
并在视频数据流向编码器之前进行所需的修改。
我试试这种方式,如果可能,我觉得这个方法既可以适用于encorder,也可以适用于decorder,把这个width,height 与stride的不一致校准过来。
本文终结。