MediaPlayer,顾名思义是用于媒体文件播放的组件。Android中MediaPlayer通常与SurfaceView一起使用,当然也可以和其他控件诸如TextureView、SurfaceTexture等可以取得holder,用于MediaPlayer.setDisplay的控件一起使用。
对于现在的移动设备来说,媒体播放时一个非常重要的功能,所以掌握MediaPlayer对于Android程序员来说,也是一个基本要求了。由于媒体播放是一个比较复杂的事情,涉及到媒体资源的加载、解码等耗时耗资源的操作,所以MediaPlayer的使用相对其他组件变得复杂了许多。
掌握MediaPlayer需要先掌握MediaPlayer的工作过程和它的一些重要的方法,在Android Developer官网上可以搜到MediaPlayer详细的讲解。
在官网上可以看到一张关于MediaPayer状态机的图,直观的阐述了MediaPlayer的工作过程,以及它的一些重要的方法的使用时机。如下:
从上图中,可以捋出MediaPlayer的一个最简单的使用流程:
新建一个MediaPlayer: mPlayer=new MediaPlayer();通常在新建一个MediaPlayer实体后,会对给它增加需要的监听事件,MediaPlayer的监听事件有:
将需要播放的资源路径交给MediaPlayer实体:mPlayer.setDataSource(source);
按照上面所说的流程来操作,我们会发现还有很多问题需要处理,比如说视频播放有声音没图像,切入后台后声音还在播放等等问题。综合一下,我们在安装上述流程走会有哪些问题以及我们解决一些问题后,还可能遇到哪些问题:
等等一些其他更多问题。最为典型的应该就是上述这些问题了。这些问题,仔细看看官网上对于MediaPlayer的讲解后,基本都不会是问题。恩,最后一个问题除外。相对MediaPlayer的状态机来说,MediaPlayer的各个方法的有效状态和无效状态为我们在使用MediaPlayer的具体方法时,提供了更好的指南。
感觉用有效状态和无效状态来翻译不太合适,干脆直接就用官方上面所说的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&¤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(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);
}
}
然后通过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地址。