媒体播放器:MediaPlayer
Android 系统提供了几种播放音频和视频的方式,其中最常用的就是 MediaPlayer,和其他功能组件一样都有很多第三方框架提供更加丰富完备的功能,但是基本用法和时序基本都是参照 MediaPlayer 来设计的,本节就来看看 MediaPlayer 的使用方法。
1. MediaPlayer 的状态
MediaPlayer 有一套完善的状态机,通常出现一些奇怪的报错或者 Crash 大概率就是状态流转出了问题,而市面上大多数的播放器也会遵循 Android 官方设计的这套状态机来实现。首先看看所有的状态:
- Idle:
空闲态,刚创建或者调用了reset()
之后的状态,此时不能进行播放
- Initailized:
初始化态,仅仅设置了媒体源,但还未进行任何网络资源的拉取或者媒体流的解析,此时仍然不能播放
- Preparing:
准备中,触发了媒体流的下载以及媒体流的解析,但均未完成,处于准备中,尚不能进行播放
- Prepared:
准备好,已经将媒体资源拉取并解析完成,随时可以开始播放
- Started:
播放态,在媒体资源准备好之后,调用了start()
触发了媒体的播放,则进入视频 / 音频播放
- Paused:
暂停态,这个很好理解,视频 / 音频播放暂停,此时可以随时调用start()
继续播放回到Started
状态
- PlaybackCompleted:
播放结束态,视频 / 音频播放到结尾,自然结束
- Stoped:
停止态,在播放或者暂停过程中主动调用stop()
停止播放,注意它和暂停态不同,“Stoped”态不能直接回到播放态;它和
播放结束态也不同,“Stoped”一定是由开发者主动触发的
- End:
释放态,播放器调用release()
触发播放器资源的释放,此时播放器资源被回收将不能使用
- Error:
错误态,如果由于某种原因 MediaPlayer 出现了错误,会触发 OnErrorListener.onError()
事件,此时 MediaPlayer 即进入 Error 状态,及时捕捉并妥善处理这些错误是很重要的,可以帮助我们及时释放相关的软硬件资源,也可以改善用户体验。通过setOnErrorListener
可以设置该监听器。如果MediaPlayer进入了Error状态,可以通过调用reset()来恢复,使得MediaPlayer重新返回到 Idle 状态。
下面可以对照着状态看看官方给的状态机流转图:
这个图非常经典,建议大家收藏此文章,今后使用 MediaPlayer 过程中出现任何问题都可以看看状态机是否出现异常。
2. MediaPlayer 常用 API
使用 MediaPlayer 的 API 之前一定要先熟悉熟悉再熟悉上一小节的状态机时序图,否则盲目使用 API 会出现很多状态错误的异常发生。
setDataSource(FileDescriptor fd):
设置音频 / 视频资源地址
- isPlaying():
判断当前视频 / 音频是否正在播放
- seekTo(position):
直接跳转到视频 / 音频的某个时间点
- getCurrentPosition():
获取当前的播放进度
- getDuration():
获取媒体文件的总时长
- reset():
重置 MediaPlayer,此后会进入 Idle 态
- release():
释放播放器,在不使用的时候调用,节省系统资源
- setVolume(float leftVolume, float rightVolume):
设置媒体音量
- selectTrack(int index):
设置媒体轨道
- getTrackInfo():
返回一个数组,包含所有的轨道信息
3. MediaPlayer 使用步骤
Android 系统为 MediaPlayer 适配了多种场景,也为不同的场景提供了不同的使用方式,但大体上有几个步骤:
- 创建播放器
创建通常有两种方法:
MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.song);
MediaPlayer mediaPlayer = new MediaPlayer();
第一种方式直接在创建的时候传入媒体流地址,而第二种仅仅是创建一个“Idle”态的闲置播放器
- 设置媒体源
Android 提供下面的方法设置媒体数据源
mediaPlayer.setDataSource(www.mc.com/mc.mp3);
如果在创建 MediaPlayer 的时候就设置了媒体文件,那么可以跳过这一步
- 开始播放
在设置好媒体源地址之后,就可以开始播放了:
mediaPlayer.start();
- 播放控制
在播放过程中可以调用一些控制 API 进行播放状态的控制 5. 结束播放
调用stop()
可以结束播放,并且记得在不用的时候还要调用release()
4. 播放器使用示例
本节来用 MediaPlayer 实现一个简单的播放器,并通过几个 API 来实现基本的播放控制。
4.1 MediaPlayer 的使用
首先看看 MainActivity:
package com.emercy.myapplication;
import android.app.Activity;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import java.util.concurrent.TimeUnit;
public class MainActivity extends Activity {
private Button b1, b2, b3, b4;
private MediaPlayer mediaPlayer;
private double startTime = 0;
private double finalTime = 0;
private Handler myHandler = new Handler();
private int forwardTime = 5000;
private int backwardTime = 5000;
private SeekBar seekbar;
private TextView tx1, tx2, tx3;
public static int oneTimeOnly = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
b1 = (Button) findViewById(R.id.button);
b2 = (Button) findViewById(R.id.button2);
b3 = (Button) findViewById(R.id.button3);
b4 = (Button) findViewById(R.id.button4);
tx1 = (TextView) findViewById(R.id.textView2);
tx2 = (TextView) findViewById(R.id.textView3);
tx3 = (TextView) findViewById(R.id.textView4);
mediaPlayer = MediaPlayer.create(this, R.raw.video);
seekbar = (SeekBar) findViewById(R.id.seekBar);
seekbar.setClickable(false);
b2.setEnabled(false);
b1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int temp = (int) startTime;
if ((temp + forwardTime) <= finalTime) {
startTime = startTime + forwardTime;
mediaPlayer.seekTo((int) startTime);
Toast.makeText(getApplicationContext(), "前进5秒", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "前方还剩不到5秒", Toast.LENGTH_SHORT).show();
}
}
});
b2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "Pausing sound", Toast.LENGTH_SHORT).show();
mediaPlayer.pause();
b2.setEnabled(false);
b3.setEnabled(true);
}
});
b3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "音频播放", Toast.LENGTH_SHORT).show();
mediaPlayer.start();
finalTime = mediaPlayer.getDuration();
startTime = mediaPlayer.getCurrentPosition();
if (oneTimeOnly == 0) {
seekbar.setMax((int) finalTime);
oneTimeOnly = 1;
}
tx2.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) finalTime),
TimeUnit.MILLISECONDS.toSeconds((long) finalTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long)
finalTime)))
);
tx1.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) startTime),
TimeUnit.MILLISECONDS.toSeconds((long) startTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes((long)
startTime)))
);
seekbar.setProgress((int) startTime);
myHandler.postDelayed(UpdateSongTime, 100);
b2.setEnabled(true);
b3.setEnabled(false);
}
});
b4.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int temp = (int) startTime;
if ((temp - backwardTime) > 0) {
startTime = startTime - backwardTime;
mediaPlayer.seekTo((int) startTime);
Toast.makeText(getApplicationContext(), "后退5秒", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getApplicationContext(), "后方还剩不到5秒", Toast.LENGTH_SHORT).show();
}
}
});
}
private Runnable UpdateSongTime = new Runnable() {
public void run() {
startTime = mediaPlayer.getCurrentPosition();
tx1.setText(String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes((long) startTime),
TimeUnit.MILLISECONDS.toSeconds((long) startTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.
toMinutes((long) startTime)))
);
seekbar.setProgress((int) startTime);
myHandler.postDelayed(this, 100);
}
};
}
通过MediaPlayer.create(this, R.raw.video)
创建一个 MediaPlayer 对象并初始化媒体流地址,然后分别设置几个控制 Button 的监听事件,实现播放器的前进、后退、播放、暂停操作,当中还有一个UpdateSongTime
的 Runnable 变量,用来每隔 100 毫秒更新一次播放进度,实现播放进度的同步刷新。
4.2 布局文件
布局文件就按照片一般播放器的摆放方式就可以,上面通常是页面的主题和描述,下方就是进度条、歌曲名称时长等等。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout\_width="match\_parent"
android:layout\_height="match\_parent"
android:padding="30dp"
tools:context=".MainActivity">
<TextView
android:id="@+id/textview"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignParentTop="true"
android:layout\_centerHorizontal="true"
android:text="音乐播放器"
android:textSize="35dp" />
<TextView
android:id="@+id/textView"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_below="@+id/textview"
android:layout\_centerHorizontal="true"
android:text="慕课Android教程"
android:textColor="#ff7aff24"
android:textSize="35dp" />
<Button
android:id="@+id/button"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignParentStart="true"
android:layout\_alignParentBottom="true"
android:text="前进" />
<Button
android:id="@+id/button2"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignParentBottom="true"
android:layout\_toRightOf="@id/button"
android:text="暂停" />
<Button
android:id="@+id/button3"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignTop="@+id/button2"
android:layout\_toEndOf="@+id/button2"
android:text="播放" />
<Button
android:id="@+id/button4"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignTop="@+id/button3"
android:layout\_toEndOf="@+id/button3"
android:layout\_toRightOf="@+id/button3"
android:text="后退" />
<SeekBar
android:id="@+id/seekBar"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_above="@+id/button"
android:layout\_alignStart="@+id/textview"
android:layout\_alignEnd="@+id/textview" />
<TextView
android:id="@+id/textView2"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_above="@+id/seekBar"
android:text="超哥音乐选集"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/textView3"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_above="@+id/seekBar"
android:layout\_alignEnd="@+id/button4"
android:text="02:23"
android:textAppearance="?android:attr/textAppearanceSmall" />
<TextView
android:id="@+id/textView4"
android:layout\_width="wrap\_content"
android:layout\_height="wrap\_content"
android:layout\_alignBaseline="@+id/textView2"
android:layout\_alignBottom="@+id/textView2"
android:layout\_centerHorizontal="true"
android:text="超哥吉他"
android:textAppearance="?android:attr/textAppearanceMedium" />
</RelativeLayout>
当然也可以按照你自己的设计去摆放播放器的布局样式,编译之后如下:
5. 小结
本节学习了 Android 内置的最常用的播放器,最关键的是要熟悉它的状态机时序图,因为可能在实际开发中你会使用更强大的第三方播放器,但是基本时序仍然是参照 Android 官方设计的,在了解时序之后就可以按照本节的步骤使用各种 API 来播放音视频了。在掌握了本节内容之后,如果感兴趣也可以研究研究市面上常用的开源播放器,可以让你对播放器有更深的理解。