Android -- 多媒体播放之Media Playback基础介绍

Android -- MediaPlayer之Media Playback基础介绍


文章翻译自Android官方文档:http://www.android-dev.cn/guide/topics/media/mediaplayer.html#viacontentresolver

多媒体回放


        Android多媒体框架包含了对各种常见媒体类型文件的播放支持,所以我们可以简单地将音频、视频、图片整合进我们的应用程序之中。我们可以使用MediaPlayer播放存储在应用程序中的原始资源,也可以播放文件系统中的独立文件,也可以播放通过网络获取的数据流。

        这篇文档向我们展示了如何写出一个拥有良好表现和体验的用户与系统交互的多媒体程序。

提醒:我们只能在标准输出设备上播放音频数据。当前,这些设备是手机设备的扬声器或者蓝牙头戴式耳机。我们不能在通话过程中播放声音文件。


基础


        下面这些类是Android framework中用来播放声音和视频的:

MediaPlayer:该类是播放声音和视频的主要API

AudioManager:该类管理一个设备上的音频数据和音频输出


清单声明


        在使用MediaPlayer开始开发你的应用之前,确保你的清单文件中有适当的声明(权限声明),让你允许使用一些关联的特性。

  • 网络权限-如果你正在使用MediaPlayer处理网络内容,你的应用程序必须需要网络接入。
<uses-permission android:name="android.permission.INTERNET" />
  • Wake Lock 权限-如果你的播放程序需要保持屏幕常亮或使设备不进入睡眠,或者调用了 MediaPlayer.setScreenOnWhilePlaying() 、MediaPlayer.setWakeMode() 方法,你就必须请求这个权限。
<uses-permission android:name="android.permission.WAKE_LOCK" />

使用MediaPlayer


        媒体框架的一个最重要的部分就是MediaPlayer类。该类的一个实例可以通过极少的步骤去获取、解码和播放音视频。它支持几种不同的媒体资源,例如:
  • 本地资源
  • 内部URI资源, 例如通过某个Content Resolver获取的URI
  • 外部URL资源(流)
        Android支持的媒体格式列表,可以参照 Android Supported Media Formats文档。

        这里是一个如何去播放本地原始资源(保存在应用程序的res/raw/目录)的音频的例子:
MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
mediaPlayer.start(); // no need to call prepare(); create() does that for you
        这个例子中,一个原始资源是一个系统不会以任何特定方式去解析的文件。但是,该资源的内容不应该是原始音频。它应该是一个被正确编码且以Android支持的格式封装成的媒体文件。

        这里是一个播放通过URI获取的系统本地文件(通过Content Resolver获取)的例子:
Uri myUri = ....; // initialize Uri here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
        播放一个通过HTTP协议传输的远端流的做法看起来是这样的:
String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepare(); // might take long! (for buffering, etc)
mediaPlayer.start();
提醒:如果你是靠传入一个URL地址去引入在线的媒体文件,你必须保证它是可以被渐进载入的。
警告:在调用setDataSource()时,你必须捕获或对外传递IllegalArgumentException和IOException异常,因为你当前引用的文件也许是不存在的。

异步Preparation


        使用MediaPlayer在条理上是简单易懂的。但是当我们将它整合进一个典型的Android应用中时,记住其他的一些必要事情是很重要的。例如,prepare()调用也许会花费很长的时间去执行,因为它会牵扯到媒体数据的获取和解码。所以,对于那些要花费较长时间执行的方法,我们不能在应用程序的UI线程中调用它。如果我们这样做,会导致UI挂起,直到方法得到返回为止;这是一个非常差的用户体验,同时也会导致一个ANR错误。即使你认为你的资源加载非常快,也必须记住UI中任何花费超过0.1秒时间响应的操作都会导致明显的卡顿,这会给用户带来你的应用程序运行很慢的印象。
        为了避免阻塞你的UI线程,我们应该创建另外一个线程准备你的MediaPlayer对象,并完成的时候去通知主线程。然而,尽管你可以自己写线程逻辑,但调用MediaPlayer框架提供的prepareAsync()方法来完成这项任务会更加方便,也更加通用。该函数会在后台开启准备媒体数据并立刻返回。当媒体数据准备完成时,通过setOnPreparedListener() 方法配置的MediaPlayer.OnPreparedListener回调的onPrepared()函数就会被调用。

管理状态


        我们需要关注的关于MediaPlayer的另一个方面就是它是基于状态的。也就是说,写代码时我们必须时刻注意MediaPlayer拥有自己的内部状态,因为只有当播放器在某个特定的状态时,一个确定的操作才是合法的。如果你在错误的状态执行一个操作,系统也许会抛出一个异常,或者出现其他令人不快的行为。
        MediaPlayer类的介绍文档展示出了一个完整的状态图,它清楚表明了某个方法会将MediaPlayer从一个状态移动到另一个状态。举个例子,当你创建MediaPlayer时,它处于Idle状态。在这个点,我们需要调用setDateSource()初始化该对象,将它转换到Initialized状态。之后,我们必须调用prepare()或者prepareAsync()方法去准备它。当MediaPlayer准备完成后,它将会进入Prepared状态,这意味着你可以调用start()方法开始播放媒体数据。这时,正如图中所示,我们可以通过调用start()、pause()和seekTo()方法让MediaPlayer在Started、Paused和PlaybackCompleted状态之间移动。要注意当你调用stop()时,直到你再次对MediaPlayer调用函数进行prepare后,我们才能再次调用start()。
        我们在写与MediaPlayer交互的代码时,要时刻将状态图记在心里,因为在错误的状态调用它的方法,是一些缺陷产生的普遍原因。

释放MediaPlayer


        一个MediaPlayer实例会占用宝贵的系统资源。因此,我们需要花费额外的精力确保应用中不会持有一个无需再被使用的MediaPlayer实例。当我们使用它完成任务时,总是要调用release()方法确以保任何分配给它的系统资源能被正确地释放。举个例子,如果我们正在使用MediaPlayer,此时你的activity收到了一个onStop()调用;我们必须释放这个MediaPlayer,因为当你的activity不再跟用户交互时,持有它是没有意义的(除非你正在后台播放媒体文件,这将会在下一节讨论)。当你的activity被resume或restart时,在重新回放之前,我们需要创建一个新的MediaPlayer并重新prepare它。
        下面是释放并废弃我们的MediaPlayer的方法:
mediaPlayer.release();
mediaPlayer = null;
        举个例子,考虑这样一种可能发生的问题:当你的activity被停止时,你没有释放当前的MediaPlayer;但是在activity再次启动时,你又创建了一个新MediaPlayer。正如你所知道的,当用户改变了屏幕方向(或者以其他的方式改变了设备配置),系统会通过重启activity来处理这种情况(默认方式);所以,当用户频繁这样做时,我们也许会很快地耗尽所有的系统资源,因为每次方向改变时,我们都创建了一个新的你从没释放的MediaPlayer。
        
        正如那些音乐程序所表现的那样,你也许想知道当用户离开了你的activity,而我继续后台播放媒体文件时会发生什么。这种情况下,你需要的是一个被Service控制的MediaPlayer,我们接下来就讨论这部分。

在Service中使用MediaPlayer


        如果你希望当你的应用程序不再屏幕当前显示时 ,让它在后台继续播放媒体文件;也就是说,你希望用户当前在与其他应用交互时,继续播放动作-这时,你必须启动一个Service,并在Service中控制MediaPlayer实例。这一步,我们应当小心;因为,用户和系统对应用程序如何启动一个后台服务与剩下的系统交互有着不同的期望。如果你的应用没有达成这些期望,就会给用户带来不好的体验。这一部分主要描述了你需要注意的内容,并且对如何实现它们提供了建议。

异步运行


        首先,与Activity一样,所有在Service中完成的工作默认都是在一个单独的线程中进行的-事实上,如果你在同一个应用中运行一个Activity和一个Service,它们都默认工作在主线程中。因此,服务必须要快速的处理来到的intent,并且在响应它们时不应该进行冗长的计算。如果你需要处理任何繁重的工作,或者存在阻塞的调用时,你都必须异步地处理这些任务:可以是在你自己实现的新线程中,或者是使用framework中提供的用于异步执行的工具。
        例如,当你在主线程中使用MediaPlayer时,你应该调用prepareAsync(),而不是prepare();并且要实现一个MediaPlayer.OnPreparedListener回调用于当准备工作完成时去提醒你可以开始播放了。例如:
public class MyService extends Service implements MediaPlayer.OnPreparedListener {
    private static final String ACTION_PLAY = "com.example.action.PLAY";
    MediaPlayer mMediaPlayer = null;

    public int onStartCommand(Intent intent, int flags, int startId) {
        ...
        if (intent.getAction().equals(ACTION_PLAY)) {
            mMediaPlayer = ... // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        }
    }

    /** Called when MediaPlayer is ready */
    public void onPrepared(MediaPlayer player) {
        player.start();
    }
}

处理异步错误


        同步操作中,一般使用异常或错误码来通知程序错误。但是任何时候,你在使用异步资源时,都要保证你的应用程序可以被正确地告知有错误发生。在MediaPlayer这个例子中,你可以通过实现MediaPlayer.OnErrorListener回调并将它设置给MediaPlayer实例来完成:
public class MyService extends Service implements MediaPlayer.OnErrorListener {
    MediaPlayer mMediaPlayer;

    public void initMediaPlayer() {
        // ...initialize the MediaPlayer here...

        mMediaPlayer.setOnErrorListener(this);
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // ... react appropriately ...
        // The MediaPlayer has moved to the Error state, must be reset!
    }
}
        我们要记住,当一个错误发生时,MediaPlayer会移动到Error状态,这是很重要的;再次使用它之前,我们必须重置这个对象。

使用唤醒锁


        在设计后台播放媒体资源的应用时,设备可能会在你的服务运行期间进入睡眠状态。因为设备睡眠时,Android系统会尝试保存电量,它会去关掉任何非必须的手机特性,包括CPU和WiFi硬件。因此,如果你的服务正在播放音乐,你会想去阻止系统妨碍你的回放过程。
        为了保证你的服务能在这些条件下继续运行,你必须使用“唤醒锁”。唤醒锁可以向系统表明,你的应用程序正在使用一些设备特性,即使手机处于空闲状态,它们也需要保持可以获取的状态。
        注意:你应该仅在你需要的时候使用唤醒锁,因为,它们会显著地降低设备电池的寿命。
        为了确保当你的MediaPlayer正在播放时CPU会继续运行,在初始化MediaPlayer时,你需要调用setWakeMode()方法。一旦这样做,MediaPlayer正在播放时会持有一个特定的锁,播放暂停或停止时都需要释放这个锁:
mMediaPlayer = new MediaPlayer();
// ... other initialization here ...
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
        然而,这个示例中获取到的锁只能保证CPU保持唤醒。如果你正在通过网络且使用WiFi载入一个网络媒体,你也许还需要持有一个WiFi Lock,你必须手动请求和释放它。所以,当你开始通过一个远程URL准备MediaPlayer时,你应该创建并请求WiFi Lock。例如:
WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
    .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

wifiLock.acquire();
        当你暂停或停止媒体播放,或者你不再需要使用网络时,你应该释放这个锁:
wifiLock.release();

作为前台服务运行


        Service通常被用来执行后台服务,例如收发邮件、同步数据、下载内容,etc。这些例子中,用户不会明显察觉到这些服务的执行,并且他们甚至不会注意到这些服务是否被中断、重启。
        但是考虑一个服务正在播放音乐,用户明显意识到它的存在,此时任何的中断操作(是该服务中断)都会影响到体验效果。除此之外,这是一个在它的运行过程,用户都希望与之交互的服务。这时,该服务应该作为“前台服务”运行。一个前台服务在系统中拥有更高的重要性等级-系统将几乎不会杀死该服务,因为它对用户而言是非常重要的。当服务运行在后台时,我们需要提供一个状态栏让用户看到当前有服务正在运行,这也允许用户可以打开一个用来与该服务交互的Activity界面。
        为了将你的服务转换成前台服务,你必须为状态栏创建一个Notification(通知),并且在服务中调用startForeground()函数。例如:
String songName;
// assign the song name to songName
PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0,
                new Intent(getApplicationContext(), MainActivity.class),
                PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new Notification();
notification.tickerText = text;
notification.icon = R.drawable.play0;
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.setLatestEventInfo(getApplicationContext(), "MusicPlayerSample",
                "Playing: " + songName, pi);
startForeground(NOTIFICATION_ID, notification);
        当你的服务运行在前台时,你配置的notification在设备的通知区域是可见的。如果用户选择了这个notification,系统就会请求你提供的PendingIntent。下面的例子中,它会打开一个Activity(MainActivity)。
        图1展示了你的notification是如何展现给用户的:


图1:前台服务notification的截图,展示了状态栏通知的图标(左)以及其展开后的视图(右)
        你应该仅在你的服务确实在展现一些用户感兴趣的事物时,才持有“前台服务”状态。一旦你不在需要,你应该通过调用stopForeground()释放它:
stopForeground(true);
        如果需要更多信息,可以看有关 ServiceStatus Bar Notification的文档。

处理音频焦点


        尽管任意给定的时刻只有一个Activity可以运行,但Android是一个多任务的系统环境。这就给那些要使用音频的应用提出了一个特殊的挑战,因为系统只存在一个音频输出,多个不同的媒体服务也许会竞争使用该资源。Android 2.2之前,系统没有内置一项机制来处理这个问题,这在某些情况下也许会带来不好的用户体验。例如,当用户正在听音乐时,其他的应用程序由于一些非常重要的事情需要提醒用户,这时用户就可能会因为吵闹的音乐而没法听到该提醒。从Android 2.2开始,平台提供了一种途径让应用程序自己协商对设备音频输出的使用。这项机制称为音频焦点。
        当你的应用程序需要输出音乐或提醒时,你应该总是请求音频焦点。一旦它拥有焦点,它就可以随意地使用声音输出,但是它必须随时监听焦点的改变情况。如果应用程序被通知到它已经失去了音频焦点,它就应该将声音关掉或者调低至一个安静的水平;此时,只有在再次得到焦点后,它才可以恢复正常的播放。
        音频焦点本质上是合作性质的。也就是说,我们希望应用程序遵从音频焦点的指示,但是系统并不会强制执行这项规则。如果一个应用程序在失去音频焦点后仍想大声播放音乐,系统并不会做任何事情来阻止它。但是,这会带来非常差的用户体验,用户更可能会去卸载这种粗鲁的应用。
        为了请求音频焦点,你必须调用AudioManager类的requestAudioFocus()方法,就像下面示例所展示的那样:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN);

if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // could not get audio focus.
}
        requestAudioFcous()方法的第一个参数是一个AudioManager.OnAudioFocusChangeListener回调对象,任何时刻如果音频焦点发生变化,它的onAudioFocusChange()方法就会被调用。因此,你应该在你的service和Activity中实现这个接口。例如:
class MyService extends Service
                implements AudioManager.OnAudioFocusChangeListener {
    // ....
    public void onAudioFocusChange(int focusChange) {
        // Do something based on focus change...
    }
}
        参数focusChange会告诉你音频焦点发生了怎样的改变,它可以是下面值中的某一个:
  • AUDIOFOCUS_GAIN:你获取到了音频焦点。
  • AUDIOFOCUS_LOSS:长时间内,你失去了音频焦点。你必须停止所有音频播放。因为长时间内你都可能不会有音频焦点,这将会是一个你尽可能清理资源的好地方。例如,这时你应该释放MediaPlayer。
  • AUDIO_LOSS_TRANSIENT:你只是暂时失去了音频焦点,在很短时间内你会重新获取到它。你必须停止所有音频播放,但是你可以保持你的资源,因为短时间内你可能将会重新得到音频焦点。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:你暂时失去了音频焦点,但允许你继续安静地播放音频(低音量),而不是彻底关掉。
        这是一个实现该接口的例子:
public void onAudioFocusChange(int focusChange) {
    switch (focusChange) {
        case AudioManager.AUDIOFOCUS_GAIN:
            // resume playback
            if (mMediaPlayer == null) initMediaPlayer();
            else if (!mMediaPlayer.isPlaying()) mMediaPlayer.start();
            mMediaPlayer.setVolume(1.0f, 1.0f);
            break;

        case AudioManager.AUDIOFOCUS_LOSS:
            // Lost focus for an unbounded amount of time: stop playback and release media player
            if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
            // Lost focus for a short time, but we have to stop
            // playback. We don't release the media player because playback
            // is likely to resume
            if (mMediaPlayer.isPlaying()) mMediaPlayer.pause();
            break;

        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
            // Lost focus for a short time, but it's ok to keep playing
            // at an attenuated level
            if (mMediaPlayer.isPlaying()) mMediaPlayer.setVolume(0.1f, 0.1f);
            break;
    }
}
        请记住,音频焦点的API只在API 8(Android 2.2)及更高版本中提供。所以如果你想支持原先老版本的Android,如果有的话,你应该采用那些允许你使用这项特性的向后兼容的策略;否则,那就只能让步。
        你可以通过反射来调用音频焦点相关的方法,或者在单独的类中(例如AudioFocusHelper)实现所有音频焦点的特性,来实现这种向后兼容。下面是这种类的一个例子:
public class AudioFocusHelper implements AudioManager.OnAudioFocusChangeListener {
    AudioManager mAudioManager;

    // other fields here, you'll probably hold a reference to an interface
    // that you can use to communicate the focus changes to your Service

    public AudioFocusHelper(Context ctx, /* other arguments here */) {
        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        // ...
    }

    public boolean requestFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.requestAudioFocus(mContext, AudioManager.STREAM_MUSIC,
            AudioManager.AUDIOFOCUS_GAIN);
    }

    public boolean abandonFocus() {
        return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
            mAudioManager.abandonAudioFocus(this);
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        // let your service know about the focus change
    }
}
        只有当检测到系统运行在API 8及以上版本时,你才可以创建一个AudioFocusHelper类的实例。例如:
if (android.os.Build.VERSION.SDK_INT >= 8) {
    mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this);
} else {
    mAudioFocusHelper = null;
}

执行清理操作


        正如之前提到的,一个MediaPlayer对象会消耗大量重要的系统资源;所以你只应该在需要的时候才持有它,当你用它完成任务后就调用release()方法。显示调用这个清理方法,而不依赖系统的垃圾回收机制,是非常重要的。因为垃圾回收器也许会在一段时间后才去回收那些可被回收的MediaPlayer,而且垃圾回收器只关心内存的需要,它并不 关心其他媒体相关资源是否充足。所以,在你使用Service的例子中,你应该总是重写onDestory()方法,以确保你释放MediaPlayer对象:
public class MyService extends Service {
   MediaPlayer mMediaPlayer;
   // ...

   @Override
   public void onDestroy() {
       if (mMediaPlayer != null) mMediaPlayer.release();
   }
}
        除了在服务在被停止时进行释放,你应该时常寻找其他的时机去释放你的MediaPlayer。例如,如果你在一段较长的时间内都不会播放媒体资源(例如,在失去了音频焦点之后),你就应该明确地释放存在的MediaPlayer,并在之后再次创建它。另一方面,如果你只是在非常短的时间内停止播放,你就大概可以继续持有你的MediaPlayer,以避免再次创建和准备它的系统开销。

处理AUDIO_BECOMING_NOISY Intent


        许多写得好的播放音频的应用在有一个会导致音频变吵闹(通过外置扬声器输出)的事件发生时,都会自动停止当前播放。例如,一位用户正在使用头戴式耳机听音乐,如果此时耳机意外地与设备断开连接,这种情况就可能会发生。然而,这种行为并不会自动产生。如果你没有实现这项特性,音频就会从设备的外置扬声器播出,这也许与并不是用户想要的。
        你可通过处理ACTION_AUDIO_BECOMING_NOISY Intent来确保你的app在这种情况下会停止播放音乐,你可以将下面的内容添加到你的清单文件中来注册你的广播接收器:
<receiver android:name=".MusicIntentReceiver">
   <intent-filter>
      <action android:name="android.media.AUDIO_BECOMING_NOISY" />
   </intent-filter>
</receiver>
        这里为该Intent注册了MusicIntentReceiver类作为广播接收器。并且你应该实现这个类:
public class MusicIntentReceiver extends android.content.BroadcastReceiver {
   @Override
   public void onReceive(Context ctx, Intent intent) {
      if (intent.getAction().equals(
                    android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
          // signal your service to stop playback
          // (via an Intent, for instance)
      }
   }
}

从Content Resolver获取媒体


        在媒体播放应用中,另一个可能有用的特性就是获取用户设备上存在的音乐的能力。你可以通过查询跟外置媒体相关的ContentResolve来完成它:
ContentResolver contentResolver = getContentResolver();
Uri uri = android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(uri, null, null, null, null);
if (cursor == null) {
    // query failed, handle error.
} else if (!cursor.moveToFirst()) {
    // no media on the device
} else {
    int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media.TITLE);
    int idColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Media._ID);
    do {
       long thisId = cursor.getLong(idColumn);
       String thisTitle = cursor.getString(titleColumn);
       // ...process entry...
    } while (cursor.moveToNext());
}
        为了在MediaPlayer中使用它,你可以这样做:
long id = /* retrieve it from somewhere */;
Uri contentUri = ContentUris.withAppendedId(
        android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(getApplicationContext(), contentUri);

// ...prepare and start...

PS:翻译如有不妥、歧义的地方,欢迎提出,我会改正,谢谢!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值