6.7.5. 设计说明

6.7.5.1. 源码说明

本模块源代码在内核目录 source/zx/gst1-plugins-aic 下,目录结构如下:

source/zx/gst1-plugins-aic
├── gstaicfb.c             // fb_sink 插件
├── gstaicfb.h
├── gstfbsink.c
├── gstfbsink.h
├── gstmppallocator.c      // mpp_frame 内存申请模块
├── gstmppallocator.h
├── gstplugin.c
├── gstvedec.c             // ve 解码插件
├── gstvedec.h
└── meson.build

6.7.5.2. 模块架构

Gstreamer 视频播放软件框图如下所示:

../../../_images/gst_aic_design.png

图 6.69 Gstreamer 视频播放软件框架

可分为 3 个模块:

  • demux 模块,文件解封装模块,实现 ts、MP4、mkv、flv 等文件格式解析;

  • decoder 模块,视频解码模块,完成视频解码功能;

  • sink 模块,视频显示模块,完成视频显示功能;

Gstreamer 已经实现了 3 个模块的调用流程并提供了模块接口,开发者只需按照 Gstreamer 定义的接口来实现一个插件就可以完成视频播放的功能。

ZX 平台提供了视频解码加速硬件模块(VE)和图像显示硬件模块(DE),为了在 Gstreamer 软件上发挥我们的硬件优势, 我们定义了 2 个插件:gstvedec 和 gstfbsink,分别作为 VE 和 DE 的 Gstreamer 插件。

同时,为了实现 VE 和 DE 共享 buffer,GstVideoDecoder 和 GstVideoSink 之间传递的 GstBuffer 是由自定义的 gstmppallocator 模块进行内存管理。

6.7.5.2.1. gstmppallocator 设计

gstmppallocator 模块主要用于管理视频帧 buffer,实现 gstvedec 和 gstfbsink 的 buffer 共享。

GstMppAllocator 继承于 GstAllocator 对象,用于管理分配和释放内存;分配的内存 GstMppMemory 则继承于 GstMemory 对象。

Gstreamer 中大多数对象都是通过引用计数进行管理,该对象在某个地方使用时,对其引用计数进行加 1 操作, 不使用时再将引用计数减 1,如果最终引用计数为 0,则表示该对象不会再被使用,应该释放该对象。 GstMppMemory 也遵循该原则。

主要功能包括以下几点:

  • 通过 gstmppallocator 模块申请的 buffer 只能存放 mpp_frame 结构体,它的长度也只能是 mpp_frame 结构体的大小;

  • 申请 buffer 的调用只会在 gstvedec 模块中,每次获取到一个解码帧后调用该接口;

  • 当 buffer 的引用计数为 0 时,会触发 buffer 释放接口的调用,因此释放 buffer 可能发生在 gstfbsink 模块或其它模块中(比如丢帧时释放 buffer 是在 GstVideoDecoder 中调用);

  • 调用释放 buffer 时,需要将 buffer 对应的 mpp_frame 还给解码器;

关键实现代码如下:

static void gst_mpp_allocator_free (GstAllocator * alloc, GstMemory * mem)
{
    GstMppAllocator *allocator = (GstMppAllocator*) alloc;
    GstMppMemory *mpp_mem = (GstMppMemory *)mem;
    struct mpp_frame *mframe = mpp_mem->frame;

    mpp_decoder_put_frame(allocator->decoder, mframe);
    GST_DEBUG_OBJECT(allocator, "return mpp_frame (%d)", mframe->id);

    free(mpp_mem);
}

static GstMemory *gst_mpp_allocator_alloc (GstAllocator *alloc, gsize size,
GstAllocationParams * params)
{
    int slice_size = 0;
    guint8 *data;
    GstMppMemory *mem = NULL;
    GstMppAllocator *allocator = (GstMppAllocator*) alloc;
    if (size != sizeof(struct mpp_frame)) {
        GST_ERROR_OBJECT(allocator, "memory size(%lu) is not right", size);
        return NULL;
    }

    slice_size = sizeof(GstMppMemory) + size;

    mem = (GstMppMemory*)malloc(slice_size);
    data = (guint8 *) mem + sizeof (GstMppMemory);
    mem->frame = (struct mpp_frame*)data;

    gst_memory_init (GST_MEMORY_CAST (mem),
            0, alloc, NULL, size, 0, 0, size);

    GST_DEBUG_OBJECT(allocator, "alloc mpp size(%lu), ref_count: %d",
        size, GST_MINI_OBJECT_REFCOUNT_VALUE(mem));

    return (GstMemory*)mem;
}

6.7.5.2.2. gstvedec 插件设计

gstvedec 插件是对 mpp_decoder 接口的二次封装。mpp_decoder 接口实现 3 个功能:

  • 把视频码流数据送给解码器

  • 调用硬件解码视频码流数据,并输出视频帧

  • 从解码器取视频帧

gstvedec 主要功能是使用 mpp_decoder 提供的这 3 个功能对接 GstVideoDecoder 的接口。

6.7.5.2.2.1. GstVideoDecoder 的接口说明

GstVideoDecoder 最重要的两个接口函数如下:

gboolean      (*set_format)     (GstVideoDecoder *decoder, GstVideoCodecState * state);
GstFlowReturn (*handle_frame)   (GstVideoDecoder *decoder, GstVideoCodecFrame *frame);

set_format 主要用于传递解码器所需的元信息,包括 extradata (MP4等帧格式文件会传递 extradata,而 TS 等流格式文件不传递)。

handle_frame 主要用于传递视频码流数据。

6.7.5.2.2.2. 对接设计

对应于 mpp_decoder 的 3 个功能,3 个功能分别在不同线程中调用,实现方式如下:

1)GstVideoDecoder 的 handle_frame 接口主要实现把码流数据传递到 mpp_decoder;

int ret = mpp_decoder_get_packet(dec->mpp_dec, &packet, input_minfo.size);
packet.size = input_minfo.size;
memcpy(packet.data, input_minfo.data, input_minfo.size);
packet.pts = in_frame->pts;
mpp_decoder_put_packet(dec->mpp_dec, &packet);

2)gstvedec 初始化时创建一个解码线程,该线程只完成解码功能;

3)gstvedec 的 srcpad 上创建一个显示线程,该线程的任务有两个:

  • 通过 GstMppAllocator 申请一个 buffer,用于传递视频帧信息;

  • 从 mpp_decoder 中获取一帧解码后的 mpp_frame,并把 mpp_frame 信息拷贝到 buffer(注意:此时不能把 mpp_frame 还给解码器);

  • 调用 gst_video_decoder_finish_frame 将 buffer 送到 sink 中。

6.7.5.2.3. gstfbsink 插件设计

gstfbsink 插件是对 Linux 标准的 framebuffer 接口的二次封装,对接 GstVideoSink 接口。

GstVideoSink 最重要的接口函数 render 如下,主要用于显示一帧视频数据。

GstFlowReturn (*render)(GstBaseSink * bsink, GstBuffer * buffer);

gstfbsink 插件设计的关键点是:必须避免同一个视频帧 VE 和 DE 同时读写的情况,否则会出现画面异常问题。

设计方案:

  • 将当前 buffer 引用计数加 1;

  • 通过 framebuffer 接口显示当前视频帧;

  • 将上一个 buffer 引用计数减 1(释放上一个buffer);

代码实现如下:

gst_memory_ref(mem);

gst_aicfb_render(fbsink->aicfb, &(mframe->buf), mframe->id);

if (fbsink->prev_mem != NULL) {
    gst_memory_unref(fbsink->prev_mem);
}
fbsink->prev_mem = mem;