# Playback tutorial 4: Progressive streaming

### Goal

Basic tutorial 12: Streaming展示了如何在糟糕的网络情况下提高用户体验，通过使用缓冲机制。这篇教程是Basic tutorial 12: Streaming的进一步拓展——启用流媒体的本地存储，并描述了这种技术的优点。其中，主要展示了：

* 如何启用渐进式下载。
* 如何知道已下载的内容。
* 如何知道已下载内容的位置。
* 如何限制保存的下载数据的数量。

### Introduction

当流启动，将从互联网获取数据，为了保证流畅的播放，保留了一小块未来数据缓冲区。然而，数据将在它被播放或渲染后立即丢弃（程序中不会存在过去的数据缓冲）。这意味着，假如用户想要从过去的某个时刻开始回放，数据需要重新下载。

为流媒体量身定制的媒体播放器，例如Youtube，通常将所有下载的数据存储在本地，以防意外情况。通常会使用一个图形窗口来展示当前文件的下载进度。`playbin`通过`DOWNLOAD`标记提供了类似的功能，为了更快的播放已下载的数据，`playbin`能够将媒体保存到一个本地临时文件中。

本教程同时展示了如何使用Buffer Query，它允许知道文件的哪些部分可用。

### A network-resilient example with local storage

```
#include <gst/gst.h>
#include <string.h>

#define GRAPH_LENGTH 78

/* playbin 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);
  g_free (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];
    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 / (stop - start);
      stop = stop * GRAPH_LENGTH / (stop - start);
      for (i = (gint)start; i < stop; i++)
        graph [i] = '-';
    }
    if (gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position) &&
        GST_CLOCK_TIME_IS_VALID (position) &&
        gst_element_query_duration (data->pipeline, GST_FORMAT_TIME, &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 ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/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

#### Setup

```
/* Set the download flag */
g_object_get (pipeline, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_DOWNLOAD;
g_object_set (pipeline, "flags", flags, NULL);
```

通过设置这个flag，`playbin`告诉它内部的queue（实际上是一个`queue2`元素）来存储所有下载的数据。

```
g_signal_connect (pipeline, "deep-notify::temp-location", G_CALLBACK (got_location), NULL);
```

当`GstObject`元素（例如`playbin`）任何子元素的属性改变时都将发出`deep-notify`信号。在这个例子中，我们关注`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);
  g_free (location);
  /* Uncomment this line to keep the temporary file after the program exits */
  /* g_object_set (G_OBJECT (prop_object), "temp-remove", FALSE, NULL); */
}
```

`got_location`将读取`queue2`中的`temp-location`属性并将其打印到屏幕上。

当pipeline的状态从`PAUSE`切换到`READY`时，这个临时文件将被删除。正如注释语句所言，你可以将`queue2`的`temp-remove`属性设为`FALSE`以禁用这一设置。

### User Interface

在main函数中，我们安装了一个定时器，用于每秒刷新一次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`将其传递给`playbin`。在Basic tutorial 4: Time management中，我们一直知道如何使用特定的方法来完成像Position和Duration这样简单的查询。类似于缓冲区这种更加复杂的查询，需要使用更通常的`gst_element_query`接口。

关于缓冲区的查询可以是各种`GstFormat`格式的，包含TIME，BYTES，PERCENTAGE等等。但不是所有的elements都能响应所有格式的查询，因此你需要检车你的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 / (stop - start);
  stop = stop * GRAPH_LENGTH / (stop - start);
  for (i = (gint)start; i < stop; i++)
    graph [i] = '-';
}
```

数据不需要从文件的开头连续下载：例如，搜索（跳播）可能会迫使用户从一个新的位置开始下载，并留下已下载的数据块。因此`gst_query_get_n_buffering_ranges`将返回文件块的数目或者是已下载的数据的范围，然后我们可以使用`gst_query_parse_nth_buffering_range`来获取每个范围的位置和大小。

查询的返回值的类型将取决于`gst_query_new_buffering`查询什么格式的信息，在这个例子中是缓冲进度的百分比。这些只将用于生成表示下载进度的文本图像。

```
if (gst_element_query_position (data->pipeline, GST_FORMAT_TIME, &position) &&
    GST_CLOCK_TIME_IS_VALID (position) &&
    gst_element_query_duration (data->pipeline, GST_FORMAT_TIME, &duration) &&
    GST_CLOCK_TIME_IS_VALID (duration)) {
  i = (gint)(GRAPH_LENGTH * (double)position / (double)(duration + 1));
  graph [i] = data->buffering_level < 100 ? 'X' : '>';
}
```

接下来将查询当前播放的位置。它可以以PERCENT的格式查询，因此代码将和查询范围差不多，但目前对于当前播放位置的PERCENT格式的查询支持还不完善，因此我们使用TIME格式代替，并且查询持续时间来计算百分比。

当前播放的位置使用一个`>`或者`X`来表示，这取决于缓冲级别。当缓冲级别低于100%时，`cb_message`将把pipeline的状态设置为`PAUSE`，于是打印的是`X`。当缓冲级别为100%时，`cb_message`将把pipeline的状态设置为`PLAYING`，打印`>`。

注：由于开发板的网络原因，我并没能将例程运行起来，因此文档中关于进度的解释我其实并没有弄明白，尤其是上面绘制进度的start和stop的计算。根据我的理解：pipeline会等待整个媒体文件缓冲完成才会开始播放，在缓冲完成之前其实打印的都是`X`，例程也并没有支持动态的切换pipeline的状态，因此这里的播放和暂停与实际播放器能够完成的动态交互不太一样。

```
if (data->buffering_level < 100) {
  g_print (" Buffering: %3d%%", data->buffering_level);
} else {
  g_print ("                ");
}
```

最后，假如缓冲级别低于100%，我们将打印这个信息。

#### Limiting the size of the downloaded file

```
/* Uncomment this line to limit the amount of downloaded data */
/* g_object_set (pipeline, "ring-buffer-max-size", (guint64)4000000, NULL); */
```

这减少了临时文件的大小，通过覆盖已经播放的区域。观察下载栏，可以看出文件中保持哪些区域可用。

### Conclusion

这篇教程展示了：

* `playbin`如何通过`GST_PLAY_FLAG_DOWNLOAD`标识实现平滑的下载。
* 如何通过缓冲区查询`GstQuery`已下载的内容。
* 如何通过`deep-notify::temp-location`获取下载文件的存储位置。
* 如何通过`ring-buffer-max-size`限制`playbin`下载的临时文件的大小。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ricardolu.gitbook.io/gstreamer/basic-theory/playback-tutorial-4-progressive-streaming.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
