转自:http://docs.gstreamer.com/display/GstSDK/Playback+tutorial+4%3A+Progressive+streaming
Basic tutorial 12: Streaming showed how to enhance the user experience in poor network conditions, by taking buffering into account. This tutorial further expands Basic tutorial 12: Streaming by enabling the local storage of the streamed media, and describes the advantages of this technique. In particular, it shows:
- How to enable progressive downloading
- How to know what has been downloaded
- How to know where it has been downloaded
- How to limit the amount of downloaded data that is kept
Introduction
When streaming, data is fetched from the network and a small buffer of future-data is kept to ensure smooth playback (see Basic tutorial 12: Streaming). However, data is discarded as soon as it is displayed or rendered (there is no past-data buffer). This means, that if a user wants to jump back and continue playback from a point in the past, data needs to be re-downloaded.
Media players tailored for streaming, like YouTube, usually keep all downloaded data stored locally for this contingency. A graphical widget is also normally used to show how much of the file has already been downloaded.
playbin2
offers similar functionalities through the DOWNLOAD
flag which stores the media in a local temporary file for faster playback of already-downloaded chunks.
This code also shows how to use the Buffering Query, which allows knowing what parts of the file are available.
A network-resilient example with local storage
Copy this code into a text file named playback-tutorial-4.c
.
![]() | This tutorial is included in the SDK since release 2012.7. If you cannot find it in the downloaded code, please install the latest release of the GStreamer SDK. |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
#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=<a href="http://docs.gstreamer.com/media/sintel_trailer-480p.webm" "="">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;
}
|
![]() |
Walkthrough
This code is based on that of Basic tutorial 12: Streaming. Let’s review only the differences.
Setup
133
134
135
136
|
/* Set the download flag */
g_object_get (pipeline,
"flags"
, &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
g_object_set (pipeline,
"flags"
, flags, NULL);
|
By setting this flag, playbin2
instructs its internal queue (a queue2
element, actually) to store all downloaded data.
157
|
g_signal_connect (pipeline,
"deep-notify::temp-location"
, G_CALLBACK (got_location), NULL);
|
deep-notify
signals are emitted by GstObject
elements (like playbin2
) when the properties of any of their children elements change. In this case we want to know when the temp-location
property changes, indicating that the queue2
has decided where to store the downloaded data.
18
19
20
21
22
23
24
|
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); */
}
|
The temp-location
property is read from the element that triggered the signal (the queue2
) and printed on screen.
When the pipeline state changes from PAUSED
to READY
, this file is removed. As the comment reads, you can keep it by setting the temp-remove
property of the queue2
to FALSE
.
![]() | On Windows this file is usually created inside the |
User Interface
In main
we also install a timer which we use to refresh the UI every second.
159
160
|
/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
|
The refresh_ui
method queries the pipeline to find out which parts of the file have been downloaded and what the currently playing position is. It builds a graph to display this information (sort of a text-mode user interface) and prints it on screen, overwriting the previous one so it looks like it is animated:
[---->------- ]
The dashes ‘-
’ indicate the downloaded parts, and the greater-than sign ‘>
’ shows the current position (turning into an ‘X
’ when the pipeline is paused). Keep in mind that if your network is fast enough, you will not see the download bar (the dashes) advance at all; it will be completely full from the beginning.
70
71
72
73
74
|
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);
|
The first thing we do in refresh_ui
is construct a new Buffering GstQuery
with gst_query_new_buffering()
and pass it to the pipeline (playbin2
) with gst_element_query()
. In Basic tutorial 4: Time management we have already seen how to perform simple queries like Position and Duration using specific methods. More complex queries, like Buffering, need to use the more general gst_element_query()
.
The Buffering query can be made in different GstFormat
(TIME, BYTES, PERCENTAGE and a few more). Not all elements can answer the query in all the formats, so you need to check which ones are supported in your particular pipeline. If gst_element_query()
returns TRUE
, the query succeeded. The answer to the query is contained in the same GstQuery
structure we created, and can be retrieved using multiple parse methods:
85
86
87
88
89
90
91
92
93
|
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] =
'-'
;
}
|
Data does not need to be downloaded in consecutive pieces from the beginning of the file: Seeking, for example, might force to start downloading from a new position and leave a downloaded chunk behind. Therefore, gst_query_get_n_buffering_ranges()
returns the number of chunks, or ranges of downloaded data, and then, the position and size of each range is retrieved with gst_query_parse_nth_buffering_range()
.
The format of the returned values (start and stop position for each range) depends on what we requested in the gst_query_new_buffering()
call. In this case, PERCENTAGE. These values are used to generate the graph.
94
95
96
97
98
99
100
|
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'
:
'>'
;
}
|
Next, the current position is queried. It could be queried in the PERCENT format, so code similar to the one used for the ranges is used, but currently this format is not well supported for position queries. Instead, we use the TIME format and also query the duration to obtain a percentage.
The current position is indicated with either a ‘>
’ or an ‘X
’ depending on the buffering level. If it is below 100%, the code in the cb_message
method will have set the pipeline to PAUSED
, so we print an ‘X
’. If the buffering level is 100% the pipeline is in the PLAYING
state and we print a ‘>
’.
102
103
104
105
106
|
if
(data->buffering_level < 100) {
g_print (
" Buffering: %3d%%"
, data->buffering_level);
}
else
{
g_print (
" "
);
}
|
Finally, if the buffering level is below 100%, we report this information (and delete it otherwise).
Limiting the size of the downloaded file
138
139
|
/* Uncomment this line to limit the amount of downloaded data */
/* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
|
Uncomment line 139 to see how this can be achieved. This reduces the size of the temporary file, by overwriting already played regions. Observe the download bar to see which regions are kept available in the file.
Conclusion
This tutorial has shown:
- How to enable progressive downloading with the
GST_PLAY_FLAG_DOWNLOAD
playbin2
flag - How to know what has been downloaded using a Buffering
GstQuery
- How to know where it has been downloaded with the
deep-notify::temp-location
signal - How to limit the size of the temporary file with the
ring-buffer-max-size
property ofplaybin2
.
It has been a pleasure having you here, and see you soon!