全部开发者教程

Android 入门教程

菜单类控件
菜单:Menu
并发编程
多线程

媒体播放器: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 进行播放状态的控制

  2. 结束播放
    调用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 来播放音视频了。在掌握了本节内容之后,如果感兴趣也可以研究研究市面上常用的开源播放器,可以让你对播放器有更深的理解。