GStreamer笔记三: Time Management

主要是关于如何使用GStreamer中的时间相关工具,如查询Pipeline的位置或者持续时间等信息,以及寻找或者跳转到Stream中的不同位置——时间点的方法。

关于GstQuery

GstQuery是GStreamer中用于查询element和pad信息的一种机制。此篇所用例中首先需要询问是否支持寻找(seek),因为有一些源,如live stream,并不支持跳转。本例在确定支持跳转后,一旦电影播放10s后,就是用seek函数跳转到一个不同的时间点。
在之前的例子中,一旦建立其了Pipeline并开始运行,主函数所做的事仅仅是坐等接收来自总线(bus)的ERROR或者EOS信息。这里将会修改这个函数来周期性的唤醒并查询Pipeline的位置,所以可以将其输出在屏幕上。有点类似于一个媒体播放器定期更新用户接口。
最终,在stream持续时间改变后就会重新查询和更新。

Seeking示例

一个有关seeking时间点的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#include <gst/gst.h>
/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
GstElement *playbin; /* Our one and only element */
gboolean playing; /* Are we in the PLAYING state? */
gboolean terminate; /* Should we terminate execution? */
gboolean seek_enabled; /* Is seeking enabled for this media? */
gboolean seek_done; /* Have we performed the seek already? */
gint64 duration; /* How long does this media last, in nanoseconds */
} CustomData;
/* Forward definition of the message processing function */
static void handle_message (CustomData *data, GstMessage *msg);
int main(int argc, char *argv[]) {
CustomData data;
GstBus *bus;
GstMessage *msg;
GstStateChangeReturn ret;
data.playing = FALSE;
data.terminate = FALSE;
data.seek_enabled = FALSE;
data.seek_done = FALSE;
data.duration = GST_CLOCK_TIME_NONE;
/* 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://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
/* 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;
}
/* Listen to the bus */
bus = gst_element_get_bus (data.playbin);
do {
msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);
/* Parse message */
if (msg != NULL) {
handle_message (&data, msg);
} else {
/* We got no message, this means the timeout expired */
if (data.playing) {
gint64 current = -1;
/* Query the current position of the stream */
if (!gst_element_query_position (data.playbin, GST_FORMAT_TIME, &current)) {
g_printerr ("Could not query current position.\n");
}
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
if (!gst_element_query_duration (data.playbin, GST_FORMAT_TIME, &data.duration)) {
g_printerr ("Could not query current duration.\n");
}
}
/* Print current position and total duration */
g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration));
/* If seeking is enabled, we have not done it yet, and the time is right, seek */
if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
g_print ("\nReached 10s, performing seek...\n");
gst_element_seek_simple (data.playbin, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
data.seek_done = TRUE;
}
}
}
} while (!data.terminate);
/* Free resources */
gst_object_unref (bus);
gst_element_set_state (data.playbin, GST_STATE_NULL);
gst_object_unref (data.playbin);
return 0;
}
static void handle_message (CustomData *data, GstMessage *msg) {
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);
data->terminate = TRUE;
break;
case GST_MESSAGE_EOS:
g_print ("End-Of-Stream reached.\n");
data->terminate = TRUE;
break;
case GST_MESSAGE_DURATION:
/* The duration has changed, mark the current one as invalid */
data->duration = GST_CLOCK_TIME_NONE;
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)) {
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
/* Remember whether we are in the PLAYING state or not */
data->playing = (new_state == GST_STATE_PLAYING);
if (data->playing) {
/* We just moved to PLAYING. Check if seeking is possible */
GstQuery *query;
gint64 start, end;
query = gst_query_new_seeking (GST_FORMAT_TIME);
if (gst_element_query (data->playbin, query)) {
gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
if (data->seek_enabled) {
g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
GST_TIME_ARGS (start), GST_TIME_ARGS (end));
} else {
g_print ("Seeking is DISABLED for this stream.\n");
}
}
else {
g_printerr ("Seeking query failed.");
}
gst_query_unref (query);
}
}
} break;
default:
/* We should not reach here */
g_printerr ("Unexpected message received.\n");
break;
}
gst_message_unref (msg);
}

该段代码会打开并显示一个伴有音频的一个电影,由于媒体来自于网络,所以窗口可能需要一会才能显示出来,具体取决于网速。并在电影十秒钟后跳转到一个新的位置。

代码分析

首先建立一个可以传递给其他函数的含有所有信息的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
GstElement *playbin; /* Our one and only element */
gboolean playing; /* Are we in the PLAYING state? */
gboolean terminate; /* Should we terminate execution? */
gboolean seek_enabled; /* Is seeking enabled for this media? */
gboolean seek_done; /* Have we performed the seek already? */
gint64 duration; /* How long does this media last, in nanoseconds */
} CustomData;
/* Forward definition of the message processing function */
static void handle_message (CustomData *data, GstMessage *msg);

这里由于信息处理代码会变得越来越大,因此将其移到了handle_message函数中。
然后建立了一个包含单个元素(playbin)的Pipeline,然而playbin本身就是一个Pipeline,而且这种情况下,他是Pipeline中唯一的element,所以直接使用playbin。
这里跳过一些细节:该clip的URI通过URI属性给playbin,并将Pipeline设置为播放状态。

1
2
msg = gst_bus_timed_pop_filtered (bus, 100 * GST_MSECOND,
GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION);

之前没有给gst_bus_timed_pop_filtered函数提供超时参数,因此它在收到消息前不会返回。这里使用100ms的超时,所以如果在0.1s内没有收到任何消息,函数将返回NULL,并通过这个方法来更新UI。
需要注意的是,所有的超时时间必须指定为GstClockTime,所以都是以纳秒为单位的,表示不同时间单位的数字应该乘以宏如GST_SECOND或GST_MSECOND。也能使代码更具可读性。
如果收到消息,则通过handle_message函数处理它。否则刷新用户接口(UI)。

用户接口刷新

1
2
/* We got no message, this means the timeout expired */
if (data.playing) {

如果Pipeline处于PLAYING状态,则刷新屏幕。在非PLAYING状态下我们不想做任何事,因为大多数查询都会失败。
这里的刷新率大约是每秒10次,对于我们的UI来说已经足够。同时将在屏幕上打印出当前媒体的位置以便了解管道查询。这涉及到几个步骤,之后再说,但是位置和持续时间是比较常见的查询,所以GStreamer提供了更容易的现成的备选方案:

1
2
3
4
/* Query the current position of the stream */
if (!gst_element_query_position (data.pipeline, GST_FORMAT_TIME, &current)) {
g_printerr ("Could not query current position.\n");
}

其中,gst_element_query_position函数隐藏了查询对象的管理并直接提供结果。

1
2
3
4
5
6
/* If we didn't know it yet, query the stream duration */
if (!GST_CLOCK_TIME_IS_VALID (data.duration)) {
if (!gst_element_query_duration (data.pipeline, GST_FORMAT_TIME, &data.duration)) {
g_printerr ("Could not query current duration.\n");
}
}

其中,gst_element_query_duration用于函数查询流的长度。

1
2
3
/* Print current position and total duration */
g_print ("Position %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT "\r",
GST_TIME_ARGS (current), GST_TIME_ARGS (data.duration));

这里使用GST_TIME_FORMATGST_TIME_ARGS宏来提供对GStreamer时间的对用户友好的表示。

1
2
3
4
5
6
7
/* If seeking is enabled, we have not done it yet, and the time is right, seek */
if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) {
g_print ("\nReached 10s, performing seek...\n");
gst_element_seek_simple (data.pipeline, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND);
data.seek_done = TRUE;
}

现在在管道上调用gst_element_seek_simple函数进行查找,这种方法的好处是隐藏了许多复杂的问题。

参数回顾

GST_FORMAT_TIME: 表示以时间单位指定目标位置,其他的查找格式使用不同的单位。
然后是一些GstSeekFlags,其中常见的一些如下:

  • GST_SEEK_FLAG_FLUSH: 会在seek之前丢弃当前Pipeline中的所有数据。当Pipeline被重新填充且新的数据开始出现时,可能会暂停一下,但是极大增加了应用程序的响应能力。因为如果没有这个标志,旧数据可能会一直显示,直到新的数据出现在Pipeline末端。
  • GST_SEEK_FLAG_KEY_UNIT: 对于大多数编码视频流,寻找到任意位置是不可能的,因为仅限于某些称为关键帧的帧。使用这个标识时,seek操作实际上会移动到最近的关键帧并开始产生数据。不使用这个标志的话,Pipeline将会在内部移动到最近的关键帧(没有其他选择),但是直到到达要求的位置才会显示数据。后面一种方法更精确,但是可能需要更长的时间。
  • GST_SEEK_FLAG_ACCURATE: 查找精度标识。在一些媒体clips没有提供足够的索引信息时,查找任意位置是耗时的。在这些情况下,GStreamer通常会估计要寻找的位置,而且完成的比较好。如果这个精度对于你的情况来说不够好(看到的不是要求的精确时间),则提供该标识。但是值得注意的是,计算寻找位置可能花费更长时间(在一些文件中很长)。
    最后提供了查找的位置。因为要求了GST_FORMAT_TIME,所以值需要用纳秒表示。为了简单起见,用秒表示时间并乘以GST_SECOND。

信息处理

handle_message函数通过管道总线(Pipeline’s bus)处理接收到的所有信息。ERROR和EOS处理之前已经说过了,所以直接跳到感兴趣的部分:

1
2
3
4
case GST_MESSAGE_DURATION:
/* The duration has changed, mark the current one as invalid */
data->duration = GST_CLOCK_TIME_NONE;
break;

该消息在流的持续时间变化时会发送给总线。这里简单地将当前持续时间标记为无效,所以稍后会被重新查询。

1
2
3
4
5
6
7
8
9
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->pipeline)) {
g_print ("Pipeline state changed from %s to %s:\n",
gst_element_state_get_name (old_state), gst_element_state_get_name (new_state));
/* Remember whether we are in the PLAYING state or not */
data->playing = (new_state == GST_STATE_PLAYING);

在PAUSED和PLAYING状态下,搜索和查询操作通常只会得到一个有效的回复,因为所有元素都有机会接收信息并进行自我配置。这里使用playing变量来跟踪管道是否处于PLAYING状态。如果刚刚进入了PLAYING状态,则执行第一次查询。然后询问Pipeline是否允许在此流上进行搜索:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (data->playing) {
/* We just moved to PLAYING. Check if seeking is possible */
GstQuery *query;
gint64 start, end;
query = gst_query_new_seeking (GST_FORMAT_TIME);
if (gst_element_query (data->pipeline, query)) {
gst_query_parse_seeking (query, NULL, &data->seek_enabled, &start, &end);
if (data->seek_enabled) {
g_print ("Seeking is ENABLED from %" GST_TIME_FORMAT " to %" GST_TIME_FORMAT "\n",
GST_TIME_ARGS (start), GST_TIME_ARGS (end));
} else {
g_print ("Seeking is DISABLED for this stream.\n");
}
}
else {
g_printerr ("Seeking query failed.");
}
gst_query_unref (query);
}

这里gst_query_new_seeking函数使用GST_FORMAT_TIME格式创建了一个新的“seeking”类型的查询对象。这表明我们有兴趣通过指定想要移动的新时间来寻找。也可以使用GST_FORMAT_BYTE格式,然后在源文件中查找特定的字节位置,不过通常不太实用。
然后gst_element_query函数将查询对象传递给Pipeline,并将结果存储在同一个查询中,因此可以通过gst_query_parse_seeking函数方便的检索。它提取出一个表示是否允许查询的布尔值和可查找的范围。
最后在完成查询后释放查询对象。
通过这些过程基本上可以建立一个媒体播放器,根据当前流的位置定期更新一个滑块,并允许通过滑块进行搜索或跳转。

孤芳自赏,不必捧场。
分享