寫在前面:
- 因項(xiàng)目需要,需要使用到視頻播放相關(guān)技術(shù),雖然系統(tǒng)提供了播放器VideoView,但由于各種原因無(wú)法滿足項(xiàng)目需要,特將播放器封裝成庫(kù),方便日后項(xiàng)目使用及自定義拓展。 此文章適合未接觸過(guò)視頻播放相關(guān)、沒(méi)有時(shí)間來(lái)研究視頻播放相關(guān)、不想寫UI交互直接用現(xiàn)成的成熟播放器 的開發(fā)者閱讀,大神大牛請(qǐng)繞路。
給大家推薦視頻播放器iPlayer,支持的特性包括但不限于:
- 支持網(wǎng)絡(luò)地址、直播流、本地Assets和Raw音視頻資源文件播放
- 支持IJKPlayer、ExoPlayer、MediaPlayer和其它更多自定義解碼器
- 支持自定義視頻解碼器、控制器、UI交互組件、視頻畫面渲染器
- 支持播放倍速、縮放模式、靜音、鏡像等功能設(shè)置
- 支持多播放器同時(shí)播放、跳轉(zhuǎn)到詳情無(wú)縫銜接播放
- 支持重力感應(yīng)橫豎屏旋轉(zhuǎn)及開關(guān)設(shè)置
- 支持無(wú)權(quán)限開啟Activity級(jí)別窗口播放及全局懸浮窗窗口播放
- 窗口播放器支持自動(dòng)吸附、懸停
- Demo仿抖音播放示例,支持視頻緩存、秒播、彈幕交互等
如Github無(wú)法訪問(wèn)可訪問(wèn)碼云項(xiàng)目地址
一、播放器框架設(shè)計(jì)

iPlayer架構(gòu)關(guān)系圖
二、播放器功能實(shí)現(xiàn)
1、畫面渲染(TextureView)
1.1、TextureView創(chuàng)建及設(shè)置Surface監(jiān)聽
TextureView textureView =new TextureView(context);
textureView .setSurfaceTextureListener(this);
1.2、在TextureView初始化完成的onSurfaceTextureAvailable回調(diào)里將SurfaceTexture與MediaPlayer綁定
private MediaTextureView mTextureView;//畫面渲染
private Surface mSurface;
private SurfaceTexture mSurfaceTexture;
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
// ILogger.d(TAG,"onSurfaceTextureAvailable-->width:"+width+",height:"+height);
if(null==mTextureView||null==mMediaPlayer) return;
if(null!=mSurfaceTexture){
mTextureView.setSurfaceTexture(mSurfaceTexture);
}else{
mSurfaceTexture = surfaceTexture;
mSurface =new Surface(surfaceTexture);
mMediaPlayer.setSurface(mSurface);
}
}
2、全屏播放
2.1、開啟全屏播放
- 全屏分三個(gè)步驟:1、保存播放器父容器ViewGroup。2、改變屏幕方向?yàn)闄M屏。3、將播放器添加到Window中。
/**
* 全屏播放
* @param bgColor 開啟全屏模式播放:橫屏?xí)r播放器的背景顏色,內(nèi)部默認(rèn)用黑色#000000
*/
@Override
public void startFullScreen(int bgColor) {
// ILogger.d(TAG,"startFullScreen");
if(mScreenOrientation==IMediaPlayer.ORIENTATION_LANDSCAPE) return;
Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
if (null != activity&& !activity.isFinishing()) {
ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
if(null==viewGroup){
return;
}
//1.保存播放器在父布局中的寬、高、index層級(jí)等屬性(如果存在的話)
mPlayerParams = new int[3];
mPlayerParams[0]=this.getMeasuredWidth();
mPlayerParams[1]=this.getMeasuredHeight();
if(null!=getParent()&& getParent() instanceof ViewGroup){
mParent = (ViewGroup) getParent();
mPlayerParams[2]=mParent.indexOfChild(this);//保存播放器本身的寬高和位于父容器的索引位置,恢復(fù)正常模式時(shí)需準(zhǔn)確的還原到父容器index
}
PlayerUtils.getInstance().removeViewFromParent(this);//從原宿主中移除自己
//2.改變屏幕方向?yàn)闄M屏狀態(tài),播放器所在的Activity需要添加屬性:android:configChanges="orientation|screenSize"
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);//改變屏幕方向
setScreenOrientation(IMediaPlayer.ORIENTATION_LANDSCAPE);//更新控制器方向狀態(tài)
findViewById(R.id.player_surface).setBackgroundColor(bgColor!=0?bgColor:Color.parseColor("#000000"));//設(shè)置一個(gè)背景顏色
//3.隱藏NavigationBar和StatusBar
hideSystemBar(viewGroup);
//4.添加到此播放器宿主context的window中
viewGroup.addView(this, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
}
}
2.2、退出全屏播放
- 退出全屏分四個(gè)步驟:1、Window窗口中移除自己。2、改變屏幕方向?yàn)樨Q屏。3、還原全屏設(shè)置為正常設(shè)置。4、將自己交給此前的宿主ViewGroup(還需要注意:還原播放器在原宿主的寬、高、index位置)
/**
* 退出全屏播放
*/
@Override
public void quitFullScreen() {
// ILogger.d(TAG,"quitLandscapeScreen");
Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
if(null!=activity&&!activity.isFinishing()){
ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
if(null==viewGroup){
return;
}
//1:從Window窗口中移除自己
PlayerUtils.getInstance().removeViewFromParent(this);
//2.改變屏幕方向?yàn)樨Q屏
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//改變屏幕方向
setScreenOrientation(IMediaPlayer.ORIENTATION_PORTRAIT);
findViewById(R.id.player_surface).setBackgroundColor(Color.parseColor("#00000000"));//設(shè)置純透明背景
//3.還原全屏設(shè)置為正常設(shè)置
showSysBar(viewGroup);
//3.將自己交給此前的宿主ViewGroup,并還原播放器在原宿主的寬、高、index位置
if(null!=mParent){
if(null!=mPlayerParams&&mPlayerParams.length>0){
// ILogger.d(TAG,"index:"+mPlayerParams[2]);
mParent.addView(this, mPlayerParams[2],new LayoutParams(mPlayerParams[0], mPlayerParams[1]));//將自己還原到父容器的index位置,取消了Gravity.CENTER屬性
}else{
mParent.addView(this, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
// ILogger.d(TAG,"quitLandscapeScreen-->已退出全屏");
}else{
//通知宿主監(jiān)聽器觸發(fā)返回事件
// ILogger.d(TAG,"quitLandscapeScreen-->退出全屏無(wú)宿主接收,銷毀播放器");
//無(wú)宿主接收時(shí)直接停止播放并銷毀播放器
onDestroy();
}
}
}
3、自定義控制器及UI交互組件
3.1、自定義控制器
- BasePlayer提供了setController(BaseController controller)方法給有需要UI交互的場(chǎng)景設(shè)置UI控制器
/**
* 設(shè)置視圖控制器
* @param controller 繼承VideoBaseController的控制器
*/
@Override
public void setController(BaseController controller) {}
3.2、自定義UI交互組件
- 為什么有自定義Controller還整個(gè)自定義UI交互組件?因?yàn)镃ontroller不適合處理所有UI交互,比如播放器的場(chǎng)景不同,UI也不盡相同,此時(shí)若把所有UI交互全寫在Controller會(huì)顯得臃腫、耦合性過(guò)高、開發(fā)者無(wú)法根據(jù)自己的需要來(lái)選擇和自定義部分UI交互。
- 自定義交互組件的使用
//播放器的準(zhǔn)備
VideoPlayer videoPlayer = new VideoPlayer(this);
videoPlayer.setBackgroundColor(Color.parseColor("#000000"));
VideoController controller=new VideoController(videoPlayer.getContext());
/**
* 給播放器設(shè)置控制器
*/
videoPlayer.setController(controller);
/**
* 給播放器控制器綁定需要的自定義UI交互組件
*/
ControlToolBarView toolBarView=new ControlToolBarView(this);//標(biāo)題欄,返回按鈕、視頻標(biāo)題、功能按鈕、系統(tǒng)時(shí)間、電池電量等組件
ControlFunctionBarView functionBarView=new ControlFunctionBarView(this);//底部時(shí)間、seek、靜音、全屏功能欄
functionBarView.showSoundMute(true,false);//啟用靜音功能交互\默認(rèn)不靜音
ControlStatusView statusView=new ControlStatusView(this);//移動(dòng)網(wǎng)絡(luò)播放提示、播放失敗、試看完成
ControlGestureView gestureView=new ControlGestureView(this);//手勢(shì)控制屏幕亮度、系統(tǒng)音量、快進(jìn)、快退UI交互
ControlCompletionView completionView=new ControlCompletionView(this);//播放完成、重試
ControlLoadingView loadingView=new ControlLoadingView(this);//加載中、開始播放
//將自定義UI交互組件設(shè)置到控制器
controller.addControllerWidget(toolBarView,functionBarView,statusView,gestureView,completionView,loadingView);
4、自定義解碼器
- SDK內(nèi)部封裝時(shí),為了方便開發(fā)者切換解碼器,將切換解碼器的入口封裝在播放器的監(jiān)聽器內(nèi),開發(fā)者可在回調(diào)方法返回自己的解碼器。
private int MEDIA_CORE=2;//這里默認(rèn)用ExoPlayer解碼器
//自定義解碼器
mVideoPlayer.setOnPlayerActionListener(new OnPlayerEventListener() {
@Override
public AbstractMediaPlayer createMediaPlayer() {
if (1 == MEDIA_CORE) {
return new JkMediaPlayer(LivePlayerActivity.this);
} else if (2 == MEDIA_CORE) {
return new ExoMediaPlayer(LivePlayerActivity.this);
} else {
return null;
}
}
});
- 自定義解碼器請(qǐng)參考Demo中的JkMediaPlayer和ExoMediaPlayer類。
5、轉(zhuǎn)場(chǎng)無(wú)縫銜接播放實(shí)現(xiàn)
5.1、列表轉(zhuǎn)場(chǎng)銜接繼續(xù)播放原理:
1、點(diǎn)擊跳轉(zhuǎn)到新的界面時(shí)將播放器從父容器中移除,并保存到全局變量
2、將全局變量播放器對(duì)象添加到新的ViewGroup容器
3、回到列表界面時(shí)如果播放的視頻源沒(méi)有被切換,關(guān)閉當(dāng)前Activity不要銷毀播放器,將播放器從當(dāng)前父容器中移除
4、重新添加到列表界面的此前正在播放的item中的ViewGroup中
5.2、列表轉(zhuǎn)場(chǎng)銜接繼續(xù)播放實(shí)現(xiàn):主要參考Demo中的ListPlayerChangedFragment、ListPlayerFragment、VideoDetailsActivity類
1、開始播放:參考ListPlayerFragment類的startPlayer()方法,注意標(biāo)記當(dāng)前mCurrentPosition和mPlayerContainer
2、點(diǎn)擊item跳轉(zhuǎn):參考ListPlayerChangedFragment類的onItemClick()方法,跳轉(zhuǎn)到新的Activity
3、新的Activity接收播放器繼續(xù)播放:參考VideoDetailsActivity類的initPlayer方法,根據(jù)mIsChange變量來(lái)確認(rèn)是否處理轉(zhuǎn)場(chǎng)播放。
4、新的Activity銷毀:新的Activity在關(guān)閉時(shí)如果播放器視頻地址未被切換,則在onDestroy中不要銷毀播放器,參考:VideoDetailsActivity類的onDestroy
5、回到列表界面:如果處理了第4步,在回到列表界面時(shí)接收并處理播放器,參考:ListPlayerChangedFragment類的onActivityResult方法和ListPlayerFragment類的recoverPlayerParent方法
6、Window窗口播放實(shí)現(xiàn)
/**
* 開啟Activity級(jí)別的小窗口播放
* @param width 窗口播放器的寬,當(dāng)小于=0時(shí)用默認(rèn)
* 開啟可拖拽的窗口播放
* 默認(rèn)寬為屏幕1/2+30dp,高為1/2+30dp的16:9比例,X起始位置為:播放器原宿主的右下方,距離原宿主View頂部15dp,右邊15dp(如果原宿主不存在,則位于屏幕右上角距離頂部60dp位置)
* 全局懸浮窗口和局部小窗口不能同時(shí)開啟
* 橫屏下不允許開啟
* @param height 窗口播放器的高,當(dāng)小于=0時(shí)用默認(rèn)
* @param startX 窗口位于屏幕中的X軸起始位置,當(dāng)小于=0時(shí)用默認(rèn)
* @param startY 窗口位于屏幕中的Y軸起始位置
* @param radius 窗口的圓角 單位:像素
* @param bgColor 窗口的背景顏色
*/
@Override
public void startWindow(int width, int height, float startX, float startY, float radius, int bgColor) {
ILogger.d(TAG,"startWindow-->width:"+width+",height:"+height+",startX:"+startX+",startY:"+startY+",radius:"+radius+",bgColor:"+bgColor+",windowProperty:"+ mIsActivityWindow +",screenOrientation:"+mScreenOrientation);
if(mIsActivityWindow ||mScreenOrientation==IMediaPlayer.ORIENTATION_LANDSCAPE) return;//已開啟窗口模式或者橫屏情況下不允許開啟小窗口
Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
if (null != activity&& !activity.isFinishing()) {
ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
if(null==viewGroup){
return;
}
int[] screenLocation=new int[2];
//保存播放器本身的寬高和位于父容器的索引位置,恢復(fù)正常模式時(shí)需準(zhǔn)確的還原到父容器index
mPlayerParams = new int[3];
mPlayerParams[0]=this.getMeasuredWidth();
mPlayerParams[1]=this.getMeasuredHeight();
//1.從原有豎屏窗口移除自己前保存自己的Parent,直接開啟全屏是不存在宿主ViewGroup的,可直接窗口轉(zhuǎn)場(chǎng)
if(null!=getParent()&& getParent() instanceof ViewGroup){
mParent = (ViewGroup) getParent();
mParent.getLocationInWindow(screenLocation);
mPlayerParams[2]=mParent.indexOfChild(this);
// ILogger.d(TAG,"startWindow-->parent_id:"+getId()+",parentX:"+screenLocation[0]+",parentY:"+screenLocation[1]+",parentWidth:"+mParent.getWidth()+",parentHeight:"+mParent.getHeight());
}
PlayerUtils.getInstance().removeViewFromParent(this);//從原宿主中移除自己
//2.改變播放器橫屏或窗口播放狀態(tài)
setWindowPropertyPlayer(true,false);
//3.獲取宿主的View屬性和startX、Y軸
//如果傳入的寬高不存在,則使用默認(rèn)的16:9的比例創(chuàng)建Window View
if(width<=0){
width = PlayerUtils.getInstance().getScreenWidth(getContext())/2+PlayerUtils.getInstance().dpToPxInt(30f);
height = width*9/16;
// ILogger.d(TAG,"startWindow-->未傳入寬高,width:"+width+",height:"+height);
}
//如果傳入的startX不存在,則startX起點(diǎn)位于屏幕寬度1/2-距離右側(cè)15dp位置,startY起點(diǎn)位于宿主View的下方15dp處
if(startX<=0&&null!=mParent){
startX=(PlayerUtils.getInstance().getScreenWidth(getContext())/2-PlayerUtils.getInstance().dpToPxInt(30f))-PlayerUtils.getInstance().dpToPxInt(15f);
startY=screenLocation[1]+mParent.getHeight()+PlayerUtils.getInstance().dpToPxInt(15f);
// ILogger.d(TAG,"startWindow-->未傳入X,Y軸,取父容器位置,startX:"+startX+",startY:"+startY);
}
//如果宿主也不存在,則startX起點(diǎn)位于屏幕寬度1/2-距離右側(cè)15dp位置,startY起點(diǎn)位于屏幕高度-Window View 高度+15dp位置處
if(startX<=0){
startX=(PlayerUtils.getInstance().getScreenWidth(getContext())/2-PlayerUtils.getInstance().dpToPxInt(30f))-PlayerUtils.getInstance().dpToPxInt(15f);
startY=PlayerUtils.getInstance().dpToPxInt(60f);
// ILogger.d(TAG,"startWindow-->未傳入X,Y軸或取父容器位置失敗,startX:"+startX+",startY:"+startY);
}
ILogger.d(TAG,"startWindow-->final:width:"+width+",height:"+height+",startX:"+startX+",startY:"+startY);
//4.轉(zhuǎn)場(chǎng)到window中,并指定寬高和x,y軸
WindowPlayerFloatView container=new WindowPlayerFloatView(viewGroup.getContext());
container.setOnWindowActionListener(new OnWindowActionListener() {
@Override
public void onMovie(float x, float y) {
}
@Override
public void onClick(BasePlayer basePlayer, Object coustomParams) {
}
@Override
public void onClose() {
// ILogger.d(TAG,"startWindow-->onClose");
quitWindow();//退出小窗口
}
});
container.setId(R.id.player_window);
container.addPlayerView(this,width,height,startX,startY,radius,bgColor);//先將播放器包裝到可托拽的容器中
viewGroup.addView(container, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
}
}
三、更多功能和全部源碼請(qǐng)移步至iPlayer
- 全部功能請(qǐng)閱讀接入文檔