【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,用於捕獲事件。該接口定義如下:
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();
}
});
}
}