章节索引 :

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

环境搭建,开发相关
Android 系统背景及结构概述 Android 开发环境搭建 Genymotion 的安装与使用 Android 工程解析及使用 Android 程序签名打包
常用 UI 布局
Android 的 UI 根基 View与View Android 线性布局 LinearLayout Android相对布局RelativeLayout Android 表格布局 TableLayout Android 网格布局 GridLayout Android 帧布局 FrameLayout Android绝对布局AbsoluteLayout
基础控件
Android 文本框 TextView Android 文本输入框 EditText 按钮 Button/ImageButton 选择框 RadioButton/Check 开关控件ToggleButton/Switch Android 图片控件 ImageView Android 进度条 ProgressBar Android 拖动条 SeekBar Android 评分条 RatingBar Android 滚动条 ScrollView 轮播滚动视图 ViewFlipper
Adapter 相关控件
Android 适配器 Adapter Android 列表控件 ListView Android 网格视图 GridView Android 下拉选择框 Spinner 自动补全文本框 AutoCompleteText 折叠列表 ExpandableListView
提示类控件
吐司提示:Toast 的使用方法 状态栏通知:Notification 对话框:AlertDialog 悬浮窗:PopupWindow
菜单类控件
菜单:Menu
其他控件
视频页面:ViewPager 侧滑菜单:DrawerLayout
事件处理机制
基于监听的事件处理机制 Handler 消息传递机制 触摸事件分发处理 AsyncTask:异步任务 Android 手势处理
Android 四大组件
活动:Activity 服务:Service 广播接收器:Broadcast Receiver 内容提供者 - Content Provider
数据存储
文件存储 SharedPreferences 存储 数据库:SQLite 的使用
网络编程
HTTP 使用详解 xml 数据解析 JSON 数据解析 网页视图:WebView Socket 网络接口
绘图与动画
图片资源:Drawable 位图:Bitmap
多媒体开发
媒体播放器:MediaPlayer 相机:Camera 音频录制:MediaRecorder
并发编程
多线程