目的
在《GStreamer基礎教程——流》裏面我們展示瞭如何在較差的網絡條件下使用緩衝這個機制來提升用戶體驗。本教程在《GStreamer基礎教程——流》的基礎上在擴展了一下,增加了把流的內容在本地存儲。並且展示了:
如何開啓既看式下載
如何知道下載的是什麼
如何知道在哪裏下載
如何限制下載數據的總量
介紹
當播放流的時候,從網絡上獲得的數據被鎖住之後,會創建稱爲future-data的一個小的緩衝區。然而,在數據播放渲染之後就會被丟棄。這就意味着,如果用戶想要倒回前面去看,相應地數據仍然需要再次下載。
像YouTube一樣,播放流時播放器往往會裁剪,通常會把所有下載的數據都在本地保存,還會提供一個圖形化的界面來顯示已經下載了多少內容。
playbin2通過DOWNLOAD標誌提供了一個比較類似的功能,它會把數據在本次臨時保存起來用於在播放已經下載的部分時可以保持順暢。
代碼裏面同時展示瞭如何使用緩衝查詢,它可以讓你知道哪部分的文件已經可用了。
一個適應網絡並在本地存儲數據的例子
#include <gst/gst.h>
#include <string.h>
#define GRAPH_LENGTH 80
/* playbin2 flags */
typedef enum {
GST_PLAY_FLAG_DOWNLOAD = (1 << 7) /* Enable progressive download (on selected formats) */
} GstPlayFlags;
typedef struct _CustomData {
gboolean is_live;
GstElement *pipeline;
GMainLoop *loop;
gint buffering_level;
} CustomData;
static void got_location (GstObject *gstobject, GstObject *prop_object, GParamSpec *prop, gpointer data) {
gchar *location;
g_object_get (G_OBJECT (prop_object), "temp-location", &location, NULL);
g_print ("Temporary file: %s\n", location);
/* Uncomment this line to keep the temporary file after the program exits */
/* g_object_set (G_OBJECT (prop_object), "temp-remove", FALSE, NULL); */
}
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:
/* If the stream is live, we do not care about buffering. */
if (data->is_live) break;
gst_message_parse_buffering (msg, &data->buffering_level);
/* Wait until buffering is complete before start/resume playing */
if (data->buffering_level < 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;
}
}
static gboolean refresh_ui (CustomData *data) {
GstQuery *query;
gboolean result;
query = gst_query_new_buffering (GST_FORMAT_PERCENT);
result = gst_element_query (data->pipeline, query);
if (result) {
gint n_ranges, range, i;
gchar graph[GRAPH_LENGTH + 1];
GstFormat format = GST_FORMAT_TIME;
gint64 position = 0, duration = 0;
memset (graph, ' ', GRAPH_LENGTH);
graph[GRAPH_LENGTH] = '\0';
n_ranges = gst_query_get_n_buffering_ranges (query);
for (range = 0; range < n_ranges; range++) {
gint64 start, stop;
gst_query_parse_nth_buffering_range (query, range, &start, &stop);
start = start * GRAPH_LENGTH / 100;
stop = stop * GRAPH_LENGTH / 100;
for (i = (gint)start; i < stop; i++)
graph [i] = '-';
}
if (gst_element_query_position (data->pipeline, &format, &position) &&
GST_CLOCK_TIME_IS_VALID (position) &&
gst_element_query_duration (data->pipeline, &format, &duration) &&
GST_CLOCK_TIME_IS_VALID (duration)) {
i = (gint)(GRAPH_LENGTH * (double)position / (double)(duration + 1));
graph [i] = data->buffering_level < 100 ? 'X' : '>';
}
g_print ("[%s]", graph);
if (data->buffering_level < 100) {
g_print (" Buffering: %3d%%", data->buffering_level);
} else {
g_print (" ");
}
g_print ("\r");
}
return TRUE;
}
int main(int argc, char *argv[]) {
GstElement *pipeline;
GstBus *bus;
GstStateChangeReturn ret;
GMainLoop *main_loop;
CustomData data;
guint flags;
/* Initialize GStreamer */
gst_init (&argc, &argv);
/* Initialize our data structure */
memset (&data, 0, sizeof (data));
data.buffering_level = 100;
/* Build the pipeline */
pipeline = gst_parse_launch ("playbin2 uri=http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);
bus = gst_element_get_bus (pipeline);
/* Set the download flag */
g_object_get (pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
g_object_set (pipeline, "flags", flags, NULL);
/* Uncomment this line to limit the amount of downloaded data */
/* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
/* 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_signal_connect (pipeline, "deep-notify::temp-location", G_CALLBACK (got_location), NULL);
/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &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);
g_print ("\n");
return 0;
}
工作流程
這份代碼是基於《GStreamer基礎教程——流》裏面例子的,我們僅僅看一下不同的地方即可。
創建
/* Set the download flag */
g_object_get (pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
g_object_set (pipeline, "flags", flags, NULL);
通過設置這個標誌,playbin2通知它內部的queue存儲所有下載的數據。
g_signal_connect (pipeline, "deep-notify::temp-location", G_CALLBACK (got_location), NULL);
當它們的子element屬性發生變化時,playbin2就會發出deep-notify信號。在這裏我們希望知道temp-location屬性是什麼時候變化的,瞭解queue2會把下載的數據存在哪裏。
static void got_location (GstObject *gstobject, GstObject *prop_object, GParamSpec *prop, gpointer data) {
gchar *location;
g_object_get (G_OBJECT (prop_object), "temp-location", &location, NULL);
g_print ("Temporary file: %s\n", location);
/* Uncomment this line to keep the temporary file after the program exits */
/* g_object_set (G_OBJECT (prop_object), "temp-remove", FALSE, NULL); */
}
這個temp-location屬性是從發出信號的element那裏獲得的並打印出來。
當pipeline狀態從PAUSED切換到READY時,這個文件會被刪除。正如註釋裏面寫的那樣,你可以通過設置queue2的temp-remove屬性位FALSE來保留下載的數據。
UI
在main函數裏我們啓動了一個1s的定時器,這樣可以每秒刷新一下UI界面。
/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
refresh_ui方法會查詢pipeline來了解當前下載的數據在文件的位置以及當前播放的位置。並且用一種動畫的方式在屏幕上顯示出來。
[---->------- ]
這裏的'-'代表的是下載的部分,'>'代表的是當前播放的位置(當暫停的時候變成'X'位置)。當你的網絡速度足夠快得時候你可能會看不到下載的動畫,在開始的時候就下載結束了。
static gboolean refresh_ui (CustomData *data) {
GstQuery *query;
gboolean result;
query = gst_query_new_buffering (GST_FORMAT_PERCENT);
result = gst_element_query (data->pipeline, query);
我們在refresh_ui裏面做的第一件事是就是用gst_query_new_buffering()創建一個GstQuery對象,並用gst_element_query()傳給playbin2。在《GStreamer基礎教程04——時間管理》裏面我們展示瞭如何用明確的方法來查詢位置/播放總時間等,如果要查詢更復雜一些的內容(比如緩衝),那麼我們會用更通用的gst_element_query()方法。
緩衝的查詢可以基於不同的GstFormat,並非所有的element都可以響應所有格式的查詢,所以需要檢查在pipeline裏支持哪些格式。如果gst_element_query()返回TRUE,那麼查詢是成功的。查詢的結果用GstQuery封裝起來,可以用下面的方法來解析:
n_ranges = gst_query_get_n_buffering_ranges (query);
for (range = 0; range < n_ranges; range++) {
gint64 start, stop;
gst_query_parse_nth_buffering_range (query, range, &start, &stop);
start = start * GRAPH_LENGTH / 100;
stop = stop * GRAPH_LENGTH / 100;
for (i = (gint)start; i < stop; i++)
graph [i] = '-';
}
數據並不需要保證被按照順序從頭開始下載,因爲跳躍播放時會讓下載從一個新的地方開始。因此,gst_query_get_n_buffering_ranges()返回下載塊的數目或者範圍,然後我們用gst_query_parse_nth_buffering_rang()方法來解析下載塊的位置和大小。
我們在調用gst_query_new_buffering()的請求會決定返回數據的格式,在這個例子裏面,返回值是比例。這些查詢到得數據用來繪製UI的下載動畫。
if (gst_element_query_position (data->pipeline, &format, &position) &&
GST_CLOCK_TIME_IS_VALID (position) &&
gst_element_query_duration (data->pipeline, &format, &duration) &&
GST_CLOCK_TIME_IS_VALID (duration)) {
i = (gint)(GRAPH_LENGTH * (double)position / (double)(duration + 1));
graph [i] = data->buffering_level < 100 ? 'X' : '>';
}
下一步就是當前位置的查詢。它也支持比例的格式,所以代碼和前面應該比較類似。不過這部分目前支持不是很好,所以我們使用了時間這個格式。
當前位置使用'>'或者'X'來表示,如果緩衝不到100%,cb_message會讓pipeline處於PAUSE狀態,那樣我們就顯示'X',如果已經滿了100%,那麼pipeline就在PLAYING狀態,我們就顯示'>'。
if (data->buffering_level < 100) {
g_print (" Buffering: %3d%%", data->buffering_level);
} else {
g_print (" ");
}
最後,如果緩衝時鐘小於100%,我們就把這個數據顯示出來。
限制下載文件的大小
/* Uncomment this line to limit the amount of downloaded data */
/* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
打開139行的註釋,讓我們看看這個是如何做到的。縮小臨時文件的大小,這樣播放過的區域就會被覆蓋。