1 # Playback tutorial 4: Progressive streaming
5 [](tutorials/basic/streaming.md) showed how to
6 enhance the user experience in poor network conditions, by taking
7 buffering into account. This tutorial further expands
8 [](tutorials/basic/streaming.md) by enabling
9 the local storage of the streamed media, and describes the advantages of
10 this technique. In particular, it shows:
12 - How to enable progressive downloading
13 - How to know what has been downloaded
14 - How to know where it has been downloaded
15 - How to limit the amount of downloaded data that is kept
19 When streaming, data is fetched from the network and a small buffer of
20 future-data is kept to ensure smooth playback (see
21 [](tutorials/basic/streaming.md)). However, data
22 is discarded as soon as it is displayed or rendered (there is no
23 past-data buffer). This means, that if a user wants to jump back and
24 continue playback from a point in the past, data needs to be
27 Media players tailored for streaming, like YouTube, usually keep all
28 downloaded data stored locally for this contingency. A graphical widget
29 is also normally used to show how much of the file has already been
32 `playbin` offers similar functionalities through the `DOWNLOAD` flag
33 which stores the media in a local temporary file for faster playback of
34 already-downloaded chunks.
36 This code also shows how to use the Buffering Query, which allows
37 knowing what parts of the file are available.
39 ## A network-resilient example with local storage
41 Copy this code into a text file named `playback-tutorial-4.c`.
43 **playback-tutorial-4.c**
49 #define GRAPH_LENGTH 78
53 GST_PLAY_FLAG_DOWNLOAD = (1 << 7) /* Enable progressive download (on selected formats) */
56 typedef struct _CustomData {
63 static void got_location (GstObject *gstobject, GstObject *prop_object, GParamSpec *prop, gpointer data) {
65 g_object_get (G_OBJECT (prop_object), "temp-location", &location, NULL);
66 g_print ("Temporary file: %s\n", location);
68 /* Uncomment this line to keep the temporary file after the program exits */
69 /* g_object_set (G_OBJECT (prop_object), "temp-remove", FALSE, NULL); */
72 static void cb_message (GstBus *bus, GstMessage *msg, CustomData *data) {
74 switch (GST_MESSAGE_TYPE (msg)) {
75 case GST_MESSAGE_ERROR: {
79 gst_message_parse_error (msg, &err, &debug);
80 g_print ("Error: %s\n", err->message);
84 gst_element_set_state (data->pipeline, GST_STATE_READY);
85 g_main_loop_quit (data->loop);
90 gst_element_set_state (data->pipeline, GST_STATE_READY);
91 g_main_loop_quit (data->loop);
93 case GST_MESSAGE_BUFFERING:
94 /* If the stream is live, we do not care about buffering. */
95 if (data->is_live) break;
97 gst_message_parse_buffering (msg, &data->buffering_level);
99 /* Wait until buffering is complete before start/resume playing */
100 if (data->buffering_level < 100)
101 gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
103 gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
105 case GST_MESSAGE_CLOCK_LOST:
106 /* Get a new clock */
107 gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
108 gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
111 /* Unhandled message */
116 static gboolean refresh_ui (CustomData *data) {
120 query = gst_query_new_buffering (GST_FORMAT_PERCENT);
121 result = gst_element_query (data->pipeline, query);
123 gint n_ranges, range, i;
124 gchar graph[GRAPH_LENGTH + 1];
125 gint64 position = 0, duration = 0;
127 memset (graph, ' ', GRAPH_LENGTH);
128 graph[GRAPH_LENGTH] = '\0';
130 n_ranges = gst_query_get_n_buffering_ranges (query);
131 for (range = 0; range < n_ranges; range++) {
133 gst_query_parse_nth_buffering_range (query, range, &start, &stop);
134 start = start * GRAPH_LENGTH / (stop - start);
135 stop = stop * GRAPH_LENGTH / (stop - start);
136 for (i = (gint)start; i < stop; i++)
139 if (gst_element_query_position (data->pipeline, GST_TIME_FORMAT, &position) &&
140 GST_CLOCK_TIME_IS_VALID (position) &&
141 gst_element_query_duration (data->pipeline, GST_TIME_FORMAT, &duration) &&
142 GST_CLOCK_TIME_IS_VALID (duration)) {
143 i = (gint)(GRAPH_LENGTH * (double)position / (double)(duration + 1));
144 graph [i] = data->buffering_level < 100 ? 'X' : '>';
146 g_print ("[%s]", graph);
147 if (data->buffering_level < 100) {
148 g_print (" Buffering: %3d%%", data->buffering_level);
159 int main(int argc, char *argv[]) {
160 GstElement *pipeline;
162 GstStateChangeReturn ret;
163 GMainLoop *main_loop;
167 /* Initialize GStreamer */
168 gst_init (&argc, &argv);
170 /* Initialize our data structure */
171 memset (&data, 0, sizeof (data));
172 data.buffering_level = 100;
174 /* Build the pipeline */
175 pipeline = gst_parse_launch ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
176 bus = gst_element_get_bus (pipeline);
178 /* Set the download flag */
179 g_object_get (pipeline, "flags", &flags, NULL);
180 flags |= GST_PLAY_FLAG_DOWNLOAD;
181 g_object_set (pipeline, "flags", flags, NULL);
183 /* Uncomment this line to limit the amount of downloaded data */
184 /* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
187 ret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
188 if (ret == GST_STATE_CHANGE_FAILURE) {
189 g_printerr ("Unable to set the pipeline to the playing state.\n");
190 gst_object_unref (pipeline);
192 } else if (ret == GST_STATE_CHANGE_NO_PREROLL) {
196 main_loop = g_main_loop_new (NULL, FALSE);
197 data.loop = main_loop;
198 data.pipeline = pipeline;
200 gst_bus_add_signal_watch (bus);
201 g_signal_connect (bus, "message", G_CALLBACK (cb_message), &data);
202 g_signal_connect (pipeline, "deep-notify::temp-location", G_CALLBACK (got_location), NULL);
204 /* Register a function that GLib will call every second */
205 g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
207 g_main_loop_run (main_loop);
210 g_main_loop_unref (main_loop);
211 gst_object_unref (bus);
212 gst_element_set_state (pipeline, GST_STATE_NULL);
213 gst_object_unref (pipeline);
219 > ![information] If you need help to compile this code, refer to the
220 > **Building the tutorials** section for your platform: [Mac] or
221 > [Windows] or use this specific command on Linux:
223 > `` gcc playback-tutorial-4.c -o playback-tutorial-4 `pkg-config --cflags --libs gstreamer-1.0` ``
225 > If you need help to run this code, refer to the **Running the
226 > tutorials** section for your platform: [Mac OS X], [Windows][1], for
227 > [iOS] or for [android].
229 > This tutorial opens a window and displays a movie, with accompanying
230 > audio. The media is fetched from the Internet, so the window might
231 > take a few seconds to appear, depending on your connection
232 > speed. In the console window, you should see a message indicating
233 > where the media is being stored, and a text graph representing the
234 > downloaded portions and the current position. A buffering message
235 > appears whenever buffering is required, which might never happen is
236 > your network connection is fast enough
238 > Required libraries: `gstreamer-1.0`
243 This code is based on that of [](tutorials/basic/streaming.md). Let’s review
244 only the differences.
249 /* Set the download flag */
250 g_object_get (pipeline, "flags", &flags, NULL);
251 flags |= GST_PLAY_FLAG_DOWNLOAD;
252 g_object_set (pipeline, "flags", flags, NULL);
255 By setting this flag, `playbin` instructs its internal queue (a
256 `queue2` element, actually) to store all downloaded
260 g_signal_connect (pipeline, "deep-notify::temp-location", G_CALLBACK (got_location), NULL);
263 `deep-notify` signals are emitted by `GstObject` elements (like
264 `playbin`) when the properties of any of their children elements
265 change. In this case we want to know when the `temp-location` property
266 changes, indicating that the `queue2` has decided where to store the
271 static void got_location (GstObject *gstobject, GstObject *prop_object, GParamSpec *prop, gpointer data) {
273 g_object_get (G_OBJECT (prop_object), "temp-location", &location, NULL);
274 g_print ("Temporary file: %s\n", location);
276 /* Uncomment this line to keep the temporary file after the program exits */
277 /* g_object_set (G_OBJECT (prop_object), "temp-remove", FALSE, NULL); */
281 The `temp-location` property is read from the element that triggered the
282 signal (the `queue2`) and printed on screen.
284 When the pipeline state changes from `PAUSED` to `READY`, this file is
285 removed. As the comment reads, you can keep it by setting the
286 `temp-remove` property of the `queue2` to `FALSE`.
289 > On Windows this file is usually created inside the `Temporary Internet Files` folder, which might hide it from Windows Explorer. If you cannot find the downloaded files, try to use the console.
293 In `main` we also install a timer which we use to refresh the UI every
297 /* Register a function that GLib will call every second */
298 g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
301 The `refresh_ui` method queries the pipeline to find out which parts of
302 the file have been downloaded and what the currently playing position
303 is. It builds a graph to display this information (sort of a text-mode
304 user interface) and prints it on screen, overwriting the previous one so
305 it looks like it is animated:
309 The dashes ‘`-`’ indicate the downloaded parts, and the greater-than
310 sign ‘`>`’ shows the current position (turning into an ‘`X`’ when the
311 pipeline is paused). Keep in mind that if your network is fast enough,
312 you will not see the download bar (the dashes) advance at all; it will
313 be completely full from the beginning.
316 static gboolean refresh_ui (CustomData *data) {
319 query = gst_query_new_buffering (GST_FORMAT_PERCENT);
320 result = gst_element_query (data->pipeline, query);
323 The first thing we do in `refresh_ui` is construct a new Buffering
324 `GstQuery` with `gst_query_new_buffering()` and pass it to the pipeline
325 (`playbin`) with `gst_element_query()`. In [](tutorials/basic/time-management.md) we have
326 already seen how to perform simple queries like Position and Duration
327 using specific methods. More complex queries, like Buffering, need to
328 use the more general `gst_element_query()`.
330 The Buffering query can be made in different `GstFormat` (TIME, BYTES,
331 PERCENTAGE and a few more). Not all elements can answer the query in all
332 the formats, so you need to check which ones are supported in your
333 particular pipeline. If `gst_element_query()` returns `TRUE`, the query
334 succeeded. The answer to the query is contained in the same
335 `GstQuery` structure we created, and can be retrieved using multiple
339 n_ranges = gst_query_get_n_buffering_ranges (query);
340 for (range = 0; range < n_ranges; range++) {
342 gst_query_parse_nth_buffering_range (query, range, &start, &stop);
343 start = start * GRAPH_LENGTH / (stop - start);
344 stop = stop * GRAPH_LENGTH / (stop - start);
345 for (i = (gint)start; i < stop; i++)
350 Data does not need to be downloaded in consecutive pieces from the
351 beginning of the file: Seeking, for example, might force to start
352 downloading from a new position and leave a downloaded chunk behind.
353 Therefore, `gst_query_get_n_buffering_ranges()` returns the number of
354 chunks, or *ranges* of downloaded data, and then, the position and size
355 of each range is retrieved with `gst_query_parse_nth_buffering_range()`.
357 The format of the returned values (start and stop position for each
358 range) depends on what we requested in the
359 `gst_query_new_buffering()` call. In this case, PERCENTAGE. These
360 values are used to generate the graph.
363 if (gst_element_query_position (data->pipeline, &format, &position) &&
364 GST_CLOCK_TIME_IS_VALID (position) &&
365 gst_element_query_duration (data->pipeline, &format, &duration) &&
366 GST_CLOCK_TIME_IS_VALID (duration)) {
367 i = (gint)(GRAPH_LENGTH * (double)position / (double)(duration + 1));
368 graph [i] = data->buffering_level < 100 ? 'X' : '>';
372 Next, the current position is queried. It could be queried in the
373 PERCENT format, so code similar to the one used for the ranges is used,
374 but currently this format is not well supported for position queries.
375 Instead, we use the TIME format and also query the duration to obtain a
378 The current position is indicated with either a ‘`>`’ or an ‘`X`’
379 depending on the buffering level. If it is below 100%, the code in the
380 `cb_message` method will have set the pipeline to `PAUSED`, so we print
381 an ‘`X`’. If the buffering level is 100% the pipeline is in the
382 `PLAYING` state and we print a ‘`>`’.
385 if (data->buffering_level < 100) {
386 g_print (" Buffering: %3d%%", data->buffering_level);
392 Finally, if the buffering level is below 100%, we report this
393 information (and delete it otherwise).
395 ### Limiting the size of the downloaded file
398 /* Uncomment this line to limit the amount of downloaded data */
399 /* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
402 Uncomment line 139 to see how this can be achieved. This reduces the
403 size of the temporary file, by overwriting already played regions.
404 Observe the download bar to see which regions are kept available in the
409 This tutorial has shown:
411 - How to enable progressive downloading with the
412 `GST_PLAY_FLAG_DOWNLOAD` `playbin` flag
413 - How to know what has been downloaded using a Buffering `GstQuery`
414 - How to know where it has been downloaded with the
415 `deep-notify::temp-location` signal
416 - How to limit the size of the temporary file with
417 the `ring-buffer-max-size` property of `playbin`.
419 It has been a pleasure having you here, and see you soon!
421 [information]: images/icons/emoticons/information.png
422 [Mac]: installing/on-mac-osx.md
423 [Windows]: installing/on-windows.md
424 [Mac OS X]: installing/on-mac-osx.md#building-the-tutorials
425 [1]: installing/on-windows.md#running-the-tutorials
426 [iOS]: installing/for-ios-development.md#building-the-tutorials
427 [android]: installing/for-android-development.md#building-the-tutorials
428 [warning]: images/icons/emoticons/warning.png