Android 實現視屏播放器與邊播邊緩存功能外加蹲坑鏟屎(IJKPlayer)android
效果
git
Android上最爲人熟知的MediaPlayer,對,就是這貨,在上兩篇音頻文章中頻頻露臉的傢伙,此次又有它的身影,然而仍是此次不講他,就連他的封裝類VideoView也不講<( ̄︶ ̄)>,呸呸呸,又扯了一堆沒用的。 github
集成工做仍是有定的工做量的,它的DEMO確定知足不了慾求不滿的設計獅和產品汪的,這裏咱們不跑分,不打廣告,不講原理,只求站在巨人的肩膀上學(cao)習(xi),快速集成。api
單例,沒得商量,它須要負責真正的播放請求與顯示邏輯,集成了IjkMediaPlayer,BILIBLI的開源小組仍是頗有心的,它的封裝和接口使用基本和MediaPlayer沒有什麼區別,只須要用起來就行了。‘緩存
這裏咱們要實現IjkMediaPlayer的播放接口,監聽IjkMediaPlayer的相關狀態回調而後封發到各個邏輯播放器中。從下方代碼能夠看到,真的和MediaPlayer好像。服務器
mediaPlayer = new IjkMediaPlayer();
//音頻類型
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
//數據源
mediaPlayer.setDataSource(((GSYModel) msg.obj).getUrl(), ((GSYModel) msg.obj).getMapHeadData());
//播放完成
mediaPlayer.setOnCompletionListener(GSYVideoManager.this);
//緩衝
mediaPlayer.setOnBufferingUpdateListener(GSYVideoManager.this);
//常亮
mediaPlayer.setScreenOnWhilePlaying(true);
//加載完畢
mediaPlayer.setOnPreparedListener(GSYVideoManager.this);
//拖動
mediaPlayer.setOnSeekCompleteListener(GSYVideoManager.this);
//失敗
mediaPlayer.setOnErrorListener(GSYVideoManager.this);
//視頻相關信息-重要
mediaPlayer.setOnInfoListener(GSYVideoManager.this);
//視頻大小
mediaPlayer.setOnVideoSizeChangedListener(GSYVideoManager.this);】
//開始加載
mediaPlayer.prepareAsync();複製代碼
監聽的回調接口裏,大部分你們都耳目能詳吧,沒聽過也不要緊,都寫上就對了,可是最主要須要關注的兩個,一個是經過setOnVideoSizeChangedListener拿到視頻寬和高,這是咱們後續正常顯示視頻的依靠之一。ide
另一個就是setOnInfoListener,這裏咱們主要是獲取到視頻相關的元信息裏視頻旋轉角度!還記得那時候對視頻播放不熟悉,和產品還有QA力爭「這個視頻原本就是轉了90度的,我就不改,你咬我嗎···」這樣的黑歷史。Σ( ° △ °|||)工具
特別是Android拍攝的豎屏視頻,旋轉不是視頻自己的圖像,而是增長了旋轉信息,而這個時候你須要作的就是識別它,而後轉了它丫的。另外,由於Android自己的MediaPlaer和VideoView自身就處理好因此不須要你旋轉。((ノO益O)ノ彡┻━┻親生的啊)佈局
這裏的接口主要是把當前播放的視頻狀態和信息到返回到邏輯播放器中。post
@Override
public void onInfo(int what, int extra) {
if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
BACKUP_PLAYING_BUFFERING_STATE = mCurrentState;
setStateAndUi(CURRENT_STATE_PLAYING_BUFFERING_START);
} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
if (BACKUP_PLAYING_BUFFERING_STATE != -1) {
setStateAndUi(BACKUP_PLAYING_BUFFERING_STATE);
BACKUP_PLAYING_BUFFERING_STATE = -1;
}
} else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED) {
//這裏返回了視頻旋轉的角度,根據角度旋轉視頻到正確的畫面
mRotate = extra;
if (mTextureView != null)
mTextureView.setRotation(mRotate);
}
}複製代碼
爲何不用SurfaceView?由於TextureView很可愛啊。這裏咱們主要針對視頻的大小和旋轉角度設置TextureView的大小,詳細就很少說了(不是懶),挑其中一類講講,由於主要也是這個。
以爲看起來有點繞口?不要緊,用着用着就習慣了····
width = widthSpecSize;
height = heightSpecSize;
···
if (videoWidth * height < width * videoHeight) {
width = height * videoWidth / videoHeight;
} else if (videoWidth * height > width * videoHeight) {
height = width * videoHeight / videoWidth;
}
···
if (getRotation() != 0 && getRotation() % 90 == 0) {
if (widthS < heightS) {
if (width > height) {
width = (int) (width * (float) widthS / height);
height = widthS;
} else {
height = (int) (height * (float) width / widthS);
width = widthS;
}
} else {
if (width > height) {
height = (int) (height * (float) width / widthS);
width = widthS;
} else {
width = (int) (width * (float) widthS / height);
height = widthS;
}
}
}複製代碼
全部的UI邏輯基本均可以寫到這裏,目前繼承了 FrameLayout,View.OnClickListener, View.OnTouchListener, SeekBar.OnSeekBarChangeListener, TextureView.SurfaceTextureListener和GSYMediaPlayerListener。
邏輯播放器實現的內容太多了,這裏主要說幾個地方,好吧,我認可我懶╮(╯_╰)╭ ,可是寫太多了也沒人看啊,因此這裏主要是說一些關鍵的點,有須要留言再開個坑聊一聊,反正有DEMO。
在邏輯播放器中統一分發各類狀態,把被播放的manager狀態同步到這裏,以後你想要在哪一個邏輯播放器裏播放只須要對應的設置狀態後把manager的監聽同步過來。
switch (mCurrentState) {
//正常初始化狀態
case CURRENT_STATE_NORMAL:
if (isCurrentMediaListener()) {
cancelProgressTimer();
GSYVideoManager.instance().releaseMediaPlayer();
}
break;
//loading中
case CURRENT_STATE_PREPAREING:
resetProgressAndTime();
break;
//播放中
case CURRENT_STATE_PLAYING:
startProgressTimer();
break;
//暫停
case CURRENT_STATE_PAUSE:
startProgressTimer();
break;
//錯誤-須要判斷是否切換了邏輯播放器
case CURRENT_STATE_ERROR:
if (isCurrentMediaListener()) {
GSYVideoManager.instance().releaseMediaPlayer();
}
break;
//結束
case CURRENT_STATE_AUTO_COMPLETE:
cancelProgressTimer();
mProgressBar.setProgress(100);
mCurrentTimeTextView.setText(mTotalTimeTextView.getText());
break;
}複製代碼
···
case MotionEvent.ACTION_MOVE:
float deltaX = x - mDownX;
float deltaY = y - mDownY;
float absDeltaX = Math.abs(deltaX);
float absDeltaY = Math.abs(deltaY);
//是全屏仍是設置了能夠觸摸
if (mIfCurrentIsFullscreen || mIsTouchWiget) {
//以前是否已經符合了觸摸邏輯條件
if (!mChangePosition && !mChangeVolume && !mBrightness) {
//若是手指動了超過必定距離就能夠判斷是滑動,防止點擊的誤判的
if (absDeltaX > mThreshold || absDeltaY > mThreshold) {
cancelProgressTimer();
//若是是左右的就是進度
if (absDeltaX >= mThreshold) {
mChangePosition = true;
mDownPosition = getCurrentPositionWhenPlaying();
if (mVideoAllCallBack != null && isCurrentMediaListener()) {
mVideoAllCallBack.onTouchScreenSeekPosition(mUrl, mObjects);
}
} else {
//若是是上下的判斷是左邊仍是右邊
if (mFirstTouch) {
mBrightness = mDownX < mScreenWidth * 0.5f;
mFirstTouch = false;
}
if (!mBrightness) {
mChangeVolume = true;
mGestureDownVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (mVideoAllCallBack != null && isCurrentMediaListener()) {
mVideoAllCallBack.onTouchScreenSeekVolume(mUrl, mObjects);
}
}
}
}
}
}
···
//根據flag執行邏輯複製代碼
這裏有一個是TextureView的動態添加,動態添加的好處是你能夠在不中止視頻的狀況下載不一樣的邏輯播放器中切換視頻播放,好比列表全屏。
protected void addTextureView() {
if (mTextureViewContainer.getChildCount() > 0) {
mTextureViewContainer.removeAllViews();
}
mTextureView = null;
mTextureView = new GSYTextureView(getContext());
mTextureView.setSurfaceTextureListener(this);
mTextureView.setRotation(mRotate);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
mTextureViewContainer.addView(mTextureView, layoutParams);
}
···
//把Surface丟給視頻播放管理
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mSurface = new Surface(surface);
GSYVideoManager.instance().setDisplay(mSurface);
}
//告訴視頻播放渲染畫面銷燬了
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
GSYVideoManager.instance().setDisplay(null);
surface.release();
return true;
}複製代碼
//這裏其實就有播放管理器的監聽分發保存的邏輯須要注意
GSYVideoManager.instance().setLastListener(this);
GSYVideoManager.instance().setListener(gsyVideoPlayer);複製代碼
傳聞每個Activity都有一個com.android.internal.R.id.content,它默默的包含了各類你塞進去的物體,並且是一個FrameLayout,谷歌有太多它的傳說了,咱們用它是就是。
既然是FrameLayout,那麼咱們往他裏面塞東西就行了,這裏咱們能夠在GSYVideoPlayer裏面寫一個方法,在點擊全屏按鈕的時候:
在切換的時候能夠作一些位移動畫,讓播放器的全屏更加友好,下面長代碼來襲((/- -)/。深夜碼字不易,不知道爲何每次這個時候老婆的意見很大啊。
Constructorconstructor = (Constructor 複製代碼) GSYBaseVideoPlayer.this.getClass().getConstructor(Context.class); final GSYBaseVideoPlayer gsyVideoPlayer = constructor.newInstance(getContext()); //記錄新建立的這個video的id,在返回的時候經過它銷燬 gsyVideoPlayer.setId(FULLSCREEN_ID); WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); final int w = wm.getDefaultDisplay().getWidth(); final int h = wm.getDefaultDisplay().getHeight(); //設置黑色背景,自動充滿全屏 FrameLayout.LayoutParams lpParent = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); FrameLayout frameLayout = new FrameLayout(context); frameLayout.setBackgroundColor(Color.BLACK); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //若是5.0的話,先讓播放器出現的位置和列表中一直,再樣式一會執行到屏幕中間的過分動畫效果 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight()); lp.setMargins(mListItemRect[0], mListItemRect[1], 0, 0); frameLayout.addView(gsyVideoPlayer, lp); vp.addView(frameLayout, lpParent); mHandler.postDelayed(new Runnable() { @Override public void run() { TransitionManager.beginDelayedTransition(vp); resolveFullVideoShow(context, gsyVideoPlayer, h, w); } }, 300); } else { //5.0一下直接顯示 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getWidth(), getHeight()); frameLayout.addView(gsyVideoPlayer, lp); vp.addView(frameLayout, lpParent); resolveFullVideoShow(context, gsyVideoPlayer, h, w); } //設置全屏邏輯播放器的狀態,動態及添加播放view gsyVideoPlayer.setUp(mUrl, mCache, mObjects); gsyVideoPlayer.setStateAndUi(mCurrentState); gsyVideoPlayer.addTextureView(); //添加監聽 GSYVideoManager.instance().setLastListener(this); GSYVideoManager.instance().setListener(gsyVideoPlayer);
這裏利用另一種實現思路,列表的邏輯播放器只用一個,由於普通的list在滑動的時候會有複用和銷燬,這會致使視頻被釋放而中止了,若是你是和今日黃(tou)條同樣的視頻列表播放效果,滑出屏幕就中止那無所謂。
若是你須要不管怎麼滑動,視頻都在原來的位置播放的話,那麼ListVideoUtil適合你,,內部它已經帶了全屏,防錯位,旋轉的各類邏輯,直接上代碼,有興趣的看DEMO。
listVideoUtil = new ListVideoUtil(this);
//設置列表最外層的佈局用於全屏,空FrameLayout
listVideoUtil.setFullViewContainer(videoFullContainer);
//全屏隱藏狀態欄,若是有的話
listVideoUtil.setHideStatusBar(true);
···
//在列表中吧列表位置,封面,哪一個列表的TAG,列表視頻的承載ViewGroup,播放按鍵傳入到Utils中
listVideoUtil.addVideoPlayer(position, imageView, TAG, holder.videoContainer, holder.playerBtn);
holder.playerBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//每次播放都要更新列表讓其餘的item恢復狀態
notifyDataSetChanged();
//設置播放的tag和位置,防止錯位
listVideoUtil.setPlayPositionAndTag(position, TAG);
//開始播放
final String url = "http://baobab.wdjcdn.com/14564977406580.mp4";
listVideoUtil.startPlay(url);
}
});複製代碼
OrientationUtils使用的是OrientationEventListener,經過手機的角度判斷須要旋轉到哪一個位置。爲何用它?由於谷歌到的時候恰好看到,緣分啊懂嗎。
這裏須要個關注的是手動點擊和自動旋轉之間的衝突,主要看代碼吧,老婆開始催我了 (ノಠ益ಠ)ノ彡┻━┻。
//判斷系統是否開了旋轉,是的,這貨不須要系統旋轉是否開啓
boolean autoRotateOn = (android.provider.Settings.System.getInt(activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) == 1);
if (!autoRotateOn) {
if (mIsLand == 0) {
return;
}
}
// 設置豎屏
if (((rotation >= 0) && (rotation <= 30))="" ||="" (rotation="">= 330)) {
//是否點擊致使的
if (mClick) {
if (mIsLand > 0 && !mClickLand) {
return;
} else {
//清除狀態
mClickPort = true;
mClick = false;
mIsLand = 0;
}
} else {
//自動旋轉
if (mIsLand > 0) {
screenType = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_enlarge);
mIsLand = 0;
mClick = false;
}
}
}
// 設置橫屏
else if (((rotation >= 230) && (rotation <= 310)))="" {="" if="" (mclick)="" (!(misland="=" 1)="" &&="" !mclickport)="" return;="" }="" else="" mclickland="true;" mclick="false;" misland="1;" 1))="" screentype="ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;" activity.setrequestedorientation(activityinfo.screen_orientation_landscape);="" gsyvideoplayer.getfullscreenbutton().setimageresource(r.drawable.video_shrink);="" 設置反向橫屏="" (rotation=""> 30 && rotation < 95) {
if (mClick) {
if (!(mIsLand == 2) && !mClickPort) {
return;
} else {
mClickLand = true;
mClick = false;
mIsLand = 2;
}
} else if (!(mIsLand == 2)) {
screenType = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
gsyVideoPlayer.getFullscreenButton().setImageResource(R.drawable.video_shrink);
mIsLand = 2;
mClick = false;
}
}
}
};
orientationEventListener.enable();
複製代碼
好吧,老婆睡了,我偷偷起來了(。・・)ノ
這個需求曾經讓我徹夜難眠,由於IJKPlayer不支持,好吧,沒見過哪一個播放器支持的,和產品爭(tuo)論(yan)需(shi)求(jian)以後,最終仍是github大法好:AndroidVideoCache。
接入簡單,使用簡單,你能夠趾高氣揚的和產品說,這個so easy了。
HttpProxyCacheServer proxy = getProxy();
//注意不能傳入本地路徑,本地的你還傳進來幹嗎。
String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
videoView.setVideoPath(proxyUrl);複製代碼
該項目的原理其實就是將url連接轉化爲本地連接 h t t p://127.0.0.1:LocalPort/url,而後它開一個服務器一邊下載緩存視頻,一邊把緩存的數據正常返回給你的播放器,若是已經緩存過的這裏會返回一個本地文件路徑。Σ( ° △ °|||)︴曾經的我真的是too young too smiple。
一、IJKPLAY的後臺播放和回到前臺恢復畫面的速度之快是其餘播放器(我坐井觀天)沒法比擬的,真的好快,並且適合你,由於你什麼都不用作。
二、IJKPLAY有一個問題,我也提過ISSUSE了 #2104,不過目前還未解決,就是某些短小的視頻會沒法seekTo,說是FFMEPG的問題,而後就太監了。
三、IJKPLAY庫裏還封裝了exoplayer谷歌乾兒子,用法也基本一致,這個播放器本身內部判斷旋轉,不會有上面的seekto問題,但是後臺或者onPause以後的畫面恢復速度堪憂啊,各位遇到過嗎?
四、千萬別開硬解碼,否則會這樣。 ( ‵o′)凸
五、拖動進度條,須要在中止拖動的時候,判斷視頻是否是已經播放完了被釋放了。
六、若是橫屏全屏的話,恢復到正常畫面是最好有一個延時,這樣畫面纔不會出現背景抖動的問題,還有最關鍵的,Maifest文件。
//不要忘記配置activity,全部背景的activity
android:configChanges="orientation|keyboardHidden|screenSize"複製代碼
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int lastVisibleItem = firstVisibleItem + visibleItemCount;
//大於0說明有播放
if (GSYVideoManager.instance().getPlayPosition() >= 0) {
//當前播放的位置
int position = GSYVideoManager.instance().getPlayPosition();
//對應的播放列表TAG
if (GSYVideoManager.instance().getPlayTag().equals(ListNormalAdapter.TAG)
&& (position < firstVisibleItem || position > lastVisibleItem)) {
//若是滑出去了上面和下面就是否,和今日頭條同樣
GSYVideoPlayer.releaseAllVideos();
listNormalAdapter.notifyDataSetChanged();
}
}
}複製代碼
友情連接: