采用Android的MediaPlayer+SurfaceView设计视频播放器

前言

android视频播放有很多方式(自带videoView等),这里简单说其中一种:MediaPlayer+SurfaceView,一个播放音频,一个播放视频(图像)。

大体结构图

播放器类图

  • BaseMediaPlayer:定义了一个播放器应该具备的基础接口;你可以跟进这个接口再加播放器底层,来实现不同的播放器;只提供播放的接口,不提供用户 UI 交互;
  • SystemMediaPlayerImpl:继承自 BaseMediaPlayer,是基于 Android 系统 MediaPlayer 的一份实现;
  • StrawMediaPlayer:继承自 FrameLayout,是 Android 上的一个布局 View,封装了播放器的所有操作,用来进行可视化和用户 UI 交互;
  • PlayerBottomControl:播放器底部控件,用于控制播放、进度、全屏调节等;
  • MediaPlayerGestureController:播放器手势控制器,用于手势识别和相应的控制;
  • ScreenOrientationSwitcher:屏幕方向切换控制器。

这里介绍的一个类框图仅作参考,自定义播放器时的确需要考虑这些结构功能。下面一个代码示例仅仅是最简单的应用。

MediaPlayer状态机

官网上可以看到一张关于MediaPayer状态机的图,直观的阐述了MediaPlayer的工作过程,以及它的一些重要的方法的使用时机:

MediaPlayer状态图

从上图中,可以捋出MediaPlayer的一个最简单的使用流程:

1.新建一个MediaPlayer: mPlayer=new MediaPlayer();通常在新建一个MediaPlayer实体后,会对给它增加需要的监听事件,MediaPlayer的监听事件有:

  • MediaPlayer.OnPreparedListener:MediaPlayer进入准备完成的状态触发,表示媒体可以开始播放了。
  • MediaPlayer.OnSeekCompleteListener:调用MediaPlayer的seekTo方法后,MediaPlayer会跳转到媒体指定的位置,当跳转完成时触发。需要注意的时,seekTo并不能精确的挑战,它的跳转点必须是媒体资源的关键帧。
  • MediaPlayer.OnBufferingUpdateListener:网络上的媒体资源缓存进度更新的时候会触发,通过这个就可以设置进度条的缓存进度。
  • MediaPlayer.OnCompletionListener:媒体播放完毕时会触发。但是当OnErrorLister返回false,或者MediaPlayer没有设置OnErrorListener时,这个监听也会被触发。
  • MediaPlayer.OnVideoSizeChangedListener:视频宽高发生改变的时候会触发。当所设置的媒体资源没有视频图像、MediaPlayer没有设置展示的holder或者视频大小还没有被测量出来时,获取宽高得到的都是0.
  • MediaPlayer.OnErrorListener:MediaPlayer出错时会触发,无论是播放过程中出错,还是准备过程中出错,都会触发。

2.将需要播放的资源路径交给MediaPlayer实体:mPlayer.setDataSource(source);

3.让MediaPlayer去获取解析资源,调用prepare()或者prepareAsync()方法,前一个是同步方法,后一个是异步方法,通常我们用的比较多的是后者:mPlayer.prepareAsync();

4.调用MediaPlayer对象的setDisplay(SurfaceHolder sh)将所播放的视频图像输出到指定的SurfaceView组件。

5.进入准备完成状态后,调用start()方法开始播放,如果是调用prepare()方法准备,在prepare()方法后,可以直接开始播放。如果是调用prepareAsync()方法准备,需要在OnPreparedListener()监听中开始播放:mPlayer.start(); 

这是一个最简单的播放流程,然而我们的需求绝不可能这么简单!通过以上流程我们会遇到很多问题。

MediaPlayer使用常见问题

按照上面所说的流程来操作,我们会发现还有很多问题需要处理,比如说视频播放有声音没图像,切入后台后声音还在播放等等问题。综合一下,我们按照上述流程走会有哪些问题以及我们解决一些问题后,还可能遇到哪些问题:

  • 视频播放有声音没图像。
  • 视频图像变形。
  • 切入后台后声音还在继续播放。
  • 切入后台再切回来,视频黑屏。
  • 暂停后切入后台,再切回来,并保持暂停状态会黑屏,seekTo也没有用。
  • 播放时会有一小段时间的黑屏。
  • 多个SurfaceView用来播放视频,滑动切换时会有上个视频的残影。

等等一些其他更多问题。最为典型的应该就是上述这些问题了。这些问题,仔细看看官网上对于MediaPlayer的讲解后,基本都不会是问题。恩,最后一个问题除外。相对MediaPlayer的状态机来说,MediaPlayer的各个方法的有效状态和无效状态为我们在使用MediaPlayer的具体方法时,提供了更好的指南。

Valid and invalid states

感觉用有效状态和无效状态来翻译不太合适,干脆直接就用官方上面所说的Valid and invalid states吧。它指出了MediaPlayer中常用公有方法在哪些状态下可以使用,在哪些状态下不可以使用。 

我们可以将所有的方法分为三类。 

-在任何状态下都可以使用的。比如设置监听,以及其他MediaPlayer中与资源无关的方法。需要特别注意的是setDisplay和setSurface两个方法。 

- 在MediaPlayer状态机中除Error状态都可以使用的。比如获取视频宽高、获取当前位置等。 

- 对状态有诸多限制,需要严格遵循状态机流程的方法。 比如start、pause、stop等等方法。 

具体的在MediaPlayer官方说明中有对应的表。

常见问题讨论

针对上面提到的问题(也许不一定会遇到),通过MediaPlayer的状态机和它的常用方法的可用状态来进行讨论,我们就能找到相应的原因,因为代码是不会欺骗的。 

1. 有声音没有图像 

视频播放有声音没图像也许是在使用MediaPlayer最容易出现的问题,几乎所有使用MediaPlayer的新手都会遇到。视频播放的图像呈现需要一个载体,需要利用MediaPlayer.setDisplay设置一个展示视频画面的SurfaceHolder,最终视频的每一帧图像是要绘制在Surface上面的。通常,设置给MediaPlayer的SurfaceHolder未被创建,视频播放就注定没有图像。 

  • * 比如你先调用了setDisplay,但是这个时候holder是没有被创建的。视频就没有图像了。 
  • * 或者你在setDisplay的时候holder确保了holder是被创建了,但是当因为一些原因holder被销毁了,视频也就没有图像了。 
  • * 再者,你没有给展示视频的view设置合适的大小,比如都设置wrap_content,或者都设置0,也会导致SurfaceHolder不能被创建,视频也就没有图像了。 

2. 视频图像变形 

Surface展示视频图像的时候,是不会去主动保证和呈现出来的图像和原始图像的宽高比例是一致的,所以我们需要自己去设置展示视频的View的宽高,以保证视频图像展示出来的时候不会变形。我认为比较合适的做法就是利用FrameLayout嵌套一个SurfaceView或者其他拥有Surface的View来作为视频图像播放的载体View,然后再OnVideoSizeChangeListener的监听回调中,对载体View的大小做更改。 

3. 切入后台后声音还在继续播放 

这个问题只需要在onPause中暂停播放即可 

4. 切入后台再切回来,视频黑屏 

诸如此类的黑屏问题,多是因为surfaceholder被销毁了,再切回来时,需要重新给MediaPlayer设置holder。 

5. 播放时会有一小段时间的黑屏 

视频准备完成后,调用play进行播放视频,承载视频播放的View会是黑屏状态,我们只需要在播放前,给对应的Surface绘制一张图即可。 

6. 多个SurfaceView用来播放视频,滑动切换时会有上个视频的残影

当视频切换出界面,设置surfaceView的visiable状态为Gone,界面切回来时再设置为visiable即可。

MediaPlayer使用示例

public class MPlayer implements IMPlayer,MediaPlayer.OnBufferingUpdateListener,
        MediaPlayer.OnCompletionListener,MediaPlayer.OnVideoSizeChangedListener,
        MediaPlayer.OnPreparedListener,MediaPlayer.OnSeekCompleteListener,
        MediaPlayer.OnErrorListener,SurfaceHolder.Callback{
 
    private MediaPlayer player;
 
    private String source;
    private IMDisplay display;
 
    private boolean isVideoSizeMeasured=false;  //视频宽高是否已获取,且不为0
    private boolean isMediaPrepared=false;      //视频资源是否准备完成
    private boolean isSurfaceCreated=false;     //Surface是否被创建
    private boolean isUserWantToPlay=false;     //使用者是否打算播放
    private boolean isResumed=false;            //是否在Resume状态
 
    private boolean mIsCrop=false;
 
    private IMPlayListener mPlayListener;
 
    private int currentVideoWidth;              //当前视频宽度
    private int currentVideoHeight;             //当前视频高度
 
    private void createPlayerIfNeed(){
        if(null==player){
            player=new MediaPlayer();
            player.setScreenOnWhilePlaying(true);
            player.setOnBufferingUpdateListener(this);
            player.setOnVideoSizeChangedListener(this);
            player.setOnCompletionListener(this);
            player.setOnPreparedListener(this);
            player.setOnSeekCompleteListener(this);
            player.setOnErrorListener(this);
        }
    }
 
    private void playStart(){
        if(isVideoSizeMeasured&&isMediaPrepared&&isSurfaceCreated&&isUserWantToPlay&&isResumed){
            player.setDisplay(display.getHolder());
            player.start();
            log("视频开始播放");
            display.onStart(this);
            if(mPlayListener!=null){
                mPlayListener.onStart(this);
            }
        }
    }
 
    private void playPause(){
        if(player!=null&&player.isPlaying()){
            player.pause();
            display.onPause(this);
            if(mPlayListener!=null){
                mPlayListener.onPause(this);
            }
        }
    }
 
    private boolean checkPlay(){
        if(source==null|| source.length()==0){
            return false;
        }
        return true;
    }
 
    public void setPlayListener(IMPlayListener listener){
        this.mPlayListener=listener;
    }
 
    /**
     * 设置是否裁剪视频,若裁剪,则视频按照DisplayView的父布局大小显示。
     * 若不裁剪,视频居中于DisplayView的父布局显示
     * @param isCrop 是否裁剪视频
     */
    public void setCrop(boolean isCrop){
        this.mIsCrop=isCrop;
        if(display!=null&&currentVideoWidth>0&&currentVideoHeight>0){
            tryResetSurfaceSize(display.getDisplayView(),currentVideoWidth,currentVideoHeight);
        }
    }
 
    public boolean isCrop(){
        return mIsCrop;
    }
 
    /**
     * 视频状态
     * @return 视频是否正在播放
     */
    public boolean isPlaying(){
        return player!=null&&player.isPlaying();
    }
 
    //根据设置和视频尺寸,调整视频播放区域的大小
    private void tryResetSurfaceSize(final View view, int videoWidth, int videoHeight){
        ViewGroup parent= (ViewGroup) view.getParent();
        int width=parent.getWidth();
        int height=parent.getHeight();
        if(width>0&&height>0){
            final FrameLayout.LayoutParams params= (FrameLayout.LayoutParams) view.getLayoutParams();
            if(mIsCrop){
                float scaleVideo=videoWidth/(float)videoHeight;
                float scaleSurface=width/(float)height;
                if(scaleVideo<scaleSurface){
                    params.width=width;
                    params.height= (int) (width/scaleVideo);
                    params.setMargins(0,(height-params.height)/2,0,(height-params.height)/2);
                }else{
                    params.height=height;
                    params.width= (int) (height*scaleVideo);
                    params.setMargins((width-params.width)/2,0,(width-params.width)/2,0);
                }
            }else{
                if(videoWidth>width||videoHeight>height){
                    float scaleVideo=videoWidth/(float)videoHeight;
                    float scaleSurface=width/height;
                    if(scaleVideo>scaleSurface){
                        params.width=width;
                        params.height= (int) (width/scaleVideo);
                        params.setMargins(0,(height-params.height)/2,0,(height-params.height)/2);
                    }else{
                        params.height=height;
                        params.width= (int) (height*scaleVideo);
                        params.setMargins((width-params.width)/2,0,(width-params.width)/2,0);
                    }
                }
            }
            view.setLayoutParams(params);
        }
    }
 
    @Override
    public void setSource(String url) throws MPlayerException {
        this.source=url;
        createPlayerIfNeed();
        isMediaPrepared=false;
        isVideoSizeMeasured=false;
        currentVideoWidth=0;
        currentVideoHeight=0;
        player.reset();
        try {
            player.setDataSource(url);
            player.prepareAsync();
            log("异步准备视频");
        } catch (IOException e) {
            throw new MPlayerException("set source error",e);
        }
    }
 
    @Override
    public void setDisplay(IMDisplay display) {
        if(this.display!=null&&this.display.getHolder()!=null){
            this.display.getHolder().removeCallback(this);
        }
        this.display=display;
        this.display.getHolder().addCallback(this);
    }
 
    @Override
    public void play() throws MPlayerException {
        if(!checkPlay()){
            throw new MPlayerException("Please setSource");
        }
        createPlayerIfNeed();
        isUserWantToPlay=true;
        playStart();
    }
 
    @Override
    public void pause() {
        isUserWantToPlay=false;
        playPause();
    }
 
    @Override
    public void onPause() {
        isResumed=false;
        playPause();
    }
 
    @Override
    public void onResume() {
        isResumed=true;
        playStart();
    }
 
    @Override
    public void onDestroy() {
        if(player!=null){
            player.release();
        }
    }
 
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
 
    }
 
    @Override
    public void onCompletion(MediaPlayer mp) {
        display.onComplete(this);
        if(mPlayListener!=null){
            mPlayListener.onComplete(this);
        }
    }
 
    @Override
    public void onPrepared(MediaPlayer mp) {
        log("视频准备完成");
        isMediaPrepared=true;
        playStart();
    }
 
    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
        log("视频大小被改变->"+width+"/"+height);
        if(width>0&&height>0){
            this.currentVideoWidth=width;
            this.currentVideoHeight=height;
            tryResetSurfaceSize(display.getDisplayView(),width,height);
            isVideoSizeMeasured=true;
            playStart();
        }
    }
 
    @Override
    public void onSeekComplete(MediaPlayer mp) {
 
    }
 
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        return false;
    }
 
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if(display!=null&&holder==display.getHolder()){
            isSurfaceCreated=true;
            //此举保证以下操作下,不会黑屏。(或许还是会有手机黑屏)
            //暂停,然后切入后台,再切到前台,保持暂停状态
            if(player!=null){
                player.setDisplay(holder);
                //不加此句360f4不会黑屏、小米note1会黑屏,其他机型未测
                player.seekTo(player.getCurrentPosition());
            }
            log("surface被创建");
            playStart();
        }
    }
 
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        log("surface大小改变");
    }
 
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if(display!=null&&holder==display.getHolder()){
            log("surface被销毁");
            isSurfaceCreated=false;
        }
    }
 
    private void log(String content){
        Log.e("MPlayer",content);
    }
}
public class PlayerActivity extends Activity {
 
    private EditText mEditAddress;
    private SurfaceView mPlayerView;
    private MPlayer player;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_player);
        initView();
        initPlayer();
    }
 
    private void initView(){
        mEditAddress= (EditText) findViewById(R.id.mEditAddress);
        mPlayerView= (SurfaceView) findViewById(R.id.mPlayerView);
    }
 
    private void initPlayer(){
        player=new MPlayer();
        player.setDisplay(new MinimalDisplay(mPlayerView));
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        player.onResume();
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        player.onPause();
    }
 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        player.onDestroy();
    }
 
    public void onClick(View view){
        switch (view.getId()){
            case R.id.mPlay:
                String mUrl=mEditAddress.getText().toString();
                if(mUrl.length()>0){
                    Log.e("wuwang","播放->"+mUrl);
                    try {
                        player.setSource(mUrl);
                        player.play();
                    } catch (MPlayerException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.mPlayerView:
                if(player.isPlaying()){
                    player.pause();
                }else{
                    try {
                        player.play();
                    } catch (MPlayerException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.mType:
                player.setCrop(!player.isCrop());
                break;
        }
    }

之后讨论一下,怎么给一个surfaceView设置了路径它就能播放了。

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值