【stagefrightplayer】3 MediaExtractor介绍

概述


本篇介绍播放器结构中的第一部分Stream+Demuxer.

Awesomeplayer中对应的数据结构主要有DataSource,MediaExtractor,MediaSource。

其中DataSource 主要负责提供原始数据,MediaSource负责提供demux后的数据(即实际的audio 或者 video 数据包)

而MediaExtractor则负责中间的过程,即将从DataSource得到的原始数据解析成解码器需要的es数据,并通过MediaSource的接口输出。

以ts为例,extractor在awesomeplayer中的位置图如下:


我们按照一个demux的基本结构结合ts的实例来分析整个结构

其中demuxer的部分我们按照ffmpeg demuxer的结构中的主要成员来分析

1 stream -- 功能

2 extractor 创建流程

3 extractor 结构介绍(ts为例)

3.1 demuxer -- read_probe

3.2 demuxer -- read_header

3.3 demuxer -- read_packet

3.4 demuxer -- read_seek

下面通过实际的代码来分析

1 stream -- 功能


stream的主要功能是,从外部介质(本地磁盘或者网络等)获取待播放的原始数据。

对应的数据结构为:DataSource

看下awesomeplayer中对应的代码

a 构造函数

  1. AwesomePlayer::AwesomePlayer(){
  2. *************
  3. DataSource::RegisterDefaultSniffers();
  4. *************
  5. }
AwesomePlayer::AwesomePlayer(){
    *************
    DataSource::RegisterDefaultSniffers();
    *************
}

看下RegisterDefaultSniffers实现

  1. // static
  2. void DataSource::RegisterSniffer(SnifferFunc func) {
  3. Mutex::Autolock autoLock(gSnifferMutex);
  4. for (List<SnifferFunc>::iterator it = gSniffers.begin();
  5. it != gSniffers.end(); ++it) {
  6. if (*it == func) {
  7. return;
  8. }
  9. }
  10. gSniffers.push_back(func);
  11. }
// static
void DataSource::RegisterSniffer(SnifferFunc func) {
    Mutex::Autolock autoLock(gSnifferMutex);
 
    for (List<SnifferFunc>::iterator it = gSniffers.begin();
         it != gSniffers.end(); ++it) {
        if (*it == func) {
            return;
        }
    }
 
    gSniffers.push_back(func);
}

void DataSource::RegisterDefaultSniffers() {

  1. RegisterSniffer(SniffMPEG4);
  2. RegisterSniffer(SniffFragmentedMP4);
  3. RegisterSniffer(SniffMatroska);
  4. RegisterSniffer(SniffOgg);
  5. RegisterSniffer(SniffWAV);
  6. RegisterSniffer(SniffFLAC);
  7. RegisterSniffer(SniffAMR);
  8. RegisterSniffer(SniffMPEG2TS);
  9. RegisterSniffer(SniffMP3);
  10. RegisterSniffer(SniffAAC);
  11. RegisterSniffer(SniffMPEG2PS);
  12. RegisterSniffer(SniffWVM);
  13. char value[PROPERTY_VALUE_MAX];
  14. if (property_get("drm.service.enabled", value, NULL)
  15. && (!strcmp(value, "1") || !strcasecmp(value, "true"))) {
  16. RegisterSniffer(SniffDRM);
  17. }
    RegisterSniffer(SniffMPEG4);
    RegisterSniffer(SniffFragmentedMP4);
    RegisterSniffer(SniffMatroska);
    RegisterSniffer(SniffOgg);
    RegisterSniffer(SniffWAV);
    RegisterSniffer(SniffFLAC);
    RegisterSniffer(SniffAMR);
    RegisterSniffer(SniffMPEG2TS);
    RegisterSniffer(SniffMP3);
    RegisterSniffer(SniffAAC);
    RegisterSniffer(SniffMPEG2PS);
    RegisterSniffer(SniffWVM);
 
    char value[PROPERTY_VALUE_MAX];
    if (property_get("drm.service.enabled", value, NULL)
            && (!strcmp(value, "1") || !strcasecmp(value, "true"))) {
        RegisterSniffer(SniffDRM);
    }
}

从代码可以看出RegisterDefaultSniffers的主要作用既是注册Sniffer函数

将所有的sniffer函数都挂在全局链表gSniffers中。

sniffer函数的主要作用就是用于探测文件的类型,每种类型的媒体文件都对应一个sniffer函数。

这里从代码可以看出原生的android播放器支持的格式还比较少

这里主要作用就是注册完成后 demuxer在read_probe阶段便可以通过调用sniffer函数来探测文件类型,具体等讲解probe的时候结合实际的例子(mpegts)分析

b setDataSource

  1. status_t AwesomePlayer::setDataSource(
  2. int fd, int64_t offset, int64_t length) {
  3. Mutex::Autolock autoLock(mLock);
  4. reset_l();
  5. sp<DataSource> dataSource = new FileSource(fd, offset, length);
  6. status_t err = dataSource->initCheck();
  7. if (err != OK) {
  8. return err;
  9. }
  10. mFileSource = dataSource;
  11. {
  12. Mutex::Autolock autoLock(mStatsLock);
  13. mStats.mFd = fd;
  14. mStats.mURI = String8();
  15. }
  16. return setDataSource_l(dataSource);
  17. }
status_t AwesomePlayer::setDataSource(
        int fd, int64_t offset, int64_t length) {
    Mutex::Autolock autoLock(mLock);
 
    reset_l();
 
    sp<DataSource> dataSource = new FileSource(fd, offset, length);
 
    status_t err = dataSource->initCheck();
 
    if (err != OK) {
        return err;
    }
 
    mFileSource = dataSource;
 
    {
        Mutex::Autolock autoLock(mStatsLock);
        mStats.mFd = fd;
        mStats.mURI = String8();
    }
 
    return setDataSource_l(dataSource);
}

代码中构造了FileSource对象赋值给mFileSource,这里FileSource继承自DataSource,提供stream功能。

下面列出filesource类的定义

  1. class FileSource : public DataSource {
  2. public:
  3. FileSource(const char *filename);
  4. FileSource(int fd, int64_t offset, int64_t length);
  5. virtual status_t initCheck() const;
  6. virtual ssize_t readAt(off64_t offset, void *data, size_t size);
  7. virtual status_t getSize(off64_t *size);
  8. virtual sp<DecryptHandle> DrmInitialization(const char *mime);
  9. virtual void getDrmInfo(sp<DecryptHandle> &handle, DrmManagerClient **client);
  10. protected:
  11. virtual ~FileSource();
  12. private:
  13. int mFd;
  14. int64_t mOffset;
  15. int64_t mLength;
  16. Mutex mLock;
  17. /*for DRM*/
  18. sp<DecryptHandle> mDecryptHandle;
  19. DrmManagerClient *mDrmManagerClient;
  20. int64_t mDrmBufOffset;
  21. int64_t mDrmBufSize;
  22. unsigned char *mDrmBuf;
  23. ssize_t readAtDRM(off64_t offset, void *data, size_t size);
  24. FileSource(const FileSource &);
  25. FileSource &operator=(const FileSource &);
  26. };
class FileSource : public DataSource {
public:
    FileSource(const char *filename);
    FileSource(int fd, int64_t offset, int64_t length);
 
    virtual status_t initCheck() const;
 
    virtual ssize_t readAt(off64_t offset, void *data, size_t size);
 
    virtual status_t getSize(off64_t *size);
 
    virtual sp<DecryptHandle> DrmInitialization(const char *mime);
 
    virtual void getDrmInfo(sp<DecryptHandle> &handle, DrmManagerClient **client);
 
protected:
    virtual ~FileSource();
 
private:
    int mFd;
    int64_t mOffset;
    int64_t mLength;
    Mutex mLock;
 
    /*for DRM*/
    sp<DecryptHandle> mDecryptHandle;
    DrmManagerClient *mDrmManagerClient;
    int64_t mDrmBufOffset;
    int64_t mDrmBufSize;
    unsigned char *mDrmBuf;
 
    ssize_t readAtDRM(off64_t offset, void *data, size_t size);
 
    FileSource(const FileSource &);
    FileSource &operator=(const FileSource &);
};

这里filesource提供了readAt方法提供原始数据获取,由于其参数有offset,则支持随机存取,支持seek功能。其构造函数如下

  1. FileSource::FileSource(const char *filename)
  2. : mFd(-1),
  3. mOffset(0),
  4. mLength(-1),
  5. mDecryptHandle(NULL),
  6. mDrmManagerClient(NULL),
  7. mDrmBufOffset(0),
  8. mDrmBufSize(0),
  9. mDrmBuf(NULL){
  10. mFd = open(filename, O_LARGEFILE | O_RDONLY);
  11. if (mFd >= 0) {
  12. mLength = lseek64(mFd, 0, SEEK_END);
  13. } else {
  14. ALOGE("Failed to open file '%s'. (%s)", filename, strerror(errno));
  15. }
  16. }
FileSource::FileSource(const char *filename)
    : mFd(-1),
      mOffset(0),
      mLength(-1),
      mDecryptHandle(NULL),
      mDrmManagerClient(NULL),
      mDrmBufOffset(0),
      mDrmBufSize(0),
      mDrmBuf(NULL){
 
    mFd = open(filename, O_LARGEFILE | O_RDONLY);
 
    if (mFd >= 0) {
        mLength = lseek64(mFd, 0, SEEK_END);
    } else {
        ALOGE("Failed to open file '%s'. (%s)", filename, strerror(errno));
    }
}

构造函数的主要功能就是open给定的文件名,并将句柄存储在mFd中,后面读取数据可以直接使用Linux标准方法读取。

2 awesomeplayer中extractor 创建流程


在setDataSource的最后,会调用setDataSource_l(dataSource);将datasource和对应的extractor对应起来,这里看下流程

  1. status_t AwesomePlayer::setDataSource_l(
  2. const sp<DataSource> &dataSource) {
  3. sp<MediaExtractor> extractor = MediaExtractor::Create(dataSource);
  4. if (extractor == NULL) {
  5. return UNKNOWN_ERROR;
  6. }
  7. if (extractor->getDrmFlag()) {
  8. checkDrmStatus(dataSource);
  9. }
  10. return setDataSource_l(extractor);
  11. }
status_t AwesomePlayer::setDataSource_l(
        const sp<DataSource> &dataSource) {
    sp<MediaExtractor> extractor = MediaExtractor::Create(dataSource);
 
    if (extractor == NULL) {
        return UNKNOWN_ERROR;
    }
 
    if (extractor->getDrmFlag()) {
        checkDrmStatus(dataSource);
    }
 
    return setDataSource_l(extractor);
}

2.1 MediaExtractor::Create

这里通过MediaExtractor::Create创建extractor ,分段来看代码实现。这里还是忽略drm等实现

  1. sp<MediaExtractor> MediaExtractor::Create(
  2. const sp<DataSource> &source, const char *mime) {
  3. sp<AMessage> meta;
  4. String8 tmp;
  5. if (mime == NULL) {
  6. float confidence;
  7. if (!source->sniff(&tmp, &confidence, &meta)) {
  8. ALOGV("FAILED to autodetect media content.");
  9. return NULL;
  10. }
  11. mime = tmp.string();
  12. ALOGV("Autodetected media content as '%s' with confidence %.2f",
  13. mime, confidence);
  14. }
sp<MediaExtractor> MediaExtractor::Create(
        const sp<DataSource> &source, const char *mime) {
    sp<AMessage> meta;
 
    String8 tmp;
    if (mime == NULL) {
        float confidence;
        if (!source->sniff(&tmp, &confidence, &meta)) {
            ALOGV("FAILED to autodetect media content.");
 
            return NULL;
        }
 
        mime = tmp.string();
        ALOGV("Autodetected media content as '%s' with confidence %.2f",
             mime, confidence);
    }

这里第一步是通过调用datasource的siniff函数探测文件类型。

看下sniff实现

  1. bool DataSource::sniff(
  2. String8 *mimeType, float *confidence, sp<AMessage> *meta) {
  3. *mimeType = "";
  4. *confidence = 0.0f;
  5. meta->clear();
  6. Mutex::Autolock autoLock(gSnifferMutex);
  7. for (List<SnifferFunc>::iterator it = gSniffers.begin();
  8. it != gSniffers.end(); ++it) {
  9. String8 newMimeType;
  10. float newConfidence;
  11. sp<AMessage> newMeta;
  12. if ((*it)(this, &newMimeType, &newConfidence, &newMeta)) {
  13. if (newConfidence > *confidence) {
  14. *mimeType = newMimeType;
  15. *confidence = newConfidence;
  16. *meta = newMeta;
  17. }
  18. }
  19. }
  20. return *confidence > 0.0;
  21. }
bool DataSource::sniff(
        String8 *mimeType, float *confidence, sp<AMessage> *meta) {
    *mimeType = "";
    *confidence = 0.0f;
    meta->clear();
 
    Mutex::Autolock autoLock(gSnifferMutex);
    for (List<SnifferFunc>::iterator it = gSniffers.begin();
         it != gSniffers.end(); ++it) {
        String8 newMimeType;
        float newConfidence;
        sp<AMessage> newMeta;
        if ((*it)(this, &newMimeType, &newConfidence, &newMeta)) {
            if (newConfidence > *confidence) {
                *mimeType = newMimeType;
                *confidence = newConfidence;
                *meta = newMeta;
            }
        }
    }
 
    return *confidence > 0.0;
}

主要是将gSniffers链表中的每种格式的函数调用一遍,选取最高的confidence作为选中的文件格式。后面会以ts为例讲解具体细节。

下面继续

  1. MediaExtractor *ret = NULL;
  2. if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
  3. || !strcasecmp(mime, "audio/mp4")) {
  4. int fragmented = 0;
  5. if (meta != NULL && meta->findInt32("fragmented", &fragmented) && fragmented) {
  6. ret = new FragmentedMP4Extractor(source);
  7. } else {
  8. ret = new MPEG4Extractor(source);
  9. }
  10. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG)) {
  11. ret = new MP3Extractor(source, meta);
  12. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
  13. || !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) {
  14. ret = new AMRExtractor(source);
  15. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)) {
  16. ret = new FLACExtractor(source);
  17. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) {
  18. ret = new WAVExtractor(source);
  19. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_OGG)) {
  20. ret = new OggExtractor(source);
  21. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MATROSKA)) {
  22. ret = new MatroskaExtractor(source);
  23. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2TS)) {
  24. ret = new MPEG2TSExtractor(source);
  25. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WVM)) {
  26. // Return now. WVExtractor should not have the DrmFlag set in the block below.
  27. return new WVMExtractor(source);
  28. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AAC_ADTS)) {
  29. ret = new AACExtractor(source, meta);
  30. } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2PS)) {
  31. ret = new MPEG2PSExtractor(source);
  32. }
  33. if (ret != NULL) {
  34. if (isDrm) {
  35. ret->setDrmFlag(true);
  36. } else {
  37. ret->setDrmFlag(false);
  38. }
  39. }
  40. return ret;
  41. }
MediaExtractor *ret = NULL;
    if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG4)
            || !strcasecmp(mime, "audio/mp4")) {
        int fragmented = 0;
        if (meta != NULL && meta->findInt32("fragmented", &fragmented) && fragmented) {
            ret = new FragmentedMP4Extractor(source);
        } else {
            ret = new MPEG4Extractor(source);
        }
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_MPEG)) {
        ret = new MP3Extractor(source, meta);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_NB)
            || !strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AMR_WB)) {
        ret = new AMRExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_FLAC)) {
        ret = new FLACExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WAV)) {
        ret = new WAVExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_OGG)) {
        ret = new OggExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MATROSKA)) {
        ret = new MatroskaExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2TS)) {
        ret = new MPEG2TSExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_WVM)) {
        // Return now.  WVExtractor should not have the DrmFlag set in the block below.
        return new WVMExtractor(source);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_AUDIO_AAC_ADTS)) {
        ret = new AACExtractor(source, meta);
    } else if (!strcasecmp(mime, MEDIA_MIMETYPE_CONTAINER_MPEG2PS)) {
        ret = new MPEG2PSExtractor(source);
    }
 
    if (ret != NULL) {
       if (isDrm) {
           ret->setDrmFlag(true);
       } else {
           ret->setDrmFlag(false);
       }
    }
 
    return ret;
}

成功的通过sniff函数确定了文件的格式之后,就可以构造extractor对象了。

例如:如果文件格式是ts格式,则会调用ret = new MPEG2TSExtractor(source);

具体的构造函数就完成了文件头的解析获取了流信息。

2.2 setDataSource_l(extractor)

创建了extractor对象之后,setDataSource_l(extractor) 的主要作用就是使用上面得到的信息来构造播放器框架了。

这里只将重要的语句列出

setVideoSource(extractor->getTrack(i));

setAudioSource(extractor->getTrack(i));

addTextSource_l(i, extractor->getTrack(i));

主要是将文件中的各个流通过上面三个方法,存放在mVideoTrack、mAudioTrack,之后作为参数传递给解码器。便建立了解码器与extractor的关联

3 MediaExtractor的结构介绍(ts为例)


这里主要是以ts为例,按照一个demuxer的具体功能组件(仿照ffmpeg结构)来介绍,extractor是如何实现一个demuxer的功能。

ts相关的代码文件是:frameworks/media/libstagefrightplayer/mpeg2ts/MPEG2TSExtractor.cpp

下面是一张MPEG2TSExtractor 的总体结构图:

 

 

下面的很多介绍都会引用此图

解释下这张图:MPEG2TSExtractor是总入口,负责解析文件头信息,提供原始数据包。在功能上讲,MPEG2TSExtractor 是ATSParser的封装,而ATSParser负责实际的解析工作,

即:MPEG2TSExtractor 从FileSource中获取数据,提供给ATSParser 进行解析。而MPEG2TSExtractor 对外提供的各种接口及文件信息都是借由ATSParser 来完成的。

比如图中:getTrack接口提供的MediaSource也就是awesomeplayer中的mAudioTrack和mVideoTrack,是MPEG2TSSource结构,而MPEG2TSSource的工作由AnotherPacketSource(mSourceImpls)完成

而实际上AnotherPacketSource 是在ATSParser中的Stream中生成的,每个Stream对应一个实际的流。后面分析代码时可以依据上面解释来理解。

3.1 demuxer -- read_probe

这里的probe主要是对应datasource的sniffer函数。具体实现如下:

  1. bool SniffMPEG2TS(
  2. const sp<DataSource> &source, String8 *mimeType, float *confidence,
  3. sp<AMessage> *) {
  4. for (int i = 0; i < 5; ++i) {
  5. char header;
  6. if (source->readAt(kTSPacketSize * i, &header, 1) != 1
  7. || header != 0x47) {
  8. return false;
  9. }
  10. }
  11. *confidence = 0.1f;
  12. mimeType->setTo(MEDIA_MIMETYPE_CONTAINER_MPEG2TS);
  13. return true;
  14. }
bool SniffMPEG2TS(
        const sp<DataSource> &source, String8 *mimeType, float *confidence,
        sp<AMessage> *) {
    for (int i = 0; i < 5; ++i) {
        char header;
        if (source->readAt(kTSPacketSize * i, &header, 1) != 1
                || header != 0x47) {
            return false;
        }
    }
 
    *confidence = 0.1f;
    mimeType->setTo(MEDIA_MIMETYPE_CONTAINER_MPEG2TS);
 
    return true;
}

基本思路很简单,对于ts文件 每个ts包为188字节,每个包的同步字为0x47

这里主要通过datasource的readAt接口读取一个字节,判断是否是0x47,主要是间隔kTSPacketSize=188读取一个字节判断5次

如果全部通过则确定此文件为mpegts文件。设置*confidence = 0.1f;返回。

3.2 demuxer -- read_header

ffmpeg中的read_header函数主要作用是解析文件头信息,获取文件中的具体流信息及参数

这里主要是通过构造函数来完成,具体看下

  1. MPEG2TSExtractor::MPEG2TSExtractor(const sp<DataSource> &source)
  2. : mDataSource(source),
  3. mParser(new ATSParser),
  4. mOffset(0) {
  5. init();
  6. }
MPEG2TSExtractor::MPEG2TSExtractor(const sp<DataSource> &source)
    : mDataSource(source),
      mParser(new ATSParser),
      mOffset(0) {
    init();
}

这里将datasource参数存放在mDataSource中,并构造了ATSParser对象,这里可以这样认定:MPEG2TSExtractor是ATSParser的封装,具体的工作都是ATSParser完成的

看下其构造函数

  1. ATSParser::ATSParser(uint32_t flags)
  2. : mFlags(flags),
  3. mAbsoluteTimeAnchorUs(-1ll),
  4. mNumTSPacketsParsed(0),
  5. mNumPCRs(0) {
  6. mPSISections.add(0 /* PID */, new PSISection);
  7. }
ATSParser::ATSParser(uint32_t flags)
    : mFlags(flags),
      mAbsoluteTimeAnchorUs(-1ll),
      mNumTSPacketsParsed(0),
      mNumPCRs(0) {
    mPSISections.add(0 /* PID */, new PSISection);
}

构造好ATSParser对象之后,调用了init()函数

  1. void MPEG2TSExtractor::init() {
  2. bool haveAudio = false;
  3. bool haveVideo = false;
  4. int numPacketsParsed = 0;
  5. while (feedMore() == OK) {
  6. ATSParser::SourceType type;
  7. if (haveAudio && haveVideo) {
  8. break;
  9. }
  10. if (!haveVideo) {
  11. sp<AnotherPacketSource> impl =
  12. (AnotherPacketSource *)mParser->getSource(
  13. ATSParser::VIDEO).get();
  14. if (impl != NULL) {
  15. haveVideo = true;
  16. mSourceImpls.push(impl);
  17. }
  18. }
  19. if (!haveAudio) {
  20. sp<AnotherPacketSource> impl =
  21. (AnotherPacketSource *)mParser->getSource(
  22. ATSParser::AUDIO).get();
  23. if (impl != NULL) {
  24. haveAudio = true;
  25. mSourceImpls.push(impl);
  26. }
  27. }
  28. if (++numPacketsParsed > 10000) {
  29. break;
  30. }
  31. }
  32. ALOGI("haveAudio=%d, haveVideo=%d", haveAudio, haveVideo);
  33. }
void MPEG2TSExtractor::init() {
    bool haveAudio = false;
    bool haveVideo = false;
    int numPacketsParsed = 0;
 
    while (feedMore() == OK) {
        ATSParser::SourceType type;
        if (haveAudio && haveVideo) {
            break;
        }
        if (!haveVideo) {
            sp<AnotherPacketSource> impl =
                (AnotherPacketSource *)mParser->getSource(
                        ATSParser::VIDEO).get();
 
            if (impl != NULL) {
                haveVideo = true;
                mSourceImpls.push(impl);
            }
        }
 
        if (!haveAudio) {
            sp<AnotherPacketSource> impl =
                (AnotherPacketSource *)mParser->getSource(
                        ATSParser::AUDIO).get();
 
            if (impl != NULL) {
                haveAudio = true;
                mSourceImpls.push(impl);
            }
        }
 
        if (++numPacketsParsed > 10000) {
            break;
        }
    }
 
    ALOGI("haveAudio=%d, haveVideo=%d", haveAudio, haveVideo);
}

这里feedMore便是解析文件头的具体实现,代码的后半部分主要是当解析成功即feedMore() == OK

feedmore作用有两个:解析头信息+缓冲数据

将audio video的source存放在栈mSourceImpls中。看下具体实现

  1. status_t MPEG2TSExtractor::feedMore() {
  2. Mutex::Autolock autoLock(mLock);
  3. uint8_t packet[kTSPacketSize];
  4. ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);
  5. if (n < (ssize_t)kTSPacketSize) {
  6. return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
  7. }
  8. mOffset += n;
  9. return mParser->feedTSPacket(packet, kTSPacketSize);
  10. }
status_t MPEG2TSExtractor::feedMore() {
    Mutex::Autolock autoLock(mLock);
 
    uint8_t packet[kTSPacketSize];
    ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);
 
    if (n < (ssize_t)kTSPacketSize) {
        return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
    }
 
    mOffset += n;
    return mParser->feedTSPacket(packet, kTSPacketSize);
}

从代码中可以看出mParser并不与datasource直接联系,而是mExtractor读取一个ts包,传递给mParser来分析

具体分析过程通过调用mParser->feedTSPacket方法完成,进入ATParser类方法中

  1. status_t ATSParser::feedTSPacket(const void *data, size_t size) {
  2. CHECK_EQ(size, kTSPacketSize);
  3. ABitReader br((const uint8_t *)data, kTSPacketSize);
  4. return parseTS(&br);
  5. }
status_t ATSParser::feedTSPacket(const void *data, size_t size) {
    CHECK_EQ(size, kTSPacketSize);
 
    ABitReader br((const uint8_t *)data, kTSPacketSize);
    return parseTS(&br);
}

  1. status_t ATSParser::parseTS(ABitReader *br) {
  2. *****************
  3. if (adaptation_field_control == 1 || adaptation_field_control == 3) {
  4. err = parsePID(
  5. br, PID, continuity_counter, payload_unit_start_indicator);
  6. }
  7. ++mNumTSPacketsParsed;
  8. return err;
  9. }
status_t ATSParser::parseTS(ABitReader *br) {
 
    *****************
 
    if (adaptation_field_control == 1 || adaptation_field_control == 3) {
        err = parsePID(
                br, PID, continuity_counter, payload_unit_start_indicator);
    }
 
    ++mNumTSPacketsParsed;
 
    return err;
}


省略部分无关代码

如果对ts文件格式不了解,网上有很多资料。这里解析ts头主要就是解析几个表,如PAT(pid==0) PMT(pid通过解析pat获取) 以及通过上面两步获取实际流pid号之后解析实际数据获取参数。

这里获取pid之后通过调用parsePID来处理。

  1. status_t ATSParser::parsePID(
  2. ABitReader *br, unsigned PID,
  3. unsigned continuity_counter,
  4. unsigned payload_unit_start_indicator) {
  5. if (PID == 0) {
  6. parseProgramAssociationTable(§ionBits);
  7. } else {
  8. bool handled = false;
  9. for (size_t i = 0; i < mPrograms.size(); ++i) {
  10. status_t err;
  11. if (!mPrograms.editItemAt(i)->parsePSISection(
  12. PID, §ionBits, &err)) {
  13. continue;
  14. }
  15. section->clear();
  16. return OK;
  17. }
  18. bool handled = false;
  19. for (size_t i = 0; i < mPrograms.size(); ++i) {
  20. status_t err;
  21. if (mPrograms.editItemAt(i)->parsePID(
  22. PID, continuity_counter, payload_unit_start_indicator,
  23. br, &err)) {
  24. if (err != OK) {
  25. return err;
  26. }
  27. handled = true;
  28. break;
  29. }
  30. }
  31. if (!handled) {
  32. ALOGV("PID 0x%04x not handled.", PID);
  33. }
  34. return OK;
  35. }
status_t ATSParser::parsePID(
        ABitReader *br, unsigned PID,
        unsigned continuity_counter,
        unsigned payload_unit_start_indicator) {
 
        if (PID == 0) {
            parseProgramAssociationTable(§ionBits);
        } else {
 
             bool handled = false;
            for (size_t i = 0; i < mPrograms.size(); ++i) {
                status_t err;
                if (!mPrograms.editItemAt(i)->parsePSISection(
                            PID, §ionBits, &err)) {
                    continue;
                }
        section->clear();
 
        return OK;
    }
 
bool handled = false;
    for (size_t i = 0; i < mPrograms.size(); ++i) {
        status_t err;
        if (mPrograms.editItemAt(i)->parsePID(
                    PID, continuity_counter, payload_unit_start_indicator,
                    br, &err)) {
            if (err != OK) {
                return err;
            }
 
            handled = true;
            break;
        }
    }
 
    if (!handled) {
        ALOGV("PID 0x%04x not handled.", PID);
    }
 
    return OK;
}


 

这里主要是根据pid的不同来分别处理,如parseProgramAssociationTable用来处理pat表,此时pid==0

如果不是,则调用mPrograms.editItemAt(i)->parsePSISection来处理pmt表

这里如果没有处理过pat表,则 mPrograms.size()==0 会进入下一循环直到找到pat为止

这里不再深入了,解析完pmt表,就知道了文件中有几个流,几路audio 几路video等信息都得到了

后面会调用每个流的parsePID -- >调用每个流的parse函数,后续调用顺序为

mStreams.editValueAt(index)->parse->flush();->parsePES->onPayloadData等

这里简单总结下:在ATSParser中有几个内嵌类,

Program类负责解析pmt表

Stream类负责解析每个流的具体信息

具体代码不再列举了,总归都是按照ts的标准来解析,若读者不熟悉ts强烈建议仔细阅读下ffmpeg或者ATSParser中的实现方式。

重点注释:

每个Stream代表一个流,可以是音频流或者是视频流,读包的操作主要就是通过Stream接口来完成的。当在extractor中的feedmore方法中读取一个ts包传递给mParser解析时,

如果是实际的数据包,则最终都会存储在Stream的buffer中,而stream中的成员mSource(AnotherPacketSource)则是最终传递上去的保存在MPEG2TSExtractor中保存在全局变量中mSourceImpl中

而实际的mAudioTrack mVideoTrack则是通过getTrack返回MPEG2TSSource,而MPEG2TSSource 封装了AnotherPacketSource ,也就和实际的stream关联起来,

最终可以通过MPEG2TSSource 读取保存在stream中的数据包

【说明】请仔细消化上面这段话,并结合代码及上面的图来理解

3.3 demuxer -- read_packet

之前分析过,在awesomeplayer中有如下语句

mAudioTrack = extractor->getTrack(*);

mVideoTrack = extractor->getTrack(*);

这里getTrack便是建立awesomeplayer与extractor连接的地方(参考上面的黑体部分)

这里分析下ts的具体实现

  1. sp<MediaSource> MPEG2TSExtractor::getTrack(size_t index) {
  2. ************
  3. return new MPEG2TSSource(this, mSourceImpls.editItemAt(index), seekable);
  4. }
sp<MediaSource> MPEG2TSExtractor::getTrack(size_t index) {
 
 
    ************
 
    return new MPEG2TSSource(this, mSourceImpls.editItemAt(index), seekable);
}

这里在MPEG2TSExtractor定义了一个新的继承自MediaSource的类MPEG2TSSource,返回给上层

其构造函数如下:

  1. MPEG2TSSource::MPEG2TSSource(
  2. const sp<MPEG2TSExtractor> &extractor,
  3. const sp<AnotherPacketSource> &impl,
  4. bool seekable)
  5. : mExtractor(extractor),
  6. mImpl(impl),
  7. mSeekable(seekable) {
  8. }
MPEG2TSSource::MPEG2TSSource(
        const sp<MPEG2TSExtractor> &extractor,
        const sp<AnotherPacketSource> &impl,
        bool seekable)
    : mExtractor(extractor),
      mImpl(impl),
      mSeekable(seekable) {
}

这里传入的参数mSourceImpls.editItemAt(index)是一个AnotherPacketSource对象(实际生成是在ATSParser中的Stream类中),主要负责缓存数据包,等上层解码器需要数据的时候会从此处读取

看下实际的读包方法:

在awesomeplayer中会调用mAudioTrack->read(*)方法,由于mAudioTrack == mVideoTrack == MPEG2TSSource ,因此调用的是MPEG2TSSource 的read方法

  1. status_t MPEG2TSSource::read(
  2. MediaBuffer **out, const ReadOptions *options) {
  3. *out = NULL;
  4. int64_t seekTimeUs;
  5. ReadOptions::SeekMode seekMode;
  6. if (mSeekable && options && options->getSeekTo(&seekTimeUs, &seekMode)) {
  7. mExtractor->seekTo(seekTimeUs);
  8. }
  9. status_t finalResult;
  10. while (!mImpl->hasBufferAvailable(&finalResult)) {
  11. if (finalResult != OK) {
  12. return ERROR_END_OF_STREAM;
  13. }
  14. status_t err = mExtractor->feedMore();
  15. if (err != OK) {
  16. mImpl->signalEOS(err);
  17. }
  18. }
  19. return mImpl->read(out, options);
  20. }
status_t MPEG2TSSource::read(
        MediaBuffer **out, const ReadOptions *options) {
    *out = NULL;
 
    int64_t seekTimeUs;
    ReadOptions::SeekMode seekMode;
    if (mSeekable && options && options->getSeekTo(&seekTimeUs, &seekMode)) {
        mExtractor->seekTo(seekTimeUs);
    }
 
    status_t finalResult;
    while (!mImpl->hasBufferAvailable(&finalResult)) {
        if (finalResult != OK) {
            return ERROR_END_OF_STREAM;
        }
 
        status_t err = mExtractor->feedMore();
        if (err != OK) {
            mImpl->signalEOS(err);
        }
    }
 
    return mImpl->read(out, options);
}


 

这里首先判断是否需要seek,若不需要。则通过mImpl->hasBufferAvailable看是否有缓存数据包,此处mImpl == mSourceImpls.editItemAt(index),(见上面构造函数参数列表)

如果没有则通过feedMore()读取包

最后通过 mImpl->read(out, options)返回给上层。

这里再解释下:首先每个流都对应一个MPEG2TSSource 对象,而每个MPEG2TSSource 对象都有一个AnotherPacketSource 对象,对应一个缓存列表。一开始extractor通过feedMore解析数据后将数据存储在各个AnotherPacketSource 的缓存列表中

举例来讲:如果某一时刻 audio 缓存类表为空,而video 缓存列表为 4 , 此时读取audio包会导致mImpl->hasBufferAvailable ==0,此时会通过mExtractor->feedMore 继续缓存数据,若下一包为video则挂在到video缓存(此处video包+1=5)

直到解析到一个audio包,则返回。此处的实现保证了不会因为包分布不合理导致解码阻塞等情况

看下feedMore实现

  1. status_t MPEG2TSExtractor::feedMore() {
  2. Mutex::Autolock autoLock(mLock);
  3. uint8_t packet[kTSPacketSize];
  4. ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);
  5. if (n < (ssize_t)kTSPacketSize) {
  6. return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
  7. }
  8. mOffset += n;
  9. return mParser->feedTSPacket(packet, kTSPacketSize);
  10. }
status_t MPEG2TSExtractor::feedMore() {
    Mutex::Autolock autoLock(mLock);
 
    uint8_t packet[kTSPacketSize];
    ssize_t n = mDataSource->readAt(mOffset, packet, kTSPacketSize);
 
    if (n < (ssize_t)kTSPacketSize) {
        return (n < 0) ? (status_t)n : ERROR_END_OF_STREAM;
    }
 
    mOffset += n;
    return mParser->feedTSPacket(packet, kTSPacketSize);
}

主要是读取一包,然后解析,调用顺序如下

feedMore->mParser->feedTSPacket->parseTS->ATSParser::parsePID->ATSParser::Program::parsePID->ATSParser::Stream::parse

在ATSParser::Stream::parse 最终会将数据包缓存起来(具体实现方式:将数据全部缓存起来,放在mBuffer中,当此包数据都完成时,即得到了一个完整的audio或者video包,调用flush解析好了存在AnotherPacketSource的缓存中。)

3.4 demuxer -- read_seek

当需要seek的时候,入口在awesomeplayer的seekto,代码如下

  1. status_t AwesomePlayer::seekTo(int64_t timeUs) {
  2. ATRACE_CALL();
  3. if (mExtractorFlags & MediaExtractor::CAN_SEEK) {
  4. Mutex::Autolock autoLock(mLock);
  5. return seekTo_l(timeUs);
  6. }
  7. return OK;
  8. }
status_t AwesomePlayer::seekTo(int64_t timeUs) {
    ATRACE_CALL();
 
    if (mExtractorFlags & MediaExtractor::CAN_SEEK) {
        Mutex::Autolock autoLock(mLock);
        return seekTo_l(timeUs);
    }
 
    return OK;
}

  1. status_t AwesomePlayer::seekTo_l(int64_t timeUs) {
  2. *********************
  3. mSeeking = SEEK;
  4. mSeekNotificationSent = false;
  5. mSeekTimeUs = timeUs;
  6. modifyFlags((AT_EOS | AUDIO_AT_EOS | VIDEO_AT_EOS), CLEAR);
  7. seekAudioIfNecessary_l();
  8. if (!(mFlags & PLAYING)) {
  9. ALOGV("seeking while paused, sending SEEK_COMPLETE notification"
  10. " immediately.");
  11. notifyListener_l(MEDIA_SEEK_COMPLETE);
  12. mSeekNotificationSent = true;
  13. if ((mFlags & PREPARED) && mVideoSource != NULL) {
  14. modifyFlags(SEEK_PREVIEW, SET);
  15. postVideoEvent_l();
  16. }
  17. }
  18. return OK;
  19. }
status_t AwesomePlayer::seekTo_l(int64_t timeUs) {
    *********************
    mSeeking = SEEK;
    mSeekNotificationSent = false;
    mSeekTimeUs = timeUs;
    modifyFlags((AT_EOS | AUDIO_AT_EOS | VIDEO_AT_EOS), CLEAR);
    seekAudioIfNecessary_l();
    if (!(mFlags & PLAYING)) {
        ALOGV("seeking while paused, sending SEEK_COMPLETE notification"
             " immediately.");
        notifyListener_l(MEDIA_SEEK_COMPLETE);
        mSeekNotificationSent = true;
        if ((mFlags & PREPARED) && mVideoSource != NULL) {
            modifyFlags(SEEK_PREVIEW, SET);
            postVideoEvent_l();
        }
    }
 
    return OK;
}


首先设置seek标志,mSeeking = SEEK,然后设置seek到的位置 mSeekTimeUs = timeUs;清空标志位。

调用seekAudioIfNecessary_l,看下实现

  1. void AwesomePlayer::seekAudioIfNecessary_l() {
  2. if (mSeeking != NO_SEEK && mVideoSource == NULL && mAudioPlayer != NULL) {
  3. mAudioPlayer->seekTo(mSeekTimeUs);
  4. mWatchForAudioSeekComplete = true;
  5. mWatchForAudioEOS = true;
  6. if (mDecryptHandle != NULL) {
  7. mDrmManagerClient->setPlaybackStatus(mDecryptHandle,
  8. Playback::PAUSE, 0);
  9. mDrmManagerClient->setPlaybackStatus(mDecryptHandle,
  10. Playback::START, mSeekTimeUs / 1000);
  11. }
  12. }
  13. }
void AwesomePlayer::seekAudioIfNecessary_l() {
    if (mSeeking != NO_SEEK && mVideoSource == NULL && mAudioPlayer != NULL) {
        mAudioPlayer->seekTo(mSeekTimeUs);
 
        mWatchForAudioSeekComplete = true;
        mWatchForAudioEOS = true;
 
        if (mDecryptHandle != NULL) {
            mDrmManagerClient->setPlaybackStatus(mDecryptHandle,
                    Playback::PAUSE, 0);
            mDrmManagerClient->setPlaybackStatus(mDecryptHandle,
                    Playback::START, mSeekTimeUs / 1000);
        }
    }
}


 

比较简单,调用mAudioPlayer->seekTo

  1. status_t AudioPlayer::seekTo(int64_t time_us) {
  2. Mutex::Autolock autoLock(mLock);
  3. mSeeking = true;
  4. mPositionTimeRealUs = mPositionTimeMediaUs = -1;
  5. mReachedEOS = false;
  6. mSeekTimeUs = time_us;
  7. // Flush resets the number of played frames
  8. mNumFramesPlayed = 0;
  9. mNumFramesPlayedSysTimeUs = ALooper::GetNowUs();
  10. if (mAudioSink != NULL) {
  11. mAudioSink->flush();
  12. } else {
  13. mAudioTrack->flush();
  14. }
  15. return OK;
  16. }
status_t AudioPlayer::seekTo(int64_t time_us) {
    Mutex::Autolock autoLock(mLock);
 
    mSeeking = true;
    mPositionTimeRealUs = mPositionTimeMediaUs = -1;
    mReachedEOS = false;
    mSeekTimeUs = time_us;
 
    // Flush resets the number of played frames
    mNumFramesPlayed = 0;
    mNumFramesPlayedSysTimeUs = ALooper::GetNowUs();
 
    if (mAudioSink != NULL) {
        mAudioSink->flush();
    } else {
        mAudioTrack->flush();
    }
 
    return OK;
}


 

从代码中看主要是设置标志位

mSeeking = true;

mSeekTimeUs = time_us;

然后清空audioplayer中的pcm数据,而设置好seek标志之后,在audioplayer的fill buffer中读取数据时便会设置要读取的时间的数据,具体代码如下

  1. size_t AudioPlayer::fillBuffer(void *data, size_t size) {
  2. *************
  3. MediaSource::ReadOptions options;
  4. options.setSeekTo(mSeekTimeUs);
  5. err = mSource->read(&mInputBuffer, &options);
  6. *************
  7. }
size_t AudioPlayer::fillBuffer(void *data, size_t size) {
    *************
    MediaSource::ReadOptions options;
    options.setSeekTo(mSeekTimeUs);
    err = mSource->read(&mInputBuffer, &options);     
    *************
}

只列出了关键语句。

通过上面分析得出:当收到seek命令时,对于audio来讲就是清空audioplayer的pcm数据,设置下次要读取的位置MediaSource::ReadOptions options,其他工作由上层完成(指的是decoder,具体在分析decoder的时候介绍)

audio的seek完成之后,看下video的seek

具体代码在onVideoEvent中

  1. void AwesomePlayer::onVideoEvent() {
  2. ********
  3. MediaSource::ReadOptions options;
  4. if (mSeeking != NO_SEEK) {
  5. options.setSeekTo(
  6. mSeekTimeUs,
  7. mSeeking == SEEK_VIDEO_ONLY
  8. ? MediaSource::ReadOptions::SEEK_NEXT_SYNC
  9. : MediaSource::ReadOptions::SEEK_CLOSEST_SYNC);
  10. } ​​
  11. status_t err = mVideoSource->read(&mVideoBuffer, &options);
  12. ********
  13. }
void AwesomePlayer::onVideoEvent() {
 
    ********
 
    MediaSource::ReadOptions options;
    if (mSeeking != NO_SEEK) {
            options.setSeekTo(
             mSeekTimeUs,
             mSeeking == SEEK_VIDEO_ONLY
                ? MediaSource::ReadOptions::SEEK_NEXT_SYNC
                : MediaSource::ReadOptions::SEEK_CLOSEST_SYNC);
   }    ​​
 
   status_t err = mVideoSource->read(&mVideoBuffer, &options);
 
    ********
 
}


 

这里的处理方式与audio类似,都是设置好seek后的位置 MediaSource::ReadOptions options ,然后再从mVideoSource读取数据时作为参数传递进去(decoder后面介绍)。

但这里可以想到,最终decoder都会设置MediaExtractor 读取位置来达到seek的效果,这里看些ts的case

这里在ts extractor中调用read方法读取数据代码如下:

  1. status_t MPEG2TSSource::read(
  2. MediaBuffer **out, const ReadOptions *options) {
  3. *out = NULL;
  4. int64_t seekTimeUs;
  5. ReadOptions::SeekMode seekMode;
  6. if (mSeekable && options && options->getSeekTo(&seekTimeUs, &seekMode)) {
  7. mExtractor->seekTo(seekTimeUs);
  8. }
  9. status_t finalResult;
  10. while (!mImpl->hasBufferAvailable(&finalResult)) {
  11. if (finalResult != OK) {
  12. return ERROR_END_OF_STREAM;
  13. }
  14. status_t err = mExtractor->feedMore();
  15. if (err != OK) {
  16. mImpl->signalEOS(err);
  17. }
  18. }
  19. return mImpl->read(out, options);
  20. }
status_t MPEG2TSSource::read(
        MediaBuffer **out, const ReadOptions *options) {
    *out = NULL;
 
    int64_t seekTimeUs;
    ReadOptions::SeekMode seekMode;
    if (mSeekable && options && options->getSeekTo(&seekTimeUs, &seekMode)) {
        mExtractor->seekTo(seekTimeUs);
    }
 
    status_t finalResult;
    while (!mImpl->hasBufferAvailable(&finalResult)) {
        if (finalResult != OK) {
            return ERROR_END_OF_STREAM;
        }
 
        status_t err = mExtractor->feedMore();
        if (err != OK) {
            mImpl->signalEOS(err);
        }
    }
 
    return mImpl->read(out, options);
}



 

可以看到option已经传递进来,继续跟进mImpl->read

  1. status_t AnotherPacketSource::read(
  2. MediaBuffer **out, const ReadOptions *) {
  3. *out = NULL;
  4. Mutex::Autolock autoLock(mLock);
  5. while (mEOSResult == OK && mBuffers.empty()) {
  6. mCondition.wait(mLock);
  7. }
  8. if (!mBuffers.empty()) {
  9. const sp<ABuffer> buffer = *mBuffers.begin();
  10. mBuffers.erase(mBuffers.begin());
  11. int32_t discontinuity;
  12. if (buffer->meta()->findInt32("discontinuity", &discontinuity)) {
  13. if (wasFormatChange(discontinuity)) {
  14. mFormat.clear();
  15. }
  16. return INFO_DISCONTINUITY;
  17. } else {
  18. int64_t timeUs;
  19. CHECK(buffer->meta()->findInt64("timeUs", &timeUs));
  20. MediaBuffer *mediaBuffer = new MediaBuffer(buffer);
  21. mediaBuffer->meta_data()->setInt64(kKeyTime, timeUs);
  22. *out = mediaBuffer;
  23. return OK;
  24. }
  25. }
  26. return mEOSResult;
  27. }
status_t AnotherPacketSource::read(
        MediaBuffer **out, const ReadOptions *) {
    *out = NULL;
 
    Mutex::Autolock autoLock(mLock);
    while (mEOSResult == OK && mBuffers.empty()) {
        mCondition.wait(mLock);
    }
 
    if (!mBuffers.empty()) {
        const sp<ABuffer> buffer = *mBuffers.begin();
        mBuffers.erase(mBuffers.begin());
 
        int32_t discontinuity;
        if (buffer->meta()->findInt32("discontinuity", &discontinuity)) {
            if (wasFormatChange(discontinuity)) {
                mFormat.clear();
            }
 
            return INFO_DISCONTINUITY;
        } else {
            int64_t timeUs;
            CHECK(buffer->meta()->findInt64("timeUs", &timeUs));
 
            MediaBuffer *mediaBuffer = new MediaBuffer(buffer);
 
            mediaBuffer->meta_data()->setInt64(kKeyTime, timeUs);
 
            *out = mediaBuffer;
            return OK;
        }
    }
 
    return mEOSResult;
}



 

从代码里看并没有使用传递进来的 ReadOptions ,预计seek动作实在decoder中解决的。等分析decoder的时候再详细看。

到此,extractor就分析完了。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值