简介
MediaPlayer是Android提供媒体文件的组件,播放视频时一般配合SurfaceView使用。
状态机
我们使用MediaPlayer前,先了解一下MediaPlayer的状态机,如图:
播放流程
我们从状态机图可以看出,使用MediaPlayer播放视频流程如下:
- 初始化MediaPlayer,监听各种事件。常见的事件说明:
OnPreparedListener: MediaPlayer进入准备完成的状态触发,表示媒体可以开始播放了。
OnSeekCompleteListener: 调用MediaPlayer的seekTo方法后,MediaPlayer会跳转到媒体指定的位置,当跳转完成时触发。需要注意的时,seekTo并不能精确的挑战,它的跳转点必须是媒体资源的关键帧。
OnCompletionListener: 媒体播放完毕时会触发。但是当OnErrorLister返回false,或者MediaPlayer没有设置OnErrorListener时,这个监听也会被触发。
OnErrorListener: MediaPlayer出错时会触发,无论是播放过程中出错,还是准备过程中出错,都会触发。 - 设置MediaPlayer的播放源,也就是设置视频文件的路径。
- 调用prepare()或者prepareAsync()方法,让MediaPlayer去获取解析资源,前一个是同步方法,后一个是异步方法,通常我们用的比较多的是后者:mPlayer.prepareAsync()。
- 进入准备完成状态后,调用start()方法开始播放,如果是调用prepare()方法准备,在prepare()方法后,可以直接开始播放;如果是调用prepareAsync()方法准备,需要在OnPreparedListener()监听中开始播放。
代码实现
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:orientation="horizontal">
<TextView
android:id="@+id/current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#666"
/>
<TextView
android:id="@+id/max"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right"
android:textSize="16sp"
android:textColor="#666"
/>
</LinearLayout>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"
android:text="开始播放"/>
</LinearLayout>
Activity:
public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnSeekCompleteListener {
private SurfaceView surfaceView;
private SeekBar seekBar;
private TextView currentView, maxView;
private Button button;
private MediaPlayer player;
private Timer timer;
private TimerTask timerTask;
private int currentPosition;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
surfaceView.getHolder().addCallback(this);
seekBar = (SeekBar) findViewById(R.id.seek_bar);
seekBar.setOnSeekBarChangeListener(this);
currentView = (TextView) findViewById(R.id.current);
maxView = (TextView) findViewById(R.id.max);
button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
// MediaPlayer没准备好,不能点击播放
button.setEnabled(false);
}
/**
* Surface创建
* 在Surface销毁时,要销毁MediaPlayer,否则会出现问题
* 所以这里要重新初始化MediaPlayer,并重新与SurfaceView关联
* @param holder
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 初始化播放器
initPlayer();
// 让MediaPlayer关联SurfaceView
// 注意: 一定要在要在Surface创建成功才能关联
player.setDisplay(holder);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
/**
* Surface销毁
* Activity的onStop方法调用时,surfaceDestroyed也会调用
* 我们要在这一步销毁MediaPlayer,并记录上次播放的位置,否则会出现问题
* 本人测试的时候,Surface销毁时没有销毁MediaPlayer,
* 发现MediaPlayer调用seekTo方法无效,也不能调用pause方法停止播放
* 也就说MediaPlayer不受控制了
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 记录上次播放位置
currentPosition = player.getCurrentPosition();
// 这里可以记录播放状态 等到MediaPlayer再次创建并准备就绪时,可以调用这个状态 选择是否播放
// 暂停播放
pause();
// 销毁MediaPlayer
destroyPlayer();
}
@Override
public void onClick(View v) {
if (player.isPlaying()) {
// 暂停播放
pause();
} else {
// 开始播放
start();
}
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
/**
* 手指滑动进度条时调用
* @param seekBar
*/
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// 让播放器播放到进度条移动的位置
player.seekTo(seekBar.getProgress());
}
/**
* 当MediaPlayer准备就绪 解析资源完成
* @param player
*/
@Override
public void onPrepared(MediaPlayer player) {
// 进度条设置最大值 也就是player的时长 单位毫秒
seekBar.setMax(player.getDuration());
// 视频时长
maxView.setText(parseTime(player.getDuration()));
// 当前时长
currentView.setText("00:00");
// 设置可以点击状态
button.setEnabled(true);
// 设置MediaPlayer播放到上次的位置
// 注意: seekTo不一定准确定位到currentPosition位置 跳转点必须是视频文件的关键帧
// 所以我们要OnSeekCompleteListener中更新UI 这才是正确的
player.seekTo(currentPosition);
// 这里根据Surface销毁时记录的状态,选择是否播放,可以自己实现
}
/**
* MediaPlayer调用seekTo方法后调用
* seekTo不一定定位到准确的位置 跳转点必须是视频文件的关键帧
* @param player
*/
@Override
public void onSeekComplete(MediaPlayer player) {
// player.getCurrentPosition()才是真正移动的进度
currentPosition = player.getCurrentPosition();
// 更新进度UI
handler.sendEmptyMessage(0);
}
/**
* 播放完成时调用
* 出错时,没有设置setOnErrorListener或onError返回false时也会调用
* @param player
*/
@Override
public void onCompletion(MediaPlayer player) {
// 停止定时器
stopTimer();
// 设置重新播放
button.setText("重新播放");
}
/**
* 播放出错时调用
* @param player
* @param what
* @param extra
* @return
*/
@Override
public boolean onError(MediaPlayer player, int what, int extra) {
// 提示
Toast.makeText(this, "播放错误:" + what, Toast.LENGTH_SHORT).show();
// 停止定时器
stopTimer();
// 设置重新播放
button.setText("重新播放");
// 不返回true会调用onCompletion方法
return true;
}
/**
* 初始化播放器
*/
private void initPlayer() {
try {
// 初始化MediaPlayer
player = new MediaPlayer();
// 设置准备监听
player.setOnPreparedListener(this);
// 设置进度完成监听
player.setOnSeekCompleteListener(this);
// 设置播放完成监听
player.setOnCompletionListener(this);
// 设置播放错误监听
player.setOnErrorListener(this);
// 设置播放源
AssetManager manager = getAssets();
AssetFileDescriptor descriptor = manager.openFd("video.mp4");
player.setDataSource(descriptor.getFileDescriptor(), descriptor.getStartOffset(), descriptor.getLength());
// 异步准备
player.prepareAsync();
} catch (IOException exception) {
exception.printStackTrace();
}
}
/**
* 销毁播放器
*/
private void destroyPlayer() {
// 暂停播放
if (player.isPlaying()) {
player.stop();
}
// 释放资源
player.release();
player = null;
// 设置按钮不能点击状态 毕竟MediaPlayer已经销毁了
button.setEnabled(false);
}
/**
* 毫秒 -> 时间 HH:mm:ss
* @param position
* @return
*/
private String parseTime(int position) {
int hour = position / 1000 / 3600;
int minute = position / 1000 % 3600 / 60;
int second = position / 1000 % 3600 % 60;
return (hour < 10 ? "0" + hour : hour) + ":" + (minute < 10 ? "0" + minute : minute) + ":" + (second < 10 ? "0" + second : second);
}
/**
* 开始播放
*/
private void start() {
player.start();
startTimer();
button.setText("暂停播放");
}
/**
* 暂停播放
*/
private void pause() {
if (player.isPlaying()) {
player.pause();
}
stopTimer();
button.setText("开始播放");
}
/**
* 开始定时器
*/
private void startTimer() {
stopTimer();
timer = new Timer();
timerTask = new TimerTask() {
@Override
public void run() {
// 更新进度UI
currentPosition = player.getCurrentPosition();
handler.sendEmptyMessage(0);
}
};
timer.schedule(timerTask, 0, 500);
}
/**
* 停止定时器
*/
private void stopTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
if (timerTask != null) {
timerTask.cancel();
timerTask = null;
}
}
/**
* 更新进度UI
*/
private Handler handler = new Handler() {
@Override
public void handleMessage(Message message) {
seekBar.setProgress(currentPosition);
currentView.setText(parseTime(currentPosition));
}
};
}
效果图
如果在代码实现中出现问题,可以参考一下MediaPlayer的状态机,理解各个状态下,哪些方法可以调用,哪些方法不能调用,相信对解决问题会很有帮助。