跳到主要内容

媒体播放器: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

这个图非常经典,建议大家收藏此文章,今后使用 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 适配了多种场景,也为不同的场景提供了不同的使用方式,但大体上有几个步骤:

  1. 创建播放器

创建通常有两种方法:

MediaPlayer mediaPlayer = MediaPlayer.create(this, R.raw.song);
MediaPlayer mediaPlayer = new MediaPlayer();

第一种方式直接在创建的时候传入媒体流地址,而第二种仅仅是创建一个“Idle”态的闲置播放器

  1. 设置媒体源

Android 提供下面的方法设置媒体数据源

mediaPlayer.setDataSource(www.mc.com/mc.mp3);

如果在创建 MediaPlayer 的时候就设置了媒体文件,那么可以跳过这一步

  1. 开始播放

在设置好媒体源地址之后,就可以开始播放了:

mediaPlayer.start();

  1. 播放控制

在播放过程中可以调用一些控制 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>

当然也可以按照你自己的设计去摆放播放器的布局样式,编译之后如下:

demo

5. 小结

本节学习了 Android 内置的最常用的播放器,最关键的是要熟悉它的状态机时序图,因为可能在实际开发中你会使用更强大的第三方播放器,但是基本时序仍然是参照 Android 官方设计的,在了解时序之后就可以按照本节的步骤使用各种 API 来播放音视频了。在掌握了本节内容之后,如果感兴趣也可以研究研究市面上常用的开源播放器,可以让你对播放器有更深的理解。