GStreamer随笔3 - 内存分配

部分图片文字来自网络文章,侵权必删


        内存分配和管理是多媒体中非常重要的主题。一个高清视频可能会使用几MB来存储单个图像帧,尽可能复用内存并减少内存拷贝非常重要。多媒体系统通常使用专用芯片(如 DSP 或 GPU)来执行繁重的工作(尤其是视频),这些专用芯片通常对其操作的内存及其访问方式有严格的要求。

        本章讨论 GStreamer 插件可用的内存管理功能。我们将首先讨论管理对内存访问的低级 GstMemory 对象,然后继续讨论GstMemory的主要使用者之一:GstBuffer,它用于在element之间和与应用程序之间交换数据。我们还将讨论 GstMeta,此对象可以放置在缓冲区上以提供额外信息。我们还将讨论 GstBufferPool,它允许更有效地管理相同大小的缓冲区。

        结束最后我们将研究 GST_QUERY_ALLOCATION 查询,它用于协商元素之间的内存管理选项。

一、GstMemory

        GstMemory 是一个管理内存区域的对象,此内存对象指向“最大尺寸(maxsize)”的内存区域。可以访问的内存区域从“偏移量(offset)”开始,大小为“size”字节。创建 GstMemory 后其最大尺寸将无法再更改,但其“offset”和“size”可以更改。

/**
 * GstMemory:
 * @mini_object: parent structure
 * @allocator: pointer to the #GstAllocator
 * @parent: parent memory block
 * @maxsize: the maximum size allocated
 * @align: the alignment of the memory
 * @offset: the offset where valid data starts
 * @size: the size of valid data
 *
 * Base structure for memory implementations. Custom memory will put this structure
 * as the first member of their structure.
 */

struct _GstMemory {
  GstMiniObject   mini_object;

  GstAllocator   *allocator;

  GstMemory      *parent;
  gsize           maxsize;
  gsize           align;
  gsize           offset;
  gsize           size;
};

1. GstAllocator

        GstMemory 对象由 GstAllocator 对象创建。大多数分配器都实现默认的 gst_allocator_alloc() 方法,但有些分配器可能会实现不同的方法,例如当需要其他参数来分配特定内存时。

        系统内存、共享内存和由 DMAbuf 文件描述符支持的内存存在不同的分配器。如果需要实现对新内存类型的支持,您必须实现新的分配器对象。

2. GstMemory 应用接口示例

        对 GstMemory 对象包装的内存的数据访问始终受到 gst_memory_map() 和 gst_memory_unmap()函数对的保护:应用程序使用gst_memory_map映射内存(指定读写访问模式),该函数返回指向有效内存区域的指针,然后可以根据请求的访问模式对其进行访问;访问结束后应用程序使用gst_memory_unmap函数解除内存映射。

        以下是创建 GstMemory 对象并使用 gst_memory_map/gst_memory_unmap函数对 访问内存区域的示例:

[...]

  GstMemory *mem;
  GstMapInfo info;
  gint i;

  /* allocate 100 bytes */
  mem = gst_allocator_alloc (NULL, 100, NULL);

  /* get access to the memory in write mode */
  gst_memory_map (mem, &info, GST_MAP_WRITE);

  /* fill with pattern */
  for (i = 0; i < info.size; i++)
    info.data[i] = i;

  /* release memory */
  gst_memory_unmap (mem, &info);

[...]

二、GstBuffer

        GstBuffer 是一个轻量级对象,包含内存和元数据,它从上游element传递到下游elenent,表征被下游element获取的多媒体内容。GstBuffer 包含一个或多个 GstMemory 对象,这些对象保存缓冲区的数据。

/**
 * GstBuffer:
 * @mini_object: the parent structure
 * @pool: pointer to the pool owner of the buffer
 * @pts: presentation timestamp of the buffer, can be #GST_CLOCK_TIME_NONE when the
 *     pts is not known or relevant. The pts contains the timestamp when the
 *     media should be presented to the user.
 * @dts: decoding timestamp of the buffer, can be #GST_CLOCK_TIME_NONE when the
 *     dts is not known or relevant. The dts contains the timestamp when the
 *     media should be processed.
 * @duration: duration in time of the buffer data, can be #GST_CLOCK_TIME_NONE
 *     when the duration is not known or relevant.
 * @offset: a media specific offset for the buffer data.
 *     For video frames, this is the frame number of this buffer.
 *     For audio samples, this is the offset of the first sample in this buffer.
 *     For file data or compressed data this is the byte offset of the first
 *       byte in this buffer.
 * @offset_end: the last offset contained in this buffer. It has the same
 *     format as @offset.
 *
 * The structure of a #GstBuffer. Use the associated macros to access the public
 * variables.
 */

struct _GstBuffer {
  GstMiniObject          mini_object;

  /*< public >*/ /* with COW */
  GstBufferPool         *pool;

  /* timestamp */
  GstClockTime           pts;
  GstClockTime           dts;
  GstClockTime           duration;

  /* media specific offset */
  guint64                offset;
  guint64                offset_end;
};

缓冲区中的元数据包括:

  1. DTS 和 PTS 时间戳(pts/dts):它们表示缓冲区内容的解码时间戳和显示时间戳,用于element间缓冲区同步调度。如果缓冲区不需要这两个时间戳,侧这两个时间戳可以是 GST_CLOCK_TIME_NONE。
  2. 缓冲区内容的持续时间(duration):当未知/未定义时此持续时间可以是 GST_CLOCK_TIME_NONE。
  3. 特定于媒体的偏移量(offset)和 偏移结束值(offset_end):对于视频这是流中的帧号;对于音频这是样本号。对于其他类型的媒体,这两个值可能有不同的定义。

1. GstBuffer的可写属性

        当GstBuffer的引用计数正好为 1 时,意味着只有一个对象持有对缓冲区的引用,此时该缓冲区是可写的。只有当缓冲区可写时您才能修改它,您可以更改时间戳、偏移量、元数据或添加和删除内存块,前提是先调用 gst_buffer_make_writable()函数表征该缓冲区可写。

2. API 示例

        您可以使用 gst_buffer_new () 创建 GstBuffer,然后向其中添加内存对象。您也可以使用便捷函数 gst_buffer_new_allocate () 同时执行两个操作,或者使用 gst_buffer_new_wrapped_full () 包装现有内存,并指定内存释放时应该调用的函数。

        您可以通过单独获取并映射 GstMemory 对象,亦或使用 gst_buffer_map () 来访问 GstBuffer 的内存。后者将所有内存合并为一个大块然后为您提供指向它的指针。下面是如何创建缓冲区并访问其内存的示例:

[...]
  GstBuffer *buffer;
  GstMemory *mem;
  GstMapInfo info;

  /* make empty buffer */
  buffer = gst_buffer_new ();

  /* make memory holding 100 bytes */
  mem = gst_allocator_alloc (NULL, 100, NULL);

  /* add the buffer */
  gst_buffer_append_memory (buffer, mem);

[...]

  /* get WRITE access to the memory and fill with 0xff */
  gst_buffer_map (buffer, &info, GST_MAP_WRITE);
  memset (info.data, 0xff, info.size);
  gst_buffer_unmap (buffer, &info);

[...]

  /* free the buffer */
  gst_buffer_unref (buffer);

[...]

三、GstMeta

        您可以使用 GstMeta 系统向缓冲区添加任意结构,这些结构描述了缓冲区的额外属性,例如裁剪、步幅、感兴趣区域等。

        元数据系统将 API 规范(元数据及其 API 的样子)与实现(工作原理)分开,这导致同一 API 可以有不同的实现。一个典型的例子就是在不同的视频解码器上,GstMeta的实现是不同的,取决于我们正在运行的硬件。

/**
 * GstMetaFlags:
 * @GST_META_FLAG_NONE: no flags
 * @GST_META_FLAG_READONLY: metadata should not be modified
 * @GST_META_FLAG_POOLED: metadata is managed by a bufferpool
 * @GST_META_FLAG_LOCKED: metadata should not be removed
 * @GST_META_FLAG_LAST: additional flags can be added starting from this flag.
 *
 * Extra metadata flags.
 */
typedef enum {
  GST_META_FLAG_NONE        = 0,
  GST_META_FLAG_READONLY    = (1 << 0),
  GST_META_FLAG_POOLED      = (1 << 1),
  GST_META_FLAG_LOCKED      = (1 << 2),

  GST_META_FLAG_LAST        = (1 << 16)
} GstMetaFlags;

/**
 * GstMetaInfo:
 * @api: tag identifying the metadata structure and api
 * @type: type identifying the implementor of the api
 * @size: size of the metadata
 * @init_func: function for initializing the metadata
 * @free_func: function for freeing the metadata
 * @transform_func: function for transforming the metadata
 * @serialize_func: function for serializing the metadata into a #GstStructure,
 *  or %NULL if not supported by this meta. (Since 1.24)
 * @deserialize_func: function for deserializing the metadata from a
 *  #GstStructure, or %NULL if not supported by this meta. (Since 1.24)
 *
 * The #GstMetaInfo provides information about a specific metadata
 * structure.
 */
struct _GstMetaInfo {
  GType                      api;
  GType                      type;
  gsize                      size;

  GstMetaInitFunction        init_func;
  GstMetaFreeFunction        free_func;
  GstMetaTransformFunction   transform_func;
  GstMetaSerializeFunction   serialize_func;
  GstMetaDeserializeFunction deserialize_func;
  GstMetaClearFunction       clear_func;

  /* No padding needed, GstMetaInfo is always allocated by GStreamer and is
   * not subclassable or stack-allocatable, so we can extend it as we please
   * just like interfaces */
};


/**
 * GstMeta:
 * @flags: extra flags for the metadata
 * @info: pointer to the #GstMetaInfo
 */
struct _GstMeta {
  GstMetaFlags       flags;
  const GstMetaInfo *info;
};

1. API 示例

        分配新的 GstBuffer 后,您可以使用元数据特定的 API 向其添加元数据。按照惯例,名为 FooBar 的元数据 API 应提供两种方法:

        agst_buffer_add_foo_bar_meta () 和 gst_buffer_get_foo_bar_meta ()

这两个函数都应返回指向包含元数据字段的 FooBarMeta 结构的指针。一些 _add_*_meta () 可以具有额外的参数,这些参数通常用于为您配置元数据结构。让我们看一下用于指定视频帧裁剪区域的元数据。

#include <gst/video/gstvideometa.h>

[...]
  GstVideoCropMeta *meta;

  /* buffer points to a video frame, add some cropping metadata */
  meta = gst_buffer_add_video_crop_meta (buffer);

  /* configure the cropping metadata */
  meta->x = 8;
  meta->y = 8;
  meta->width = 120;
  meta->height = 80;
[...]

element可以在渲染帧时使用缓冲区上的元数据,如下所示:

#include <gst/video/gstvideometa.h>

[...]
  GstVideoCropMeta *meta;

  /* buffer points to a video frame, get the cropping metadata */
  meta = gst_buffer_get_video_crop_meta (buffer);

  /* 如果存在meda元数据信息,则按照元数据信息渲染;否则之间渲染*/
  if (meta) {
    /* render frame with cropping */
    _render_frame_cropped (buffer, meta->x, meta->y, meta->width, meta->height);
  } else {
    /* render frame */
    _render_frame (buffer);
  }
[...]

2. 实施新的 GstMeta

在下一部分中,我们将展示如何向系统添加新的元数据并在缓冲区上使用它。

2.1 定义metadata应用接口

        首先需要定义我们的 API 是什么样子,并且将此 API 注册到系统。这一点非常重要,因为当element协商它们准备交换哪种元数据时会使用我们定义的API。API 定义同时包含任意标签(tags),这些标签提示元数据中包含的内容,这在缓冲区通过pipeline时保护元数据非常重要(换言之,element之间会交换buffer和meta两种信息:buffer带有数据源,meta表征数据源信息)。

        首先我们开始制作 my-example-meta.h 头文件,该文件将包含 API 的定义和元数据的结构。

#include <gst/gst.h>

typedef struct _MyExampleMeta {
  GstMeta       meta;

  gint          age;
  gchar        *name;
} MyExampleMeta;

GType my_example_meta_api_get_type (void);
#define MY_EXAMPLE_META_API_TYPE (my_example_meta_api_get_type())

#define gst_buffer_get_my_example_meta(b) \
  ((MyExampleMeta*)gst_buffer_get_meta((b),MY_EXAMPLE_META_API_TYPE))

元数据 API 定义了 gint 和gchar的结构,结构中的第一个字段必须是 GstMeta。同时我们还定义了一个 my_example_meta_api_get_type () 函数用来注册我们的元数据 API,以及一个便捷的 gst_buffer_get_my_example_meta ()函数 ,该函数将使用我们的新 API 查找并返回元数据。

        接下来看看 my_example_meta_api_get_type () 函数在 my-example-meta.c 文件中是如何实现的:

#include "my-example-meta.h"

GType my_example_meta_api_get_type (void)
{
  static volatile GType type;
  static const gchar *tags[] = { "foo", "bar", NULL };

  if (g_once_init_enter (&type)) {
    GType _type = gst_meta_api_type_register ("MyExampleMetaAPI", tags);
    g_once_init_leave (&type, _type);
  }
  return type;
}

可以看到它只是使用 gst_meta_api_type_register() 函数为 API 注册一个名称和一些标签,返回一个新的 GType 指针。

2.2 实现metadata应用接口

        接下来,我们可以为已注册的元数据 API GType 实现一个实现。

        元数据 API 的实现细节保存在 GstMetaInfo 结构中,您可以使用 my_example_meta_get_info () 函数和 MY_EXAMPLE_META_INFO 宏将该结构提供给元数据 API 实现的用户,您还可以提供一种将元数据实现添加到 aGstBuffer 的方法。

您的 my-example-meta.h 头文件将需要以下附加内容:

[...]

/* implementation */
const GstMetaInfo *my_example_meta_get_info (void);
#define MY_EXAMPLE_META_INFO (my_example_meta_get_info())

MyExampleMeta * gst_buffer_add_my_example_meta (GstBuffer      *buffer,
                                                gint            age,
                                                const gchar    *name);

让我们看看这些函数在 my-example-meta.c 文件中是如何实现的。

[...]

static gboolean
my_example_meta_init (GstMeta * meta, gpointer params, GstBuffer * buffer)
{
  MyExampleMeta *emeta = (MyExampleMeta *) meta;

  emeta->age = 0;
  emeta->name = NULL;

  return TRUE;
}

static gboolean
my_example_meta_transform (GstBuffer * transbuf, GstMeta * meta,
    GstBuffer * buffer, GQuark type, gpointer data)
{
  MyExampleMeta *emeta = (MyExampleMeta *) meta;

  /* we always copy no matter what transform */
  gst_buffer_add_my_example_meta (transbuf, emeta->age, emeta->name);

  return TRUE;
}

static void
my_example_meta_free (GstMeta * meta, GstBuffer * buffer)
{
  MyExampleMeta *emeta = (MyExampleMeta *) meta;

  g_free (emeta->name);
  emeta->name = NULL;
}

const GstMetaInfo *my_example_meta_get_info (void)
{
  static const GstMetaInfo *meta_info = NULL;

  if (g_once_init_enter (&meta_info)) {
    const GstMetaInfo *mi = gst_meta_register (MY_EXAMPLE_META_API_TYPE,
        "MyExampleMeta",
        sizeof (MyExampleMeta),
        my_example_meta_init,
        my_example_meta_free,
        my_example_meta_transform);
    g_once_init_leave (&meta_info, mi);
  }
  return meta_info;
}

MyExampleMeta *gst_buffer_add_my_example_meta (GstBuffer   *buffer,
                                gint         age,
                                const gchar *name)
{
  MyExampleMeta *meta;

  g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);

  meta = (MyExampleMeta *) gst_buffer_add_meta (buffer,
      MY_EXAMPLE_META_INFO, NULL);

  meta->age = age;
  meta->name = g_strdup (name);

  return meta;
}

gst_meta_register () 函数完成注册细节:您实现的 API 和元数据结构的大小,以及初始化和释放内存区域的方法。您还可以实现一个转换函数,当对缓冲区执行特定转换时该函数将被调用。

最后您实现一个 gst_buffer_add_*_meta(),它将元数据实现添加到缓冲区并设置元数据的值。

四、GstBufferPool

        GstBufferPool 对象提供了一个方便的基类,用于管理可重用缓冲区列表。此对象的关键是所有缓冲区都具有相同的属性,例如大小、填充、元数据和对齐。

        可以配置 GstBufferPool 来管理特定大小的缓冲区的最小和最大数量。它还可以配置为使用特定的 GstAllocator 来分配缓冲区的内存。缓冲池还支持启用特定的选项,例如将 GstMeta 添加到池的缓冲区或在缓冲区的内存上启用特定填充。

        GstBufferPool 可以处于非活动状态或活动状态:在非活动状态下您可以配置池;在活动状态下您无法再更改配置,但可以从池中获取和释放缓冲区。在以下部分中,我们将介绍如何使用 GstBufferPool。

**
 * GstBufferPool:
 * @object: the parent structure
 * @flushing: whether the pool is currently gathering back outstanding buffers
 *
 * The structure of a #GstBufferPool. Use the associated macros to access the public
 * variables.
 */
struct _GstBufferPool {
  GstObject            object;

  /*< protected >*/
  gint                 flushing;

  /*< private >*/
  GstBufferPoolPrivate *priv;

  gpointer _gst_reserved[GST_PADDING];
};


struct _GstBufferPoolPrivate
{
  GstAtomicQueue *queue;
  GstPoll *poll;

  GRecMutex rec_lock;

  gboolean started;
  gboolean active;
  gint outstanding;             /* number of buffers that are in use */

  gboolean configured;
  GstStructure *config;

  guint size;
  guint min_buffers;
  guint max_buffers;
  guint cur_buffers;
  GstAllocator *allocator;
  GstAllocationParams params;
};


/**
 * GstAllocator:
 * @mem_map: the implementation of the GstMemoryMapFunction
 * @mem_unmap: the implementation of the GstMemoryUnmapFunction
 * @mem_copy: the implementation of the GstMemoryCopyFunction
 * @mem_share: the implementation of the GstMemoryShareFunction
 * @mem_is_span: the implementation of the GstMemoryIsSpanFunction
 * @mem_map_full: the implementation of the GstMemoryMapFullFunction.
 *      Will be used instead of @mem_map if present. (Since: 1.6)
 * @mem_unmap_full: the implementation of the GstMemoryUnmapFullFunction.
 *      Will be used instead of @mem_unmap if present. (Since: 1.6)
 *
 * The #GstAllocator is used to create new memory.
 */
struct _GstAllocator
{
  GstObject  object;

  const gchar               *mem_type;

  /*< public >*/
  GstMemoryMapFunction       mem_map;
  GstMemoryUnmapFunction     mem_unmap;

  GstMemoryCopyFunction      mem_copy;
  GstMemoryShareFunction     mem_share;
  GstMemoryIsSpanFunction    mem_is_span;

  GstMemoryMapFullFunction   mem_map_full;
  GstMemoryUnmapFullFunction mem_unmap_full;

  /*< private >*/
  gpointer _gst_reserved[GST_PADDING - 2];

  GstAllocatorPrivate *priv;
};

1. API示例

        可以有许多不同的 GstBufferPool 实现,它们都是 GstBufferPoolbase 类的子类。对于此示例,我们假设我们以某种方式可以访问缓冲池,要么是因为我们自己创建了它,要么是因为我们通过 ALLOCATION 查询获得了一个缓冲池,如下所示。

GstBufferPool 最初处于非活动状态,因此我们可以对其进行配置。尝试配置非非活动状态的 GstBufferPool 将失败。同样尝试激活未配置的缓冲池也将失败。

GstStructure *config;

[...]

  /* get config structure */
  config = gst_buffer_pool_get_config (pool);

  /* set caps, size, minimum and maximum buffers in the pool */
  gst_buffer_pool_config_set_params (config, caps, size, min, max);

  /* configure allocator and parameters */
  gst_buffer_pool_config_set_allocator (config, allocator, &params);

  /* store the updated configuration again */
  gst_buffer_pool_set_config (pool, config);

[...]

        GstBufferPool 的配置保存在通用的 GstStructure 中,可以使用 gst_buffer_pool_get_config() 获取,有API可以获取和设置此结构中的配置选项。结构更改完成后使用 gst_buffer_pool_set_config() 函数更新 GstBufferPool 中的配置。

GstBufferPool支持如下配置选项:

  • 要分配的缓冲区的上限(caps)。
  • 缓冲区的大小:这是池中缓冲区的建议大小,池子可能会决定分配更大的缓冲区以添加填充。
  • 池中缓冲区的最小和最大数量:当最小值不为0 时缓冲池将预先分配此数量的缓冲区;当最大值不为 0 时,缓冲池将分配最多最大数量的缓冲区。
  • 要使用的分配器和参数:某些缓冲池可能会忽略分配器并使用其内部分配器。
  • 用字符串标识的其他任意缓冲池选项:缓冲池使用 gst_buffer_pool_get_options() 列出支持的选项,您可以使用 gst_buffer_pool_has_option() 询问是否支持某个选项。可以通过使用 gst_buffer_pool_config_add_option () 将该选项添加到配置结构中来启用该选项,这些选项用于启用诸如让池在缓冲区上设置元数据,或为填充添加额外的配置选项之类的功能。

        在缓冲池上设置配置后,可以使用 gst_buffer_pool_set_active (pool, TRUE) 激活该池。此后您可以使用 gst_buffer_pool_acquire_buffer () 从池中检索缓冲区,如下所示:

[...]

  GstFlowReturn ret;
  GstBuffer *buffer;

  ret = gst_buffer_pool_acquire_buffer (pool, &buffer, NULL);
  if (G_UNLIKELY (ret != GST_FLOW_OK))
    goto pool_failed;

[...]

        检查 acquire 函数的返回值很重要,因为它可能会失败:当您的元素关闭时它将停用缓冲池,然后对 acquire 的所有调用都将返回 GST_FLOW_FLUSHING。

        从池中获取的所有缓冲区都将将其池成员设置为原始池。当缓冲区上的引用减少为0时,GStreamer 将自动调用 gst_buffer_pool_release_buffer() 将缓冲区释放回池。应用程序(或任何其他下游element)不需要知道缓冲区是否来自于池,您只需取消引用即可。


总结

这里介绍了GStreamer缓存的核心内容:memory,buffer,meta以及bufferpool,完成了element间交互数据的来龙去脉。

这段代码是在Docker容器中执行的一系列命令,用于安装一些软件包和依赖项。具体来说,它执行以下操作: 1. `apt-get clean`:清理apt-get缓存,以释放磁盘空间。 2. `apt-get update`:更新apt-get软件包列表。 3. `apt-get install -y`:安装以下软件包和依赖项: - `python3`:Python 3的主要二进制文件。 - `python3-pip`:Python 3的包管理工具pip。 - `libopencv-dev`:OpenCV开发库的头文件和静态库。 - `python3-opencv`:Python 3的OpenCV绑定。 - `build-essential`:构建软件包所需的基本工具和编译器。 - `yasm`:视频编解码器的汇编器。 - `cmake`:跨平台的构建工具。 - `libtool`:通用库支持脚本工具。 - `libc6`、`libc6-dev`:C标准库的运行时库和开发文件。 - `unzip`:解压缩工具。 - `wget`:网络下载工具。 - `libnuma1`、`libnuma-dev`:NUMA(非统一内存访问)系统的库和开发文件。 - `libgstreamer1.0-0`:GStreamer多媒体框架的核心库。 - `gstreamer1.0-plugins-base`、`gstreamer1.0-plugins-good`、`gstreamer1.0-plugins-bad`、`gstreamer1.0-plugins-ugly`、`gstreamer1.0-libav`:GStreamer插件和解码器。 - `gstreamer1.0-doc`、`gstreamer1.0-tools`、`gstreamer1.0-x`、`gstreamer1.0-alsa`、`gstreamer1.0-gl`、`gstreamer1.0-gtk3`、`gstreamer1.0-qt5`、`gstreamer1.0-pulseaudio`:GStreamer的文档、工具和相关库。 - `libglib2.0-dev`:GLib开发库的头文件。 - `libgstrtspserver-1.0-dev`:GStreamer RTSP服务器库的开发文件。 - `gstreamer1.0-rtsp`:GStreamer的RTSP插件。 这些操作旨在为容器配置一个适合开发的环境,使其能够支持Python编程、OpenCV图像处理和GStreamer多媒体处理等任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值