摘要
我们把直接从网络播放一个媒体文件的方式称为在线播放(Online Streaming),我们已经在以往的例子中体验了GStreamer的在线播放功能,当我们指定播放URI为 http:// 时,GStreamer内部会自动通过网络获取媒体数据。在今天的示例中,我们将进一步了解如何处理由网络问题导致的视频缓冲及时钟丢失的问题。
在线播放
在我们进行在线播放时,我们会将收到的媒体数据立即进行解码并送入显示队列显示。当网络不理想时,我们通常不能及时的接收数据,显示队列中的数据会被耗尽而不能得到及时的补充,这会导致播放出现卡顿。
一种通用的处理方式是创建一个缓冲队列,在队列的数据量达到一定阀值时才进行播放,这样会导致起播时间会有一定的延迟,但会使后续的播放更加流畅,避免了因部分数据无法及时到达造成的停顿。
GStreamer框架已经实现了缓冲队列,但在以往的示例中我们并没有使用其相关的功能。某些Element(例如playbin中使用的queue2及multiqueue)可以创建缓冲队列,并在超过/低于指定的数据阀值时产生相应的信号。应用程序可以监听此类信号,在数据不足时(buffer值小于100%)主动暂停播放,在数据充足时恢复播放。
为了达到多个Sink的同步(例如音视频同步),我们需要使用一个全局的参考时钟,GStreamer会在播放时自动选取一个时钟。在某些网络在线播放的情况下原有时钟会失效,我们需要重新选取一个参考时钟。例如,RTP Source切换流或者改变输出设备。
在参考时钟丢失时,GStreamer框架会产生相应的事件,应用层需要对其作出响应,由于GStreamer在进入PLAYING状态时会自动选取参考时钟,所以我们只需在收到时钟丢失事件时将Pipeline的状态切换到PUASED,再切换到PLAYING即可。
示例代码
#include <gst/gst.h> #include <string.h> typedef struct _CustomData { gboolean is_live; GstElement *pipeline; GMainLoop *loop; } CustomData; static void cb_message (GstBus *bus, GstMessage *msg, CustomData *data) { switch (GST_MESSAGE_TYPE (msg)) { case GST_MESSAGE_ERROR: { GError *err; gchar *debug; gst_message_parse_error (msg, &err, &debug); g_print ("Error: %s ", err->message); g_error_free (err); g_free (debug); gst_element_set_state (data->pipeline, GST_STATE_READY); g_main_loop_quit (data->loop); break; } case GST_MESSAGE_EOS: /* end-of-stream */ gst_element_set_state (data->pipeline, GST_STATE_READY); g_main_loop_quit (data->loop); break; case GST_MESSAGE_BUFFERING: { gint percent = 0; /* If the stream is live, we do not care about buffering. */ if (data->is_live) break; gst_message_parse_buffering (msg, &percent); g_print ("Buffering (%3d%%) ", percent); /* Wait until buffering is complete before start/resume playing */ if (percent < 100) gst_element_set_state (data->pipeline, GST_STATE_PAUSED); else gst_element_set_state (data->pipeline, GST_STATE_PLAYING); break; } case GST_MESSAGE_CLOCK_LOST: /* Get a new clock */ gst_element_set_state (data->pipeline, GST_STATE_PAUSED); gst_element_set_state (data->pipeline, GST_STATE_PLAYING); break; default: /* Unhandled message */ break; } } int main(int argc, char *argv[]) { GstElement *pipeline; GstBus *bus; GstStateChangeReturn ret; GMainLoop *main_loop; CustomData data; /* Initialize GStreamer */ gst_init (&argc, &argv); /* Initialize our data structure */ memset (&data, 0, sizeof (data)); /* Build the pipeline */ pipeline = gst_parse_launch ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL); bus = gst_element_get_bus (pipeline); /* Start playing */ ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr ("Unable to set the pipeline to the playing state. "); gst_object_unref (pipeline); return -1; } else if (ret == GST_STATE_CHANGE_NO_PREROLL) { data.is_live = TRUE; } main_loop = g_main_loop_new (NULL, FALSE); data.loop = main_loop; data.pipeline = pipeline; gst_bus_add_signal_watch (bus); g_signal_connect (bus, "message", G_CALLBACK (cb_message), &data); g_main_loop_run (main_loop); /* Free resources */ g_main_loop_unref (main_loop); gst_object_unref (bus); gst_element_set_state (pipeline, GST_STATE_NULL); gst_object_unref (pipeline); return 0; }
将代码保存为basic-tutorial-10.c,执行下列命令编译可得到运行程序。
gcc basic-tutorial-10.c -o basic-tutorial-10 `pkg-config --cflags --libs gstreamer-1.0`
由于通过网络获取数据,视频显示窗口可能会有短暂的等待时间,在终端的buffering达到100%时才会开始播放。
源码分析
GStreamer Pipeline相关的处理与以往示例相同,我们只关注在线播放相关的处理。
/* Start playing */ ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr ("Unable to set the pipeline to the playing state. "); gst_object_unref (pipeline); return -1; } else if (ret == GST_STATE_CHANGE_NO_PREROLL) { data.is_live = TRUE; }
对于实时的媒体流,我们无法将其设置为PAUSED状态,所以在通过gst_element_set_state 将Pipeline设置成PAUSED状态时,我们会收到GST_STATE_CHANGE_NO_PREROLL。正常情况会返回GST_STATE_CHANGE_SUCCESS 。由于GStreamer的状态会依次从NULL, READY, PAUSED转换为PLAYING,所以我们将状态设置为PLAYING时,也会收到NO_PREROLL返回值。
这里设置is_live标识是因为我们不对其进行缓冲处理。
case GST_MESSAGE_BUFFERING: { gint percent = 0; /* If the stream is live, we do not care about buffering. */ if (data->is_live) break; gst_message_parse_buffering (msg, &percent); g_print ("Buffering (%3d%%) ", percent); /* Wait until buffering is complete before start/resume playing */ if (percent < 100) gst_element_set_state (data->pipeline, GST_STATE_PAUSED); else gst_element_set_state (data->pipeline, GST_STATE_PLAYING); break; }
在非实时流的情况下,如果缓存队列的数据不足,我们会收到GST_MESSAGE_BUFFERING事件,收到此事件时,我们可以通过gst_message_parse_buffering()得到缓冲进度,如果进度小于100%我们就暂停播放,在缓冲完成后我们再恢复播放。如果使用playbin,我们可以直接通过buffer-size或buffer-duration属性去修改缓冲区大小。
case GST_MESSAGE_CLOCK_LOST: /* Get a new clock */ gst_element_set_state (data->pipeline, GST_STATE_PAUSED); gst_element_set_state (data->pipeline, GST_STATE_PLAYING); break;
针对于时钟丢失的这种情况,我们只需在收到GST_MESSAGE_CLOCK_LOST事件时,改变Pipline的状态,由GStreamer自动选取参考时钟即可。
总结
通过本文,我们了解了如何应对两种简单的网络播放问题:
- 通过缓冲消息来控制播放状态。
- 在时钟丢失时重新选择时钟。
通过使用缓冲队列,可以使得网络播放更加流畅。
引用
https://gstreamer.freedesktop.org/documentation/tutorials/basic/streaming.html?gi-language=c