MediaPlayer封裝原生視頻播放器

MediaPlayer可以用來控制視頻和音頻文件流,也就是說可以通過它播放音樂和視頻。通常如果我們不用第三方的框架,有三種方式可以去播放視頻。

1.VideoView
2.MediaPlayer+SurfaceView
3.MediaPlayer+TextureView

首先VideoView是繼承自SurfaceView,內部維護著一個MediaPlayer,用過VideoView的人都知道,它的控制界面是比較丑陋的,當然我們一般在開發中是不會使用它的。而后面兩種都可以自定義不妨界面,當然它們的區別是一個是SurfaceView,而另一個是TextureView。SurfaceView的原理就是在View的位置上重新創建一個Window,所有界面的繪制和渲染都是在新的Window中執行,不會影響主線程的執行,當然這也存在線程同步問題。另一個問題就是由于SurfaceView的顯示是在新的Windows中,它不會受到View的屬性控制,也不能放在RecycelerView或ListView中,也需要自己管理其生命周期。TextureView是在API14之后被加入的,和SurfaceView不同的是,TextureView不會在View的位置上創建一個新的窗口,所有的界面的繪制渲染都是在View上,這樣就允許TextureView能夠被移動,縮放或做些其它的動畫。TextureView只能在硬件加速窗口中使用,當在軟件中渲染時,TextureView將不會做任何繪制。

好了,說了這么多,決定用第三種封裝自己的播放器,當然本文是參考這篇文章做的
用MediaPlayer+TextureView封裝一個完美實現全屏、小窗口的視頻播放器

TextureView使用

TexureView的使用是比較簡單的,首先你需要做的就是獲取它的SurfaceTexture,它能夠被用來去渲染內容。它的使用大致如下

private TextureView mTextureView;

mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);

//監聽回調
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//SurfaceTexture準備就緒
}

public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
//SurfaceTexture緩沖大小變化
}

public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    //SurfaceTexture即將銷毀
    return false;
}

public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    //SurfaceTexture狀態更新
}

TextureView不能直接被使用,只有當它attached到一個Window之后,SurfaceTexture準備就緒之后,TextureView才能起作用。當監聽的回調onSurfaceTextureAvailable被調用后,可以通過傳入的的surface關聯Mediaplayer,SurfaceTexture作為數據通道,把從數據源Mediaplayer中獲取到的圖像幀數據轉化為GL外部紋理,交給TextureView作為View heirachy中的一個硬件加速層來顯示,從而實現視頻播放的功能。

//關聯Mediaplayer,之所以要做判斷,是因為當mTextureView重新繪制之后,生命
//周期方法會被回調,監聽器也會被回調,而mediaplayer不會被銷毀任然持
//有SurfaceTexture的引用,所以當生命周期回調之后直接使用持有的
//SurfaceTexture,任然可以繼續播放
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    if(mSurfaceTexture == null){
        mSurfaceTexture = surface;
        mediaPlayerStart();
    }else {
        mTextureView.setSurfaceTexture(mSurfaceTexture);
    }
}

代碼設計

為了使業務邏輯分離解耦,將數據源和控制器部分分成兩個類處理

//數據源播放
public class VideoPlayer extends FrameLayout implements TextureView.SurfaceTextureListener,
                                     VideoPlayerControl{}

//控制器部分
public class VideoPlayerController extends FrameLayout implements
        View.OnClickListener,
        SeekBar.OnSeekBarChangeListener,
        View.OnTouchListener{}

其中數據源部分專門負責播放視頻,處理播放狀態的初始化和狀態變化,而控制器部分專門負責播放界面的 播放,暫停,全屏,小窗口 操作。而兩個類之間通過一個接口VideoPlayerControl關聯。

public interface VideoPlayerControl {

    void start();         //播放
    void pause();         //暫停
    void seekTo(int pos); //進度條拖動
    void restart();
    void release();

    boolean isIdle();  //是否是空閑
    boolean isError();
    boolean isPreparing();
    boolean isPrepared();
    boolean isBufferingPlaying();
    boolean isBufferingPaused();
    boolean isPlaying();
    boolean isPaused();
    boolean isCompleted();

    int getDuration();
    int getCurrentProgress();
    int getBufferPercent();
    FrameLayout getContainer();

    boolean isFullScreen();  //全屏
    boolean isNormalScreen();//普通窗口
    boolean isTinyScreen();  //小窗口

    void enterFullScreen();    //進入全屏
    boolean exitFullScreen();  //退出全屏
    void enterTinyScreen();    //進入小屏
    boolean exitTinyScreen();  //退出小屏
}

VideoPlayer

常量

定義幾個常量來標注播放狀態和界面窗口狀態

public static final int STATE_ERROR = -1;          // 播放錯誤
public static final int STATE_IDLE = 0;            // 播放未開始
public static final int STATE_PREPARING = 1;       // 播放準備中
public static final int STATE_PREPARED = 2;        // 播放準備就緒
public static final int STATE_PLAYING = 3;         // 正在播放
public static final int STATE_PAUSED = 4;          // 暫停播放
/**
 * 正在緩沖(播放器正在播放時,緩沖區數據不足,進行緩沖,緩沖區數據足夠后恢復播放)
 **/
public static final int STATE_BUFFERING_PLAYING = 5;
/**
 * 正在緩沖(播放器正在播放時,緩沖區數據不足,進行緩沖,此時暫停播放器,繼續緩沖,緩沖區數據足夠后恢復暫停)
 **/
public static final int STATE_BUFFERING_PAUSED = 6;
public static final int STATE_COMPLETED = 7;       // 播放完成

public static final int PLAYER_NORMAL = 10;        // 普通播放器
public static final int PLAYER_FULL_SCREEN = 11;   // 全屏播放器
public static final int PLAYER_TINY_WINDOW = 12;   // 小窗口播放器

mContainer

初始化界面時,mController和mTextureView是添加到mContainer中的,這樣做的好處就是,方便移除和添加窗口,所有的操作只需要通過對mContainer操作來完成

關聯視頻播放器

public void setController(VideoPlayerController controller){
   mController = controller;
   mController.setVideoPlayer(this);
   updateVideoPlayerState();
   mContainer.removeView(mController);
   LayoutParams lp = new LayoutParams(
           ViewGroup.LayoutParams.MATCH_PARENT,
           ViewGroup.LayoutParams.MATCH_PARENT);
   mContainer.addView(mController, lp);
}

我們需要傳入一個controller然后關聯此VideoPlayer就可以了。然后添加到mContainer中就可以了。

設置視頻播放uri

public void setPlayUri(String uri){
    mUri = uri;
}

MediaPlayer

要使用MediaPlayer需要設置幾個監聽器,

mMediaPlayer.setOnPreparedListener(mOnPreparedListener);   //mediaplayer準備就緒回調
mMediaPlayer.setOnCompletionListener(mOnCompletionListener); //mediaplayer播放完成監聽
mMediaPlayer.setOnErrorListener(mOnErrorListener);   //播放錯誤監聽回調
mMediaPlayer.setOnInfoListener(mOnInfoListener);     //播放器渲染狀態變化監聽
mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener); //播放器緩沖進度0-100回調

初始化和播放

//初始化
private void mediaPlayerInit() {
    if(mMediaPlayer == null){
        mMediaPlayer = new MediaPlayer();

        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        mMediaPlayer.setScreenOnWhilePlaying(true);  //在播放時屏幕一直開啟著

        mMediaPlayer.setOnPreparedListener(mOnPreparedListener);
        mMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangeListener);
        mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
        mMediaPlayer.setOnErrorListener(mOnErrorListener);
        mMediaPlayer.setOnInfoListener(mOnInfoListener);
        mMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);
    }
}

//播放
private void mediaPlayerStart(){
    try {
        //設置數據源
        mMediaPlayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUri));
        //設置surface
        mMediaPlayer.setSurface(new Surface(mSurfaceTexture));
        //異步網絡準備
        mMediaPlayer.prepareAsync();

        mCurrentState = STATE_PREPARING;
        updateVideoPlayerState();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

全屏和小窗口進入和退出

全屏和小窗口差不多,它們的實現原理大致是,先從自己的view中移除mContainer,然后設置LayoutParams,最后在android.R.id.content中添加mContainer就可以了。只要設置不同的lp參數就可以實現全屏和小窗口播放。退出的話就只需要移除然后添加到FrameLayout容器中就可以了。注意全屏要設置屏幕方向,通過setRequestedOrientation請求屏幕方向。ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE為橫屏,SCREEN_ORIENTATION_PORTRAIT為豎屏。

注意: 變化的狀態都可以通過updateVideoPlayerState()回調控制器更新窗口

 private void updateVideoPlayerState() {
    mController.setControllerState(mCurrentState, mWindowState);
 }

VideoPlayerController

這個類為自定義的一個控制器,所有播放器的操作都是通過這個控制器回調播放器VideoPlayer,這個控制器定義了一系列方法控制播放狀態

setTitle 設置頁面標題
setImage 設置封面背景
setTopBottomVisible 頭部和底部是否隱藏
setControllerState 設置播放器工作狀態
updateProgress 更新進度

同時做了一個小窗口界面拖拽

@Override
public boolean onTouch(View v, MotionEvent event) {

    if(!mVideoPlayerControl.isTinyScreen()) return super.onTouchEvent(event);

    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            startX = event.getRawX();
            startY = event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:

            float endX = event.getRawX();
            float endY = event.getRawY();

            float dx = endX - startX;
            float dy = endY - startY;

            LayoutParams params = (LayoutParams) mVideoPlayerControl.getContainer().getLayoutParams();

            int left = (int) (params.leftMargin + dx);
            int top = (int) (params.topMargin + dy);
            int viewHeight = mVideoPlayerControl.getContainer().getHeight() + 50;
            int viewWidth = mVideoPlayerControl.getContainer().getWidth();

            if(left < -1){
                left = 0;
            }else if(left > screenWidth - viewWidth){
                left = screenWidth - viewWidth;
            }

            if(top < -1){
                top = 0;
            }else if(top > screenHeight - viewHeight){
                top = screenHeight - viewHeight;
            }

            params.leftMargin = left;
            params.topMargin = top;
            mVideoPlayerControl.getContainer().setLayoutParams(params);

            startX = endX;
            startY = endY;
            break;
    }

    return super.onTouchEvent(event);
}

比較簡單,就不多解釋了。

VideoPlayerManager

這個類的作用就是保證在recyclerview或者listview中播放時只有一個列表可以播放。

/**
 * 單例管理視頻播放  在listview或者recyclerview中保證頁面只有一個播放器在播放
 */
public class VideoPlayerManager {

    private VideoPlayer mVideoPlayer;

    private VideoPlayerManager(){}

    private static VideoPlayerManager sInstance;

    public static VideoPlayerManager getInstance(){

        if(sInstance == null){
            sInstance = new VideoPlayerManager();
        }

        return sInstance;
    }

    public void releaseMediaplayer(){
        if(mVideoPlayer != null){
            mVideoPlayer.release();
            mVideoPlayer = null;
        }
    }

    public void setCurrentVideoPlayer(VideoPlayer videoPlayer){
        mVideoPlayer = videoPlayer;
    }

    /**
     *  釋放資源
     */
    public boolean onBackPress(){

        if(mVideoPlayer.isFullScreen()){
            mVideoPlayer.exitFullScreen();
            return true;
        }else if(mVideoPlayer.isTinyScreen()){
            mVideoPlayer.exitTinyScreen();  //退出小屏
            return true;
        }

        if(mVideoPlayer != null){
            mVideoPlayer.release();
        }

        return false;
    }
}

用法

private VideoPlayer videoPlayer;
private VideoPlayerController mController;

videoPlayer = (VideoPlayer) findViewById(R.id.vp);
videoPlayer.setPlayUri("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-33-30.mp4");
mController = new VideoPlayerController(this);
mController.setTitle("辦公室小野開番外了,居然在辦公室開澡堂!老板還點贊?");
mController.setImage("http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg");
videoPlayer.setController(mController);

效果

這里寫圖片描述

源碼:github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容