Android(安卓)MediaPlayer+SurfaceView播放视频(附Demo)
MediaPlayer,顾名思义是用于媒体文件播放的组件。Android中MediaPlayer通常与SurfaceView一起使用,当然也可以和其他控件诸如TextureView、SurfaceTexture等可以取得holder,用于MediaPlayer.setDisplay的控件一起使用。
对于现在的移动设备来说,媒体播放时一个非常重要的功能,所以掌握MediaPlayer对于Android程序员来说,也是一个基本要求了。由于媒体播放是一个比较复杂的事情,涉及到媒体资源的加载、解码等耗时耗资源的操作,所以MediaPlayer的使用相对其他组件变得复杂了许多。
掌握MediaPlayer需要先掌握MediaPlayer的工作过程和它的一些重要的方法,在Android Developer官网上可以搜到MediaPlayer详细的讲解。
MediaPlayer状态机
在官网上可以看到一张关于MediaPayer状态机的图,直观的阐述了MediaPlayer的工作过程,以及它的一些重要的方法的使用时机。如下:
从上图中,可以捋出MediaPlayer的一个最简单的使用流程:
新建一个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出错时会触发,无论是播放过程中出错,还是准备过程中出错,都会触发。
将需要播放的资源路径交给MediaPlayer实体:mPlayer.setDataSource(source);
- 让MediaPlayer去获取解析资源,调用prepare()或者prepareAsync()方法,前一个是同步方法,后一个是异步方法,通常我们用的比较多的是后者:mPlayer.prepareAsync();
- 进入准备完成状态后,调用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使用示例
将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&¤tVideoWidth>0&¤tVideoHeight>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(scaleVideoint) (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/(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); } } } 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); }}
然后通过MPlayer即可更为简单方便的播放视频:
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; } }
完整Demo地址。
更多相关文章
- Android音乐播放器开发小记——项目简介
- 专辑: 善知堂android 4.0.3 就业视频教程
- 14天学会安卓开发(第十三天)Android多媒体开发
- Android(安卓)6.0耳机hook按键接听和挂断电话;音乐中短按下一首,
- Android应用的构成
- Android之MediaPlayer
- android中使用MediaPlayer播放视频
- 4款手机中必备的APP,用过之后一定会让你舍不得卸载
- Android网络收音机项目