本文的目的是说明如何用MediaPlayer和SurfaceView来播放视频,并对项目中遇到的问题进行总结,视频编解码以及界面美化不在本文说明范围以内。
本视频播放器具备播放、暂停、停止功能,支持旋转屏幕,在发生屏幕旋转、Activity切换等事件之后,能记住之前的播放状态和播放进度。
先介绍一下在做这个项目的时候遇到的一些问题,供大家借鉴:
1.SeekBar的使用
播放器肯定需要一个能够拖动的进度条,自然会想到用SeekBar。SeekBar是从ProgressBar派生出来的,ProgressBar的方法在SeekBar上同样适用,二者不同之处在于SeekBar多了一个可以拖动的滑块,用来改变进度。
这里主要介绍OnSeekBarChangeListener接口。
1) 在创建Activity的过程中,onProgressChanged方法会先于onSurfaceCreated方法执行,如果是在onSurfaceCreated中对MediaPlayer初始化,那么在执行onProgressChanged方法的时候MediaPlayer还没有被初始化,此时如果在方法中调用MediaPlayer的某些方法(如seekTo、getCurrentPosition等)就会出错。
2) 在视频播放过程中onProgressChanged方法也会不断被调用,此时就会出现问题:视频播放时会改变进度条的进度,而进度条进度的改变也会影响到视频播放的进度。所以在onProgressChanged中需要设定判断条件,只有在拖动滑块的时候才允许通过onProgressChanged方法改变视频播放的进度,这里的判断条件就是canSeek变量。如下面代码所示:
mProgressBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// TODO Auto-generated method stub
canSeek=false;
// 恢复改变SeekBar之前的播放状态
if(isPlayingBefore)
{
mPlayer.start();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// TODO Auto-generated method stub
canSeek=true;
// 记录改变SeekBar之前的播放状态
if(mPlayer.isPlaying())
{
isPlayingBefore=true;
mPlayer.pause();
}
else
{
isPlayingBefore=false;
}
}
// 在创建Activity的过程中,此方法会在onSurfaceCreated之前调用一次
// 此时MediaPlayer还没有被初始化,如果调用MediaPlayer的某些方法(如get方法)会报错
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// TODO Auto-generated method stub
// System.out.println("onProgressChanged");
if(canSeek)
{
mPlayer.seekTo(progress);
System.out.println("progress:"+progress);
}
}
});
2.旋转屏幕事件
情形1:在Manifest文件中Activity标签下配置android:configChanges="orientation|screenSize",这样在旋转屏幕的时候就不会销毁Activity,可以在程序中通过onConfigurationChanged方法监听屏幕旋转事件。
情形2:默认情况,旋转屏幕会销毁当前Activity,然后重新创建Activity。
这里为了在旋转屏幕以后改变视频的尺寸以适应屏幕,选择情形2。在这一情形下,必定会执行onSaveInstanceState方法和onRestoreInstanceState方法,所以可以在这两个方法中保存和恢复数据,这里需要保存的数据主要是播放进度position和播放状态isPlayingBefore。
3.Activity切换事件
Activity切换不一定会触发onSaveInstanceState方法,所以需要在onPause方法中保存上述两个数据。结合2,3两点,将保存数据的代码写成如下形式:
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
System.out.println("onPause");
position=mPlayer.getCurrentPosition();
// System.out.println("onPause: "+position);
isPlayingBefore=mPlayer.isPlaying();
if(mPlayer.isPlaying())
{
mPlayer.stop();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
System.out.println("onSaveInstanceState");
// System.out.println("onSaveInstanceState: "+position);
outState.putInt("position", position);
outState.putBoolean("isPlayingBefore", isPlayingBefore);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onRestoreInstanceState(savedInstanceState);
System.out.println("onRestoreInstanceState");
position=savedInstanceState.getInt("position");
// System.out.println("onRestoreInstanceState: "+position);
isPlayingBefore=savedInstanceState.getBoolean("isPlayingBefore");
}
4.资源释放和子线程终止
1)当Activity销毁以后需要将MediaPlayer的资源释放,以免发生意想不到的错误。
2)旋转屏幕会先销毁Activity然后重新创建,如果再Activity销毁之前没有将Timer的所有调度任务取消,那么在新的Activity创建之后,子线程仍然在执行,此时如果MediaPlayer还没有初始化,将会发生错误。所以这里采用两个手段保证子线程被终止:
第一,在onDestroy方法中取消Timer,但是取消Timer不会影响到正在执行的任务;
第二,在handleMessage方法中添加canUpdate标志,当Timer取消以后立即将canUpdate设为false,这样Timer取消以后,正在执行的子线程也无法更新进度,如下代码所示:
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
canUpdate=false;
// 释放资源
if(mPlayer!=null)
{
mPlayer.release();
}
// 取消Timer的所有子线程任务
if(mTimer!=null)
{
mTimer.cancel();
}
super.onDestroy();
System.out.println("onDestroy");
}
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
// 子线程用于更新进度条
case UPDATE:
if(canUpdate)
{
mProgressBar.setProgress(mPlayer.getCurrentPosition());
}
break;
default:
break;
}
}
最后附上项目源码,有比较详细的注释:
源码下载