視頻播放是咱們開發中比較常見的場景。這兩年關於視頻方面的熱度不斷提高,能夠說前兩年是直播年,今年是小視頻年,各類短視頻應用鋪天蓋地。對於視頻的業務場景也愈來愈豐富,功能也愈來愈多。對於咱們開發來講播放相關組件的代碼變得也愈來愈複雜,管理維護成本也愈來愈高,面對不斷迭代的業務,咱們須要一種有效的方案來應對這種頻繁的業務變化。java
這幾年一直在作視頻相關的業務,手機端和TV端均作過適配開發。MediaPlayer、exoplayer、ijkplayer、VLC、FFmpeg等都摸索使用過。這一路遇到不少問題……說多了都是淚,爲了適應多變的產品需求,中間重構了N多個版本。最終PlayerBase也就誕生了。PlayerBase3 版本進行了完整重構設計,目前大體框架基本已穩定下來。對於大部分應用視頻播放組件場景都能輕鬆處理。android
^_^ star傳送門--->項目地址:github.com/jiajunhui/P…git
QQ交流羣:600201778 ,有問題羣裏直接提出,看到後會一一解答。github
P圖技術有限,文中圖片就湊合着看吧!服務器
請注意! 請注意! 請注意! PlayerBase區別於大部分播放器封裝庫。markdown
PlayerBase是一種將解碼器和播放視圖組件化處理的解決方案框架。您須要什麼解碼器實現框架定義的抽象引入便可,對於視圖,不管是播放器內的控制視圖仍是業務視圖,都可以作到組件化處理。將播放器的開發變得清晰簡單,更利於產品的迭代。網絡
PlayerBase不會爲您作任何多餘的功能業務組件,有別於大部分播放器封裝庫的經過配置或者繼承而後重寫而後定製你須要的功能組件和屏蔽你不須要的功能組件(這種以前我也經歷過,上層可能須要常常改動,感受很low!!!)。正確的方向應該是須要什麼組件就拓展添加什麼組件,不須要時移除便可,而不是已經提供了該組件去選擇用不用。框架
public class App extends Application {
@Override
public void onCreate() {
//...
//若是您想使用默認的網絡狀態事件生產者,請添加此行配置。
//並須要添加權限 android.permission.ACCESS_NETWORK_STATE
PlayerConfig.setUseDefaultNetworkEventProducer(true);
//設置默認解碼器
int defaultPlanId = 1;
PlayerConfig.addDecoderPlan(new DecoderPlan(defaultPlanId, IjkPlayer.class.getName(), "IjkPlayer"));
PlayerConfig.setDefaultPlanId(defaultPlanId);
//初始化庫
PlayerLibrary.init(this);
}
}
複製代碼
ReceiverGroup receiverGroup = new ReceiverGroup();
//Loading組件
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
//Controller組件
receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
//CompleteCover組件
receiverGroup.addReceiver(KEY_COMPLETE_COVER, new CompleteCover(context));
//Error組件
receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
複製代碼
BaseVideoView videoView = findViewById(R.id.videoView);
videoView.setReceiverGroup(receiverGroup);
DataSource data = new DataSource("http://url...");
videoView.setDataSource(data);
videoView.start();
複製代碼
//player event
videoView.setOnPlayerEventListener(new OnPlayerEventListener(){
@Override
public void onPlayerEvent(int eventCode, Bundle bundle){
//...
}
});
//receiver event
videoView.setOnReceiverEventListener(new OnReceiverEventListener(){
@Override
public void onReceiverEvent(int eventCode, Bundle bundle) {
//...
}
});
複製代碼
詳細使用示例請參閱github項目主頁及wiki介紹ide
別小看一個小小的播放器,裏面真的是別有洞天。有時視圖組件複雜到你懷疑人生。oop
咱們先看下播放器開發時常見的一些視圖場景:
以上是咱們最多見到的一些視圖(其實還有不少,好比清晰度切換、視頻列表、播放完成提示頁等等),這些視圖若是沒有一個行之有效的方案來進行管理,將逐漸會亂到失控。
上面只是列出了控制器視圖、加載視圖、手勢視圖、錯誤視圖、彈幕視圖和廣告視圖,這一股腦的視圖都是和播放緊密相連的,徹底由播放狀態驅動,視圖之間可能共存、可能制約。
那麼這些視圖如何進行統一的管理呢?光佈局文件就夠喝一壺了吧,即使用include來管理依然擺脫不了顯示層級的管理問題。要是一股腦全寫到一個xml中,想一想均可怕……, 改進型的通常都是把每一個組件封裝成View了,而後再分別寫到佈局中,顯然比前一種要輕鬆一些。可是,可是播放器和組件間的通訊、組件與組件間的通訊是個問題。依然有問題存在:
接下來,且看PlayerBase如何作。
作過播放器開發的應該都很清楚一點,全部視圖的工做都是由狀態事件來驅動的,這是一條主線。有多是來自播放器的事件(好比解碼器出錯了),也有多是來自某個視圖的事件(好比手勢調節播放進度),還有多是外部事件(好比網絡狀態變化)。
這些信息咱們能夠歸結爲
也就是說咱們把視圖當作事件接收者,同時視圖具有發送事件的能力。
解碼器不斷髮出本身工做狀態的事件要傳遞給視圖。
外部的某些事件也須要傳遞給視圖
至此,框架內部定義了事件接收者的概念,接收者做爲事件消費者的同時也能生產事件,而覆蓋層繼承自接收者引入了視圖View。
public abstract class BaseReceiver implements IReceiver {
//...
protected final void notifyReceiverEvent(int eventCode, Bundle bundle){
//..
}
/** * all player event dispatch by this method. */
void onPlayerEvent(int eventCode, Bundle bundle);
/** * error event. */
void onErrorEvent(int eventCode, Bundle bundle);
/** * receivers event. */
void onReceiverEvent(int eventCode, Bundle bundle);
/** * you can call this method dispatch private event for a receiver. * * @return Bundle Return value after the receiver's response, nullable. */
@Nullable
Bundle onPrivateEvent(int eventCode, Bundle bundle);
}
複製代碼
public abstract class BaseCover extends BaseReceiver{
//...
public abstract View onCreateCoverView(Context context);
//...
}
複製代碼
且看代碼,有播放器的事件、有錯誤事件、有組件(Receiver)間的事件。這衆多事件如何下發呢,若是有N多個接收者呢,如何破?
ReceiverGroup的出現目的就是對衆多接收者進行統一的管理,統一的事件下發,固然還有下面的數據共享問題。來張圖:
在ReceiverGroup中包含Cover(其實也是Receiver)和Receiver,提供了Receiver的添加、移除、遍歷、銷燬等操做。當有事件須要下發時,即可經過ReceiverGroup進行統一的遍歷下發。
public interface IReceiverGroup {
void setOnReceiverGroupChangeListener(OnReceiverGroupChangeListener onReceiverGroupChangeListener);
/** * add a receiver, you need put a unique key for this receiver. * @param key * @param receiver */
void addReceiver(String key, IReceiver receiver);
/** * remove a receiver by key. * @param key */
void removeReceiver(String key);
/** * loop all receivers * @param onLoopListener */
void forEach(OnLoopListener onLoopListener);
/** * loop all receivers by a receiver filter. * @param filter * @param onLoopListener */
void forEach(OnReceiverFilter filter, OnLoopListener onLoopListener);
/** * get receiver by key. * @param key * @param <T> * @return */
<T extends IReceiver> T getReceiver(String key);
/** * get the ReceiverGroup group value. * @return */
GroupValue getGroupValue();
/** * clean receivers. */
void clearReceivers();
}
複製代碼
播放器開發中不少時候咱們須要依據某個視圖的狀態來限制另外視圖的功能或狀態,好比當處於加載中時禁止拖動進度條或者播放出錯顯示error後禁止其餘視圖操做等等。這些都屬於狀態上的相互制約。
GroupValue就至關於提供了一個共享的數據池,當某個數據被刷新時,監聽該數據的回調接口能及時收到通知,固然也能夠直接去主動獲取數據狀態。你能夠指定你要監聽那些數據的更新事件,若是您註冊了您要監聽的數據的key值,其對應的value被更新時,您就會收到回調。而後您能夠在回調中進行UI視圖的控制。
public class CustomCover extends BaseCover{
//...
@Override
public void onReceiverBind() {
super.onReceiverBind();
getGroupValue().registerOnGroupValueUpdateListener(mOnGroupValueUpdateListener);
}
//...
private IReceiverGroup.OnGroupValueUpdateListener mOnGroupValueUpdateListener =
new IReceiverGroup.OnGroupValueUpdateListener() {
@Override
public String[] filterKeys() {
return new String[]{ DataInter.Key.KEY_COMPLETE_SHOW };
}
@Override
public void onValueUpdate(String key, Object value) {
//...
}
};
//...
@Override
public void onReceiverUnBind() {
super.onReceiverUnBind();
getGroupValue().unregisterOnGroupValueUpdateListener(mOnGroupValueUpdateListener);
}
}
複製代碼
上文中常見的視圖組件,咱們在使用中確定會遇到覆蓋優先級的問題。舉個栗子,好比Error視圖出現後其餘的視圖一律不可見,也就是說Error視圖的優先級是最高的,誰都不能擋着它,咱們建立了一個個的Cover視圖,對於視圖的放置就須要一個視圖的優先級標量(CoverLevel)來進行控制,不一樣的Level的Cover視圖會被放置於不一樣級別的容器內。
總結爲如下:
示意圖 代碼示例
public class CustomCover extends BaseCover{
//...
@Override
public int getCoverLevel() {
return ICover.COVER_LEVEL_LOW;
}
//...
}
複製代碼
默認的視圖容器管理器
public class DefaultLevelCoverContainer extends BaseLevelCoverContainer {
//...
@Override
protected void onAvailableCoverAdd(BaseCover cover) {
super.onAvailableCoverAdd(cover);
switch (cover.getCoverLevel()){
case ICover.COVER_LEVEL_LOW:
mLevelLowCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
break;
case ICover.COVER_LEVEL_MEDIUM:
mLevelMediumCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
break;
case ICover.COVER_LEVEL_HIGH:
mLevelHighCoverContainer.addView(cover.getView(),getNewMatchLayoutParams());
break;
}
}
//...
}
複製代碼
如圖:
顧名思義,就是它是產生事件的源。好比系統網絡狀態發生了變化,發出了通知,而後各個應用根據本身的狀況來調整顯示或設置等。又或者電池電量的變化和低電量預警通知事件等。
再好比,咱們上文中的彈幕視圖中須要顯示彈幕數據,彈幕數據來自服務器,咱們須要源源不斷的從服務器上取數據,而後顯示在彈幕視圖。取回數據傳給視圖的這個過程咱們能夠將其看做是一個事件生產者在不斷生產彈幕數據更新事件,彈幕數據更新時不斷將事件發送給彈幕視圖來刷新顯示。
框架內自帶了一個網絡變化事件生產者的示例:
public class NetworkEventProducer extends BaseEventProducer {
//...
private Handler mHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case MSG_CODE_NETWORK_CHANGE:
int state = (int) msg.obj;
//...將網絡狀態發送出去
getSender().sendInt(InterKey.KEY_NETWORK_STATE, state);
PLog.d(TAG,"onNetworkChange : " + state);
break;
}
}
};
//...
public NetworkEventProducer(Context context){
//...
}
//...
public static class NetChangeBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//...
//post state message
}
//...
}
}
複製代碼
因爲事件生產者所發出的事件是針對Receiver的,因此會被回調到onReceiverEvent()中,若是發送的是key-value的數據,會被放置於GroupValue中。以下代碼:
public class CustomCover extends BaseCover{
//...
@Override
public void onReceiverEvent(int eventCode, Bundle bundle) {
//...
}
//...
}
複製代碼
DataProvider是爲了播控的統一以及使用上的優雅而設計的。
在開發中,咱們可能會遇到以下場景:你拿到的數據源可能只是個id之類的標識,並非能直接播放的uri或者url,須要你再用這個id去請求一個接口才能拿到播放的源地址。一般咱們都是先去請求接口,而後在成功回調中用拿到的源數據再設置給播放器去播放。
DataProvider的設計就是爲了將此過程獨立出來包裝爲一個數據提供者(其實也能夠叫數據生產者),拿到數據後發送出去便可。而您只須要把那個id標識給DataProvider便可,接下來的過程就由DataProvider來完成了。DataProvider的具體實現須要由用戶完成。
public class MonitorDataProvider extends BaseDataProvider {
//...
public MonitorDataProvider(){
//...
}
private Handler mHandler = new Handler(Looper.getMainLooper());
@Override
public void handleSourceData(DataSource sourceData) {
this.mDataSource = sourceData;
//...provider start
onProviderDataStart();
//...
//...將數據回調出去
onProviderMediaDataSuccess(bundle);
//...
//...異常時
onProviderError(-1, null)
}
//...
@Override
public void cancel() {
//...cancel something
}
@Override
public void destroy() {
//...destroy something
}
}
複製代碼
注意: 數據提供者必需要設置在啓動播放前。
大體歸結爲如下步驟:
public class VideoViewActivity extends AppCompatActivity implements OnPlayerEventListener{
//...
BaseVideoView mVideoView;
@Override
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
mVideoView = findViewById(R.id.videoView);
mVideoView.setOnPlayerEventListener(this);
//設置數據提供者 MonitorDataProvider
MonitorDataProvider dataProvider = new MonitorDataProvider();
mVideoView.setDataProvider(dataProvider);
//...
ReceiverGroup receiverGroup = new ReceiverGroup();
//Loading組件
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
//Controller組件
receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context));
//CompleteCover組件
receiverGroup.addReceiver(KEY_COMPLETE_COVER, new CompleteCover(context));
//Error組件
receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
//...
DataSource data = new DataSource("monitor_id");
videoView.setDataSource(data);
videoView.start();
}
//...
public void onPlayerEvent(int eventCode, Bundle bundle){
switch (eventCode){
case OnPlayerEventListener.PLAYER_EVENT_ON_VIDEO_RENDER_START:
//...
break;
case OnPlayerEventListener.PLAYER_EVENT_ON_PLAY_COMPLETE:
//...
break;
}
}
//...
@Override
public void onPause(){
super.onPause();
mVideoView.pause();
//...
}
@Override
public void onResume(){
super.onResume();
mVideoView.onResume();
//...
}
@Override
public void onDestroy(){
super.onDestroy();
mVideoView.stopPlayback();
//...
}
}
複製代碼
若是您想直接使用AVPlayer本身進行處理播放,那麼大體步驟以下:
SuperContainer mSuperContainer = new SuperContainer(context);
ReceiverGroup receiverGroup = new ReceiverGroup();
//...add some covers
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
mSuperContainer.setReceiverGroup(receiverGroup);
//...
final RenderTextureView render = new RenderTextureView(mAppContext);
render.setTakeOverSurfaceTexture(true);
//....
mPlayer.setOnPlayerEventListener(new OnPlayerEventListener() {
@Override
public void onPlayerEvent(int eventCode, Bundle bundle) {
//...此處須要根據事件自行實現一些特定的設置
//...好比視頻的尺寸須要傳遞Render刷新測量或者視頻的角度等等
//將事件分發給子視圖
mSuperContainer.dispatchPlayEvent(eventCode, bundle);
}
});
mPlayer.setOnErrorEventListener(new OnErrorEventListener() {
@Override
public void onErrorEvent(int eventCode, Bundle bundle) {
//將事件分發給子視圖
mSuperContainer.dispatchErrorEvent(eventCode, bundle);
}
});
//...
render.setRenderCallback(new IRender.IRenderCallback() {
@Override
public void onSurfaceCreated(IRender.IRenderHolder renderHolder, int width, int height) {
mRenderHolder = renderHolder;
bindRenderHolder(mRenderHolder);
}
@Override
public void onSurfaceChanged(IRender.IRenderHolder renderHolder, int format, int width, int height) {
}
@Override
public void onSurfaceDestroy(IRender.IRenderHolder renderHolder) {
mRenderHolder = null;
}
});
mSuperContainer.setRenderView(render.getRenderView());
mPlayer.setDataSource(dataSource);
mPlayer.start();
複製代碼
若是非必須,請儘可能使用框架封裝好的BaseVideoView進行播放,框架相對來講處理的比較完善且提供了豐富的回調和定製性。
如今的短視頻應用都有這樣的場景:
對於第一條在列表中播放,理論上VideoView就能完成,可是VideoView用在列表中量級較重,不太適合。須要一個輕量化處理的方案。
而對於第二條,VideoView就不行了,VideoView是對解碼器進行了包裝,當跳到下一個頁面時,是一個新的頁面天然有新的視圖,沒法使用前一個頁面的播放器實例去渲染當前頁面播放。
其實對於這種無縫的續播,原理很簡單。就是不一樣的渲染視圖使用同一個解碼實例便可。能夠簡單比做一個MediaPlayer去不斷設置不一樣的surface呈現播放。若是本身處理這個過程的話想對比較繁瑣,你須要處理Render的回調並關聯給解碼器,還須要本身處理Render的測量以及顯示比例、角度等等問題。
RelationAssist 就是爲了簡化這個過程而設計的。在不一樣的頁面或視圖切換播放時,您只須要提供並傳入對應位置的視圖容器(ViewGroup類型)便可。內部複雜的設置項和關聯由RelationAssist完成。
public class TestActivity extends AppcompatActivity{
//...
RelationAssist mAssist;
ViewGroup view2;
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
//...
mAssist = new RelationAssist(this);
mAssist.setEventAssistHandler(eventHandler);
mReceiverGroup = ReceiverGroupManager.get().getLiteReceiverGroup(this);
mAssist.setReceiverGroup(mReceiverGroup);
DataSource dataSource = new DataSource();
dataSource.setData("http://...");
dataSource.setTitle("xxx");
mAssist.setDataSource(dataSource);
mAssist.attachContainer(mVideoContainer);
mAssist.play();
//...
switchPlay(view2);
}
//...
private void switchPlay(ViewGroup container){
mAssist.attachContainer(container);
}
}
複製代碼
若是您想跨頁面進行關聯,只須要本身將RelationAssist包裝爲一個單例便可。此處不作代碼展現,詳細代碼可參見github項目demo代碼。
視圖中的一些基本操做,好比暫停播放、重播、重試、恢復播放等等,這些事件最終都要傳遞給解碼器進行相關操做。可能還有用戶自定義的事件好比播放下一個或上一個等。
對於基本的操做事件(暫停、恢復、重播等),框架內部可自動完成,而用戶自定的事件須要讓用戶自行處理。框架內部BaseVideoView和RelationAssist均作了EventAssistHandler的對接,使用時須要傳入一個可用的事件處理器對象,可根據不一樣的事件參數進行相應處理。以下代碼:
mVideoView.setOnVideoViewEventHandler(new OnVideoViewEventHandler(){
@Override
public void onAssistHandle(BaseVideoView assist, int eventCode, Bundle bundle) {
//基本的事件處理已在父類super中完成,若是須要重寫,重寫相應方法便可。
super.onAssistHandle(assist, eventCode, bundle);
switch (eventCode){
case DataInter.Event.EVENT_CODE_REQUEST_NEXT:
//...播放下一個
break;
}
}
});
複製代碼
咱們有時可能爲了避免打斷用戶的瀏覽須要小窗播放。框架特地設計了window播放的使用。框架提供了兩種window相關的組件。
WindowVideoView使用上幾乎和VideoView是同樣的,只不過WindowVideoView是以window的形式呈現的。window默認是能夠拖動的,若是您不須要,能夠禁止,window的每一個設置項都有默認值,window的設置示例代碼:
FloatWindowParams windowParams = new FloatWindowParams();
windowParams.setWindowType(WindowManager.LayoutParams.TYPE_TOAST)
.setFormat(PixelFormat.RGBA_8888)
.setFlag(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
.setDefaultAnimation(true)
.setX(100)
.setY(100)
.setWidth(width)
.setHeight(height)
.setGravity(Gravity.TOP | Gravity.LEFT));
mWindowVideoView = new WindowVideoView(this,windowParams);
//...
複製代碼
而FloatWindow只是一個懸浮窗View,您能夠傳入您要顯示的佈局View。能夠用於窗口切換播放時的無縫續播。此處不作代碼示例展現。
樣式的設置是針對 VideoView、WindowVideoView 和 FloatWindow 的。固然框架提供的StyleSetter您也能夠用於別處。提供了以下的樣式設置:
public interface IStyleSetter {
//設置圓角
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setRoundRectShape(float radius);
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setRoundRectShape(Rect rect, float radius);
//設置爲圓形
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setOvalRectShape();
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setOvalRectShape(Rect rect);
//清除樣式設置
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void clearShapeStyle();
//設置陰影
//注意陰影的設置要求對應的View對象必需要有背景色(不能是TRANSPARENT)
//若是您沒設置,框架內部會自定設置爲黑色
void setElevationShadow(float elevation);
void setElevationShadow(int backgroundColor, float elevation);
}
複製代碼
框架自帶了系統的MediaPlayer的解碼實現,項目demo中示例接入了ijkplayer和exoplayer,若是您想接入其餘的解碼器,請參見示例代碼,如下爲簡單示例,更詳細的請參見項目源碼。
接入步驟
public class XXXPlayer extends BaseInternalPlayer{
public XXXPlayer() {
//...
}
//...
//implements some abstract methods.
}
複製代碼
經過配置設置使用該解碼器。
int planId = 2;
PlayerConfig.addDecoderPlan(new DecoderPlan(planId, XXXPlayer.class.getName(), "XXXPlayer"));
PlayerConfig.setDefaultPlanId(planId);
複製代碼
以上對於PlayerBase的講解基本完成。碼字好累!!!
主要的模塊差很少就這麼多了,更詳細的可參見項目源碼。
若有問題聯繫:junhui_jia@163.com
QQ交流羣:600201778
最後再附上項目地址:PlayerBase