Android MediaPlayer+SurfaceView播放視訊(附Demo)
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.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(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/(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;
}
}