【Android】 從頭搭建視頻播放器(1)——概述

【Android】 從頭搭建視頻播放器(1)——概述


        轉載請註明出處http://blog.csdn.net/arnozhang12/article/details/48731443
 
 
        近來有做播放器方面的需求,在搭建過程中,逐漸對 Android 上面視頻播放器的實現有了一些初步的瞭解,在此總結一下,在 Android 上面,如何從頭考慮設計並最終實現一個功能完備的視頻播放器。

1、功能 & 思路

        我們通常看到一個通用的播放器如下:

半屏播放器

        在點擊全屏按鈕或者旋轉屏幕後,可以展開到全屏:

全屏播放器

        我們可以看出,一個通用的播放器有如下一些功能點:

  • 播放/暫停
  • 全屏切換
  • SeekBar 進度調節
  • 手勢調節屏幕亮度/音量/播放進度
  • 屏幕旋轉支持

        其中,播放器的基礎功能我們可以使用系統的 MediaPlayer 或者第三方的一些 Player 實現。全屏切換可以通過更改 ScreenOrientation屏幕布局來完成。其他的手勢調節可以通過 GestureDetector 來完成。屏幕旋轉通過 OrientationEventListener 來實現。

2、基礎設計

        有了一個大致的功能描述以及實現思路之後,我們需要設計底層的基本交互及類。經過功能提煉及交互劃分,首先的模塊設計如下:

基礎結構設計

  • BaseMediaPlayer:定義了一個播放器應該具備的基礎接口;你可以跟進這個接口再加播放器底層,來實現不同的播放器;只提供播放的接口,不提供用戶 UI 交互;
  • SystemMediaPlayerImpl:繼承自 BaseMediaPlayer,是基於 Android 系統 MediaPlayer 的一份實現;
  • StrawMediaPlayer:繼承自 FrameLayout,是 Android 上的一個佈局 View,封裝了播放器的所有操作,用來進行可視化和用戶 UI 交互;
  • PlayerBottomControl:播放器底部控件,用於控制播放、進度、全屏調節等;
  • MediaPlayerGestureController:播放器手勢控制器,用於手勢識別和相應的控制;
  • ScreenOrientationSwitcher:屏幕方向切換控制器。

        基礎的模塊就這麼一些,其中應該還有一些用於展示屏幕亮度、聲音、進度的小的 View,自己可以輕鬆實現,在此沒有列出來。

        StrawMediaPlayer 就是我們最終提供出去的播放器控件,上層可以直接使用這個控件。

3、播放器佈局

        整個 StrawMediaPlayer 的佈局如下:

界面佈局

        通過結合功能點進行初步設計,整個播放器層級如下:

  • FrameLayout:用於容納所有的 View;
  • SurfaceView:用於展示視頻內容;
  • LoadingLayout:在視頻加載的時候,用於展示 Loading 畫面;
  • TopBarControl:頂部的 Bar,用於展示視頻信息、返回按鈕等等;
  • PlayerBottomControl:底部的 Bar,用於展示視頻控制按鈕、播放進度、全屏切換等 View。

4、BaseMediaPlayer

        結合模塊設計及佈局劃分,我們先定義 BaseMediaPlayer 應該具備的接口。必須具備的接口如下:

接口 功能
BaseMediaPlayer(context, surfaceView) 構造函數
play(videoInfo) 播放
addPlayerListener(listener) 添加播放回調 Listener
pause() 暫停
resume() 恢復播放
stop() 停止播放
seekTo(millSeconds) 跳轉到某個進度播放
setVolume(volume) 調節音量
getDuration() 獲取整個視頻的時間,返回 ms
getCurrentPosition() 獲取視頻當前播放的進度,返回 ms
doDestroy() 銷燬播放器,釋放系統資源
isPlaying() 是否正在播放
isLoading() 是否正在加載視頻
getBufferPercent() 獲取視頻緩衝百分比

5、BaseMediaPlayerListener

        外部有可能需要監聽一系列 Player 的事件,這時候,我們需要通過 addPlayerListener 添加一個 Listener,用於捕獲事件。該接口定義如下:

BaseMediaPlayerListener

6、BaseMediaPlayer 代碼片段

/**
 * BaseMediaPlayer.java
 *
 * @author arnozhang
 * @email  [email protected]
 * @date   2015.9.25
 */
public abstract class BaseMediaPlayer {

    protected static final String TAG = "MediaPlayer";


    protected static interface NotifyListenerRunnable {
        void run(BaseMediaPlayerListener listener);
    }


    protected Context mContext;
    protected SurfaceView mSurfaceView;
    protected SurfaceHolder.Callback mSurfaceCallback;
    protected List<BaseMediaPlayerListener> mPlayerListeners = new ArrayList<>();
    protected VideoInfo mVideoInfo;
    protected int mVideoWidth;
    protected int mVideoHeight;
    protected boolean mIsLoading;
    protected boolean mMediaPlayerIsPrepared;
    protected boolean mVideoSizeInitialized;
    protected int mBufferPercent;
    protected int mVideoContainerZoneWidth;
    protected boolean mAutoPlayWhenHolderCreated;


    public BaseMediaPlayer(Context context, SurfaceView surfaceView) {
        mContext = context;
        mSurfaceView = surfaceView;

        initSurfaceCallback();
        SurfaceHolder videoHolder = mSurfaceView.getHolder();
        videoHolder.addCallback(mSurfaceCallback);
    }

    public void play(VideoInfo videoInfo) {
        if (!PlayerUtils.isNetworkAvailable()) {
            StrawToast.makeText(mContext, R.string.network_connection_failed).show();
            return;
        }

        mIsLoading = true;
        mVideoSizeInitialized = false;
        mMediaPlayerIsPrepared = false;
        mVideoInfo = videoInfo;

        Handler handler = ThreadManager.getInstance().getUIHandler();
        handler.removeCallbacks(mLoadingFailedRunnable);
        handler.postDelayed(mLoadingFailedRunnable, 30 * 1000);
    }

    public void addPlayerListener(BaseMediaPlayerListener listener) {
        mPlayerListeners.add(listener);
    }

    public abstract void pause();

    public abstract void resume();

    public abstract void stop();

    public abstract void seekTo(int millSeconds);

    public abstract void setVolume(float volume);

    public abstract int getDuration();

    public abstract int getCurrentPosition();

    public abstract void doDestroy();

    public abstract boolean isPlaying();

    protected abstract void playWithDisplayHolder(SurfaceHolder holder);

    public boolean isLoading() {
        return mIsLoading;
    }

    public boolean isLoadingOrPlaying() {
        return isLoading() || isPlaying();
    }

    public int getBufferPercent() {
        return mBufferPercent;
    }

    public Context getContext() {
        return mContext;
    }

    private void initSurfaceCallback() {
        mSurfaceCallback = new SurfaceHolder.Callback() {
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }

            public void surfaceCreated(SurfaceHolder holder) {
                if (mAutoPlayWhenHolderCreated) {
                    mAutoPlayWhenHolderCreated = false;
                    playWithDisplayHolder(holder);
                }
            }

            public void surfaceDestroyed(SurfaceHolder holder) {
                if (isPlaying()) {
                    stop();
                }
            }
        };
    }

    protected void updateSurfaceSize() {
        updateSurfaceSize(mVideoContainerZoneWidth);
    }

    public void updateSurfaceSize(int containerWidth) {
        if (mVideoContainerZoneWidth == containerWidth) {
            return;
        }

        mVideoContainerZoneWidth = containerWidth;
        if (mVideoWidth == 0 || mVideoHeight == 0) {
            return;
        }

        final float ratio = (float) mVideoWidth / (float) mVideoHeight;
        int width = 0;
        int height = (int) (containerWidth / ratio);
        if (height > mVideoHeight) {
            width = containerWidth;
        } else {
            height = mVideoHeight;
            width = (int) (mVideoWidth * ratio);
        }

        ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();
        params.width = width;
        params.height = height;
        mSurfaceView.setLayoutParams(params);
    }

    protected Runnable mLoadingFailedRunnable = new Runnable() {
        @Override
        public void run() {
            notifyLoadFailed();
        }
    };

    protected void notifyListener(NotifyListenerRunnable runnable) {
        for (BaseMediaPlayerListener listener : mPlayerListeners) {
            runnable.run(listener);
        }
    }

    protected void notifyLoading() {
        LogUtils.e(TAG, "MediaPlayer Loading...");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onLoading();
            }
        });
    }

    protected void notifyFinishLoading() {
        LogUtils.e(TAG, "MediaPlayer Finish Loading!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onFinishLoading();
            }
        });
    }

    protected void notifyLoadFailed() {
        LogUtils.e(TAG, "MediaPlayer Load **Failed**!!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onLoadFailed();
            }
        });
    }

    protected void notifyError(final int what, final String message) {
        LogUtils.e(TAG, "MediaPlayer Error. what = %d, message = %s.", what, message);

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onError(what, message);
            }
        });
    }

    protected void notifyStartPlay() {
        LogUtils.e(TAG, "MediaPlayer Will Play!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onStartPlay();
            }
        });
    }

    protected void notifyPlayComplete() {
        LogUtils.e(TAG, "MediaPlayer Play Current Complete!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onPlayComplete();
            }
        });
    }

    protected void notifyPaused() {
        LogUtils.e(TAG, "MediaPlayer Paused.");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onPaused();
            }
        });
    }

    protected void notifyResumed() {
        LogUtils.e(TAG, "MediaPlayer Resumed.");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onResumed();
            }
        });
    }

    protected void notifyStopped() {
        LogUtils.e(TAG, "MediaPlayer Stopped!");

        notifyListener(new NotifyListenerRunnable() {
            @Override
            public void run(BaseMediaPlayerListener listener) {
                listener.onStopped();
            }
        });
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章