系列文章目录
- 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.1 生成插件模板代码)
- [2.2 理解 gstmyemptyfilter 代码](#2.2 理解 gstmyemptyfilter 代码)
- 三、开发一个音频插件
-
- [3.1 与 C++ 混编](#3.1 与 C++ 混编)
- [3.2 设置 Caps 模板](#3.2 设置 Caps 模板)
- [3.3 插件属性与算法参数](#3.3 插件属性与算法参数)
- [3.4 创建 Delay Line](#3.4 创建 Delay Line)
- [3.3 处理音频数据](#3.3 处理音频数据)
- [3.4 Impl 资源的创建与销毁](#3.4 Impl 资源的创建与销毁)
- [3.5 测试我们的插件](#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 仓库
shell
git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git
- 生成模板文件,执行下面命令,会生成
gstmyemptyfilter.c/h
两个文件 ,将 .c 文件后缀修改为 .cpp 文件,以便进行 c++ 编译
shell
cd /path/to/gst-template/gst-plugin/src
../tools/make_element MyEmptyFilter
- 拷贝模板文件到我们工程下,添加如下 messon.build 即可进行编译。具体代码参考 meson.build
python
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 基本信息。如果能正常查询,说明插件正确生成了。
shell
./gst-inspect-1.0 myemptyfilter
txt
Factory Details:
Rank none (0)
Long-name MyEmptyFilter
Klass FIXME:Generic
Description FIXME:Generic Template Element
Author <<user@hostname.org>>
Documentation https://gstreamer.freedesktop.org/documentation/myemptyfilter/#my_empty_filter-page
Plugin Details:
Name myemptyfilter
Description my_empty_filter
Filename /Users/user/Documents/develop/x/gstreamer/buildDir/subprojects/gst-examples/my_plugin/libgstmyemptyfilter.dylib
Version 1.25.0.1
License LGPL
Source module gstreamer
Documentation https://gstreamer.freedesktop.org/documentation/myemptyfilter/
Binary package GStreamer git
Origin URL Unknown package origin
GObject
+----GInitiallyUnowned
+----GstObject
+----GstElement
+----GstMyEmptyFilter
Pad Templates:
SINK template: 'sink'
Availability: Always
Capabilities:
ANY
SRC template: 'src'
Availability: Always
Capabilities:
ANY
Element 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 object
flags: readable, writable
String. Default: "myemptyfilter0"
parent : The parent of the object
flags: readable, writable
Object of type "GstObject"
silent : Produce verbose output ?
flags: readable, writable
Boolean. Default: false
在 myemptyfilter_init
函数中有插件的信息,其中 my_empty_filter
是元素的名字,myemptyfilter
是插件名字。
cpp
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
添加一个指针:
c
struct _GstMyFilter
{
GstElement element;
GstPad *sinkpad, *srcpad;
void* impl;
};
接着,在 .cpp 文件中,构建一个 Impl 类,将必要的数据放到这个类中进行管理
cpp
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 之类的,此外还需要处理多声道的情况,对于我们这个简单的教程来说这些情况过于复杂了。
cpp
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 个参数
cpp
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
方法,以便控制算法参数的值
cpp
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
cpp
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
cpp
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 buffer
GstMapInfo 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);
// parameters
auto 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 data
for(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
函数,并在其中释放这个指针
cpp
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。