Android音视频学习(一):MediaPlayer

448 阅读6分钟

0. 前言

视频播放功能由播放器来提供,而播放器的实现在音视频开发中可以算得上是一个综合性的任务,包含解码、渲染、音视频同步等多个子模块工作。音视频开发工作者最熟悉的当然是ffmpeg来实现播放器,但如果需要在Android系统上使用ffmpeg,我们不可避免地需要引入较复杂的集成任务,比如交叉编译动态库、编写JNI等,对于一些非定制需要的场景其实也没有必要。对于一些常规的播放功能,Android系统自带的MediaPlayer就可以满足我们的需要。

1. MediaPlayer介绍

MediaPlayer是Android多媒体框架中的播放器组件,使用它,开发者可以轻松地实现本地/网络媒体文件的播放、暂停、停止、跳转等操作。MediaPlayer支持多种常用的媒体文件,如mp3、mp4等,且能够很好地配合SurfaceView实现视频渲染,因此应用广泛。

2. 主要的使用流程。

graph TD
创建MediaPlayer 
--> setDataSource设置输入源
--> 设置状态回调
--> prepare
--> start
--> 退出时stop

3. MediaPlayer的状态

MediaPlayer主要是使用状态机进行维护,其状态转移图如下。图中单箭头为同步调用,双箭头的为异步调用。 image.png

3.1 Idle状态、End状态

当使用new来创建MediaPlayer,或者是对已经运行的MediaPlayer调用reset()方法之后,播放器就处于Idle状态。在调用rlease()方法后,播放器就会进入End状态,在这两种状态之间的其他状态,就是MediaPlayer的生命周期。
Idle状态下 MediaPlayer 并未与具体的音视频文件或流关联,针对具体文件和流的操作都不可用。

3.2 Error状态

在MediaPlayer创建后,调用其他控制方法时,可能会出现错误,届时会进入Error状态,如果有设置过OnErrorListener.onError(),则会触发对应的回调。
如果需要从Error状态恢复到Idle状态,可以调用reset()方法。

3.3 Initialized状态

当调用setDataSource()系列方法后,播放器与具体的文件或流相关联,此时进入Initialized状态。需要注意的是,setDataSource()系列方法只能在Idle状态下调用,否则会抛出异常。

3.4 prepared状态

当MediaPlayer初始化完成,已经进入Initialized状态后,需要进入Prepared状态,才能开始播放。MediaPlayer提供了同步和异步两种方式来进行播放准备。

  1. 调用prepare()同步方法,状态从Initialized -> Prepared,主要使用文件
  2. 调用prepareAsync()异步方法,状态从Initialized -> Preparing -> Prepared,主要使用网络数据,需要缓冲数据
    如果设置过OnPreparedListener.onPrepared(),进入Prepared时会触发对应的回调。
    需要注意的是,使用MediaPlayer.create()系列方法创建的MediaPlayer实例,其创建成功时即处于Prepared状态,而不是Idle状态。

3.5 Started状态

MediaPlayer 准备完成,状态变为 Prepared 状态后,就可以开始播放了。要开始播放,必须调用MediaPlayer.start()方法。start()方法调用成功后,MediaPlayer对象会转换成Started状态。处于Started状态时,MediaPlayer的内部播放器引擎会调用用户提供的 OnBufferingUpdateListener.onBufferingUpdate()回调方法,此回调允许 app在音视频流中跟踪缓冲状态。 注意如果 MediaPlayer 对象已经处于 Started 状态,此时再调用 start() 方法不会有任何效果。我们可以调用 MediaPlayer.isPlaying() 方法检测 MediaPlayer 对象是否处于 Started 状态。

3.6 Paused状态

音视频内容开始播放,已处于Started状态后,可以暂停播放。调用 MediaPlayer.pause()方法可以暂停播放。当pause()方法调用成功后,MediaPlayer对象会进入Paused 状态。如果 MediaPlayer对象已经处于 Paused 状态,此时再调用pause()方法不会有任何效果。
Paused 状态状态下可以调用 start() 方法继续播放,继续播放开始的位置与暂停位置相同。
从Started 状态到 Paused 状态的转换(反之亦然)在播放器引擎中是异步发生的,因此调用 isPlaying() 方法获取到的状态可能有延时,需要过一段时间才能更新。对于流式内容,这个时间可能长达几秒。

3.7 Stopped状态

当调用stop()函数时,无论MediaPlayer处于Started、Paused、Prepared或PlaybackCompleted中的何种状态,都将进入Stopped状态,此时播放停止。
一旦处于Stopped状态,MediaPlayer不能再调用start()方法恢复播放,如果需要重新开始播放,则需要调用prepare()系列方法,重新进入Prepared状态。

3.8 PlaybackCompleted状态

即播放完成状态。如果MediaPlayer播放到数据流末尾时,一次播放过程完成,如果播放器没有设置过循环播放,则会进入PlaybackCompleted状态。如果有设置OnCompletionListener.onCompletion()回调,则会触发对应的回调。
循环播放可以通过setLooping()方法调用。

3.9 Seek操作

Seek操作严格来算不是状态转移,只是跳转进度控制,只是在这里一起说明。如果有设置过OnSeekComplete.onSeekComplete()回调,则会在Seek后触发对应的回调方法。

4 示例

下面通过一个例子来说明MediaPlayer的使用流程,其中也加入了进度条同步的功能。

4.1 代码

播放功能代码:

package com.zzakafool.mediaplayertest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.media.MediaPlayer;
import android.media.PlaybackParams;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.widget.SeekBar;
import android.widget.Toast;

public class MediaPlayerActivity extends AppCompatActivity implements SurfaceHolder.Callback {

    // SurfaceView用于渲染
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;

    // 播放器组件
    private MediaPlayer mMediaPlayer;

    // 进度条组件
    private SeekBar mSeekBar;
    // 用于同步进度
    private Handler handler;
    private Runnable syncSeekBarRunnable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mediaplayer);

        initView();
    }

    protected void initView() {
        mSurfaceView = findViewById(R.id.surfaceView);
        mSurfaceHolder = mSurfaceView.getHolder();
        mSurfaceHolder.addCallback(this);

        mSeekBar = findViewById(R.id.seekBar);

        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                // 如果时用户拖动进度条,则调用播放器seek到指定的时间
                if(mMediaPlayer != null && fromUser) {
                    mMediaPlayer.seekTo(progress);  // 会根据seekbar设置的max值变动
                }
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });

        // 设置定时器,每100ms同步一次seekbar和播放器进度
        handler = new Handler();
        syncSeekBarRunnable = new Runnable() {
            @Override
            public void run() {
                if(mMediaPlayer != null && mMediaPlayer.isPlaying()) {
                    int currentPosition = mMediaPlayer.getCurrentPosition();
                    mSeekBar.setProgress(currentPosition);
                    handler.postDelayed(this, 100); // 每100ms同步一次
                }
            }
        };
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        mMediaPlayer = MediaPlayer.create(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.big_buck_bunny), mSurfaceView.getHolder());
        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mediaPlayer) {
                // prepared后可以获取到文件相关的属性,这里设置进度条的最大值为duration(ms)
                mSeekBar.setMax(mMediaPlayer.getDuration());
                // 打开同步的定时起
                handler.post(syncSeekBarRunnable);
            }
        });

        mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mediaPlayer) {
                // 手动的循环播放,seek到0,再调start()
                mediaPlayer.seekTo(0);
                mediaPlayer.start();
            }
        });

        mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MediaPlayerActivity.this, "MediaPlayer error", Toast.LENGTH_SHORT).show();
                    }
                });
                return false;
            }
        });

        mMediaPlayer.start();
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int i, int i1, int i2) {

    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {

    }

    @Override
    protected void onDestroy() {
        mMediaPlayer.stop();
        super.onDestroy();
    }
}

对应的layout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    tools:context=".MediaPlayerActivity">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="400dp"
        android:layout_marginTop="10dp" />

    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>

4.2 效果展示

image.png