系列文章目录
- GStreamer 简明教程(一):环境搭建,运行 Basic Tutorial 1 Hello world!
- GStreamer 简明教程(二):基本概念介绍,Element 和 Pipeline
- GStreamer 简明教程(三):动态调整 Pipeline
- GStreamer 简明教程(四):Seek 以及获取文件时长
- GStreamer 简明教程(五):Pad 相关概念介绍,Pad Capabilities/Templates
- GStreamer 简明教程(六):利用 Tee 复制流数据,巧用 Queue 实现多线程
- GStreamer 简明教程(七):实现管道的动态数据流
- GStreamer 简明教程(八):常用工具介绍
文章目录
- 系列文章目录
- 前言
- Seek Events
- Step Events
- Show me the code
- 参考
前言
本文对 Basic tutorial 13: Playback speed 内容进行说明,重点是理解 GStreamer 中 seek events 和 step events。
Seek Events
在看视频的过程中,用户拖动滑动条跳转到指定播放位置是基本需求,在 GStreamer 中我们可以像 pipeline 发送一个 seek event 来实现。
GStreamer 中创建 seek events 接口原型如下:
GstEvent *
gst_event_new_seek (gdouble rate, GstFormat format, GstSeekFlags flags,
GstSeekType start_type, gint64 start, GstSeekType stop_type, gint64 stop);
让我详细解释各个参数:
-
rate
(gdouble):- 播放速率倍数
- 1.0 表示正常速度
- 2.0 表示双倍速度
- 负值表示倒放
- 0.5 表示半速播放
-
format
(GstFormat):- 指定查找的格式单位
- 常用值包括:
- GST_FORMAT_TIME: 时间格式(纳秒)
- GST_FORMAT_BYTES: 字节格式
- GST_FORMAT_DEFAULT: 默认格式(如对音频来说是采样数)
-
flags
(GstSeekFlags):- seek 操作的标志位组合
- 常用标志:
- GST_SEEK_FLAG_FLUSH: 清空管道中的数据
- GST_SEEK_FLAG_ACCURATE: 精确定位
- GST_SEEK_FLAG_KEY_UNIT: 定位到关键帧
- GST_SEEK_FLAG_SEGMENT: 执行片段播放
-
start_type
(GstSeekType):- 定义如何解释 start 参数
- 常用值:
- GST_SEEK_TYPE_NONE: 忽略起始位置
- GST_SEEK_TYPE_SET: 绝对位置
- GST_SEEK_TYPE_CUR: 相对当前位置
-
start
(gint64):- 开始位置的值
- 具体含义取决于 format 和 start_type
-
stop_type
(GstSeekType):- 定义如何解释 stop 参数
- 与 start_type 使用相同的值
-
stop
(gint64):- 结束位置的值
- 具体含义取决于 format 和 stop_type
使用示例:
// 跳转到视频的 2 秒位置
GstEvent *seek_event = gst_event_new_seek(
1.0, // 正常播放速度
GST_FORMAT_TIME, // 使用时间格式
GST_SEEK_FLAG_FLUSH, // 清空管道
GST_SEEK_TYPE_SET, // 绝对位置
2 * GST_SECOND, // 开始位置:2秒
GST_SEEK_TYPE_NONE, // 不设置结束位置
0 // 结束位置(未使用)
);
在 GStreamer - Seeking 中对一些细节内容做了补充,大家有兴趣可以自己过一遍,我这里罗列几个我感兴趣的点:
- Seek 可以指定一个时间段进行播放,如果 flag 中不包含 GST_SEEK_FLAG_SEGMENT,那么片段播放结束后返回
GST_MESSAGE_EOS
(“message::eos”),如果包含那么返回的是GST_MESSAGE_SEGMENT_DONE
(“mesaage::segment-done”)。我们可以监听GST_MESSAGE_SEGMENT_DONE
消息,重新再发送一个 seek 时间以便循环播放某个片段。 - 对于一个 Pipeline ,我们向其 Sink 节点发送 seek 事件即可,seek 事件会通过管道向上游传播,直到到达源元素(source element)。你当然可以向一个 bin 发送 seek 事件,默认情况下它的所有 sink 节点都会收到 seek 事件。
- Trick mode flags 可以跳过一些帧,这在某些场景下是有用的,例如:
- GST_SEEK_FLAG_TRICKMODE_KEY_UNITS: 只解码/显示关键帧
- GST_SEEK_FLAG_TRICKMODE_FORWARD_PREDICTED: 跳过 B 帧
- GST_SEEK_FLAG_TRICKMODE_NO_AUDIO: 不解码音频
Step Events
在某些场景我们需要精细控制回放,比如在视频编辑软件中进行逐帧查看,或者在某些诊断和测试应用中使用。这时候我们使用 step event 来控制精确的步进播放,即逐帧或逐块地处理媒体流。创建 step event 接口原型如下:
GstEvent *
gst_event_new_step (GstFormat format, guint64 amount, gdouble rate, gboolean flush, gboolean intermediate)
函数参数说明:
-
GstFormat format
: 指定步进的格式,常见的格式包括:- GST_FORMAT_TIME: 时间格式(纳秒)
- GST_FORMAT_BUFFERS: 缓冲区数量
- GST_FORMAT_DEFAULT: 默认格式(帧数)
-
guint64 amount
: 指定步进的数量(根据 format 参数解释)- 如果 format 是 GST_FORMAT_TIME,则表示纳秒
- 如果 format 是 GST_FORMAT_BUFFERS,则表示缓冲区数量
-
gdouble rate
: 指定步进的速率- 1.0 表示正常速度
-
1.0 表示快进
- <1.0 表示慢放
-
gboolean flush
: 是否在步进前清空管道- TRUE: 清空管道中的数据
- FALSE: 保留管道中的数据
-
gboolean intermediate
: 是否为中间步进- TRUE: 表示这是一系列步进中的一个
- FALSE: 表示这是单独的步进
使用示例:
// 创建一个步进事件,向前移动 1 秒(1000000000 纳秒)
GstEvent *step_event = gst_event_new_step (
GST_FORMAT_TIME, // 使用时间格式
1000000000, // 1秒 (纳秒单位)
1.0, // 正常速度
TRUE, // 清空管道
FALSE // 非中间步进
);
// 创建一个步进事件,向前移动 1 帧
GstEvent *step_event = gst_event_new_step (
GST_FORMAT_BUFFERS, // 使用时间格式
1, // 1 帧
1.0, // 正常速度
TRUE, // 清空管道
FALSE // 非中间步进
);
主要用途:
- 实现帧步进功能(逐帧播放)
- 实现快进/跳跃播放,例如快进 5s
- 在视频编辑应用中进行精确定位
- 用于调试和分析媒体流
通常我们会在视频暂停的时候发送 step event 进行跳帧,另外注意到 step event 也有一个 rate 参数,实际体验下来这个 rate 并不是播放速度,而是计算步进的倍率。比如 amount = 1s ,rate = 2.0 时,发送这个 step event 后实际快进了 2s。
此外,step event 只会影响 sink 节点元素,而 seek event 则会影响整个 pipeline,每个元素都会对 seek event 做出反应,但 step event 速度更快。当你只对 video sink 节点进行快进后,你会发现音画发生了不同步,因为 audio sink 节点没有快进,因此可以同时对 video sink 和 audio sink 发送 step event 来避免音画不同步的问题。
Show me the code
本文的代码在 Basic tutorial 13: Playback speed 基础上做了一些修改和添加,主要是为了说明循环播放和跳转下一个 5s 要如何实现。详细代码参考 my_examples/basic-tutorial-13.c
这里对代码中重要部分进行说明
/* Print usage map */
g_print ("USAGE: Choose one of the following options, then press enter:\n"
" 'P' to toggle between PAUSE and PLAY\n"
" 'S' to increase playback speed, 's' to decrease playback speed\n"
" 'k' to seek with segment [0, 10] and play looping\n"
" 'D' to toggle playback direction\n"
" 'N' to move to next frame (in the current direction, better in PAUSE)\n"
" 'n' to move to 5s \n"
" 'Q' to quit\n");
这里对程序功能进行说明,主要包含以下功能:
- ‘P’: 播放/暂停切换
- ‘S/s’: 调整播放速度(S增加,s减少)
- ‘k’: 在0-10秒区间循环播放
- ‘D’: 切换播放方向
- ‘N’: 下一帧(在当前方向,暂停状态下效果更好)
- ‘n’: 跳转5秒
- ‘Q’: 退出程序
注意,你需要在命令的执行窗口输入这些参数,而不是在播放窗口。
data.pipeline =
gst_parse_launch
("playbin uri=file:///Users/user/Documents/work/测试视频/video_1280x720_30fps_180sec.mp4",
NULL);
上述代码中构建了一个 pipeline,播放本地文件,文件路径根据你自己电脑上的情况进行修改即可。
g_io_add_watch (io_stdin, G_IO_IN, (GIOFunc) handle_keyboard, &data);
这行代码是在设置键盘输入的监听器(事件处理),具体分解如下:
g_io_add_watch 是 GLib 库中的一个函数,用于添加对 I/O 事件的监听。它有三个主要参数:
- io_stdin:输入源(这里是标准输入,即键盘输入)
- G_IO_IN:表示监听输入事件
- handle_keyboard:回调函数,当有键盘输入时会调用这个函数
- &data:传递给回调函数的数据
当用户在键盘上输入内容时,handle_keyboard 函数会被触发,从而处理用户的输入命令。这是实现交互式命令行界面的关键部分,使程序能够响应用户的键盘输入。那么看看各个事件都在做什么。
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) {
return TRUE;
}
switch (g_ascii_tolower (str[0])) {
case 'p':
data->playing = !data->playing;
gst_element_set_state (data->pipeline,
data->playing ? GST_STATE_PLAYING : GST_STATE_PAUSED);
g_print ("Setting state to %s\n", data->playing ? "PLAYING" : "PAUSE");
break;
case 's':
if (g_ascii_isupper (str[0])) {
data->rate *= 2.0;
} else {
data->rate /= 2.0;
}
send_seek_event (data);
break;
case 'k':
send_segment_seek_event(data);
break;
case 'd':
data->rate *= -1.0;
send_seek_event (data);
break;
case 'n':
if (data->video_sink == NULL) {
/* If we have not done so, obtain the sink through which we will send the step events */
g_object_get (data->pipeline, "video-sink", &data->video_sink, NULL);
}
if(data->audio_sink == NULL) {
g_object_get (data->pipeline, "audio-sink", &data->audio_sink, NULL);
}
if (g_ascii_isupper (str[0])) {
gst_element_send_event (data->video_sink,
gst_event_new_step (GST_FORMAT_BUFFERS, 1, ABS (data->rate), TRUE,
FALSE));
g_print ("Stepping one frame\n");
}else {
gst_element_send_event (data->video_sink,
gst_event_new_step (GST_FORMAT_TIME, 5 * GST_SECOND, ABS (data->rate), TRUE,
FALSE));
gst_element_send_event (data->audio_sink,
gst_event_new_step (GST_FORMAT_TIME, 5 * GST_SECOND, ABS (data->rate), TRUE,
FALSE));
g_print ("Stepping 5s\n");
}
break;
case 'q':
g_main_loop_quit (data->loop);
break;
default:
break;
}
这是一个键盘事件处理函数,针对不同的按键执行不同的操作:
- ‘p/P’:播放/暂停切换
- 切换 data->playing 的状态
- 通过 gst_element_set_state 设置播放器状态(PLAYING 或 PAUSED)
- ‘s/S’:调整播放速度
- 大写’S’:速度翻倍(速率×2)
- 小写’s’:速度减半(速率÷2)
- 调用 send_seek_event 应用新的速率
- ‘k/K’:设置片段循环播放
- 调用 send_segment_seek_event 进行片段循环播放
- ‘d/D’:改变播放方向
- 将速率乘以-1(改变正负号)
- 调用 send_seek_event 应用新的方向
- ‘n/N’:帧进/时间进
- 首次使用时获取视频和音频接收器(sink)
- 大写’N’:前进一帧(gst_event_new_step with GST_FORMAT_BUFFERS)
- 小写’n’:前进5秒(gst_event_new_step with GST_FORMAT_TIME)
- ‘q/Q’:退出程序
- 调用 g_main_loop_quit 退出主循环
GstBus *bus = gst_element_get_bus (data.pipeline);
gst_bus_add_signal_watch(bus);
g_signal_connect(G_OBJECT(bus), "message::segment-done", (GCallback)segment_done_callback, &data);
为了实现片段循环播放,我们这里监听 “message::segment-done” 信号,如果收到了信息,则调用 segment_done_callback
static void segment_done_callback(GstBus *bus, GstMessage *msg, CustomData *data) {
g_print("Segment done, seeking again\n");
// 重新执行 seek
gst_element_seek(data->pipeline,
1.0,
GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_SEGMENT,
GST_SEEK_TYPE_SET,
0 * GST_SECOND,
GST_SEEK_TYPE_SET,
5 * GST_SECOND);
data->segment_loop_count++;
g_print("segment loop count:%d\n", data->segment_loop_count);
}
segment_done_callback
函数中,我们重新发送一个 seek event,范围是 [0, 5s],并且仍然带着 GST_SEEK_FLAG_SEGMENT
,如此一来当播放片段结束后, GStreamer 会发送一个 “message::segment-done” 信号,然后又会触发回调函数,如此一直循环。
其他代码大家都比较熟悉了,这里就不再赘述了。
参考
- Basic tutorial 13: Playback speed
- GStreamer - Seeking
- my_examples/basic-tutorial-13.c