官网地址
Playback tutorial 1: Playbin usage
一、目标
我们已经使用过 playbin 元素,它能够构建一个完整的播放管道,而无需我们做太多工作。本教程展示了如何进一步自定义 playbin,以便在其默认值不满足我们的特定需求时进行调整。
我们将学习:
- 如何查找文件中包含的流数量,以及如何在它们之间切换。
- 如何收集每个流的相关信息。
二、介绍
通常情况下,单个文件中可能包含多个音频、视频和字幕流。最常见的例子是普通电影,它们通常包含一个视频流和一个音频流(立体声或 5.1 音轨被视为单个流)。现在,越来越多的电影包含一个视频流和多个音频流,以支持不同的语言。在这种情况下,用户可以选择一个音频流,应用程序将只播放该流,而忽略其他流。
为了能够选择合适的流,用户需要了解有关这些流的某些信息,例如它们的语言。这些信息以“元数据”(附加数据)的形式嵌入在流中,本教程展示了如何检索这些信息。
字幕也可以嵌入文件中,与音频和视频一起,但它们在播放教程 2:字幕管理中会有更详细的介绍。最后,单个文件中也可能包含多个视频流,例如 DVD 中同一场景的多个角度,但这种情况相对较少见。
将多个流嵌入单个文件中称为“多路复用”或“混流”,这样的文件被称为“容器”。常见的容器格式包括 Matroska (.mkv)、Quicktime (.qt, .mov, .mp4)、Ogg (.ogg) 或 Webm (.webm)。
从容器中提取单个流的过程称为“解复用”或“解流”。
以下代码演示了如何恢复文件中的流数量、它们的相关元数据,并允许在媒体播放时切换音频流。
三、多语言播放器
将此代码复制到一个名为 playback-tutorial-1.c 的文本文件中(或在 GStreamer 安装目录中找到它)。
playback-tutorial-1.c
#include <gst/gst.h>
#include <stdio.h>
/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
GstElement *playbin; /* Our one and only element */
gint n_video; /* Number of embedded video streams */
gint n_audio; /* Number of embedded audio streams */
gint n_text; /* Number of embedded subtitle streams */
gint current_video; /* Currently playing video stream */
gint current_audio; /* Currently playing audio stream */
gint current_text; /* Currently playing subtitle stream */
GMainLoop *main_loop; /* GLib's Main Loop */
} CustomData;
/* playbin flags */
typedef enum {
GST_PLAY_FLAG_VIDEO = (1 << 0), /* We want video output */
GST_PLAY_FLAG_AUDIO = (1 << 1), /* We want audio output */
GST_PLAY_FLAG_TEXT = (1 << 2) /* We want subtitle output */
} GstPlayFlags;
/* Forward definition for the message and keyboard processing functions */
static gboolean handle_message (GstBus *bus, GstMessage *msg, CustomData *data);
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data);
int main(int argc, char *argv[]) {
CustomData data;
GstBus *bus;
GstStateChangeReturn ret;
gint flags;
GIOChannel *io_stdin;
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Create the elements */
data.playbin = gst_element_factory_make ("playbin", "playbin");
if (!data.playbin) {
g_printerr ("Not all elements could be created.\n");
return -1;
}
/* Set the URI to play */
g_object_set (data.playbin, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_cropped_multilingual.webm", NULL);
/* Set flags to show Audio and Video but ignore Subtitles */
g_object_get (data.playbin, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO;
flags &= ~GST_PLAY_FLAG_TEXT;
g_object_set (data.playbin, "flags", flags, NULL);
/* Set connection speed. This will affect some internal decisions of playbin */
g_object_set (data.playbin, "connection-speed", 56, NULL);
/* Add a bus watch, so we get notified when a message arrives */
bus = gst_element_get_bus (data.playbin);
gst_bus_add_watch (bus, (GstBusFunc)handle_message, &data);
/* Add a keyboard watch so we get notified of keystrokes */
#ifdef G_OS_WIN32
io_stdin = g_io_channel_win32_new_fd (fileno (stdin));
#else
io_stdin = g_io_channel_unix_new (fileno (stdin));
#endif
g_io_add_watch (io_stdin, G_IO_IN, (GIOFunc)handle_keyboard, &data);
/* Start playing */
ret = gst_element_set_state (data.playbin, 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.playbin);
return -1;
}
/* Create a GLib Main Loop and set it to run */
data.main_loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (data.main_loop);
/* Free resources */
g_main_loop_unref (data.main_loop);
g_io_channel_unref (io_stdin);
gst_object_unref (bus);
gst_element_set_state (data.playbin, GST_STATE_NULL);
gst_object_unref (data.playbin);
return 0;
}
/* Extract some metadata from the streams and print it on the screen */
static void analyze_streams (CustomData *data) {
gint i;
GstTagList *tags;
gchar *str;
guint rate;
/* Read some properties */
g_object_get (data->playbin, "n-video", &data->n_video, NULL);
g_object_get (data->playbin, "n-audio", &data->n_audio, NULL);
g_object_get (data->playbin, "n-text", &data->n_text, NULL);
g_print ("%d video stream(s), %d audio stream(s), %d text stream(s)\n",
data->n_video, data->n_audio, data->n_text);
g_print ("\n");
for (i = 0; i < data->n_video; i++) {
tags = NULL;
/* Retrieve the stream's video tags */
g_signal_emit_by_name (data->playbin, "get-video-tags", i, &tags);
if (tags) {
g_print ("video stream %d:\n", i);
gst_tag_list_get_string (tags, GST_TAG_VIDEO_CODEC, &str);
g_print (" codec: %s\n", str ? str : "unknown");
g_free (str);
gst_tag_list_free (tags);
}
}
g_print ("\n");
for (i = 0; i < data->n_audio; i++) {
tags = NULL;
/* Retrieve the stream's audio tags */
g_signal_emit_by_name (data->playbin, "get-audio-tags", i, &tags);
if (tags) {
g_print ("audio stream %d:\n", i);
if (gst_tag_list_get_string (tags, GST_TAG_AUDIO_CODEC, &str)) {
g_print (" codec: %s\n", str);
g_free (str);
}
if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
g_print (" language: %s\n", str);
g_free (str);
}
if (gst_tag_list_get_uint (tags, GST_TAG_BITRATE, &rate)) {
g_print (" bitrate: %d\n", rate);
}
gst_tag_list_free (tags);
}
}
g_print ("\n");
for (i = 0; i < data->n_text; i++) {
tags = NULL;
/* Retrieve the stream's subtitle tags */
g_signal_emit_by_name (data->playbin, "get-text-tags", i, &tags);
if (tags) {
g_print ("subtitle stream %d:\n", i);
if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
g_print (" language: %s\n", str);
g_free (str);
}
gst_tag_list_free (tags);
}
}
g_object_get (data->playbin, "current-video", &data->current_video, NULL);
g_object_get (data->playbin, "current-audio", &data->current_audio, NULL);
g_object_get (data->playbin, "current-text", &data->current_text, NULL);
g_print ("\n");
g_print ("Currently playing video stream %d, audio stream %d and text stream %d\n",
data->current_video, data->current_audio, data->current_text);
g_print ("Type any number and hit ENTER to select a different audio stream\n");
}
/* Process messages from GStreamer */
static gboolean handle_message (GstBus *bus, GstMessage *msg, CustomData *data) {
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);
g_main_loop_quit (data->main_loop);
break;
case GST_MESSAGE_EOS:
g_print ("End-Of-Stream reached.\n");
g_main_loop_quit (data->main_loop);
break;
case GST_MESSAGE_STATE_CHANGED: {
GstState old_state, new_state, pending_state;
gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->playbin)) {
if (new_state == GST_STATE_PLAYING) {
/* Once we are in the playing state, analyze the streams */
analyze_streams (data);
}
}
} break;
}
/* We want to keep receiving messages */
return TRUE;
}
/* Process keyboard input */
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data) {
gchar *str = NULL;
if (g_io_channel_read_line (source, &str, NULL, NULL, NULL) == G_IO_STATUS_NORMAL) {
int index = g_ascii_strtoull (str, NULL, 0);
if (index < 0 || index >= data->n_audio) {
g_printerr ("Index out of bounds\n");
} else {
/* If the input was a valid audio stream index, set the current audio stream */
g_print ("Setting current audio stream to %d\n", index);
g_object_set (data->playbin, "current-audio", index, NULL);
}
}
g_free (str);
return TRUE;
}
Linux 安装库(Install GStreamer on Ubuntu or Debian)
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
其他的系统看官网教程。
执行编译
gcc playback-tutorial-1.c -o playback-tutorial-1 `pkg-config --cflags --libs gstreamer-1.0`
必需的库:gstreamer-1.0
四、代码解析
/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
GstElement *playbin; /* Our one and only element */
gint n_video; /* Number of embedded video streams */
gint n_audio; /* Number of embedded audio streams */
gint n_text; /* Number of embedded subtitle streams */
gint current_video; /* Currently playing video stream */
gint current_audio; /* Currently playing audio stream */
gint current_text; /* Currently playing subtitle stream */
GMainLoop *main_loop; /* GLib's Main Loop */
} CustomData;
我们像往常一样,将所有变量放在一个结构体中,以便传递给函数。在本教程中,我们需要每种流的数量以及当前正在播放的流。此外,我们将使用一种不同的机制来等待消息,以实现交互性,因此需要一个 GLib 的主循环对象。
/* playbin flags */
typedef enum {
GST_PLAY_FLAG_VIDEO = (1 << 0), /* We want video output */
GST_PLAY_FLAG_AUDIO = (1 << 1), /* We want audio output */
GST_PLAY_FLAG_TEXT = (1 << 2) /* We want subtitle output */
} GstPlayFlags;
稍后我们将设置一些 playbin 的标志位。我们希望有一个方便的枚举来轻松操作这些标志位,但由于 playbin 是一个插件而不是 GStreamer 核心的一部分,这个枚举对我们不可用。这里的“技巧”是简单地在代码中声明这个枚举,就像它在 playbin 文档中显示的那样:GstPlayFlags。GObject 允许自省,因此可以在运行时检索这些标志位的可能值,而无需使用这种技巧,但那样会更加繁琐。
/* Forward definition for the message and keyboard processing functions */
static gboolean handle_message (GstBus *bus, GstMessage *msg, CustomData *data);
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data);
这是我们将会使用的两个回调函数的前向声明。handle_message
用于处理 GStreamer 消息,正如我们已经看到的,而 handle_keyboard
用于处理键盘输入,因为本教程引入了有限的交互性。
我们跳过了管道的创建、playbin
的实例化以及通过 uri
属性指向测试媒体的部分。playbin
本身就是一个管道,在这种情况下,它是管道中唯一的元素,因此我们完全跳过了管道的创建,直接使用了 playbin
元素。
不过,我们关注一些 playbin
的其他属性:
/* Set flags to show Audio and Video but ignore Subtitles */
g_object_get (data.playbin, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO;
flags &= ~GST_PLAY_FLAG_TEXT;
g_object_set (data.playbin, "flags", flags, NULL);
playbin 的行为可以通过其 flags 属性进行更改,该属性可以是 GstPlayFlags 的任意组合。最有趣的标志位包括:
在我们的例子中,出于演示目的,我们启用了音频和视频,并禁用了字幕,其余标志位保留默认值(这就是为什么我们在使用 g_object_set() 覆盖之前,先用 g_object_get() 读取当前标志位的值)。
/* Set connection speed. This will affect some internal decisions of playbin */
g_object_set (data.playbin, "connection-speed", 56, NULL);
这个属性在本例中并不真正有用。connection-speed 用于告知 playbin 我们的网络连接的最大速度,以便在服务器上有多个版本的请求媒体时,playbin 可以选择最合适的版本。这通常与流媒体协议(如 hls 或 rtsp)结合使用。
我们可以通过一次调用 g_object_set() 来设置所有这些属性:
g_object_set (data.playbin, "uri", "https://gstreamer.freedesktop.org/data/media/sintel_cropped_multilingual.webm", "flags", flags, "connection-speed", 56, NULL);
这就是为什么 g_object_set() 需要以 NULL 作为最后一个参数。
/* Add a keyboard watch so we get notified of keystrokes */
#ifdef _WIN32
io_stdin = g_io_channel_win32_new_fd (fileno (stdin));
#else
io_stdin = g_io_channel_unix_new (fileno (stdin));
#endif
g_io_add_watch (io_stdin, G_IO_IN, (GIOFunc)handle_keyboard, &data);
这些行将回调函数连接到标准输入(键盘)。这里展示的机制是 GLib 特有的,与 GStreamer 没有直接关系,因此不需要深入讨论。应用程序通常有自己的处理用户输入的方式,GStreamer 除了在 [教程 17:DVD 播放] 中简要讨论的导航接口外,与此关系不大。
/* Create a GLib Main Loop and set it to run */
data.main_loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (data.main_loop);
为了允许交互性,我们将不再手动轮询 GStreamer 总线。相反,我们创建了一个 GMainLoop(GLib 主循环)并使用 g_main_loop_run() 运行它。此函数会阻塞,直到调用 g_main_loop_quit() 才会返回。在此期间,它会在适当的时间调用我们注册的回调函数:当总线上出现消息时调用 handle_message,当用户按下任何键时调用 handle_keyboard。
handle_message 中没有什么新内容,只是当管道进入 PLAYING 状态时,它会调用 analyze_streams 函数:
/* Extract some metadata from the streams and print it on the screen */
static void analyze_streams (CustomData *data) {
gint i;
GstTagList *tags;
gchar *str;
guint rate;
/* Read some properties */
g_object_get (data->playbin, "n-video", &data->n_video, NULL);
g_object_get (data->playbin, "n-audio", &data->n_audio, NULL);
g_object_get (data->playbin, "n-text", &data->n_text, NULL);
正如注释所说,此函数只是从媒体中收集信息并将其打印到屏幕上。视频、音频和字幕流的数量可以直接通过 n-video、n-audio 和 n-text 属性获取。
for (i = 0; i < data->n_video; i++) {
tags = NULL;
/* Retrieve the stream's video tags */
g_signal_emit_by_name (data->playbin, "get-video-tags", i, &tags);
if (tags) {
g_print ("video stream %d:\n", i);
gst_tag_list_get_string (tags, GST_TAG_VIDEO_CODEC, &str);
g_print (" codec: %s\n", str ? str : "unknown");
g_free (str);
gst_tag_list_free (tags);
}
}
现在,对于每个流,我们希望检索其元数据。元数据以标签的形式存储在 GstTagList 结构中,这是一个由名称标识的数据片段列表。可以通过 g_signal_emit_by_name() 恢复与流关联的 GstTagList,然后使用 gst_tag_list_get_* 函数(例如 gst_tag_list_get_string())提取单个标签。
这种检索标签列表的方式称为“动作信号”(Action Signal)。动作信号由应用程序发出到特定元素,然后该元素执行操作并返回结果。它们的行为类似于动态函数调用,其中类的方法通过其名称(信号的名称)而不是内存地址来标识。这些信号在文档中与常规信号一起列出,并标记为“动作”。例如,参见 playbin。
playbin 定义了 3 个动作信号来检索元数据:get-video-tags、get-audio-tags 和 get-text-tags。标签的名称是标准化的,列表可以在 GstTagList 文档中找到。在本例中,我们对流的 GST_TAG_LANGUAGE_CODE 和它们的 GST_TAG_*_CODEC(音频、视频或文本)感兴趣。
g_object_get (data->playbin, "current-video", &data->current_video, NULL);
g_object_get (data->playbin, "current-audio", &data->current_audio, NULL);
g_object_get (data->playbin, "current-text", &data->current_text, NULL);
一旦我们提取了所有需要的元数据,我们通过 playbin 的另外 3 个属性获取当前选择的流:current-video、current-audio 和 current-text。
始终检查当前选择的流并不要做任何假设是很有趣的。多种内部条件可能导致 playbin 在不同的执行中表现不同。此外,流的列出顺序可能会因运行而异,因此检查元数据以识别特定流变得至关重要。
/* Process keyboard input */
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data) {
gchar *str = NULL;
if (g_io_channel_read_line (source, &str, NULL, NULL, NULL) == G_IO_STATUS_NORMAL) {
int index = g_ascii_strtoull (str, NULL, 0);
if (index < 0 || index >= data->n_audio) {
g_printerr ("Index out of bounds\n");
} else {
/* If the input was a valid audio stream index, set the current audio stream */
g_print ("Setting current audio stream to %d\n", index);
g_object_set (data->playbin, "current-audio", index, NULL);
}
}
g_free (str);
return TRUE;
}
最后,我们允许用户切换正在播放的音频流。这个非常基本的函数只是从标准输入(键盘)读取一个字符串,将其解释为一个数字,并尝试设置 playbin 的 current-audio 属性(之前我们只读取过)。
请记住,切换不是立即的。一些先前解码的音频仍将通过管道流动,而新流变为活动状态并被解码。延迟取决于容器中流的特定多路复用方式,以及 playbin 为其内部队列选择的长度(这取决于网络条件)。
如果你运行此教程,你将能够在电影播放时通过按 0、1 或 2(然后按 ENTER)从一种语言切换到另一种语言。本教程到此结束。
五、结论
本教程展示了:
- playbin 的一些属性:flags、connection-speed、n-video、n-audio、n-text、current-video、current-audio 和 current-text。
- 如何使用 g_signal_emit_by_name() 检索与流关联的标签列表。
- 如何使用 gst_tag_list_get_string() 或 gst_tag_list_get_uint() 从列表中检索特定标签。
- 如何通过写入 current-audio 属性来切换当前音频。
下一个播放教程将展示如何处理字幕,无论是嵌入在容器中还是外部文件中。 请记住,本页附件中应包含教程的完整源代码以及构建它所需的任何辅助文件。