摘要
在常见的媒体播放器中,通常可以看到快进,快退,慢放等功能,这部分功能被称为“特技模式(Trick Mode)”,这些模式有个共同点:都通过修改播放的速率来达到相应的目的。 本文将介绍如何通过GStreamer去实现快进,快退,慢放以及单帧播放。
GStreamer Seek与Step事件
快进(Fast-Forward),快退(Fast-Rewind)和慢放(Slow-Motion)都是通过修改播放的速率来达到相应的目的。在GStreamer中,将1倍速作为正常的播放速率,将大于1倍速的2倍,4倍,8倍等倍速称为快进,慢放则是播放速率的绝对值小于1倍速,当播放速率小于0时,则进行倒放。
在GStreamer中,我们通过seek与step事件来控制Element的播放速率及区域。Step事件允许跳过指定的区域并设置后续的播放速率(此速率必须大于0)。Seek事件允许跳转到播放文件中的的任何位置,并且播放速率可以大于0或小于0.
在播放时间控制中,我们使用gst_element_seek_simple 来快速的跳转到指定的位置,此函数是对seek事件的封装。实际使用时,我们首先需要构造一个seek event,设置seek的绝对起始位置和停止位置,停止位置可以设置为0,这样会执行seek的播放速率直到结束。同时可以支持按buffer的方式进行seek,以及设置不同的标志指定seek的行为。
Step事件相较于Seek事件需要更少的参数,更易用于修改播放速率,但是不够灵活。Step事件只会作用于最终的sink,Seek事件则可以作用于Pipeline中所有的Element。Step操作的效率高于Seek。
在GStreamer中,单帧播放(Frame Stepping)与快进相同,也是通过事件实现。单帧播放通常在暂停的状态下,构造并发送step event每次播放一帧。
需要注意的是,seek event需要直接作用于sink element(eg: audio sink或video sink),如果直接将seek event作用于Pipeline,Pipeline会自动将事件转发给所有的sink,如果有多个sink,就会造成多次seek。通常是先获取Pipeline中的video-sink或audio-sink,然后发送seek event到指定的sink,完成seek的操作。 Seek时间的构造及发送示例如下:
GstEvent *event;gboolean result;...// construct a seek event to play the media from second 2 to 5, flush// the pipeline to decrease latency.event = gst_event_new_seek (1.0,GST_FORMAT_TIME,GST_SEEK_FLAG_FLUSH,GST_SEEK_TYPE_SET, 2 * GST_SECOND,GST_SEEK_TYPE_SET, 5 * GST_SECOND);...result = gst_element_send_event (video_sink, event);if (!result)g_warning ("seek failed");...
示例代码
下面通过一个完整的示例,来查看GStreamer是如何通过seek和step达到相应的播放速度。
#include <string.h> #include <stdio.h> #include <gst/gst.h>typedef struct _CustomData {GstElement *pipeline;GstElement *video_sink;GMainLoop *loop;gboolean playing; /* Playing or Paused */gdouble rate; /* Current playback rate (can be negative) */ } CustomData;/* Send seek event to change rate */ static void send_seek_event (CustomData * data) {gint64 position;GstEvent *seek_event;/* Obtain the current position, needed for the seek event */if (!gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position)) {g_printerr ("Unable to retrieve current position.\n");return;}/* Create the seek event */if (data->rate > 0) {seek_event =gst_event_new_seek (data->rate, GST_FORMAT_TIME,GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET,position, GST_SEEK_TYPE_END, 0);} else {seek_event =gst_event_new_seek (data->rate, GST_FORMAT_TIME,GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE, GST_SEEK_TYPE_SET, 0,GST_SEEK_TYPE_SET, position);}if (data->video_sink == NULL) {/* If we have not done so, obtain the sink through which we will send the seek events */g_object_get (data->pipeline, "video-sink", &data->video_sink, NULL);}/* Send the event */gst_element_send_event (data->video_sink, seek_event);g_print ("Current rate: %g\n", data->rate); }/* 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) {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 '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);}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");break;case 'q':g_main_loop_quit (data->loop);break;default:break;}g_free (str);return TRUE; }int main (int argc, char *argv[]) {CustomData data;GstStateChangeReturn ret;GIOChannel *io_stdin;/* Initialize GStreamer */gst_init (&argc, &argv);/* Initialize our data structure */memset (&data, 0, sizeof (data));/* 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"" 'D' to toggle playback direction\n"" 'N' to move to next frame (in the current direction, better in PAUSE)\n"" 'Q' to quit\n");/* Build the pipeline */data.pipeline =gst_parse_launch("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm",NULL);/* Add a keyboard watch so we get notified of keystrokes */ #ifdef G_OS_WIN32io_stdin = g_io_channel_win32_new_fd (fileno (stdin)); #elseio_stdin = g_io_channel_unix_new (fileno (stdin)); #endifg_io_add_watch (io_stdin, G_IO_IN, (GIOFunc) handle_keyboard, &data);/* Start 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;}data.playing = TRUE;data.rate = 1.0;/* Create a GLib Main Loop and set it to run */data.loop = g_main_loop_new (NULL, FALSE);g_main_loop_run (data.loop);/* Free resources */g_main_loop_unref (data.loop);g_io_channel_unref (io_stdin);gst_element_set_state (data.pipeline, GST_STATE_NULL);if (data.video_sink != NULL)gst_object_unref (data.video_sink);gst_object_unref (data.pipeline);return 0; }
通过下面的命令编译即可得到可执行文件,在终端输入相应指令可修改播放速率。
gcc basic-tutorial-7.c -o basic-tutorial-7 `pkg-config --cflags --libs gstreamer-1.0`
源码分析
本例中,Pipeline的创建与其他示例相同,通过playbin播放文件,采用GLib的I/O接口来处理键盘输入。
/* 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) {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;
在终端输入P时,使用gst_element_set_state ()设置播放状态。
case 's':if (g_ascii_isupper (str[0])) {data->rate *= 2.0;} else {data->rate /= 2.0;}send_seek_event (data);break; case 'd':data->rate *= -1.0;send_seek_event (data);break;
通过S和s增加和降低播放速度,d用于改变播放方向(倒放),这里在修改rate后,调用send_seek_event实现真正的处理。
/* Send seek event to change rate */ static void send_seek_event (CustomData *data) {gint64 position;GstEvent *seek_event;/* Obtain the current position, needed for the seek event */if (!gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position)) {g_printerr ("Unable to retrieve current position.\n");return;}
这个函数会构造一个SeekEvent发送到Pipeline以调节速率。因为Seek Event会跳转到指定的位置,但我们在此例汇总只想改变速率,不跳转到其他位置,所以首先通过gst_element_query_position ()获取当前的播放位置。
/* Create the seek event */ if (data->rate > 0) {seek_event = gst_event_new_seek (data->rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE,GST_SEEK_TYPE_SET, position, GST_SEEK_TYPE_END, 0); } else {seek_event = gst_event_new_seek (data->rate, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE,GST_SEEK_TYPE_SET, 0, GST_SEEK_TYPE_SET, position); }
通过gst_event_new_seek()创建SeekEvent,设置新的rate,flag,起始位置,结束位置。需要注意的是,起始位置需要小于结束位置。
if (data->video_sink == NULL) {/* If we have not done so, obtain the sink through which we will send the seek events */g_object_get (data->pipeline, "video-sink", &data->video_sink, NULL); } /* Send the event */ gst_element_send_event (data->video_sink, seek_event);
正如上文提到的,为了避免Pipeline执行多次的seek,我们在此处获取video-sink,并向其发送SeekEvent。我们直到执行Seek时才获取video-sink是因为实际的sink有可能会根据不同的媒体类型,在PLAYING状态时才创建。
以上部分内容就是速率的修改,关于单帧播放的情况,实现方式更加简单:
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);}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");break;
我们通过gst_event_new_step()创建了StepEvent,并指定只跳转一帧,且不改变当前速率。单帧播放通常都是先暂停,然后再进行单帧播放。
以上就是通过GStreamer实现播放速率的控制,实际中,有些Element对倒放支持不是很好,不能达到理想的效果。
总结
通过本文我们掌握了:
- 如何通过gst_event_new_seek()构造SeekEvent,通过gst_element_send_event()发送到sink改变速率。
- 如何通过gst_event_new_step()实现单帧播放。
引用
https://gstreamer.freedesktop.org/documentation/tutorials/basic/playback-speed.html?gi-language=c
https://gstreamer.freedesktop.org/documentation/additional/design/seeking.html?gi-language=c
https://gstreamer.freedesktop.org/documentation/additional/design/framestep.html?gi-language=c