1.Linux下载与编译gstreamer
对于Ubuntu系统,使用如下命令可以下载gstreamer(Installing on Linux)
sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio
当然也可以自己编译源码,需要使用meson工具(Building from source using Meson),gstreamer的源码可以在github上找到(gstreamer),不过直接用命令行安装方便很多
在下载gstreamer之后,如果想要编译自己编写的代码,需要依赖gcc的支持,例如
gcc test.c -o test $(pkg-config --cflags --libs gstreamer-1.0)
这里的pkg-config描述了gcc过程中需要的gstreamer文件的链接,使用pkg-config --cflags --libs gstreamer-1.0可以打印链接信息,会输出gstreamer文件地址和glib的地址
lighthouse@VM-12-10-ubuntu:~$ pkg-config --cflags --libs gstreamer-1.0
-I/usr/include/gstreamer-1.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -pthread -I/usr/include/x86_64-linux-gnu -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0
2.gstreamer命令行工具的简单使用
gstreamer支持简单的命令行使用,安装之后提供3种工具可以快捷使用:
- gst-inspect-1.0
gst-inspect-1.0是一个工具,它可以打印出有关可用的 GStreamer 插件、特定插件或特定元素的信息 - gst-launch-1.0
gst-launch-1.0 是一个构建和运行基本 GStreamer 流水线的工具,例如可以使用这个工具快速的播放一个视频(这类似于FFmpeg中的ffplay.exe) - ges-launch-1.0
ges-launch-1.0 是 GStreamer Editing Services (GES) 的一个工具,可以创建一个多媒体时间线,能够灵活的处理时间问题(该工具也可以快速播放视频)
(1)gst-inspect-1.0
使用gst-inspect-1.0打印gstreamer中注册的插件和元素的列表。下列输出中的第一列为插件库名称(如1394、a52dec和aasink等),第二列为该元素提供的具体元素名称(例如dv1394src、a52dec和aatv等),第三列为这些元素的简短描述,说明了元素功能,例如 Firewire (1394) DV video source 表示dv1394src是一个从Firewire(IEEE 1394)获取DV视频信号的源元素
lighthouse@VM-12-10-ubuntu:~$ gst-inspect-1.0
1394: dv1394src: Firewire (1394) DV video source
1394: hdv1394src: Firewire (1394) HDV video source
a52dec: a52dec: ATSC A/52 audio decoder
aasink: aasink: ASCII art video sink
aasink: aatv: aaTV effect
accurip: accurip: AccurateRip(TM) CRC element
adaptivedemux2: dashdemux2: DASH Demuxer
adaptivedemux2: hlsdemux2: HLS Demuxer
adaptivedemux2: mssdemux2: Smooth Streaming demuxer (v2)
adder: adder: Adder
adpcmdec: adpcmdec: ADPCM decoder
adpcmenc: adpcmenc: ADPCM encoder
aes: aesdec: aesdec
aes: aesenc: aesenc
aiff: aiffmux: AIFF audio muxer
aiff: aiffparse: AIFF audio demuxer
alaw: alawdec: A Law audio decoder
alaw: alawenc: A Law audio encoder
alpha: alpha: Alpha filter
alphacolor: alphacolor: Alpha color filter
alsa: alsadeviceprovider (GstDeviceProviderFactory)
alsa: alsamidisrc: AlsaMidi Source
.... 后续省略
(2)gst-launch-1.0
使用gst-launch-1.0可以快速播放.webm格式的视频,如下所示
gst-launch-1.0 playbin uri=https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm
其中playbin是gstreamer中的一个预先构建的高级元素,内部集成了多个其他元素,如uridecodebin、decodebin等,如果用户在CLI中指定了这个元素,gstreamer会自动构建一条适合输入uri的一条流水线,实现解码和渲染的功能。尽管playbin内部包含了构建流水线所需的许多组件,但它本身仍然是一个单一的元素。播放webm的效果为
也可以使用gst-launch-1.0播放yuv、mp4和h264格式的视频,如下所示
yuv格式
// blocksize = 832x480x1.5 = 599040,表示一帧的大小(缓冲区大小为一帧)
gst-launch-1.0 filesrc blocksize=599040 location=/home/ubuntu/Desktop/workspace/BasketballDrill_832x480_50.yuv ! queue ! "video/x-raw, format=I420, width=832, height=480, framerate=50/1" ! rawvideoparse use-sink-caps=true ! autovideosink
// 对比之下ffplay要简单不少
ffplay -f rawvideo -video_size 832x480 -pixel_format yuv420p /home/ubuntu/Desktop/workspace/BasketballDrill_832x480_50.yuv
mp4格式和h264格式
// mp4格式
gst-launch-1.0 playbin uri=file:///home/ubuntu/Desktop/workspace/Crew.mp4
// h264格式
gst-launch-1.0 filesrc location=/home/ubuntu/Desktop/workspace/Crew.h264 ! h264parse ! avdec_h264 ! videoconvert ! videoscale ! video/x-raw,width=1280,height=720 ! autovideosink
(3)ges-launch-1.0
ges-launch-1.0可以创建一个多媒体时间线,能够灵活的处理时间问题。可以加载现有文件的时间线,或者根据指定的命令创建一个新的时间线,最简单的命令是
ges-launch-1.0 +clip test.webm
也可以指定开始播放的时间点
ges-launch-1.0 +clip test.webm inpoint=4.0
3.基于gstreamer的webm视频播放器
gstreamer允许用户按照需求,自定义媒体通道(如videosink、audiosink、demux等),实现特定的功能。使用GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS()这个宏能够输出当前的graph,输出的文件格式为dot类型,再使用Graphviz工具将dot文件转换成为png文件,就可以查阅了。这个宏的简单用法是
// arg1@: GST_BIN(pipeline),通常会将pipeline定义为GstElement类型,这里转换成GstBin类型,这是允许的,因为GstElement是GstBin的父类
// arg2@: GST_DEBUG_GRAPH_SHOW_ALL,打印所有信息
// arg3@: pipeline,表示输出文件的名称
GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline")
参考gstreamer/subprojects/gst-docs/example/tutorial/basic-tutorial-3.c,将音频输出改成视频输出。为了打印其graph,需要在gst_init()初始化gst之前,设置dot文件的存储路径(GST_DEBUG_DUMP_DOT_DIR)。使用视频输出的代码如下所示,主要不同点在于将获取的音频元素修改成了视频元素
#include <gst/gst.h>
#include <stdio.h>
/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
GstElement *pipeline;
GstElement *source;
GstElement *videoconvert;
GstElement *videoscale;
GstElement *videosink;
} CustomData;
/* Handler for the pad-added signal */
static void pad_added_handler (GstElement *src, GstPad *pad, CustomData *data);
int main(int argc, char *argv[]) {
CustomData data;
GstBus *bus;
GstMessage *msg;
GstStateChangeReturn ret;
gboolean terminate = FALSE;
// 设置dot文件存储路径
g_setenv("GST_DEBUG_DUMP_DOT_DIR", "/home/ubuntu/Desktop/workspace", TRUE);
const gchar* dot_dir = g_getenv("GST_DEBUG_DUMP_DOT_DIR");
printf("dot_dir:%s\n", dot_dir);
/* Initialize GStreamer */
// 初始化gstreamer
gst_init (&argc, &argv);
/* Create the elements */
// 这里修改成获取视频元素
data.source = gst_element_factory_make ("uridecodebin", "source");
data.videoconvert = gst_element_factory_make ("videoconvert", "videoconvert");
data.videoscale = gst_element_factory_make ("videoscale", "videoscale");
data.videosink = gst_element_factory_make ("autovideosink", "videosink");
/* Create the empty pipeline */
// 创建空的pipeline
data.pipeline = gst_pipeline_new ("test-pipeline");
if (!data.pipeline || !data.source || !data.videoconvert || !data.videoscale || !data.videosink) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Build the pipeline. Note that we are NOT linking the source at this
* point. We will do it later. */
// 将pipeline, source, videoconvert, videoscale, videosink添加到同一个bin当中
gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.videoconvert, data.videoscale, data.videosink, NULL);
// 将videoconvert, videoscale, videosink链接到一起
if (!gst_element_link_many (data.videoconvert, data.videoscale, data.videosink, NULL)) {
g_printerr ("Elements could not be linked.\n");
gst_object_unref (data.pipeline);
return -1;
}
/* Set the URI to play */
// 设置想要播放的URI路径,这里也可以改成本地,如file:///home/ubuntu/Desktop/workspace/video.webm
g_object_set (data.source, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm", NULL);
/* Connect to the pad-added signal */
// 将pad-added这个信号与source链接,如果source收到外部输入,会激活这个信号,随后调用
// pad_added_handler这个回调函数
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);
/* Start playing */
// 将pipeline的状态设置为GST_STATE_PLAYING,表示这条媒体通道开始播放
ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
g_printerr ("Unable to set the pipeline to the playing state.\n");
gst_object_unref (data.pipeline);
return -1;
}
/* Listen to the bus */
// 创建pipeline上的总线
bus = gst_element_get_bus (data.pipeline);
do {
// 监听pipeline上的总线消息
// GST_CLOCK_TIME_NONE表示无限制时间,即会一直阻塞进程,直到总线上有新消息出现
// GST_MESSAGE_STATE_CHANGE,GST_MESSAGE_ERROR,GST_MESSAGE_EOS为三种消息类型,这里表示只过滤这三种消息
msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE,
GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
/* Parse message */
if (msg != NULL) {
GError *err;
gchar *debug_info;
switch (GST_MESSAGE_TYPE (msg)) {
case GST_MESSAGE_ERROR:
// 检测总线上出现了错误消息
gst_message_parse_error (msg, &err, &debug_info);
g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
g_clear_error (&err);
g_free (debug_info);
terminate = TRUE;
break;
case GST_MESSAGE_EOS:
// 到达流的结尾
g_print ("End-Of-Stream reached.\n");
terminate = TRUE;
break;
case GST_MESSAGE_STATE_CHANGED:
/* We are only interested in state-changed messages from the pipeline */
// pipeline的状态发生了变化
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data.pipeline)) {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
}
break;
default:
/* We should not reach here */
g_printerr ("Unexpected message received.\n");
break;
}
gst_message_unref (msg);
}
} while (!terminate);
/* Free resources */
// 释放资源
gst_object_unref (bus);
gst_element_set_state (data.pipeline, GST_STATE_NULL);
gst_object_unref (data.pipeline);
return 0;
}
/* This function will be called by the pad-added signal */
// 回调函数,当这个函数被调用时,会创建一个新的pad,用以连接source和videoconvert
// 其中,source的pad为发送端(src),videoconvert的pad为接收端(sink)
// 输入参数中的new_pad就是source中的pad
static void pad_added_handler (GstElement *src, GstPad *new_pad, CustomData *data) {
// 从videoconvert中获取sink这个默认的pad
GstPad *sink_pad = gst_element_get_static_pad (data->videoconvert, "sink");
GstPadLinkReturn ret;
GstCaps *new_pad_caps = NULL;
GstStructure *new_pad_struct = NULL;
const gchar *new_pad_type = NULL;
g_print ("Received new pad '%s' from '%s':\n", GST_PAD_NAME (new_pad), GST_ELEMENT_NAME (src));
/* If our converter is already linked, we have nothing to do here */
// 检查当前的sink pad是否已经链接上了
if (gst_pad_is_linked (sink_pad)) {
g_print ("We are already linked. Ignoring.\n");
goto exit;
}
/* Check the new pad's type */
// 检查new pad类型
new_pad_caps = gst_pad_get_current_caps (new_pad);
new_pad_struct = gst_caps_get_structure (new_pad_caps, 0);
new_pad_type = gst_structure_get_name (new_pad_struct);
if (!g_str_has_prefix (new_pad_type, "video/x-raw")) {
g_print ("It has type '%s' which is not raw video. Ignoring.\n", new_pad_type);
goto exit;
}
/* Attempt the link */
// 尝试连接两个pad
ret = gst_pad_link (new_pad, sink_pad);
if (GST_PAD_LINK_FAILED (ret)) {
g_print ("Type is '%s' but link failed.\n", new_pad_type);
} else {
g_print ("Link succeeded (type '%s').\n", new_pad_type);
}
// 输出dot文件
GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(data->pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline_graph");
exit:
/* Unreference the new pad's caps, if we got them */
if (new_pad_caps != NULL)
gst_caps_unref (new_pad_caps);
/* Unreference the sink pad */
gst_object_unref (sink_pad);
}
编译运行
gcc basic-tutorial-3.c -o basic_3 $(pkg-config --cflags --libs gstreamer-1.0)
./basic_3
输出的视图框和前面命令行的一致
同时,在对应路径下会找到对应的dot文件,我这里是0.00.00.015909702-pipeline_graph.dot,转换成png图片
dot -Tpng -o graph_pipeline.png 0.00.00.015909702-pipeline_graph.dot
需要注意的是,在pad_added_handler()中存储dot文件,因为该函数被调用时,才会执行数据流传输,graph才完整,如果在tutorial_main()中存储dot文件,会缺少前面接收远端数据流的元素,是不完整的。从图中看,一共有4个元素,分别是:
- GstURIDecodeBin中的source,用于接收远端数据(如网络URL),能够自动选择合适的解码器
source元素中又包含了很多子元素,其中:
(1)GstSoupHTTPSrc中的source,用于从指定的URI远程读取数据
(2)GstTypeFindElement typefindelement0,用于确定媒体流的类型。通过分析流中的数据,尝试识别媒体类型(如音频、视频),并设置相应的媒体类型信息(caps)
(3)GstQueue2 queue2-0,用于在管道中存储和转发数据
(4)GstDecodeBin中的decodebin0,用于解码媒体流,能够自动选择合适的解码器进行解码
(a)GstTypeFindElement typefind,用于确定媒体流类型
(b)GstMatroskaDemux matroskademux0,用于将Matroska容器格式(如.mkv)解复用成单独的音视频流,将容器中的数据流分离成可以被进一步处理的音视频轨道
(c)GstMultiQueue multiqueue0,多输入队列元素,可以接收多个输入流,并将它们合并为一个输出流
(d)GstVorbisDec vorbisdec0,用于解码Vorbis编码的音频流
(e)GstVP8Dec vp8dec0,用于解码VP8视频流
GstURIDecodeBin中的数据流向是:
(1)GstSoupHTTPSrc中的source从远端location(https://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.webm)拉取数据。图中的src端口是发送端口,">"符号表示"playing"状态,“bfb”符号代表三种状态,第一个"b"表示远端接收过来的数据是否阻塞,"f"表示是否清空缓存,第二个"b"表示当前模块输出的数据是否阻塞,这三个状态的flag如果为大写(如"bFb"表示正在flushing),表示有效,"T"符号表示任务已经开始("t"符号表示任务暂停)。图中表示source模块正在启用,获取远端数据并且向后续模块传输数据
(2)GstTypeFindElement typefindelement0解析数据流,标识caps=video/webm,即数据类型为视频中的webm格式
(3)GstQueue2 queue2-0对数据流进行缓冲处理,确保数据流的平滑处理,max-size-buffers=0表示不限制缓冲区的数量,队列可以无限长;use-buffering=TRUE,GstQueue2会根据low-watermark和high-watermark属性的值来控制缓冲区的填充率,并在缓冲区的填充水平低于或高于阈值时发出GST_MESSAGE_BUFFERING消息;avg-in-rate=1548424,表示平均输入速率,单位是字节/秒,用于估算数据流入队列的平均速度;bitrate=1538934,用于限制队列的大小,影响如何根据比特率计算队列中数据的时间长度
(4)GstDecodeBin中的decodebin0,用于解码媒体流
(a)GstTypeFindElement typefind,确定媒体流类型为caps=video/webm
(b)GstMatroskaDemux matroskademux0,将前面获取的媒体数据解复用,分为audio和video两个部分,其中audio为vorbis格式(channel=2,rate=48000),video为vp8格式(width=854,height=480,帧率未设置)
(c)GstMultiQueue multiqueue0,获取前面的媒体数据,合并为一个数据流(这里没有合并)。current-level-buffers表示队列中当前的缓冲区数量,current-level-bytes表示队列中当前的数据量(单位为字节)
(d)GstVorbisDec vorbisdec0,接收音频数据,传输到proxypad5_0x70a5fc077430,这里没有后续的使用,应该是一个虚拟的pad
(e)GstVP8Dec vp8dec0,接收视频数据,格式为format=I420,width=854,height=480,interlace-mode=progressive,framerate=0/1(没有设置),输出数据到proxypad4这个端口,这个端口会作为decodebin0的输出端口src_0,输出到后续的模块
-
GstVideoConvert中的videoconvert,用于数据格式转换
videoconvert常用于格式转换,如果没有特定的要求,该模块不会修改格式。在dot图中,打印了videoconvert支持的能力集,video/x-raw format ; {(string)A444_16LE, …}, …, video/x-raw(ANY) format : {(string) DMA_DRM, …}。图中的qos=TRUE,表示videoconvert会根据下游元素的反馈信息调整自己的处理速度和输出质量,以优化整个管道的Qos表现。 -
GstVideoScale中的videoscale,用于改变视频数据的尺寸,调整视频的宽高
videoscale用于调整视频宽高,如果没有特定的要求,该模块不会修改格式。 -
GstAutoVideoSink中的videosink,用于播放数据,自动选择合适的视频输出设备,将视频帧渲染到屏幕
(1)GstGLUploadElement gluploadelement0,将CPU内存中的图像数据上传到GPU内存中,以便后续OpenGL的处理,这里不会改变图像数据内容,只是修改了内存域
(2)GstColorConvertElement glcolorconvertelement0,使用OpenGL来处理视频格式和颜色空间,例如从YUV转换到RGB
(3)GstGLColorBalance glcolorbalance0,调整视频流的亮度、对比度、色调和饱和度
(4)GstGLImageSink sink,基于OpenGL或OpenGL ES的视频输出元素,实现了VideoOverlay接口,实现输出视频的功能