简介

MediaPlayer是Android提供媒体文件的组件,播放视频时一般配合SurfaceView使用。

状态机

我们使用MediaPlayer前,先了解一下MediaPlayer的状态机,如图:

android 开源播放视频控件 安卓视频播放器开发_视频播放器

播放流程

我们从状态机图可以看出,使用MediaPlayer播放视频流程如下:

  1. 初始化MediaPlayer,监听各种事件。常见的事件说明:
    OnPreparedListener: MediaPlayer进入准备完成的状态触发,表示媒体可以开始播放了。
    OnSeekCompleteListener: 调用MediaPlayer的seekTo方法后,MediaPlayer会跳转到媒体指定的位置,当跳转完成时触发。需要注意的时,seekTo并不能精确的挑战,它的跳转点必须是媒体资源的关键帧。
    OnCompletionListener: 媒体播放完毕时会触发。但是当OnErrorLister返回false,或者MediaPlayer没有设置OnErrorListener时,这个监听也会被触发。
    OnErrorListener: MediaPlayer出错时会触发,无论是播放过程中出错,还是准备过程中出错,都会触发。
  2. 设置MediaPlayer的播放源,也就是设置视频文件的路径。
  3. 调用prepare()或者prepareAsync()方法,让MediaPlayer去获取解析资源,前一个是同步方法,后一个是异步方法,通常我们用的比较多的是后者:mPlayer.prepareAsync()。
  4. 进入准备完成状态后,调用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));
        }
    };

}

效果图

android 开源播放视频控件 安卓视频播放器开发_状态机_02

如果在代码实现中出现问题,可以参考一下MediaPlayer的状态机,理解各个状态下,哪些方法可以调用,哪些方法不能调用,相信对解决问题会很有帮助。