摘要
我們把直接從網絡播放一個媒體文件的方式稱為在線播放(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\n", 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%%)\r", 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.\n"); 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.\n"); 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%%)\r", 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