关于android电话录音问题的详细分析

关于android电话录音问题的详细分析

作者:老猫 

一直以来都是在网络上看别人的文章,老老实实的做潜水员,今天一时兴起,写点东西,希望对大家有所帮助,不要再走同样的弯路。

本文是关于Android下录音问题的分析,网络上都说Android录音时记录下的语音信号都是混音器的信号。但是都没有给出详细说明为什么是这样。

我们知道Android下进行电话录音的代码很简单:

大致流程如下:

recorder = new MediaRecorder();

//这里mode可以设置为 VOICE_UPLINK|VOICE_DOWNLINK|VOICE_CALL

recorder.setAudioSource(mode);

recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);

recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);

recorder.setOutputFile(recFile.getAbsolutePath());

//准备录音

recorder.prepare();

//启动录音

recorder.start();

//停止录音

recorder.stop();

MediaRecorder.AudioSource中定义了以下常量可以用于recorder.setAudioSource

这里和电话录音相关的有3个常量

Voice_call 录制上行线路和下行线路

Voice_uplink 录制上行线路,应该是对方的语音

Voice_downlink 录制下行线路,应该是我方的语音

网络上关于java层如何调用native代码的介绍很多,这里只做简单介绍。JAVA中MediaRecorder的方法会掉用本地C++代码,这些代码编译后为libmedia.so,再通过进程间通信机制Binder和MediaServer通信,MediaServer收到请求后,把这些请求转发给opencore。

以下是Android的媒体库框架图,从网络上下载的。

从上图可以看出,客户端调用的本地代码位于libmedia.so中,媒体服务进程调用的代码位于libmediaplayerservice.so中。libmediaplayerservice.so再调用底层的libopencoreplayer.so完成具体功能。

以下通过代码介绍媒体服务进程如何转发请求到opencore中。关于客户端mediarecorder如何与媒体服务进程交互请搜索网络,这方面文章很多,这里就不多介绍。

总而言之,客户端的一个mediarecorder对象和服务器端的MediaRecorderClient对象对应,客户端通过mediarecorder发送的请求,通过进程间通信机制最终都会发送到服务端的MediaRecorderClient类中。我们来看下内部类client的声明,代码位于frameworks\base\media\libmediaplayerservice\MediaRecorderClient.h

class MediaRecorderClient : public BnMediaRecorder

{

public:

    virtual     status_t setCamera(const sp<ICamera>& camera);

    virtual     status_t        setPreviewSurface(const sp<ISurface>& surface);

    virtual     status_t        setVideoSource(int vs);

    virtual     status_t        setAudioSource(int as);

    virtual     status_t        setOutputFormat(int of);

    virtual     status_t        setVideoEncoder(int ve);

    virtual     status_t        setAudioEncoder(int ae);

    virtual     status_t        setOutputFile(const char* path);

    virtual     status_t        setOutputFile(int fd, int64_t offset, int64_t length);

    virtual     status_t        setVideoSize(int width, int height);

    virtual     status_t        setVideoFrameRate(int frames_per_second);

    virtual     status_t        setParameters(const String8& params);

    virtual     status_t        setListener(const sp<IMediaPlayerClient>& listener);

    virtual     status_t        prepare();

    virtual     status_t        getMaxAmplitude(int* max);

    virtual     status_t        start();

    virtual     status_t        stop();

    virtual     status_t        reset();

    virtual     status_t        init();

    virtual     status_t        close();

    virtual     status_t        release();

。。。

}

可以看到,大部分客户端方法在MediaRecorderClient中都有对应方法。这样当我们调用客户端的recorder.start();时,最后会调用到MediaRecorderClient类中的start方法。

status_t MediaRecorderClient::start()

{

    LOGV("start");

    Mutex::Autolock lock(mLock);

    if (mRecorder == NULL) {

        LOGE("recorder is not initialized");

        return NO_INIT;

    }

    return mRecorder->start(); //转发给mRecorder

//这里的mRecorder是在MediaRecorderClient构造函数中创建的。

MediaRecorderClient::MediaRecorderClient(const sp<MediaPlayerService>& service, pid_t pid)

{

  。。。

#ifndef NO_OPENCORE

    {

   //创建了PVMediaRecorder用于录音

        mRecorder = new PVMediaRecorder();

    }

#else

    {

        mRecorder = NULL;

    }

#endif

    mMediaPlayerService = service;

其他的调用也是一样,所有的请求基本都转发给了PVMediaRecorder,这个PVMediaRecorder就是opencore中的对应的录音的类。

这样,我们就直接进入opencore分析,先看看PVMediaRecorder的声明,代码位于frameworks\base\include\media\PVMediaRecorder.h,可以看到,客户端的方法在这里基本都有对应的方法。

class PVMediaRecorder : public MediaRecorderBase {

public:

    PVMediaRecorder();

    virtual ~PVMediaRecorder();

    virtual status_t init();

    virtual status_t setAudioSource(audio_source as);

    virtual status_t setVideoSource(video_source vs);

    virtual status_t setOutputFormat(output_format of);

    virtual status_t setAudioEncoder(audio_encoder ae);

    virtual status_t setVideoEncoder(video_encoder ve);

    virtual status_t setVideoSize(int width, int height);

    virtual status_t setVideoFrameRate(int frames_per_second);

    virtual status_t setCamera(const sp<ICamera>& camera);

    virtual status_t setPreviewSurface(const sp<ISurface>& surface);

    virtual status_t setOutputFile(const char *path);

    virtual status_t setOutputFile(int fd, int64_t offset, int64_t length);

    virtual status_t setParameters(const String8& params);

    virtual status_t setListener(const sp<IMediaPlayerClient>& listener);

    virtual status_t prepare();

    virtual status_t start();

    virtual status_t stop();

    virtual status_t close();

    virtual status_t reset();

    virtual status_t getMaxAmplitude(int *max);

private:

    status_t doStop();

    AuthorDriverWrapper*            mAuthorDriverWrapper;

    PVMediaRecorder(const PVMediaRecorder &);

    PVMediaRecorder &operator=(const PVMediaRecorder &);

};

Opencore是一个第3方的库,体系比较复杂,关于opencore的详细资料请参阅android源代码树下的external\opencore\doc,网络上也有这方面资料,不过不全。

总而言之,Opencore提供了一个多媒体开发框架,要使用Opencore进行多媒体应用,开发人员应该在顶层提供包装接口,在底层提供硬件接口,Opencore提供中间层功能。接收顶层发送的请求,经过处理后,最终交给底层硬件完成任务。在android系统上,顶层和底层代码都位于目录external\opencore\android下,其中external\opencore\android\author目录下是关于录音部分的代码。Opencore其他子目录下是原生代码。

以下是通过逆向编译后得到的关于录音部分的类模型。这里我们只关注主要部分。

PVMediaRecorder类收到的请求都会转发给AuthorDriverWrapper类,AuthorDriverWrapper类收到请求后又会转发给AuthorDriver类,这样,我们只要关注AuthorDriver类就可以了。

AuthorDriver中定义了如下的方法:

    void handleInit(author_command *ac);

    //##ModelId=4DE0871D000D

    void handleSetAudioSource(set_audio_source_command *ac);

    //##ModelId=4DE0871D0010

    void handleSetCamera(set_camera_command *ac);

    //##ModelId=4DE0871D0015

    void handleSetVideoSource(set_video_source_command *ac);

    //##ModelId=4DE0871D001C

    void handleSetOutputFormat(set_output_format_command *ac);

    //##ModelId=4DE0871D0021

    void handleSetAudioEncoder(set_audio_encoder_command *ac);

    //##ModelId=4DE0871D0024

    void handleSetVideoEncoder(set_video_encoder_command *ac);

    //##ModelId=4DE0871D0029

    void handleSetVideoSize(set_video_size_command *ac);

    //##ModelId=4DE0871D002E

    void handleSetVideoFrameRate(set_video_frame_rate_command *ac);

    //##ModelId=4DE0871D0031

    void handleSetPreviewSurface(set_preview_surface_command *ac);

    //##ModelId=4DE0871D0035

    void handleSetOutputFile(set_output_file_command *ac);

    //##ModelId=4DE0871D003A

    void handleSetParameters(set_parameters_command *ac);

    //##ModelId=4DE0871D003D

    void handlePrepare(author_command *ac);

    //##ModelId=4DE0871D0042

    void handleStart(author_command *ac);

    //##ModelId=4DE0871D0046

    void handleStop(author_command *ac);

    //##ModelId=4DE0871D004A

    void handleReset(author_command *ac);

    //##ModelId=4DE0871D004E

    void handleClose(author_command *ac);

    //##ModelId=4DE0871D0052

    void handleQuit(author_command *ac);

其中每个方法对应于客户端一个请求的处理,这里需要注意的是opencore使用了事件调度机制,这种调度机制在opencore的大部分类中都出现,了解这种机制有助于我们分析代码。简单来说,opencore的大部分类收到一个请求后,会把该请求包装成1个命令对象,然后添加到命令对象队列中,在通过调度对这个命令对象进行处理。通常接收请求的方法名和处理请求的方法名字都有对应关系。比如:

status_t PVMediaRecorder::start()

{

    LOGV("start");

    if (mAuthorDriverWrapper == NULL) {

        LOGE("author driver wrapper is not initialized yet");

        return UNKNOWN_ERROR;

    }

//把请求包装成命令

    author_command *ac = new author_command(AUTHOR_START);

    if (ac == NULL) {

        LOGE("failed to construct an author command");

        return UNKNOWN_ERROR;

}

//调用mAuthorDriverWrapper的enqueueCommand方法

    return mAuthorDriverWrapper->enqueueCommand(ac, 0, 0);

}

status_t AuthorDriverWrapper::enqueueCommand(author_command *ac, media_completion_f comp, void *cookie)

{

    if (mAuthorDriver) {

//再转发给mAuthorDriver

        return mAuthorDriver->enqueueCommand(ac, comp, cookie);

    }

    return NO_INIT;

}

status_t AuthorDriver::enqueueCommand(author_command *ac, media_completion_f comp, void *cookie)

{

。。。

//把命令请求添加到命令请求队列中

mCommandQueue.push_front(ac);

。。。

}

//在opencore调度线程中调用

void AuthorDriver::Run()

{

。。。

//调用handleStart处理客户的start请求

case AUTHOR_START: handleStart(ac); break;

。。。

}

这样当客户端调用recorder.start();时,这个请求通过层层转发会调用AuthorDriver的handleStart方法,其他请求也一样,后面就不再列出。

void AuthorDriver::handleStart(author_command *ac)

{

    LOGV("handleStart");

int error = 0;

//调用opencore的引擎的start方法

    OSCL_TRY(error, mAuthor->Start(ac));

    OSCL_FIRST_CATCH_ANY(error, commandFailed(ac));

}

mAuthor成员的初始化在AuthorDriver::authorThread()方法中完成。流程图如下,图中Client对象对应于我们的AuthorDriver对象。

PVAuthorEngine类定义在文件external\opencore\engines\author\src\pvauthorengine.h中

其中引擎的start方法定义如下

OSCL_EXPORT_REF PVCommandId PVAuthorEngine::Start(const OsclAny* aContextData)

{

    PVLOGGER_LOGMSG(PVLOGMSG_INST_LLDBG, iLogger, PVLOGMSG_STACK_TRACE,

                    (0, "PVAuthorEngine::Start: aContextData=0x%x", aContextData));

    PVEngineCommand cmd(PVAE_CMD_START, iCommandId, (OsclAny*)aContextData);

    Dispatch(cmd);

    return iCommandId++;

}

经过调度机制由DoStart方法处理该请求

PVMFStatus PVAuthorEngine::DoStart(PVEngineCommand& aCmd)

{

    PVLOGGER_LOGMSG(PVLOGMSG_INST_LLDBG, iLogger, PVLOGMSG_STACK_TRACE, (0, "PVAuthorEngine::DoStart"));

    OSCL_UNUSED_ARG(aCmd);

    if (GetPVAEState() != PVAE_STATE_INITIALIZED)

    {

        return PVMFErrInvalidState;

    }

    iNodeUtil.Start(iComposerNodes);

    if (iEncoderNodes.size() > 0)

        iNodeUtil.Start(iEncoderNodes);

    //调用PVAuthorEngineNodeUtility的start方法完成请求

    iNodeUtil.Start(iDataSourceNodes);

    return PVMFPending;

}

PVMFStatus PVAuthorEngineNodeUtility::Start(const PVAENodeContainerVector& aNodes, OsclAny* aContext)

{

。。。

    PVAENodeUtilCmd cmd;

PVMFStatus status = cmd.Construct(PVAENU_CMD_START, aNodes, aContext);

。。。又是调度机制,由DoStart处理

    return AddCmdToQueue(cmd);

}

PVMFStatus PVAuthorEngineNodeUtility::DoStart(const PVAENodeUtilCmd& aCmd)

{

。。。

 for (uint32 i = 0; i < aCmd.iNodes.size(); i++)

{

    nodeContainer = aCmd.iNodes[i];

        nodeContainer->iNode->Start(nodeContainer->iSessionId, aCmd.iContext);

    }

            );

。。。。

}

注意到这里面调用了nodeContainer->iNode->Start(nodeContainer->iSessionId, aCmd.iContext);

这个iNode是在调用start函数前面的函数时保存到opencore的引擎中的,具体分析过程比较复杂,这里就不详细列出。

总而言之,inode指向PvmfMediaInputNode对象,代码位于external\opencore\nodes\pvmediainputnode\src\pvmf_media_input_node.h

OSCL_EXPORT_REF PVMFCommandId PvmfMediaInputNode::Start(PVMFSessionId s, const OsclAny* aContext)

{

    PVLOGGER_LOGMSG(PVLOGMSG_INST_LLDBG, iLogger, PVLOGMSG_STACK_TRACE,

                    (0, "PvmfMediaInputNode::Start() called"));

    PvmfMediaInputNodeCmd cmd;

    cmd.PvmfMediaInputNodeCmdBase::Construct(s, PVMF_GENERIC_NODE_START, aContext);

    return QueueCommandL(cmd);

}

通过调度机制又调用了DoStart方法

PVMFStatus PvmfMediaInputNode::DoStart(PvmfMediaInputNodeCmd& aCmd)

{

。。。

    //Start the MIO

PVMFStatus status = SendMioRequest(aCmd, EStart);

。。。

}

PVMFStatus PvmfMediaInputNode::SendMioRequest(PvmfMediaInputNodeCmd& aCmd, EMioRequest aRequest)

{       

。。。 

case EStart:

        {

OSCL_TRY(err, iMediaIOCmdId = iMediaIOControl->Start(););

。。。

}

调用iMediaIOControl的start方法,这个iMediaIOControl成员是在创建该节点时候传递近来的,具体分析过程就不说了,太长。总之,iMediaIOControl指向AndroidAudioInput类,还记得我们说过,使用opencore要提供底层接口吗,这个就是android提供的底层接口,代码位于external\opencore\android\author\android_audio_input.h

PVMFCommandId AndroidAudioInput::Start(const OsclAny* aContext)

{

    LOGV("Start");

    if(iState != STATE_INITIALIZED && iState != STATE_PAUSED)

    {

        LOGE("Start: Invalid state (%d)", iState);

        OSCL_LEAVE(OsclErrInvalidState);

        return -1;

    }

    return AddCmdToQueue(AI_CMD_START, aContext);

}

通过调度机制由DoStart完成

PVMFStatus AndroidAudioInput::DoStart()

{

OsclThread AudioInput_Thread;

//创建一个线程,线程入口函数start_audin_thread_func

    OsclProcStatus::eOsclProcError ret = AudioInput_Thread.Create(

            (TOsclThreadFuncPtr)start_audin_thread_func, 0,

            (TOsclThreadFuncArg)this, Start_on_creation);

。。。

int AndroidAudioInput::start_audin_thread_func(TOsclThreadFuncArg arg)

{

    prctl(PR_SET_NAME, (unsigned long"audio in", 0, 0, 0);

sp<AndroidAudioInput> obj =  (AndroidAudioInput *)arg;

//调用audin_thread_func函数

    return obj->audin_thread_func();

}

//注意这里创建了一个AudioRecord来完成实际的录音底层工作

int AndroidAudioInput::audin_thread_func() {

    // setup audio record session

//最后调用的是AudioRecord类完成音频录制

    LOGV("create AudioRecord %p"this);

    AudioRecord

            * record = new AudioRecord(

                    iAudioSource, iAudioSamplingRate,

                    android::AudioSystem::PCM_16_BIT,

                    (iAudioNumChannels > 1) ? AudioSystem::CHANNEL_IN_STEREO : AudioSystem::CHANNEL_IN_MONO,

                    4*kBufferSize/iAudioNumChannels/sizeof(int16), flags);

。。。。。

好了,绕了一整圈,我们知道opencore实际上并不负责底层录音的设置,最终工作是由AudioRecord来完成的,那我们来分析AudioRecord是如何完成录音工作的。

AudioRecord类声明在文件frameworks\base\media\libmedia\AudioRecord.h中,是audioflinger的一部分。先看下构造函数

AudioRecord::AudioRecord(

        int inputSource,

        uint32_t sampleRate,

        int format,

        uint32_t channels,

        int frameCount,

        uint32_t flags,

        callback_t cbf,

        void* user,

        int notificationFrames)

    : mStatus(NO_INIT)

{

   //调用set方法

    mStatus = set(inputSource, sampleRate, format, channels,

            frameCount, flags, cbf, user, notificationFrames);

}

status_t AudioRecord::set(

        int inputSource,

        uint32_t sampleRate,

        int format,

        uint32_t channels,

        int frameCount,

        uint32_t flags,

        callback_t cbf,

        void* user,

        int notificationFrames,

        bool threadCanCallJava)

{

// input实际上是录音线程句柄,调用AudioSystem::getInput

    audio_io_handle_t input = AudioSystem::getInput(inputSource,

                                    sampleRate, format, channels, (AudioSystem::audio_in_acoustics)flags);

}

10audiosystem.cpp

audio_io_handle_t AudioSystem::getInput(int inputSource,

                                    uint32_t samplingRate,

                                    uint32_t format,

                                    uint32_t channels,

                                    audio_in_acoustics acoustics)

{

    const sp<IAudioPolicyService>& aps = AudioSystem::get_audio_policy_service();

    if (aps == 0) return 0;

    return aps->getInput(inputSource, samplingRate, format, channels, acoustics);

}

调用的是AudioPolicyService的getInput,代码位于frameworks\base\libs\audioflinger\AudioPolicyService.cpp

audio_io_handle_t AudioPolicyService::getInput(int inputSource,

                                    uint32_t samplingRate,

                                    uint32_t format,

                                    uint32_t channels,

                                    AudioSystem::audio_in_acoustics acoustics)

{

    if (mpPolicyManager == NULL) {

        return 0;

    }

    Mutex::Autolock _l(mLock);

//调用audioPolicyManagerbasegetinput

    return mpPolicyManager->getInput(inputSource, samplingRate, format, channels, acoustics);

}

audio_io_handle_t AudioPolicyManagerBase::getInput(int inputSource,

                                    uint32_t samplingRate,

                                    uint32_t format,

                                    uint32_t channels,

                                    AudioSystem::audio_in_acoustics acoustics)

{

    audio_io_handle_t input = 0;

// case AUDIO_SOURCE_VOICE_UPLINK:case AUDIO_SOURCE_VOICE_DOWNLINK:case AUDIO_SOURCE_VOICE_CALL:device = AudioSystem::DEVICE_IN_VOICE_CALL;

// 对于电话录音,返回的是DEVICE_IN_VOICE_CALL

    uint32_t device = getDeviceForInputSource(inputSource);

    // 设置选择的录音通道,如果是电话录音,设置相应得通道标志

    switch(inputSource) {

    case AUDIO_SOURCE_VOICE_UPLINK:

        channels = AudioSystem::CHANNEL_IN_VOICE_UPLINK;

        break;

    case AUDIO_SOURCE_VOICE_DOWNLINK:

        channels = AudioSystem::CHANNEL_IN_VOICE_DNLINK;

        break;

    case AUDIO_SOURCE_VOICE_CALL:

        channels = (AudioSystem::CHANNEL_IN_VOICE_UPLINK | AudioSystem::CHANNEL_IN_VOICE_DNLINK);

        break;

    default:

        break;

    }

//调用AudioPolicyServiceopenInput

    input = mpClientInterface->openInput(&inputDesc->mDevice,

                                    &inputDesc->mSamplingRate,

                                    &inputDesc->mFormat,

                                    &inputDesc->mChannels,

                                    inputDesc->mAcoustics);

    return input;

}

audio_io_handle_t AudioPolicyService::openInput(uint32_t *pDevices,

                                uint32_t *pSamplingRate,

                                uint32_t *pFormat,

                                uint32_t *pChannels,

                                uint32_t acoustics)

{

    sp<IAudioFlinger> af = AudioSystem::get_audio_flinger();

    if (af == 0) {

        LOGW("openInput() could not get AudioFlinger");

        return 0;

    }

//调用AudioFlingeropenInput

    return af->openInput(pDevices, pSamplingRate, (uint32_t *)pFormat, pChannels, acoustics);

}

AudioFlinger类定义在frameworks\base\libs\audioflinger\AudioFlinger.cpp中

int AudioFlinger::openInput(uint32_t *pDevices,

                                uint32_t *pSamplingRate,

                                uint32_t *pFormat,

                                uint32_t *pChannels,

                                uint32_t acoustics)

//打开录音输入流,对于电话录音参数,这里调用肯定失败

    AudioStreamIn *input = mAudioHardware->openInputStream(*pDevices,

                                                             (int *)&format,

                                                             &channels,

                                                             &samplingRate,

                                                             &status,

                                                             (AudioSystem::audio_in_acoustics)acoustics);

//错误的话会返回,使用单通道模式重新打开

        input = mAudioHardware->openInputStream(*pDevices,

                                                 (int *)&format,

                                                 &channels,

                                                 &samplingRate,

                                                 &status,

                                                 (AudioSystem::audio_in_acoustics)acoustics);

上面的mAudioHardware指向AudioHardware

AudioStreamIn* AudioHardware::openInputStream(

        uint32_t devices, int *format, uint32_t *channels, uint32_t *sampleRate, status_t *status, AudioSystem::audio_in_acoustics acoustic_flags)

{

AudioStreamInMSM72xx* in = new AudioStreamInMSM72xx();

//对硬件进行设置

    status_t lStatus = in->set(this, devices, format, channels, sampleRate, acoustic_flags);

status_t AudioHardware::AudioStreamInMSM72xx::set(

        AudioHardware* hw, uint32_t devices, int *pFormat, uint32_t *pChannels, uint32_t *pRate,

        AudioSystem::audio_in_acoustics acoustic_flags)

{

    。。。。

//不支持AUDIO_SOURCE_VOICE_UPLINK |  AUDIO_SOURCE_VOICE_DOWNLINK 

//什么样的音频硬件支持?

如果通道不是CHANNEL_IN_MONO或者CHANNEL_IN_STEREO,把通道设置成单通道

//返回错误,还记得在AudioFlinger::openInput中检测到错误会重新调用吗?

//重新调用时使用AUDIO_HW_IN_CHANNELS,

// #define AUDIO_HW_IN_CHANNELS (AudioSystem::CHANNEL_IN_MONO)

//实际上就是单通道,然后在af的openinput中重新调用,就成功了

    if (pChannels == 0 || (*pChannels != AudioSystem::CHANNEL_IN_MONO &&

        *pChannels != AudioSystem::CHANNEL_IN_STEREO)) {

        *pChannels = AUDIO_HW_IN_CHANNELS;

        return BAD_VALUE;

}

分析到这里终于知道了,当我们设置录音方式为

Voice_call 录制上行线路和下行线路

Voice_uplink 录制上行线路,应该是对方的语音

Voice_downlink 录制下行线路,应该是我方的语音

第1次打开设备时肯定失败,失败后android把通道标志设置为CHANNEL_IN_MONO,然后调用成功,实际上此时使用的应该是混音器录音。

希望大家不要再犯相同的错误,谢谢!!

    

参考文档:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值