系列文章目录
- GStreamer 简明教程(一):环境搭建,运行 Basic Tutorial 1 Hello world!
- GStreamer 简明教程(二):基本概念介绍,Element 和 Pipeline
- GStreamer 简明教程(三):动态调整 Pipeline
- GStreamer 简明教程(四):Seek 以及获取文件时长
- GStreamer 简明教程(五):Pad 相关概念介绍,Pad Capabilities/Templates
- GStreamer 简明教程(六):利用 Tee 复制流数据,巧用 Queue 实现多线程
- GStreamer 简明教程(七):实现管道的动态数据流
- GStreamer 简明教程(八):常用工具介绍
- GStreamer 简明教程(九):Seek 与跳帧
文章目录
- 系列文章目录
- 前言
- 一、目标
- 二、准备工作
- 2.1 生成插件模板代码
- 2.2 理解 gstmyemptyfilter 代码
- 三、开发一个音频插件
- 3.1 与 C++ 混编
- 3.2 设置 Caps 模板
- 3.3 插件属性与算法参数
- 3.4 创建 Delay Line
- 3.3 处理音频数据
- 3.4 Impl 资源的创建与销毁
- 3.5 测试我们的插件
- 总结
- 参考
前言
GStreamer 简明教程系列已经更新了 9 期,这些教程基本是我个人在学习官方教程中的一些理解和总结。官方的基础教程中远不止 9 期,但后续的基础教程我决定不再更新了,因为后面的内容基本还是围绕如何使用 GStreamer 中的某种功能来展开的,它不涉及 GStreamer 底层代码的实现逻辑。
回看我最初学习 GStreamer 的目的,最重要是掌握 GStreamer 的设计思想,学习消化后以便自己能设计出一套类似的音视频框架,因此我对 GStreamer 底层的实现逻辑更感兴趣。
此外,在学习基础教程的过程中,越来越多疑问出现在我的脑海中:GStreamer 中的线程模型是怎么样的?资源的声明周期如何控制?线程安全如何保证?状态流转有什么实现技巧?Element 应该处理不同状态的?Element 之间的negotiate(协商)的逻辑是怎么样的?音画同步是怎么做的?Push 模式和Pull 模式之间如何切换?如何处理各类时间?Element 之间如何进行消息通信?…
这些疑问在基础教程并不能给我很好的答案,GStreamer 的官方文档中虽然对这些内容或多或少有所提及,但看过一遍后仍然不得要领,无法准确理解官方文档中所说内容。
那么怎么办?我想到的办法是写更多的代码,更多涉及底层逻辑的代码,最简单的方法就是写 GStreamer 插件。因此,本章我将说明如何写一个 GStreamer 插件。
本文所有代码你可以在 my_plugin - github 找到。
本文基本参考官方教程 Writing a Plugin。更多细节欢迎读者自行阅读官方教程。
一、目标
- 写一个音频音效插件,实现音频 Delay(回声) 效果。算法实现可以参考 【音效处理】Delay/Echo 简介,但并不重要,我们只需要知道有这么个算法就行。
- 可以通过参数对算法效果进行控制。就像其他 Element 有很多 Property(属性)一样,我们算法可以通过参数来控制行为。
- 正确的处理插件资源的生命周期。
- 用 C++ 语法实现。这个纯粹是因为写 C++ 代码比较方便,比如可以用智能指针、atomic 等特性。
- 提供一个可以运行的示例,方便验证效果。
二、准备工作
2.1 生成插件模板代码
Gstreamer 提供了一些工具,方便我们编写插件,我们可以生成一个插件代码模板,具体步骤如下:
- 下载 gst-template.git 仓库
git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git
- 生成模板文件,执行下面命令,会生成
gstmyemptyfilter.c/h
两个文件 ,将 .c 文件后缀修改为 .cpp 文件,以便进行 c++ 编译
cd /path/to/gst-template/gst-plugin/src
../tools/make_element MyEmptyFilter
- 拷贝模板文件到我们工程下,添加如下 messon.build 即可进行编译。具体代码参考 meson.build
plugin_c_args = ['-DHAVE_CONFIG_H']
plugin_cpp_args = ['-DHAVE_CONFIG_H', '-std=c++17']
plugins_install_dir = join_paths(get_option('libdir'), 'gstreamer-1.0')gstmyemptyfilter_sources = ['gstmyemptyfilter.cpp',
]gstmyemptyfilter = library('gstmyemptyfilter',gstmyemptyfilter_sources,c_args: plugin_c_args,cpp_args: plugin_cpp_args,dependencies : [gst_dep, gstbase_dep, gstaudio_dep],install : true,install_dir : plugins_install_dir,
)
- 搭建测试环境。编译 gstmyemptyfilter 后会在某个目录下生成一个动态库,你需要将动态库所在目录添加到
GST_PLUGIN_PATH
环境变量下。具体步骤:- 参考 GStreamer 源码编译,在 Clion 下搭建调试环境 生成调试用的环境变量,例如存放在 env.sh 文件中
- 在 env.sh 文件中,找到
GST_PLUGIN_PATH
变量,并在前添加 gstmyemptyfilter 动态库的目录,在本人机器上这个目录是/Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/
- 运行测试示例,验证插件是否运行正常。我提供了一个 gstmyfilter_example 示例,修改几处代码进行运行:
- 创建
my_empty_filter
。创建data.my_filter
时使用 “my_empty_filter”,而不是 “my_filter” - 修改
data.source
的 uri 参数为你本地的测试视频路径。完成后,运行示例测试音频是否正常播放。注意记得导入测试环境的环境变量。
- 创建
2.2 理解 gstmyemptyfilter 代码
我们可以先用 gst-inspect-1.0 查询下 gstmyemptyfilter 基本信息。如果能正常查询,说明插件正确生成了。
./gst-inspect-1.0 myemptyfilter
Factory Details:Rank none (0)Long-name MyEmptyFilterKlass FIXME:GenericDescription FIXME:Generic Template ElementAuthor <<user@hostname.org>>Documentation https://gstreamer.freedesktop.org/documentation/myemptyfilter/#my_empty_filter-pagePlugin Details:Name myemptyfilterDescription my_empty_filterFilename /Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/libgstmyemptyfilter.dylibVersion 1.25.0.1License LGPLSource module gstreamerDocumentation https://gstreamer.freedesktop.org/documentation/myemptyfilter/Binary package GStreamer gitOrigin URL Unknown package originGObject+----GInitiallyUnowned+----GstObject+----GstElement+----GstMyEmptyFilterPad Templates:SINK template: 'sink'Availability: AlwaysCapabilities:ANYSRC template: 'src'Availability: AlwaysCapabilities:ANYElement has no clocking capabilities.
Element has no URI handling capabilities.Pads:SINK: 'sink'Pad Template: 'sink'SRC: 'src'Pad Template: 'src'Element Properties:name : The name of the objectflags: readable, writableString. Default: "myemptyfilter0"parent : The parent of the objectflags: readable, writableObject of type "GstObject"silent : Produce verbose output ?flags: readable, writableBoolean. Default: false
在 myemptyfilter_init
函数中有插件的信息,其中 my_empty_filter
是元素的名字,myemptyfilter
是插件名字。
static gboolean
myemptyfilter_init (GstPlugin * myemptyfilter)
{return GST_ELEMENT_REGISTER (my_empty_filter, myemptyfilter);
}
在 gst_my_empty_filter_class_init
函数中,进行了类的初始化,做了这么几个事情:
- 覆写
set_property
和get_property
函数,以便控制属性变化的行为。 - 设置属性。示例代码中只添加了一个
silent
参数 gst_element_class_set_details_simple
设置元素的一些元数据信息。gst_element_class_add_pad_template
添加 Pads 的模板信息。
在 gst_my_empty_filter_init
函数中,进行了实例的初始化,做了这么几个事情:
- 为插件添加一个 sink pad,设置 sink pad 的 event function 和 chain function。其中 event function 负责处理各种事件,例如 EOS 或者格式信息等;chain function 负责处理各种 buffer 数据,buffer 中包含视频、音频等数据。并设置
GST_PAD_SET_PROXY_CAPS
。 - 为插件添加一个 src pad,并设置
GST_PAD_SET_PROXY_CAPS
。 - 初始化
silent
的值。
gst_my_empty_filter_set_property
和 gst_my_empty_filter_get_property
是属性的设置和获取,这块代码很简单,不再说明。
以上就是插件模板代码的基本内容,内容不多,但有挺多细节。以及最重要的 event function 和 chain function 到底要怎么写,目前我们还不知道。
三、开发一个音频插件
这部分完整代码参考 gstmyfilter.cpp
3.1 与 C++ 混编
音频特效算法部分,我准备复用之前写的一些代码,这些代码是 C++ 的,它们是一些类。为了让 gstmyfilter.h 头文件保持纯粹的 C 代码,需要将引入的 C++ 代码隐藏在 .cpp 文件中。因此可以用类似与 C++ Impl 的实现方式,在 GstMyFilter
添加一个指针:
struct _GstMyFilter
{GstElement element;GstPad *sinkpad, *srcpad;void* impl;
};
接着,在 .cpp 文件中,构建一个 Impl 类,将必要的数据放到这个类中进行管理
class GstMyFilterImpl {
public:std::atomic<float> delay = {kDefaultDelay};std::atomic<float> feedback = {kDefaultFeedback};std::atomic<float> dry = {kDefaultDry};std::atomic<float> wet = {kDefaultWet};GstAudioInfo info;std::unique_ptr<libaa::DelayLine<float>> delay_line;
};
- delay、feedback 等是算法参数的值,使用了 atomic 确保线程安全。
- GstAudioInfo 用来存放音频参数,包括采样率、声道数等等。
- delay_line 是一个
libaa::DelayLine
的一个智能指针,用它来实现音频特效算法。libaa::DelayLine
源码也加入到了工程中,你可以参看 aa_delay_line.h 文件。另外,再说明一次,音频特效算法原理不重要,刚兴趣可以参考 【音效处理】Delay/Echo 简介,不感兴趣跳过即可。
3.2 设置 Caps 模板
为了简单起见,我们限定 Caps 之间只能处理 F32LE 单声道数据。这是因为如果有多种格式要处理,那么 delay_line
的需要设置不同的模板值,可能是 int16 或者 double 之类的,此外还需要处理多声道的情况,对于我们这个简单的教程来说这些情况过于复杂了。
const char *ALLOWED_CAPS = R"(audio/x-raw,
format=(string) {F32LE},
rate=(int)[1,48000],
channels=(int)1,
layout=(string) interleaved)";static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS(ALLOWED_CAPS));static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS(ALLOWED_CAPS));
3.3 插件属性与算法参数
在 gst_my_filter_class_init
类初始化函数中,我们添加了 4 个属性,分别对应算法 4 个参数
g_object_class_install_property(gobject_class, PROP_DELAY,g_param_spec_float("delay", "Delay", "Delay time in milliseconds", 0.0f, kMaxDelayMs, kDefaultDelay,static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |GST_PARAM_CONTROLLABLE)));g_object_class_install_property(gobject_class, PROP_FEEDBACK,g_param_spec_float("feedback", "Feedback", "Feedback factor", 0.0f, 1.0f, kDefaultFeedback,static_cast<GParamFlags>(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS |GST_PARAM_CONTROLLABLE)));
// ....
同样地,我们覆写 set_property
和 get_property
方法,以便控制算法参数的值
static void gst_my_filter_class_init(GstMyFilterClass *klass) {// ....gobject_class->set_property = gst_my_filter_set_property;gobject_class->get_property = gst_my_filter_get_property;//...
}
static void gst_my_filter_set_property(GObject *object, guint prop_id,const GValue *value, GParamSpec *pspec) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);switch (prop_id) {case PROP_DELAY:priv->delay = g_value_get_float(value);break;case PROP_FEEDBACK:priv->feedback = g_value_get_float(value);break;case PROP_DRY:priv->dry = g_value_get_float(value);break;case PROP_WET:priv->wet = g_value_get_float(value);break;default:G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);break;}
}
static void gst_my_filter_get_property(GObject *object, guint prop_id,GValue *value, GParamSpec *pspec) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);switch (prop_id) {case PROP_DELAY:g_value_set_float(value, priv->delay.load());break;case PROP_FEEDBACK:g_value_set_float(value, priv->feedback.load());break;case PROP_DRY:g_value_set_float(value, priv->dry.load());break;case PROP_WET:g_value_set_float(value, priv->wet.load());break;default:G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);break;}
}
3.4 创建 Delay Line
我们需要知道音频的采样率才能初始化 Delay Line,音频格式信息在 caps 之间协商确定后,通过 GST_EVENT_CAPS 事件发送给 Pipeline 进行处理,因此我们可以在 event function 中拿到音频格式信息,从而初始化 Delay Line
static gboolean gst_my_filter_sink_event(GstPad *pad, GstObject *parent,GstEvent *event) {//....switch (GST_EVENT_TYPE(event)) {case GST_EVENT_CAPS: {GstCaps *caps;gst_event_parse_caps(event, &caps);/* do something with the caps */if(!gst_audio_info_from_caps(&priv->info, caps)) {g_printerr("Failed to parse caps\n");return FALSE;}if(priv->delay_line == nullptr) {priv->delay_line = std::make_unique<libaa::DelayLine<float>>();}const auto max_delay_samples = static_cast<size_t>(kMaxDelayMs / 1000.0f * priv->info.rate);priv->delay_line->resize(max_delay_samples);/* and forward */ret = gst_pad_event_default(pad, parent, event);break;}//....
}
我们根据 kMaxDelayMs
和采样率计算出最大的 buffer size,并对 delay_line 进行设置。
3.3 处理音频数据
chain function 中处理具体的音频数据,拿到数据后进行音频算法处理,接着将数据送给 src pad
static GstFlowReturn gst_my_filter_chain(GstPad *pad, GstObject *parent,GstBuffer *buf) {GstMyFilter *filter;filter = GST_MYFILTER(parent);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);// get bufferGstMapInfo map;gst_buffer_map (buf, &map, GST_MAP_READWRITE);auto num_samples = map.size / GST_AUDIO_INFO_BPS(&priv->info);auto* float_buffer = reinterpret_cast<float*>(map.data);// parametersauto delay_samples = priv->delay.load() / 1000.0f * priv->info.rate;auto feedback = priv->feedback.load();auto dry = priv->dry.load();auto wet = priv->wet.load();// process audio datafor(auto i = 0; i < num_samples; ++i) {auto in = float_buffer[i];auto d_y = priv->delay_line->get(delay_samples);auto d_x = in + feedback * d_y;priv->delay_line->push(d_x);float_buffer[i] = dry * in + wet * d_y;}/* just push out the incoming buffer without touching it */return gst_pad_push(filter->srcpad, buf);
}
GstBuffer 无法直接获取数据,你需要使用 gst_buffer_map
来访问 GstBuffer 中的数据;map.size
中存放的是数据的字节大小,而音频数据个数需要进一步计算,例如 map.size = 2048,音频格式是 F32LE 的话,那么每个音频数据大小应该是 8,因此音频数据个数是 2048/8 = 512。
接着获取到算法参数的当前值,然后遍历整个 float_buffer 进行音频算法处理。
最后,将处理完的 buffer 送给 src pad 即可。
3.4 Impl 资源的创建与销毁
类似于 C++ 中构造函数和析构函数的概念,我们在 gst_my_filter_init
函数中对实例进行初始化,创建 Impl 指针;而析构函数则需要覆写 finalize
函数,并在其中释放这个指针
static void gst_my_filter_class_init(GstMyFilterClass *klass) {//...gobject_class->finalize = gst_my_filter_finalize;//...
}
static void gst_my_filter_init(GstMyFilter *filter) {filter->impl = new GstMyFilterImpl();//...
}
static void gst_my_filter_finalize(GObject* object) {GstMyFilter *filter = GST_MYFILTER(object);auto *priv = static_cast<GstMyFilterImpl *>(filter->impl);delete priv;
}
3.5 测试我们的插件
完成了上述内容,我们的音频插件就完成了,可以运行 gstmyfilter_example 中的代码,只需要修改 data.source
的 uri 属性即可。运行成功的话,你可以听到音频有一个回声。
总结
本文介绍了 GStreamer 插件开发的基本流程,通过开发一款音频特效插件,我们了解了很多 GStreamer 的细节,包括 event function、chain function 的运行机制等等。接下来我们将开发更多插件,以便更加深入了解 GStreamer。
参考
- Writing a Plugin
- my_plugin - github
- gstmyfilter.cpp
- 【音效处理】Delay/Echo 简介
- gstmyfilter_example