Android视频播放器开发—— 探究MediaPlayer

来源:互联网 发布:湖北文理学院网络 编辑:程序博客网 时间:2024/06/05 04:01

概述

之前在公司都是做金融和博彩相关的app,很少接触到视频播放类的应用开发,趁最近比较闲,想逐步学习相关的知识,给自己增加点视频类app开发的经验,也希望读者能够从我个人的学习当中得到一些启发。

一、学习MediaPlayer的API

对于我来讲,学习新东西第一步就是看相关的api,于是我找到了Android中最基本的MediaPlayer的开发文档进行简单的学习。

这里写图片描述
从图我们可以看出,MediaPlayer是个单独的子类,继承Object,说明他就是最基本的实现类,学习起来就相对容易。

紧接着,api中列出了MediaPlayer的运作周期的状态图,如下图所示:

这里写图片描述

其中,蓝色椭圆内是代表MediaPlayer的某个周期状态,单箭头表示同步,双箭头表示异步,弧形表示程序执行期间的调用,或者说在某个类执行期间调用某些方法。

从上图中我们能看到MediaPlayer类中有以下一些状态:

  • Idle

    • 当一个MediaPlayer类通过new实例化或者执行了reset()方法之后,就会进入到Idle状态。

    • API中提到,如果在Idle状态下调用诸如 getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() or prepareAsync()等等获取媒体文件属性等方法时,会报错,并回调OnErrorListener.OnError()方法。

  • End

    • 当状态在Idle时,若调用release()方法后,状态将变为End结束。

    • 该状态会释放一切播放器所持有的资源,结束所有回调和引用的实例,进入此状态后,将不能回到其他的状态中去。

  • Error

    • 如果在MediaPlayer刚被构建出来的时候,就调用获取媒体文件属性等方法时,播放器内部将不会执行用户所构造出来的类似OnErrorListener.onError()这类回调,并且状态也不会变为Error。但是如果用户是在reset()方法执行之后再调用那些方法时,就会回调监听方法,并且状态变为Error。

    • 一旦播放器在播放过程中发生错误,即使开发者没有注册相关错误回调方法,播放器也会变成Error状态。
      若在Error中调用reset()方法,将会回到Idle状态。

    • API也建议我们注册一个错误监听器能更有效的监听播放器在运行过程中的错误状态原因,并随时修正我们的代码。

  • Initialized

    • 当调用setDateSource()方法后,播放器当前状态将从Idle装变为Initialized

    • 如果setDateSource()在其他状态下被调用,将会报错

  • Prepared

  • Preparing

    • 如果我们要播放某个媒体文件,在播放器进入Started状态之前,需要在Initialized状态调用prepare()方法让播放器进入Prepared(准备)状态。

    • 有两种方式(同步/异步)可以到达Prepared状态。一种是直接执行prepare()方法,当方法返回后,就将进入Prepared状态,也称之为同步的方法。如果是异步的方法,可以调用prepareAsync()方法让播放引擎进入准备中的状态。当准备完成或prepare()方法返回后,播放引擎就会回调开发者所注册的回调接口setOnPreparedListener()。

    • 需要注意的是,Preparing的状态是一个过渡状态,改状态的时候,一些回调方法将会出现不会执行的情况

    • 在Prepared状态时,可以设置一些播放器配置方法。

  • Started

    • 在执行start()方法后,播放器进入started状态,该状态时,播放器将会开始播放,isPlaying()方法将会调用,也会告诉你是否已经在Started状态中。

    • 如果开发者注册了setOnBufferingUpdateListener(OnBufferingUpdateListener)监听,那么在这个状态时成功后,会回调该监听。该接口可以监听媒体流的状态。

  • Stopped

  • Paused

    • 当调用stop()方法或者pause()方法后,相应的,播放器将会进入Stopped和Paused状态。注意的是,这个转变状态的过程是异步过程,所以在改变状态的时候不是立即转变,而是需要一定时间的延时。

    • 如果播放引擎处于Paused状态,这时调用start()方法,播放引擎将会重新回到started状态,并开始从暂停出开始播放。

    • 如果调用stop()方法,那么如果播放器处于Started, Paused, Prepared or PlaybackCompleted 状态时,都将转化成Stopped状态,并且播放器将不能继续播放,除非让播放器重新回到Prepared状态后,才能开启播放。

  • PlaybackCompleted

    • 重放可以通过seekTo(int)方法进行设置,当然该方法其实也属于异步方法,在调用完成后,播放引擎会回调OnSeekComplete.onSeekComplete()接口方法来告知开发者重放设置完成。getCurrentPosition()可以获取重放的真实位置。图中Prepared, Paused,started和PlaybackCompleted 这些状态都可以调用seekTo()方法进行重放。

    • 当媒体流文件到最后时,播放将会完成并结束,如果回放模式被设置setLooping(boolean),那么播放器将保持started状态。如果没有设置回放,则将调用OnCompletion.onCompletion()方法,也会调用开发者注册的接口setOnCompletionListener(OnCompletionListener),并进入PlaybackCompleted状态。

    • 在该状态下,如果调用start()方法,则会重置播放器资源到初始状态,并回到started状态。

二、常用API方法

简单的介绍完API给我们展示的状态模型之后,我给大家列出部分常用的方法,其他的方法请大家自行查阅开发者文档。

  • getCurrentPosition( ):得到当前的播放位置
  • getDuration() :得到文件的时间
  • getVideoHeight() :得到视频高度
  • getVideoWidth() :得到视频宽度
  • isLooping():是否循环播放
  • isPlaying():是否正在播放
  • pause():暂停
  • prepare():准备(同步)
  • prepareAsync():准备(异步)
  • release():释放MediaPlayer对象
  • reset():重置MediaPlayer对象
  • seekTo(int msec):指定播放的位置(以毫秒为单位的时间)
  • setAudioStreamType(int streamtype):指定流媒体的类型
  • setDisplay(SurfaceHolder sh):设置用SurfaceHolder来显示多媒体
  • setLooping(boolean looping):设置是否循环播放
  • setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener): 网络流媒体的缓冲监听
  • setOnCompletionListener(MediaPlayer.OnCompletionListener listener): 网络流媒体播放结束监听
  • setOnErrorListener(MediaPlayer.OnErrorListener listener): 设置错误信息监听
  • setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener): 视频尺寸监听
  • setScreenOnWhilePlaying(boolean screenOn):设置是否使用SurfaceHolder显示
  • setVolume(float leftVolume, float rightVolume):设置音量
  • start():开始播放
  • stop():停止播放

以上是部分比较常用的方法及监听接口,接下来我将写一个小项目来运用这些方法和接口。

三、制作简单的视频播放器

或许你的公司会开始做一个视频项目,那么现在有个需求是这样的:封装一个简单的视频播放器,当然这个播放器的样式是可以自定义的,基本功能满足市面上其他视频类APP的播放功能。好,那么现在我们开始调研视频类APP的产品,并设计相应的功能。

1)设计思路

以爱奇艺视频播放应用为例,我模拟了一个点进某个视频,进入播放详情页的情景。
这里写图片描述

从上图我们可以看到,此播放器有4个功能,分别为播放、暂停、调节播放进度、全屏,那么我先实现最简单的播放暂停和进度选取的功能。

通过对图表的分析,我们知道,播放器的每一个状态在变换之前,都只能根据图上箭头所表明的状态进行变更,例如:started状态之后,你可以变成paused状态或者stopped状态,但是,如果程序已经为stopped状态,这时候你调用pause()方法,那么程序就会报错,甚至崩溃。所以在封装的时候,需要弄清楚播放器当前是处于什么样的状态。

我们知道surfaceview的绘制是在子线程中绘制完成的,所以诸如视频 、游戏这类开发都会用到surfaceview。所以在开发的过程中,我们可以把视频有关的功能一起和surfaceview进行统一封装,留出一些方法进行调用,以免引起阻塞主线程的操作(播放进度条的视图刷新),造成卡顿。网上有一些框架就是这么做的。

这里就针对上面几个功能贴上代码。代码里没有将功能和surfaceview一起封装,仅仅作为参考。

最终效果图如图所示:

这里写图片描述

这里除了上述3个功能外,加入了”停止播放”功能和“装碟”功能,意思就是需要先装载媒体文件,再进行播放,模拟了网络异步加载的情景。

首先我写了几个接口,用于回调改变view的状态。

public interface MediaControl {/** * 开始播放*/void startPlayer();/** * 停止播放 */void stopPlayer();/** * 暂停播放 */void pausePlayer();/** * 播放总长度 * @param value 时间显示 例如 03:59 * @param val   当前时间毫秒数 */void totalLengthSecond(String value,int val);

}

这里发下我自定义surfaceview的写法吧

package com.example.mediaplayertest;import android.content.Context;import android.content.res.AssetFileDescriptor;import android.media.AudioManager;import android.media.MediaPlayer;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.util.Log;import android.view.SurfaceHolder;import android.view.SurfaceView;import java.io.IOException;public class MediaSurfaceView extends SurfaceView implements SurfaceHolder.Callback, MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {public static String TAG = "Media";private MediaPlayer mPlayer;private MediaControl viewInterface;//未加载媒体文件状态private int STATE_UNLOADING = 0x000;//准备状态private int STATE_PREPARED = 0x001;//暂停状态private int STATE_PAUSED = 0x002;//停止状态private int STATE_STOPPED = 0x003;//开始状态private int STATE_STARTED = 0x004;//当前状态private int currentState = STATE_UNLOADING;public MediaSurfaceView(Context context, AttributeSet attrs) {    this(context, attrs, -1);}public MediaSurfaceView(Context context) {    this(context, null);}public MediaSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {    super(context, attrs, defStyleAttr);    initial();}private void initial() {    loadingResource();    getHolder().addCallback(this);    getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);}private void loadingResource() {    if (mPlayer == null) {        mPlayer = new MediaPlayer();        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);        mPlayer.setOnErrorListener(this);        mPlayer.setOnPreparedListener(this);        mPlayer.setOnCompletionListener(this);    }}@Overridepublic void surfaceCreated(SurfaceHolder surfaceHolder) {    mPlayer.setDisplay(surfaceHolder);}@Overridepublic void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {    Log.d(TAG, "surfaceChanged");}@Overridepublic void surfaceDestroyed(SurfaceHolder surfaceHolder) {    Log.d(TAG, "surfaceDestroyed");    ifStart = false;    if (mPlayer == null) return;    if (mPlayer.isPlaying()) {        mPlayer.stop();    }    mPlayer.release();}/** * 开始播放 */public void startPlayer() {    if (STATE_PREPARED != currentState &&            STATE_PAUSED != currentState) return;    mPlayer.start();    if (viewInterface != null) {        viewInterface.startPlayer();    }    currentState = STATE_STARTED;}/** * 停止播放 */public void stopPlayer() {    if (STATE_UNLOADING == currentState ||            STATE_STOPPED == currentState) return;    mPlayer.stop();    mPlayer.prepareAsync();    if (viewInterface != null) {        viewInterface.stopPlayer();    }    currentState = STATE_STOPPED;}/** * 暂停播放 */public void pausePlayer() {    if (STATE_STARTED != currentState) return;    mPlayer.pause();    if (viewInterface != null) {        viewInterface.pausePlayer();    }    currentState = STATE_PAUSED;}/** * 装载 */public void loadingPlayer() {    try {        AssetFileDescriptor fileDescriptor = getContext().getAssets().openFd("gaobaiqiqiu.mp4");        mPlayer.setDataSource(fileDescriptor.getFileDescriptor(),                fileDescriptor.getStartOffset(),                fileDescriptor.getLength());        mPlayer.prepareAsync();    } catch (IOException e) {        e.printStackTrace();    }}public void setViewInterface(MediaControl viewInterface) {    this.viewInterface = viewInterface;}/** * 播放器错误状态监听 * * @param mediaPlayer * @param what * @param extra * @return */@Overridepublic boolean onError(MediaPlayer mediaPlayer, int what, int extra) {    if (MediaPlayer.MEDIA_ERROR_UNKNOWN == what) {//未知错误        Log.d(TAG, "onError what    未知错误");    } else if (MediaPlayer.MEDIA_ERROR_SERVER_DIED == what) {//媒体服务中断,应用必须释放类新初始化        Log.d(TAG, "onError what    媒体崩溃");    } else {        Log.d(TAG, "onError what    其他错误");    }    switch (extra) {        case MediaPlayer.MEDIA_ERROR_IO:            Log.d(TAG, "onError extra    文件或网络关联错误");            break;        case MediaPlayer.MEDIA_ERROR_MALFORMED:            Log.d(TAG, "onError extra    比特流未遵守相关编码标准或者文件细则");            break;        case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:            Log.d(TAG, "onError extra    比特流未遵守相关编码标准或者文件细则");            break;        case MediaPlayer.MEDIA_ERROR_TIMED_OUT:            Log.d(TAG, "onError extra    超时");            break;        default:            Log.d(TAG, "onError extra    其他错误");            break;    }    return false;}/** * 播放器装在准备 * * @param mediaPlayer */@Overridepublic void onPrepared(MediaPlayer mediaPlayer) {    currentState = STATE_PREPARED;    int totalLength = mPlayer.getDuration();    if (viewInterface != null) {        viewInterface.totalLengthSecond(formatTime(totalLength),totalLength);    }}/** * 格式化时间,将毫秒转换为分:秒格式 * * @param time * @return */public static String formatTime(long time) {    if (time < 0) return "-1";    String min = time / (1000 * 60) + "";    String sec = time % (1000 * 60) + "";    if (min.length() < 2) {        min = "0" + time / (1000 * 60) + "";    } else {        min = time / (1000 * 60) + "";    }    if (sec.length() == 4) {        sec = "0" + (time % (1000 * 60)) + "";    } else if (sec.length() == 3) {        sec = "00" + (time % (1000 * 60)) + "";    } else if (sec.length() == 2) {        sec = "000" + (time % (1000 * 60)) + "";    } else if (sec.length() == 1) {        sec = "0000" + (time % (1000 * 60)) + "";    }    return min + ":" + sec.trim().substring(0, 2);}@Overridepublic void onCompletion(MediaPlayer mediaPlayer) {    stopPlayer();}/** * 是否开启循环线程 */private boolean ifStart = false;private Thread ProgressThread = new Thread() {    private Bundle mBundle = new Bundle();    @Override    public void run() {        super.run();        while (ifStart && handler != null) {            if (mPlayer != null && mPlayer.isPlaying()) {                Message message = new Message();                message.what = 0x00;                //当前格式化后的时间                mBundle.putString("TIME", formatTime(mPlayer.getCurrentPosition()));                //当前毫秒                int curtime = mPlayer.getCurrentPosition();                message.setData(mBundle);                mBundle.putInt("CURRENT_TIME",curtime);                handler.sendMessage(message);            }            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();                ifStart = false;            }        }    }};private Handler handler;public void setHandler(Handler handler) {    this.handler = handler;    ifStart = true;    ProgressThread.start();}/** * 进度调至某个时间点后继续播放 * @param progress */public void seekTo(int progress) {    if(mPlayer!=null){        if(STATE_PREPARED==currentState ||                STATE_STARTED == currentState||                STATE_PAUSED  == currentState){            mPlayer.seekTo(progress);        }    }}}

我将几个状态做了限制,规定了在什么状态下才能执行那种功能。比如“停止”之后就不能“暂停”。以免报错。进度条我用了线程来实时获取当前进度,通过handler进行线程间的通信,实现UI的进度条更新。
接下来是主界面代码:

package com.example.mediaplayertest;import android.app.Activity;import android.os.Bundle;import android.os.Handler;import android.os.Message;import android.view.View;import android.widget.Button;import android.widget.LinearLayout;import android.widget.SeekBar;import android.widget.TextView;import myview.LoadingView;public class MediaActivity extends Activity implements View.OnClickListener,MediaControl, SeekBar.OnSeekBarChangeListener {private LoadingView mLoadingView;private MediaSurfaceView mSurfaceView;private LinearLayout LLloading;private Button btnStart;private Button btnStop;private Button btnPause;private Button btnLoading;private TextView currentTime;private TextView totalTime;private SeekBar mSeekBar;@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.mediaplayer_layout);    mLoadingView = (LoadingView) findViewById(R.id.loadingView);    mLoadingView.animationOpen();    mSurfaceView = (MediaSurfaceView) findViewById(R.id.media_player_sv);    mSurfaceView.setViewInterface(this);    mSurfaceView.setHandler(mHandler);    LLloading = (LinearLayout) findViewById(R.id.media_loading);    btnStart = (Button) findViewById(R.id.btn_start);    btnStop = (Button) findViewById(R.id.btn_stop);    btnPause = (Button) findViewById(R.id.btn_pause);    btnLoading = (Button) findViewById(R.id.btn_loading);    btnStart.setOnClickListener(this);    btnStop.setOnClickListener(this);    btnPause.setOnClickListener(this);    btnLoading.setOnClickListener(this);    currentTime = (TextView) findViewById(R.id.current_time);    totalTime = (TextView) findViewById(R.id.total_time);    mSeekBar = (SeekBar) findViewById(R.id.my_seekBar);    mSeekBar.setOnSeekBarChangeListener(this);}private Handler mHandler = new Handler(){    @Override    public void handleMessage(Message msg) {        super.handleMessage(msg);        switch(msg.what){            case 0x00:                Bundle mBundle  = msg.getData();                String curTime = (String) mBundle.get("TIME");                int intCurTime = (int) mBundle.get("CURRENT_TIME");                mSeekBar.setProgress(intCurTime);                currentTime.setText(curTime);            break;        }    }};@Overridepublic void onClick(View view) {    switch (view.getId()){        case  R.id.btn_start:            mSurfaceView.startPlayer();            break;        case R.id.btn_stop:            mSurfaceView.stopPlayer();            break;        case R.id.btn_pause:            mSurfaceView.pausePlayer();            break;        case R.id.btn_loading:            mSurfaceView.loadingPlayer();            break;    }}@Overridepublic void startPlayer() {    mSurfaceView.setVisibility(View.VISIBLE);    LLloading.setVisibility(View.INVISIBLE);    mLoadingView.animationClose();}@Overridepublic void stopPlayer() {}@Overridepublic void pausePlayer() {}@Overridepublic void totalLengthSecond(String value,int val) {    totalTime.setText(value);    mSeekBar.setMax(val);}@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {    if(progress>0){        if(fromUser){            mSurfaceView.seekTo(progress);        }    }}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}}

代码中主要实现界面的改变。布局文件这里就不在贴 了,主要就是surfaceview上方叠了一层布局来显示loading时候的画面,当播放时,将loading布局隐藏后就能显示播放的画面了。

当然,图中的状态循环还涉及到回放等功能,因为用得不多,所以这里就不再赘述了。如果想自己做类似的视频框架的话,可以再进一步的封装,比如在surfaceview中加入“切换全屏”的功能,根据手势调整声音或者屏幕亮度的功能。

制作视频播放器功能,首先保证视频能够正常播放,并且不影响UI线程的运作;第二就是媒体文件播放的时候,屏幕适配的问题;第三就是视频的解码等等可能涉及底层的知识。今后在工作中继续学习吧。

0 0
原创粉丝点击