FFMpeg 源码分析 (3)avformat_open_input()

这个函数主要用来打开媒体资源。完成媒体格式的探测和获取相关的媒体信息的工作。

函数完成定义如下:

int avformat_open_input(AVFormatContext **ps, const char *filename,
                    AVInputFormat *fmt, AVDictionary **options)
{
    AVFormatContext *s = *ps;
    int i, ret = 0;
    AVDictionary *tmp = NULL;
    ID3v2ExtraMeta *id3v2_extra_meta = NULL;

    if (!s && !(s = avformat_alloc_context()))
        return AVERROR(ENOMEM);
    if (!s->av_class) {
        av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
        return AVERROR(EINVAL);
    }
    if (fmt)
        s->iformat = fmt;

    if (options)
        av_dict_copy(&tmp, *options, 0);

    if (s->pb) // must be before any goto fail
        s->flags |= AVFMT_FLAG_CUSTOM_IO;

    if ((ret = av_opt_set_dict(s, &tmp)) < 0)
        goto fail;

    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;
    s->probe_score = ret;

    if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
        s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
        if (!s->protocol_whitelist) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
        s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
        if (!s->protocol_blacklist) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
        av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist);
        ret = AVERROR(EINVAL);
        goto fail;
    }

    avio_skip(s->pb, s->skip_initial_bytes);

    /* Check filename in case an image number is expected. */
    if (s->iformat->flags & AVFMT_NEEDNUMBER) {
        if (!av_filename_number_test(filename)) {
            ret = AVERROR(EINVAL);
            goto fail;
        }
    }

    s->duration = s->start_time = AV_NOPTS_VALUE;
    av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename));

    /* Allocate private data. */
    if (s->iformat->priv_data_size > 0) {
        if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        if (s->iformat->priv_class) {
            *(const AVClass **) s->priv_data = s->iformat->priv_class;
            av_opt_set_defaults(s->priv_data);
            if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
                goto fail;
        }
    }

    /* e.g. AVFMT_NOFILE formats will not have a AVIOContext */
    if (s->pb)
        ff_id3v2_read(s, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta, 0);

    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
        if ((ret = s->iformat->read_header(s)) < 0)
            goto fail;

    if (id3v2_extra_meta) {
        if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
            !strcmp(s->iformat->name, "tta")) {
            if ((ret = ff_id3v2_parse_apic(s, &id3v2_extra_meta)) < 0)
                goto fail;
        } else
            av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
    }
    ff_id3v2_free_extra_meta(&id3v2_extra_meta);

    if ((ret = avformat_queue_attached_pictures(s)) < 0)
        goto fail;

    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->internal->data_offset)
        s->internal->data_offset = avio_tell(s->pb);

    s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;

    update_stream_avctx(s);

    for (i = 0; i < s->nb_streams; i++)
        s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;

    if (options) {
        av_dict_free(options);
        *options = tmp;
    }
    *ps = s;
    return 0;

fail:
    ff_id3v2_free_extra_meta(&id3v2_extra_meta);
    av_dict_free(&tmp);
    if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
        avio_closep(&s->pb);
    avformat_free_context(s);
    *ps = NULL;
    return ret;
}

我们一点点来分析,先分析下面这段代码。

if (!s && !(s = avformat_alloc_context()))
    return AVERROR(ENOMEM);
if (!s->av_class) {
    av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
        return AVERROR(EINVAL);
}

主要做的就是初始化了一个AVFormatContext 结构体,也是最后的返回参数,通过第一个参数 AVFormatContext **ps 返回。那我们接下来具体看这个初始化时怎么做的吧,需要完成哪些工作。

AVFormatContext *avformat_alloc_context(void)
{
    AVFormatContext *ic;
    ic = av_malloc(sizeof(AVFormatContext)); //申请内存空间
    if (!ic) return ic;
    avformat_get_context_defaults(ic); //给ic 结构体成员赋默认值

    ic->internal = av_mallocz(sizeof(*ic->internal));
    if (!ic->internal) {
        avformat_free_context(ic);
        return NULL;
    }
    ic->internal->offset = AV_NOPTS_VALUE;
    ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;

    return ic;
}

我们看一下avformat_get_context_defaults(ic) 这一句做了些什么

static void avformat_get_context_defaults(AVFormatContext *s)
{
    memset(s, 0, sizeof(AVFormatContext)); 

    s->av_class = &av_format_context_class; //指向一个定义好的全局 format context class

    s->io_open  = io_open_default; // 默认以open 操作函数
    s->io_close = io_close_default; //默认的 close 操作函数

    av_opt_set_defaults(s); //所有的option 赋默认值
}  

av_format_context_class.option 会指向一个 avformat_options的结构体数组,里面存储了默认的options 的值。

接下来这一段

 if (options)
    av_dict_copy(&tmp, *options, 0);

if (s->pb) // must be before any goto fail
    s->flags |= AVFMT_FLAG_CUSTOM_IO;

if ((ret = av_opt_set_dict(s, &tmp)) < 0)
    goto fail;

就是如果avformat_open_input 这个函数调用的时候带了options参数,则把这些options 参数赋值到定义好的 AVFormatContext 结构体中。

接下来就是调用init_input() 函数去探测媒体资源的类型了。

static int init_input(AVFormatContext *s, const char *filename,
                      AVDictionary **options)
{
    int ret;
    AVProbeData pd = { filename, NULL, 0 };
    int score = AVPROBE_SCORE_RETRY;

    if (s->pb) { 
        s->flags |= AVFMT_FLAG_CUSTOM_IO;
        if (!s->iformat)
            return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                         s, 0, s->format_probesize);
        else if (s->iformat->flags & AVFMT_NOFILE)
            av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                      "will be ignored with AVFMT_NOFILE format.\n");
        return 0;
    }

    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
        return score;

    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;

    if (s->iformat)
        return 0;
    return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                 s, 0, s->format_probesize);
}

一般情况我们播放一个视频文件的话,我们在代码调用的时候不会指定好pb 所以一般就是直接执行 s->iformat = av_probe_input_format2(&pd, 0, &score) 这一句。看这个函数调用的结果。而在 av_probe_input_format2 里面又是直接调用 av_probe_input_format3 这个函数。函数调用语句为:AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret); 其中is_opened 的值为0。

那我们就一起来看看这个时候av_probe_input_format3(pd, 0, &score_ret) 这个函数调用里面做了些什么吧,先看这一段

if (!lpd.buf)
        lpd.buf = (unsigned char *) zerobuffer;

if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
        int id3len = ff_id3v2_tag_len(lpd.buf);
        if (lpd.buf_size > id3len + 16) {
            if (lpd.buf_size < 2LL*id3len + 16)
                nodat = ID3_ALMOST_GREATER_PROBE;
            lpd.buf      += id3len;
            lpd.buf_size -= id3len;
        } else if (id3len >= PROBE_BUF_MAX) {
            nodat = ID3_GREATER_MAX_PROBE;
        } else
            nodat = ID3_GREATER_PROBE;
}

我们回顾一下,我们的pd是怎么定义的:AVProbeData pd = { filename, NULL, 0 };AVProbeData 结构体的定义是:

typedef struct AVProbeData {
    const char *filename;
    unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
    int buf_size;       /**< Size of buf except extra allocated bytes */
    const char *mime_type; /**< mime_type, when known. */
} AVProbeData;

那我们可以知道, 元素filename就是我们调用 avformat_open_input 传入的第二个参数 filenamebufNULL, buf_size 为 0, 那么mime_type 也为 NULL。所以我们发现这段代码是不会执行的。那么就直接到了

while ((fmt1 = av_iformat_next(fmt1))) {
    ..............
}

这个循环体里面。

我们先来看一下 av_iformat_next(fmt1) 这段代码的结果是什么吧,av_iformat_next 函数的源码如下:

AVInputFormat *av_iformat_next(const AVInputFormat *f)
{
    if (f)
        return f->next;
    else
        return first_iformat;
}

一目了然,就是取链表的下一个节点。那么这个链表是干嘛的呢,就是一个类型为AVInputFormat 的链表,链表里面存储的是FFMpeg所有支持的解复用器的引用。具体参见我的上一篇博文 FFMpeg 源码分析(1)av_register_all()

那么下面的事情就是这个 while 循环到底在做什么呢?好的,我们一步步来看下去。我们先来看一个if ... else ... 语句。

    if (fmt1->read_probe) {
        score = fmt1->read_probe(&lpd);
        if (score)
            av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
        if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
            switch (nodat) {
            case NO_ID3:
                score = FFMAX(score, 1);
                break;
            case ID3_GREATER_PROBE:
            case ID3_ALMOST_GREATER_PROBE:
                score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
                break;
            case ID3_GREATER_MAX_PROBE:
                score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
                break;
            }
        }
    } else if (fmt1->extensions) {
        if (av_match_ext(lpd.filename, fmt1->extensions))
            score = AVPROBE_SCORE_EXTENSION;
    }

如果有 read_probe 这个方法,就试着去读取探测一下。如果没有就走到else 部分,就比较 AVInputFormat 的解复用器里面定义的扩展名和我们的传进来的文件扩展名是否匹配,如果匹配,就得到一个文件扩展名匹配这个等级的分数。

好的,我们现在回过头来看一下read_probe 会做些什么吧。这个要到特定的协议格式实现里面去找。我们一 avi 格式为例吧。avi 格式对应的 AVInputFormat 类型结构定义如下:

AVInputFormat ff_avi_demuxer = {
    .name           = "avi",
    .long_name      = NULL_IF_CONFIG_SMALL("AVI (Audio Video Interleaved)"),
    .priv_data_size = sizeof(AVIContext),
    .extensions     = "avi",
    .read_probe     = avi_probe,
    .read_header    = avi_read_header,
    .read_packet    = avi_read_packet,
    .read_close     = avi_read_close,
    .read_seek      = avi_read_seek,
    .priv_class = &demuxer_class,
};

read_probe 函数指向 avi_probe, 所以avi 格式的read_probe 调用就是执行 avi_probe。具体实现如下:

static int avi_probe(AVProbeData *p)
{
    int i;

    /* check file header */
    for (i = 0; avi_headers[i][0]; i++)
        if (AV_RL32(p->buf    ) == AV_RL32(avi_headers[i]    ) &&
            AV_RL32(p->buf + 8) == AV_RL32(avi_headers[i] + 4))
            return AVPROBE_SCORE_MAX;

    return 0;
}

那其实比较明显,就是对比文件头部信息是否匹配。如果完全匹配就直接返回 AVPROBE_SCORE_MAX 这个分数。不过这个时候 p->buf 里面应该是还没有东西的。所以应该是return 0 。那边接下来就是又去判断文件扩展名。如果匹配,那就根据 nodat 的值进入不同的分支,我们知道,这里我们的 nodat 的值应该是 NO_ID3 所以最后的 score 应该为 1。

接下来就是去判断mime_type 是否匹配。匹配则更新score 的值。
最后如果整个探测过程完成后如果score > 0 则返回我们探测到的 AVInputFormat 结构体。如果是 0 分,则认为探测失败。

那回到av_probe_input_format2 这个函数,我们在调用的时候,传入的 score的阈值是 AVPROBE_SCORE_RETRY(AVPROBE_SCORE_MAX/4) AVPROBE_SCORE_MAX 为100 所以阈值就是25。
那我看下 av_probe_input_format2 里面的逻辑

AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)
{
    int score_ret;
    AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
    if (score_ret > *score_max) {
        *score_max = score_ret;
        return fmt;
    } else
        return NULL;
}

如果 av_probe_input_format3 探测的分数大于25分,那么我们返回av_probe_input_format3 探测到的这个类型。如果小于等于,认为探测失败,返回 NULL。这样 (s->iformat = av_probe_input_format2(&pd, 0, &score)) 这次探测就失败了。函数继续往下执行。

 if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;

尝试去打开这个媒体资源。那么 s->io_open 又是什么呢? 其实就是 io_open_default 这个函数。在前面说到的 avformat_get_context_defaults 函数中指定。好了,那么接下来就是要看 io_open_default 这个函数的功能是什么。

static int io_open_default(AVFormatContext *s, AVIOContext **pb,
                           const char *url, int flags, AVDictionary **options)
{
    #if FF_API_OLD_OPEN_CALLBACKS
    FF_DISABLE_DEPRECATION_WARNINGS
        if (s->open_cb)
            return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options);
    FF_ENABLE_DEPRECATION_WARNINGS
    #endif

        return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}

代码很简单,刨除条件编译部分,就只剩下 return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist); 这一句了。

那么ffio_open_whitelist 函数的实现在哪里呢?在 libavformat/aviobuf.c里面。我们来分析这个函数的具体实现。

int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                     const AVIOInterruptCB *int_cb, AVDictionary **options,
                     const char *whitelist, const char *blacklist
                    )
{
    URLContext *h;
    int err;

    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist);
    if (err < 0)
        return err;
    err = ffio_fdopen(s, h);
    if (err < 0) {
        ffurl_close(h);
        return err;
    }
    return 0;
}

那我们先看 ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist); 这句。先确定一下参数,filename 就是媒体资源的url , flagsAVIO_FLAG_READ | s->avio_flags 我们知道 avio_flag 为空,所以就是 AVIO_FLAG_READint_cb 也为NULL,options 为NULL, whitelist 为NULL, blacklist 也为NULL。

int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
    int ret = ffurl_alloc(puc, filename, flags, int_cb);
    if (ret < 0)
        return ret;
    if (options &&
        (ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;
    if (options && (*puc)->prot->priv_data_class &&
        (ret = av_opt_set_dict((*puc)->priv_data, options)) < 0)
        goto fail;

    if (!options)
        options = &tmp_opts;

    av_assert0(!whitelist ||
               !(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               !strcmp(whitelist, e->value));
    av_assert0(!blacklist ||
               !(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               !strcmp(blacklist, e->value));

    if ((ret = av_dict_set(options, "protocol_whitelist", whitelist, 0)) < 0)
        goto fail;

    if ((ret = av_dict_set(options, "protocol_blacklist", blacklist, 0)) < 0)
        goto fail;

    if ((ret = av_opt_set_dict(*puc, options)) < 0)
        goto fail;

    ret = ffurl_connect(*puc, options);

    if (!ret)
        return 0;
fail:
    ffurl_close(*puc);
    *puc = NULL;
    return ret;
}

这段代码我们简化一下来看就是这样

int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
    int ret = ffurl_alloc(puc, filename, flags, int_cb);
    if (ret < 0)
        return ret;

    ret = ffurl_connect(*puc, options);

    if (!ret)
        return 0;
fail:
    ffurl_close(*puc);
    *puc = NULL;
    return ret;
}

ffurl_alloc(puc, filename, AVIO_FLAG_READ , NULL); 然后是 ffurl_connect(*puc, NULL); 接下来我们一个个分析下去。ffurl_alloc() 的完整代码如下

int ffurl_alloc(URLContext **puc, const char *filename, int flags,
                const AVIOInterruptCB *int_cb)
{
    const URLProtocol *p = NULL;

    p = url_find_protocol(filename);
    if (p)
       return url_alloc_for_protocol(puc, p, filename, flags, int_cb);

    *puc = NULL;
    if (av_strstart(filename, "https:", NULL))
        av_log(NULL, AV_LOG_WARNING, "https protocol not found, recompile FFmpeg with "
                                     "openssl, gnutls "
                                     "or securetransport enabled.\n");
    return AVERROR_PROTOCOL_NOT_FOUND;
}

函数的目的是最终得到一个 URLContext 的结构体。那首先,根据filename 找到一个对应的 URLProtocol 结构体。

那我们就来看看 url_find_protocol 这个函数吧。这个函数比较简单,总结一下吧,就是截取filename 中的协议标识字符串,跟 url_protocols 这个全局数组中的每一个协议的 name 字段做比较。匹配则返回这个URLProtocol 结构体,举个例子。假如是 file 类型的。那么就会返回 ff_file_protocol 这个结构体指针。

完整定义如下:

const URLProtocol ff_file_protocol = {
    .name                = "file",
    .url_open            = file_open,
    .url_read            = file_read,
    .url_write           = file_write,
    .url_seek            = file_seek,
    .url_close           = file_close,
    .url_get_file_handle = file_get_handle,
    .url_check           = file_check,
    .url_delete          = file_delete,
    .url_move            = file_move,
    .priv_data_size      = sizeof(FileContext),
    .priv_data_class     = &file_class,
    .url_open_dir        = file_open_dir,
    .url_read_dir        = file_read_dir,
    .url_close_dir       = file_close_dir,
    .default_whitelist   = "file,crypto"
};

那如果拿到了 URLProtocol, 就执行 url_alloc_for_protocol(puc, p, filename, flags, int_cb); 根据 URLProtocol 结构体的信息去完成一个URLContext 结构体的初始化工作。这样ffurl_alloc 的工作就完成了。

接下来就是ffurl_connect(*puc, options); 没有传options的话,代码就简化到

err = uc->prot->url_open2 ? uc->prot->url_open2(uc,
                                                  uc->filename,
                                                  uc->flags,
                                                  options) :
        uc->prot->url_open(uc, uc->filename, uc->flags);

我们的例子里面就是调用url_open 函数。也就是file_open 就是打开文件,获得一个文件句柄,存放在 FileContext 结构体即 URLContext 结构体的 priv_data 里面。
至此 ffurl_open_whitelist 函数就结束了。

接下来就是 ffio_fdopen 函数了这个函数就通过刚刚得到了一个 URLContext 结构体得到一个 AVIOContext 结构体。

接下来就是通过 av_probe_input_buffer2 来探测具体的媒体资源格式了。这个函数里面重要的部分就是

 for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;
         probe_size = FFMIN(probe_size << 1,
                            FFMAX(max_probe_size, probe_size + 1))) {

         .......
        if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
            goto fail;
        if ((ret = avio_read(pb, buf + buf_offset,
                             probe_size - buf_offset)) < 0) {
            /* Fail if error was not end of file, otherwise, lower score. */
            if (ret != AVERROR_EOF)
                goto fail;

            score = 0;
            ret   = 0;          /* error was end of file, nothing read */
        }
        .......
        *fmt = av_probe_input_format2(&pd, 1, &score);
        ........
}

去读取媒体资源的内容。把读取到的内容放到 AVProbeDatabuf 字段中。然后再调用 av_probe_input_format2 函数去探测媒体资源的格式。只不过这一次 pb.buf 不再是空,是有内容的。
而关于 av_probe_input_format2 的具体探测过程参考前面的分析就行。是一样的。

init_imput(s, filename, &tmp) 这条语句执行完成之后,就会调用之前初始化好的AVInputFormat 结构体中的 read_header 方法。如果我们以FLV媒体封装格式为例的话,那么read_header 函数指针就指向的 flv_read_header 函数,函数定义在 libavformat/flvdec.c 中。完整代码如下:

static int flv_read_header(AVFormatContext *s)
{
    FLVContext *flv = s->priv_data;
    int offset;

    avio_skip(s->pb, 4);
    avio_r8(s->pb); // flags

    s->ctx_flags |= AVFMTCTX_NOHEADER;

    offset = avio_rb32(s->pb);
    avio_seek(s->pb, offset, SEEK_SET);
    avio_skip(s->pb, 4);

    s->start_time = 0;
    flv->sum_flv_tag_size = 0;

    return 0;
}

主要读取的头部信息。在现在的版本里面,read_header 并不会把媒体流给创建出来。而是要等到 read_packet 的时候。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值