Android MediaPlayer

MediaPlayer 是 Android 中的一个多媒体播放类,能通过它控制音视频流或本地音视频资源的播放过程。

Play

示例一

图片旋转音乐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
/**
* 媒体播放
*/
public class MediaPlayerManager {

/**
* 播放 0。 暂停 1。 停止 2。
* 当前状态默认等于停止状态。
*/
public static final int MEDIA_STATUS_PLAY = 0;
public static final int MEDIA_STATUS_PAUSE = 1;
public static final int MEDIA_STATUS_STOP = 2;
public int MEDIA_STATUS = MEDIA_STATUS_STOP;

private static final int H_PROGRESS = 1000;
private MediaPlayer mediaPlayer;
private OnMusicProgressListener musicProgressListener;

/**
* 通过 Handler 获取进度
*
* 计算歌曲的进度:
* 1.开始播放的时候就开启循环计算时长
* 2.将进度计算结果对外抛出
*/
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message message) {
switch (message.what){
case H_PROGRESS:
if (musicProgressListener != null){
// 拿到当前时长
int currentPosition = getCurrentPosition();
// 当前进度百分比
int pos = (int) (((float)currentPosition) / ((float)getDuration())*100);
musicProgressListener.OnProgress(currentPosition,pos);
// 每隔一秒
mHandler.sendEmptyMessageDelayed(H_PROGRESS,1000);
}
break;
}
return false;
}
});

public MediaPlayerManager(){
mediaPlayer = new MediaPlayer();
}

/**
* 判断是否在播放
* @return
*/
public boolean isPlaying(){
return mediaPlayer.isPlaying();
}

/**
* 开始播放
* @param path
*/
public void startPlay(AssetFileDescriptor path){
try {
mediaPlayer.reset();
// mediaPlayer.setDataSource(path); // Android N 以上
mediaPlayer.setDataSource(path.getFileDescriptor(),
path.getStartOffset(), path.getLength());
mediaPlayer.prepare();
mediaPlayer.start();
MEDIA_STATUS = MEDIA_STATUS_PLAY;
mHandler.sendEmptyMessage(H_PROGRESS);
} catch (IOException e) {
LogUtils.e(e.toString());
e.printStackTrace();
}
}

/**
* 开始播放的重载方法
* @param path
*/
public void startPlay(String path){
try {
mediaPlayer.reset();
mediaPlayer.setDataSource(path);
mediaPlayer.prepare();
mediaPlayer.start();
MEDIA_STATUS = MEDIA_STATUS_PLAY;
mHandler.sendEmptyMessage(H_PROGRESS);
} catch (IOException e) {
LogUtils.e(e.toString());
e.printStackTrace();
}
}

/**
* 暂停播放
*/
public void pausePlay(){
if (isPlaying()){
mediaPlayer.pause();
MEDIA_STATUS = MEDIA_STATUS_PAUSE;
mHandler.removeMessages(H_PROGRESS);
}
}

/**
* 继续播放
*/
public void continuePlay(){
mediaPlayer.start();
MEDIA_STATUS = MEDIA_STATUS_PLAY;
mHandler.sendEmptyMessage(H_PROGRESS);
}

/**
* 停止播放
*/
public void stopPlay(){
mediaPlayer.stop();
MEDIA_STATUS = MEDIA_STATUS_STOP;
mHandler.removeMessages(H_PROGRESS);
}

/**
* 获取当前位置
* @return
*/
public int getCurrentPosition(){
return mediaPlayer.getCurrentPosition();
}

/**
* 获取总时长
* @return
*/
public int getDuration(){
return mediaPlayer.getDuration();
}

/**
* 是否循环
*/
public void setLooping(boolean isLooping){
mediaPlayer.setLooping(isLooping);
}

/**
* 跳转位置
* @param ms 时间段
*/
public void seekTo(int ms){
mediaPlayer.seekTo(ms);
}

/**
* 播放结束监听
*/
public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener){
mediaPlayer.setOnCompletionListener(listener);
}

/**
* 播放错误监听
*/
public void setOnErrorListener(MediaPlayer.OnErrorListener listener){
mediaPlayer.setOnErrorListener(listener);
}

/**
* 播放进度监听
*/
public void setOnProgressListener(OnMusicProgressListener listener){
musicProgressListener = listener;
}

public interface OnMusicProgressListener{
void OnProgress(int progress,int pos);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private MediaPlayerManager mGuideMusic;
private ObjectAnimator mAnim;

/**
* 播放音乐
*/
private void startMusic() {

mGuideMusic = new MediaPlayerManager();
mGuideMusic.setLooping(true);
AssetFileDescriptor file = getResources().openRawResourceFd(R.raw.guide);
mGuideMusic.startPlay(file);

mGuideMusic.setOnComplteionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mGuideMusic.startPlay(file);
}
});

//旋转动画
mAnim = AnimUtils.rotation(iv_music_switch);
mAnim.start();
}

@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.iv_music_switch:
if(mGuideMusic.MEDIA_STATUS == MediaPlayerManager.MEDIA_STATUS_PAUSE){
mAnim.start();
mGuideMusic.continuePlay();
iv_music_switch.setImageResource(R.drawable.img_guide_music);
}else if(mGuideMusic.MEDIA_STATUS == MediaPlayerManager.MEDIA_STATUS_PLAY){
mAnim.pause();
mGuideMusic.pausePlay();
iv_music_switch.setImageResource(R.drawable.img_guide_music_off);
}
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
mGuideMusic.stopPlay();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Profile: 动画工具类
*/
public class AnimUtils {

/**
* 旋转动画
* @param view
* @return
*/
public static ObjectAnimator rotation(View view) {
ObjectAnimator mAnim = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
mAnim.setDuration(2 * 1000);
mAnim.setRepeatMode(ValueAnimator.RESTART);
mAnim.setRepeatCount(ValueAnimator.INFINITE);
mAnim.setInterpolator(new LinearInterpolator());
return mAnim;
}
}

###示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
/**
* 播放音频文件
* 一般使用 MediaPlayer 类实现,可以用于播放网络、本地以及应用程序安装包中的音频。
* 工作流程,首先需要创建一个 MediaPlayer 对象,然后调用 setDataSource() 设置音频文件的路径,
* 再调用 prepare() 使 MediaPlayer 进入准备状态,接下来调用 start() 播放音频,pause() 暂停,reset() 停止播放。
* ----------------------------------------------------------------------------------------------------
* 播放视频文件
* 主要使用 VideoView 类实现,它将视频的显示和控制集于一身。
* VideoView 不支持直接播放 assets 目录下的视频资源,但像音频、视频之类的资源文件也可以放在 res 目录下的 raw 目录内。
* ----------------------------------------------------------------------------------------------------
* VideoView 的背后仍然是使用 MediaPlayer 类对视频文件进行控制的,
* 并且 VideoView 在视频格式的支持以及播放效率方面都存在着较大的不足,所以更适合用于播放一些游戏的片头动画,或是某个应用的视频宣传。
*/

class PlayActivity : BaseActivity() {

/**
* 播放音频(1) or 播放视频(2)
*/
private var type = 1

/**
* 类初始化时,先创建了一个 MediaPlayer 实例。
*/
private val mediaPlayer = MediaPlayer()


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_play)

initView()
initData()
}

private fun initData() {
// 为 MediaPlayer 对象进行初始化操作
initMediaPlayer()
initVideoView()
}

private fun initVideoView() {
// 将 raw 目录下的 mp4 文件解析成了一个 Uri 对象,这里使用的写法是 Android 要求的固定写法。
val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
// 初始化 VideoView 完成
videoView.setVideoURI(uri)
}

/**
* 这里测试使用应用程序安装包中的音频,借助 AssetManager 类提供的接口对 assets 目录下的文件进行读取。
*/
private fun initMediaPlayer() {
// 通过 getAssets() 得到一个 AssetMessager 的实例,AssetMessager 可用于读取 assets 目录下的任何资源。
val assetMessager = assets
// 调用 openFd() 将音频文件句柄打开
val fd = assetMessager.openFd("music.mp3")
mediaPlayer.setDataSource(fd.fileDescriptor,fd.startOffset,fd.length)
mediaPlayer.prepare()
}

/**
* MediaPlayer 类中常用的控制方法:
* mediaPlayer.setDataSource() 设置要播放的音频文件的位置。(设置播放资源)
* mediaPlayer.prepare() 开始播放前调用,已完成准备工作。(同步的方式装载流媒体文件)
* mediaPlayer.prepareAsync() 异步的方式装载流媒体文件
* mediaPlayer.start() 开始或继续播放音频
* mediaPlayer.pause() 暂停播放
* mediaPlayer.reset() 将 MediaPlayer 对象重置到刚刚创建的状态
* mediaPlayer.seekTo() 从指定的位置开始播放音频(跳转)
* mediaPlayer.getCurrentPosition() 获取当前流媒体的播放的位置
* mediaPlayer.stop() 停止播放音频。调用后的 MediaPlayer 对象无法再播放音频。
* mediaPlayer.release() 释放 与 MediaPlayer 对象相关的资源(回收流媒体资源)
* mediaPlayer.isPlaying() 判断当前 MediaPlayer 是否正在播放音频
* mediaPlayer.isLooping() 判断是否循环播放
* mediaPlayer.setLooping() 设置是否循环播放
* mediaPlayer.duration 获取载入的音频文件的时长
* mediaPlayer.setAudioStreamType() 设置播放流媒体类型
* mediaPlayer.setWakeMode() 设置 CPU 唤醒的状态
*
* MediaPlayer 的公开接口
* mediaPlayer.setOnCompletionListener(); 当流媒体播放完毕的时候回调
* mediaPlayer.setOnErrorListener(); 当播放中发生错误的时候回调
* mediaPlayer.setOnPreparedListener(); 当装载流媒体完毕的时候回调
* mediaPlayer.setOnSeekCompleteListener(); 当使用 seekTo() 设置播放位置的时候回调
*
* -----------------------------------------------------------------
*
* VideoView 的常用方法:
* videoView.setVideoPath() 设置要播放的视频文件的位置
* videoView.start() 开始或继续播放视频
* videoView.pause() 暂停播放视频
* videoView.resume() 将视频从头开始播放
* videoView.seekTo() 从制定的位置开始播放视频
* videoView.isPlaying 判断当前是否正在播放视频
* videoView.duration 获取载入的视频文件的时长
*
*/
private fun initView() {
btnType.setOnClickListener{
if (type == 1){
type = 2
btnPlay.setText("视频 Play")
btnPause.setText("视频 Pause")
btnStop.setText("视频 Stop")
}else if (type == 2){
type = 1
btnPlay.setText("音频 Play")
btnPause.setText("音频 Pause")
btnStop.setText("音频 Stop")
}else {
type = -1
btnPlay.setText("-1")
btnPause.setText("-1")
btnStop.setText("-1")
}
}

btnPlay.setOnClickListener{
if (type == 1){
if (!mediaPlayer.isPlaying){
mediaPlayer.start() // 开始播放
}
}else if (type == 2){
if(!videoView.isPlaying){
videoView.start() // 开始播放
}
}else {
Toast.makeText(this,"error",Toast.LENGTH_SHORT).show()
}
}

btnPause.setOnClickListener{
if (type == 1){
if (mediaPlayer.isPlaying){
mediaPlayer.pause() // 暂停播放
}
}else if (type == 2){
if(videoView.isPlaying){
videoView.pause() // 暂停播放
}
}else {
Toast.makeText(this,"error",Toast.LENGTH_SHORT).show()
}
}

btnStop.setOnClickListener{
if (type == 1){
if (mediaPlayer.isPlaying){
mediaPlayer.reset() // 停止播放
initMediaPlayer()
}
}else if (type == 2){
if(videoView.isPlaying){
videoView.resume() // 重新播放
}
}else {
Toast.makeText(this,"error",Toast.LENGTH_SHORT).show()
}
}
}

override fun onDestroy() {
super.onDestroy()

mediaPlayer.stop()
mediaPlayer.release()

videoView.suspend()
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, PlayActivity::class.java)
context.startActivity(intent)
}
}
}

状态图及生命周期

MediaPlayer 用于控制视频/音频文件及流的播放,由状态机进行管理。它有下面的一些状态:

  • Idle 状态及 End 状态:
    • 在 MediaPlayer 创建实例或者调用 reset() 后,播放器就被创建了,这是处于 Idle(就绪)状态,调用 release() 后,就会变成 End(结束)状态,在这两种状态之间的就是 MediaPlayer 的生命周期。
  • Error (错误)状态:
    • 当处于 Idle 状态时,上层应用程序调用一些方法时,用户提供的回调函数 OnErrorListener.onError 将触发 MediaPlayer 状态到 Error 状态(或者其他原因导致播放失败时),可从中获取错误原因,并且可以调用 reset() 重新恢复到 Idle 状态。
    • 当不再使用 MediaPlayer 时,需调用 release() ,以便 MediaPlayer 资源得到合理释放,因为当处于 End 时,代表本次生命周期已经终止,MediaPlayer 将不能再被使用,这时不能再回到其他状态。
  • Initialized(初始化)状态:
    • 当调用 setDataSource() 时,由 Idle 状态将变为 Initialized 状态,如果在非 Idle 状态时调用此方法,会抛出异常。
  • Prepared(准备)状态:
    • 由 Initialized 状态到达此状态有两种方式,同步(prepare() -> Prepared 状态)和异步(prepareAsync() -> Preparing 状态,这是一个瞬时状态,可理解为时间比较短。 -> Prepared 状态)。
    • 前者主要使用本地音视频文件,后者主要使用网络数据,需要缓冲数据。
    • 当应用层事先注册过 setOnPrepareListener 时,播放器内部将回调用户设置的 OnPrepareListener 中的 onPrapared()。
    • 此状态时,上层应用可设置一些属性,如音视频的音量、screenOnWhilePlaying、looping 等。
  • Started(播放控制)状态:
    • 由 Prepared 状态时调用 start() 并成功返回时,开始播放控制状态。
    • 回调函数为 OnBufferingUpdateListener.onBufferingUpdate,此函数主要使应用程序保持跟踪音视频流的 buffering(缓冲)。
    • 在播放控制时可以是 Paused 和 Stopped 状态的,且当前的播放进度可以被调整(seekTo())。
  • Paused(暂停)状态:
    • 调用 MediaPlayer.pause() 时,进入此状态,这个过程是瞬时的,反之在播放器内部是异步过程的。
    • 在状态更新并调用 isPlaying 函数前,将有一些耗时。已经缓冲过的数据流,也要耗费数秒。
    • 可调用 start() 恢复到 Started 状态,playback 恢复到之前暂停时的位置,并开始播放。
  • Stopped(停止)状态:
    • 当调用 stop() 时,MediaPlayer 无论正处于 Started、Paused、Prepared 或 PlaybackCompleted 中的哪种状态,都将进入 Stopped 状态。
    • 此状态时,playback 将不能开始,直到重新调用 prepare() 或 prepareAsync() 并处于 Prepared 状态时才可以开始。
  • PlaybackCompleted(播放完成)状态:
    • 可通过 getCurrentPosition() 跟踪和获取播放器的播放进度。
    • 当在 Started 状态中,事先调用 setLooping(boolean) 并设置为 true,表示循环播放,MediaPlayer 依然处于 Started 状态,如果设置为 false,表示不循环播放,回调函数为 OnCompletion.onCompletion,并进入 PlaybackCompleted 状态。
    • 此状态中,调用 start() ,将重启播放器从头开始播放数据。

备注:

  • Seek 操作:
    • 在 Seek 操作完成后,如果事先在 MediaPlayer 注册了 setOnSeekCompleteListener,播放器内部将回调 OnSeekComplete.onSeekComplete 函数。
    • seekTo() 在 Prepared、Paused、PlaybackCompleted 状态时都可被调用。
  • 播放器内部将回调的函数,需要用户事先通过 setOnXXXListener 来进行注册。
  • 当已经处于 Started 、Paused、Stopped 状态时,此时再调用相应函数 start() 、pause()、stop() 是没有任何作用的,依然处于当前状态中。

从创建到 setDataSource 过程

create 到 setDisplay 过程

通过 getService 从 ServiceManager 获取对应的 MediaPlayerService,然后调用 native_setup 函数创建播放器,接着调用 setDataSource 把 URL 地址传入底层。当准备好后,通过 setDisplay 传入 SurfaceHolder,以便将解码出的数据放到 SurfaceHolder 中的 Surface。最后显示在 SurfaceView 上。

创建过程

实例化 MediaPlayer 有两种方式:

1、使用 new 的方式:

MediaPlayer mp = newMediaPlayer();

2、使用 create 的方式:这时不需要再调用 setDataSource 了,更适用于使用现成的资源。

MediaPlayer mp = MediaPlayer.create(this, R.raw.test);

MediaPlayer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static MediaPlayer create(Context context, Uri uri, SurfaceHolder holder,
AudioAttributes audioAttributes, int audioSessionId) {

try {
MediaPlayer mp = new MediaPlayer();
// 声音相关处理,若为空,就创建一个。
final AudioAttributes aa = audioAttributes != null ? audioAttributes :
new AudioAttributes.Builder().build();
// 设置音频属性。
mp.setAudioAttributes(aa);
// 设置声音的会话 ID,视频和音频是分开渲染的。
mp.setAudioSessionId(audioSessionId);
// 从这里开始 setDataSource。uri 即统一资源标识符。
mp.setDataSource(context, uri);
// 判断 SurfaceHolder 是否为空,这是一个控制器。
if (holder != null) {
// 给 Surface 设置一个控制器,用来操纵 Surface,处理它在 canvas 上作画的效果和动画,控制表面、大小、像素等。
mp.setDisplay(holder);
}
// 开始准备
mp.prepare();
return mp;
} catch (IOException ex) {
Log.d(TAG, "create failed:", ex);
// fall through
} catch (IllegalArgumentException ex) {
Log.d(TAG, "create failed:", ex);
// fall through
} catch (SecurityException ex) {
Log.d(TAG, "create failed:", ex);
// fall through
}

return null;
}

备注

参考资料

Android 音视频开发