Android Audio代码分析15 - testPlaybackHeadPositionAfterFlush

上次看到的testPlaybackHeadPositionIncrease函数中,先play了一会,然后获取position。
今天看个复杂点的,先play,然后stop,之后在flush,此时再获取position会是什么情况呢?


*****************************************源码*************************************************
//Test case 3: getPlaybackHeadPosition() is 0 after flush();
@LargeTest
public void testPlaybackHeadPositionAfterFlush() throws Exception {
// constants for test
final String TEST_NAME = "testPlaybackHeadPositionAfterFlush";
final int TEST_SR = 22050;
final int TEST_CONF = AudioFormat.CHANNEL_OUT_STEREO;
final int TEST_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
final int TEST_MODE = AudioTrack.MODE_STREAM;
final int TEST_STREAM_TYPE = AudioManager.STREAM_MUSIC;

//-------- initialization --------------
int minBuffSize = AudioTrack.getMinBufferSize(TEST_SR, TEST_CONF, TEST_FORMAT);
AudioTrack track = new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, TEST_FORMAT,
minBuffSize, TEST_MODE);
byte data[] = new byte[minBuffSize/2];
//-------- test --------------
assumeTrue(TEST_NAME, track.getState() == AudioTrack.STATE_INITIALIZED);
track.write(data, 0, data.length);
track.write(data, 0, data.length);
track.play();
Thread.sleep(100);
track.stop();
track.flush();
log(TEST_NAME, "position ="+ track.getPlaybackHeadPosition());
assertTrue(TEST_NAME, track.getPlaybackHeadPosition() == 0);
//-------- tear down --------------
track.release();
}
**********************************************************************************************
源码路径:
frameworks\base\media\tests\mediaframeworktest\src\com\android\mediaframeworktest\functional\MediaAudioTrackTest.java


#######################说明################################
//Test case 3: getPlaybackHeadPosition() is 0 after flush();
@LargeTest
public void testPlaybackHeadPositionAfterFlush() throws Exception {
// constants for test
final String TEST_NAME = "testPlaybackHeadPositionAfterFlush";
final int TEST_SR = 22050;
final int TEST_CONF = AudioFormat.CHANNEL_OUT_STEREO;
final int TEST_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
final int TEST_MODE = AudioTrack.MODE_STREAM;
final int TEST_STREAM_TYPE = AudioManager.STREAM_MUSIC;

//-------- initialization --------------
int minBuffSize = AudioTrack.getMinBufferSize(TEST_SR, TEST_CONF, TEST_FORMAT);
AudioTrack track = new AudioTrack(TEST_STREAM_TYPE, TEST_SR, TEST_CONF, TEST_FORMAT,
minBuffSize, TEST_MODE);
byte data[] = new byte[minBuffSize/2];
//-------- test --------------
assumeTrue(TEST_NAME, track.getState() == AudioTrack.STATE_INITIALIZED);
track.write(data, 0, data.length);
track.write(data, 0, data.length);
track.play();
Thread.sleep(100);
track.stop();
++++++++++++++++++++++++++++stop++++++++++++++++++++++++++++++++++++
/**
* Stops playing the audio data.
* @throws IllegalStateException
*/
public void stop()
throws IllegalStateException {
if (mState != STATE_INITIALIZED) {
throw(new IllegalStateException("stop() called on uninitialized AudioTrack."));
}


// stop playing
synchronized(mPlayStateLock) {
native_stop();
++++++++++++++++++++++++++++++android_media_AudioTrack_stop++++++++++++++++++++++++++++++++++
static void
android_media_AudioTrack_stop(JNIEnv *env, jobject thiz)
{
AudioTrack *lpTrack = (AudioTrack *)env->GetIntField(
thiz, javaAudioTrackFields.nativeTrackInJavaObj);
if (lpTrack == NULL ) {
jniThrowException(env, "java/lang/IllegalStateException",
"Unable to retrieve AudioTrack pointer for stop()");
return;
}


lpTrack->stop();
+++++++++++++++++++++++++++++++AudioTrack::stop+++++++++++++++++++++++++++++++++
void AudioTrack::stop()
{
// mAudioTrackThread在函数AudioTrack::set中被赋值。
// mAudioTrackThread = new AudioTrackThread(*this, threadCanCallJava);
sp<AudioTrackThread> t = mAudioTrackThread;


LOGV("stop %p", this);
if (t != 0) {
t->mLock.lock();
}


if (android_atomic_and(~1, &mActive) == 1) {
mCblk->cv.signal();
// mAudioTrack在函数AudioTrack::createTrack中被赋值,其最终指向的其实是一个TrackHandle对象
mAudioTrack->stop();
++++++++++++++++++++++++++++++AudioFlinger::TrackHandle::stop++++++++++++++++++++++++++++++++++
void AudioFlinger::TrackHandle::stop() {
mTrack->stop();
+++++++++++++++++++++++++++++AudioFlinger::PlaybackThread::Track::stop+++++++++++++++++++++++++++++++++++
void AudioFlinger::PlaybackThread::Track::stop()
{
LOGV("stop(%d), calling thread %d", mName, IPCThreadState::self()->getCallingPid());
// 在Track的构造函数中赋值,指向创建该Track对象的PlaybackThread
sp<ThreadBase> thread = mThread.promote();
if (thread != 0) {
Mutex::Autolock _l(thread->mLock);
int state = mState;
if (mState > STOPPED) {
++++++++++++++++++++++++++++++++track_state++++++++++++++++++++++++++++++++
enum track_state {
IDLE,
TERMINATED,
STOPPED,
RESUMING,
ACTIVE,
PAUSING,
PAUSED
};
--------------------------------track_state--------------------------------
mState = STOPPED;
// If the track is not active (PAUSED and buffers full), flush buffers
PlaybackThread *playbackThread = (PlaybackThread *)thread.get();
if (playbackThread->mActiveTracks.indexOf(this) < 0) {
reset();
++++++++++++++++++++++++++++++AudioFlinger::PlaybackThread::Track::reset++++++++++++++++++++++++++++++++++
void AudioFlinger::PlaybackThread::Track::reset()
{
// Do not reset twice to avoid discarding data written just after a flush and before
// the audioflinger thread detects the track is stopped.
if (!mResetDone) {
TrackBase::reset();
++++++++++++++++++++++++++++++AudioFlinger::ThreadBase::TrackBase::reset++++++++++++++++++++++++++++++++++
void AudioFlinger::ThreadBase::TrackBase::reset() {
audio_track_cblk_t* cblk = this->cblk();


cblk->user = 0;
cblk->server = 0;
cblk->userBase = 0;
cblk->serverBase = 0;
mFlags &= (uint32_t)(~SYSTEM_FLAGS_MASK);
LOGV("TrackBase::reset");
}
------------------------------AudioFlinger::ThreadBase::TrackBase::reset----------------------------------
// Force underrun condition to avoid false underrun callback until first data is
// written to buffer
mCblk->flags |= CBLK_UNDERRUN_ON;
mCblk->flags &= ~CBLK_FORCEREADY_MSK;
mFillingUpStatus = FS_FILLING;
mResetDone = true;
}
}
------------------------------AudioFlinger::PlaybackThread::Track::reset----------------------------------
}
LOGV("(> STOPPED) => STOPPED (%d) on thread %p", mName, playbackThread);
}
if (!isOutputTrack() && (state == ACTIVE || state == RESUMING)) {
thread->mLock.unlock();
AudioSystem::stopOutput(thread->id(),
(AudioSystem::stream_type)mStreamType,
mSessionId);
++++++++++++++++++++++++++++++AudioSystem::stopOutput++++++++++++++++++++++++++++++++++
status_t AudioSystem::stopOutput(audio_io_handle_t output,
AudioSystem::stream_type stream,
int session)
{
const sp<IAudioPolicyService>& aps = AudioSystem::get_audio_policy_service();
if (aps == 0) return PERMISSION_DENIED;
return aps->stopOutput(output, stream, session);
+++++++++++++++++++++++++++++++++AudioPolicyService::stopOutput+++++++++++++++++++++++++++++++
status_t AudioPolicyService::stopOutput(audio_io_handle_t output,
AudioSystem::stream_type stream,
int session)
{
if (mpPolicyManager == NULL) {
return NO_INIT;
}
LOGV("stopOutput() tid %d", gettid());
Mutex::Autolock _l(mLock);
return mpPolicyManager->stopOutput(output, stream, session);
++++++++++++++++++++++++++++++AudioPolicyManagerBase::stopOutput++++++++++++++++++++++++++++++++++
status_t AudioPolicyManagerBase::stopOutput(audio_io_handle_t output,
AudioSystem::stream_type stream,
int session)
{
LOGV("stopOutput() output %d, stream %d, session %d", output, stream, session);
ssize_t index = mOutputs.indexOfKey(output);
if (index < 0) {
LOGW("stopOutput() unknow output %d", output);
return BAD_VALUE;
}


AudioOutputDescriptor *outputDesc = mOutputs.valueAt(index);
routing_strategy strategy = getStrategy((AudioSystem::stream_type)stream);


// handle special case for sonification while in call
if (isInCall()) {
handleIncallSonification(stream, false, false);
}


if (outputDesc->mRefCount[stream] > 0) {
// decrement usage count of this stream on the output
outputDesc->changeRefCount(stream, -1);
++++++++++++++++++++++++++++AudioPolicyManagerBase::AudioOutputDescriptor::changeRefCount++++++++++++++++++++++++++++++++++++
void AudioPolicyManagerBase::AudioOutputDescriptor::changeRefCount(AudioSystem::stream_type stream, int delta)
{
// forward usage count change to attached outputs
if (isDuplicated()) {
mOutput1->changeRefCount(stream, delta);
mOutput2->changeRefCount(stream, delta);
}
if ((delta + (int)mRefCount[stream]) < 0) {
LOGW("changeRefCount() invalid delta %d for stream %d, refCount %d", delta, stream, mRefCount[stream]);
mRefCount[stream] = 0;
return;
}
// 函数AudioPolicyManagerBase::AudioOutputDescriptor::refCount会使用mRefCount
mRefCount[stream] += delta;
+++++++++++++++++++++++++++++AudioPolicyManagerBase::AudioOutputDescriptor::refCount+++++++++++++++++++++++++++++++++++
uint32_t AudioPolicyManagerBase::AudioOutputDescriptor::refCount()
{
uint32_t refcount = 0;
for (int i = 0; i < (int)AudioSystem::NUM_STREAM_TYPES; i++) {
refcount += mRefCount[i];
}
return refcount;
}
函数AudioPolicyManagerBase::releaseOutput中有调用函数AudioPolicyManagerBase::AudioOutputDescriptor::refCount。
+++++++++++++++++++++++++++++AudioPolicyManagerBase::releaseOutput+++++++++++++++++++++++++++++++++++
void AudioPolicyManagerBase::releaseOutput(audio_io_handle_t output)
{
LOGV("releaseOutput() %d", output);
ssize_t index = mOutputs.indexOfKey(output);
if (index < 0) {
LOGW("releaseOutput() releasing unknown output %d", output);
return;
}


#ifdef AUDIO_POLICY_TEST
int testIndex = testOutputIndex(output);
if (testIndex != 0) {
AudioOutputDescriptor *outputDesc = mOutputs.valueAt(index);
if (outputDesc->refCount() == 0) {
mpClientInterface->closeOutput(output);
delete mOutputs.valueAt(index);
mOutputs.removeItem(output);
mTestOutputs[testIndex] = 0;
}
return;
}
#endif //AUDIO_POLICY_TEST


// 如果没定义AUDIO_POLICY_TEST,只对设置了OUTPUT_FLAG_DIRECT标志的output做delete处理
// 原因,其实在以前我们看AudioPolicyManagerBase::getOutput函数时已经看到了,
// 在get output的时候,只有需要一个direct output的时候,才会调用函数AudioPolicyService::openOutput函数来打开一个output
// 否则,只是将成员变量(在构造函数中打开的output)返回
// 当然,我们没有考虑AUDIO_POLICY_TEST时的情况
if (mOutputs.valueAt(index)->mFlags & AudioSystem::OUTPUT_FLAG_DIRECT) {
++++++++++++++++++++++++++++++output_flags++++++++++++++++++++++++++++++++++
// request to open a direct output with getOutput() (by opposition to sharing an output with other AudioTracks)
enum output_flags {
OUTPUT_FLAG_INDIRECT = 0x0,
OUTPUT_FLAG_DIRECT = 0x1
};
------------------------------output_flags----------------------------------
mpClientInterface->closeOutput(output);
+++++++++++++++++++++++++++++AudioPolicyService::closeOutput+++++++++++++++++++++++++++++++++++
status_t AudioPolicyService::closeOutput(audio_io_handle_t output)
{
sp<IAudioFlinger> af = AudioSystem::get_audio_flinger();
if (af == 0) return PERMISSION_DENIED;


return af->closeOutput(output);
+++++++++++++++++++++++++++AudioFlinger::closeOutput++++++++++++++++++++++++++++++++++++
status_t AudioFlinger::closeOutput(int output)
{
// keep strong reference on the playback thread so that
// it is not destroyed while exit() is executed
sp <PlaybackThread> thread;
{
Mutex::Autolock _l(mLock);
thread = checkPlaybackThread_l(output);
if (thread == NULL) {
return BAD_VALUE;
}


LOGV("closeOutput() %d", output);


// 如果thread 为mixer
// 删除所有DUPLICATING thread中包含的该thread
if (thread->type() == PlaybackThread::MIXER) {
for (size_t i = 0; i < mPlaybackThreads.size(); i++) {
if (mPlaybackThreads.valueAt(i)->type() == PlaybackThread::DUPLICATING) {
DuplicatingThread *dupThread = (DuplicatingThread *)mPlaybackThreads.valueAt(i).get();
dupThread->removeOutputTrack((MixerThread *)thread.get());
}
}
}
void *param2 = 0;
audioConfigChanged_l(AudioSystem::OUTPUT_CLOSED, output, param2);
mPlaybackThreads.removeItem(output);
}
thread->exit();
++++++++++++++++++++++++++++AudioFlinger::ThreadBase::exit++++++++++++++++++++++++++++++++++++
void AudioFlinger::ThreadBase::exit()
{
// keep a strong ref on ourself so that we wont get
// destroyed in the middle of requestExitAndWait()
sp <ThreadBase> strongMe = this;


LOGV("ThreadBase::exit");
{
AutoMutex lock(&mLock);
mExiting = true;
requestExit();
+++++++++++++++++++++++++++++Thread::requestExit+++++++++++++++++++++++++++++++++++
void Thread::requestExit()
{
// threadloop函数中会判断该成员变量,以判断是否要结束线程
mExitPending = true;
}
-----------------------------Thread::requestExit----------------------------------
mWaitWorkCV.signal();
}
requestExitAndWait();
+++++++++++++++++++++++++++++Thread::requestExitAndWait+++++++++++++++++++++++++++++++++++
status_t Thread::requestExitAndWait()
{
if (mThread == getThreadId()) {
LOGW(
"Thread (this=%p): don't call waitForExit() from this "
"Thread object's thread. It's a guaranteed deadlock!",
this);


return WOULD_BLOCK;
}

requestExit();


Mutex::Autolock _l(mLock);
while (mRunning == true) {
mThreadExitedCondition.wait(mLock);
}
mExitPending = false;


return mStatus;
}
-----------------------------Thread::requestExitAndWait-----------------------------------
}
----------------------------AudioFlinger::ThreadBase::exit------------------------------------


if (thread->type() != PlaybackThread::DUPLICATING) {
// output是在函数AudioFlinger::openOutput中调用函数mAudioHardware->openOutputStream打开的。
mAudioHardware->closeOutputStream(thread->getOutput());
+++++++++++++++++++++++++++AudioHardwareALSA::closeOutputStream+++++++++++++++++++++++++++++++++++++
void
AudioHardwareALSA::closeOutputStream(AudioStreamOut* out)
{
AutoMutex lock(mLock);
delete out;
}
---------------------------AudioHardwareALSA::closeOutputStream-------------------------------------
}
return NO_ERROR;
}
----------------------------AudioFlinger::closeOutput------------------------------------
}
-----------------------------AudioPolicyService::closeOutput-----------------------------------
delete mOutputs.valueAt(index);
mOutputs.removeItem(output);
}
}
-----------------------------AudioPolicyManagerBase::releaseOutput-----------------------------------
-----------------------------AudioPolicyManagerBase::AudioOutputDescriptor::refCount-----------------------------------
LOGV("changeRefCount() stream %d, count %d", stream, mRefCount[stream]);
}
----------------------------AudioPolicyManagerBase::AudioOutputDescriptor::changeRefCount------------------------------------
// store time at which the last music track was stopped - see computeVolume()
if (stream == AudioSystem::MUSIC) {
mMusicStopTime = systemTime();
}


setOutputDevice(output, getNewDevice(output));


#ifdef WITH_A2DP
if (mA2dpOutput != 0 && !a2dpUsedForSonification() &&
strategy == STRATEGY_SONIFICATION) {
setStrategyMute(STRATEGY_MEDIA,
false,
mA2dpOutput,
mOutputs.valueFor(mHardwareOutput)->mLatency*2);
}
#endif
if (output != mHardwareOutput) {
setOutputDevice(mHardwareOutput, getNewDevice(mHardwareOutput), true);
}
return NO_ERROR;
} else {
LOGW("stopOutput() refcount is already 0 for output %d", output);
return INVALID_OPERATION;
}
}
------------------------------AudioPolicyManagerBase::stopOutput----------------------------------
}
---------------------------------AudioPolicyService::stopOutput-------------------------------
}
------------------------------AudioSystem::stopOutput----------------------------------
thread->mLock.lock();
}
}
}
-----------------------------AudioFlinger::PlaybackThread::Track::stop-----------------------------------
}
------------------------------AudioFlinger::TrackHandle::stop----------------------------------
// Cancel loops (If we are in the middle of a loop, playback
// would not stop until loopCount reaches 0).
setLoop(0, 0, 0);
+++++++++++++++++++++++++++++AudioTrack::setLoop+++++++++++++++++++++++++++++++++++
status_t AudioTrack::setLoop(uint32_t loopStart, uint32_t loopEnd, int loopCount)
{
audio_track_cblk_t* cblk = mCblk;


Mutex::Autolock _l(cblk->lock);


if (loopCount == 0) {
cblk->loopStart = ULLONG_MAX;
cblk->loopEnd = ULLONG_MAX;
cblk->loopCount = 0;
mLoopCount = 0;
return NO_ERROR;
}


if (loopStart >= loopEnd ||
loopEnd - loopStart > cblk->frameCount) {
LOGE("setLoop invalid value: loopStart %d, loopEnd %d, loopCount %d, framecount %d, user %lld", loopStart, loopEnd, loopCount, cblk->frameCount, cblk->user);
return BAD_VALUE;
}


if ((mSharedBuffer != 0) && (loopEnd > cblk->frameCount)) {
LOGE("setLoop invalid value: loop markers beyond data: loopStart %d, loopEnd %d, framecount %d",
loopStart, loopEnd, cblk->frameCount);
return BAD_VALUE;
}


cblk->loopStart = loopStart;
cblk->loopEnd = loopEnd;
cblk->loopCount = loopCount;
mLoopCount = loopCount;


return NO_ERROR;
}
-----------------------------AudioTrack::setLoop-----------------------------------
// the playback head position will reset to 0, so if a marker is set, we need
// to activate it again
mMarkerReached = false;
// Force flush if a shared buffer is used otherwise audioflinger
// will not stop before end of buffer is reached.
if (mSharedBuffer != 0) {
flush();
++++++++++++++++++++++++++++AudioTrack::flush++++++++++++++++++++++++++++++++++++
void AudioTrack::flush()
{
LOGV("flush");


// clear playback marker and periodic update counter
mMarkerPosition = 0;
mMarkerReached = false;
mUpdatePeriod = 0;




if (!mActive) {
mAudioTrack->flush();
++++++++++++++++++++++++++++++AudioFlinger::TrackHandle::flush++++++++++++++++++++++++++++++++++
void AudioFlinger::TrackHandle::flush() {
mTrack->flush();
++++++++++++++++++++++++++++++AudioFlinger::PlaybackThread::Track::flush++++++++++++++++++++++++++++++++++
void AudioFlinger::PlaybackThread::Track::flush()
{
LOGV("flush(%d)", mName);
sp<ThreadBase> thread = mThread.promote();
if (thread != 0) {
Mutex::Autolock _l(thread->mLock);
if (mState != STOPPED && mState != PAUSED && mState != PAUSING) {
return;
}
// No point remaining in PAUSED state after a flush => go to
// STOPPED state
mState = STOPPED;


mCblk->lock.lock();
// NOTE: reset() will reset cblk->user and cblk->server with
// the risk that at the same time, the AudioMixer is trying to read
// data. In this case, getNextBuffer() would return a NULL pointer
// as audio buffer => the AudioMixer code MUST always test that pointer
// returned by getNextBuffer() is not NULL!
// reset函数在stop函数中已经看过
reset();
mCblk->lock.unlock();
}
}
------------------------------AudioFlinger::PlaybackThread::Track::flush----------------------------------
}
------------------------------AudioFlinger::TrackHandle::flush----------------------------------
// Release AudioTrack callback thread in case it was waiting for new buffers
// in AudioTrack::obtainBuffer()
mCblk->cv.signal();
}
}
----------------------------AudioTrack::flush------------------------------------
}
if (t != 0) {
t->requestExit();
} else {
setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_NORMAL);
}
}


if (t != 0) {
t->mLock.unlock();
}
}
-------------------------------AudioTrack::stop---------------------------------
}
------------------------------android_media_AudioTrack_stop----------------------------------
mPlayState = PLAYSTATE_STOPPED;
}
}
----------------------------stop------------------------------------
track.flush();
++++++++++++++++++++++++++++flush++++++++++++++++++++++++++++++++++++
/**
* Flushes the audio data currently queued for playback.
*/


public void flush() {
if (mState == STATE_INITIALIZED) {
// flush the data in native layer
native_flush();
++++++++++++++++++++++++++++android_media_AudioTrack_flush++++++++++++++++++++++++++++++++++++
static void
android_media_AudioTrack_flush(JNIEnv *env, jobject thiz)
{
AudioTrack *lpTrack = (AudioTrack *)env->GetIntField(
thiz, javaAudioTrackFields.nativeTrackInJavaObj);
if (lpTrack == NULL ) {
jniThrowException(env, "java/lang/IllegalStateException",
"Unable to retrieve AudioTrack pointer for flush()");
return;
}


lpTrack->flush();
+++++++++++++++++++++++++++++AudioTrack::flush+++++++++++++++++++++++++++++++++++
void AudioTrack::flush()
{
LOGV("flush");


// clear playback marker and periodic update counter
mMarkerPosition = 0;
mMarkerReached = false;
mUpdatePeriod = 0;




if (!mActive) {
mAudioTrack->flush();
++++++++++++++++++++++++++++AudioFlinger::TrackHandle::flush++++++++++++++++++++++++++++++++++++
void AudioFlinger::TrackHandle::flush() {
// 函数AudioFlinger::PlaybackThread::Track::flush在上面已经看过
mTrack->flush();
}
----------------------------AudioFlinger::TrackHandle::flush------------------------------------
// Release AudioTrack callback thread in case it was waiting for new buffers
// in AudioTrack::obtainBuffer()
mCblk->cv.signal();
}
}
-----------------------------AudioTrack::flush-----------------------------------
}
----------------------------android_media_AudioTrack_flush------------------------------------
}


}
----------------------------flush------------------------------------
log(TEST_NAME, "position ="+ track.getPlaybackHeadPosition());
assertTrue(TEST_NAME, track.getPlaybackHeadPosition() == 0);
//-------- tear down --------------
track.release();
}
###########################################################


&&&&&&&&&&&&&&&&&&&&&&&总结&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
stop的时候:
若AudioTrack不是active的,在将其audio_track_cblk_t中的user, server等清0。
若是direct output,则将其close。


flush的时候:
会将audio_track_cblk_t中的user, server等清0。
因此,flush 之后再get position的话,肯定是0.
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值