Android進(jìn)階之MediaPlayer和TextureView封裝視頻播放器詳解(完美實現(xiàn)全屏、小窗)
前言
上一篇文章我們介紹了SurfaceView和TextureView的基礎(chǔ)知識點;
SurfaceView 以及 TextureView 均繼承于 android.view.View,屬于 Android 提供的控件體系的一部分。與普通 View 不同,它們都在獨立的線程中繪制和渲染。所以,相比于普通的 ImageView 它們的性能更高,因此常被用在對繪制的速率要求比較高的應(yīng)用場景中,用來解決普通 View 因為繪制的時間延遲而帶來的掉幀的問題,比如用作相機(jī)預(yù)覽、視頻播放的媒介等;
今天我們就來簡單的用TextureView封裝下視頻播放器;
一、視頻播放器方案介紹
1、videoView+mediaPlayer
videoView繼承自SurfaceView。surfaceView是在現(xiàn)有View上創(chuàng)建一個新的Window,
內(nèi)容顯示和渲染是在新的Window中,這使得SurfaceView的繪制和刷新可以在單獨的線程中進(jìn)行。
由于SurfaceView的內(nèi)容是在新建的Window中,這使得SurfaceView不能放在RecyclerView或ScrollView中,一些View中的特性也無法使用。
2、textureView+mediaPlayer
textureView不會創(chuàng)建新的窗口,它的使用跟其他普通View一樣。
考慮到以后的可擴(kuò)展性,最終采用這個方案
3、為什么使用TextureView
TextureView是在4.0(API level 14)引入的,與SurfaceView相比,它不會創(chuàng)建新的窗口來顯示內(nèi)容。它是將內(nèi)容流直接投放到View中,并且可以和其它普通View一樣進(jìn)行移動,旋轉(zhuǎn),縮放,動畫等變化。TextureView必須在硬件加速的窗口中使用。
二、TextureView使用介紹
1、TextureView被創(chuàng)建后不能直接使用,必須將其添加到ViewGroup中。
2、TextureView必須要等SurfaceTexture準(zhǔn)備就緒才能起作用,這里通常需要給TextureView設(shè)置監(jiān)聽器SurfaceTextureListener。等待onSurfaceTextureAvailable回調(diào)后,才能使用
3、TextureView創(chuàng)建和初始化
- //初始化一個TextureView并添加至ViewGroup或找到你的TextureView 組件
- mTextureView=new TextureView(getContext());
- //設(shè)置畫布監(jiān)聽
- textureView.setSurfaceTextureListener(this);
- //添加至布局
- fragment.addView(textureView,new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT, Gravity.CENTER));
- /**
- * TextureView準(zhǔn)備好了回調(diào)
- * @param surface 內(nèi)部畫布渲染surface
- * @param width TextureView布局寬
- * @param height TextureView布局高
- */
- @Override
- public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
- Logger.d(TAG,"onSurfaceTextureAvailable-->width:"+width+",height:"+height);
- //這里對畫面改變、轉(zhuǎn)場播放做了處理,聲明一個mSurfaceTexture ,在TextureView發(fā)生變化時更新
- if (mSurfaceTexture == null) {
- mSurfaceTexture = surface;
- //prepare();
- } else {
- mTextureView.setSurfaceTexture(mSurfaceTexture);
- }
- }
- /**
- * TextureView寬高發(fā)生變化時回調(diào)
- * @param surface 內(nèi)部surface
- * @param width 新的TextureView布局寬
- * @param height 新的TextureView布局高
- */
- @Override
- public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
- Logger.d(TAG,"onSurfaceTextureSizeChanged-->width:"+width+",height:"+height);
- }
- /**
- * TextureView銷毀時回調(diào)
- * @param surface 內(nèi)部surface
- * @return Most applications should return true.
- */
- @Override
- public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
- Logger.d(TAG,"onSurfaceTextureDestroyed");
- return null==mSurfaceTexture;
- }
- /**
- * TextureView刷新時回調(diào)
- * @param surface 內(nèi)部surface
- */
- @Override
- public void onSurfaceTextureUpdated(SurfaceTexture surface) {
- }
三、MediaPlayer介紹
1、重要的狀態(tài)
- idle:空閑狀態(tài)。當(dāng)mediaPlayer沒有prepareAsync之前,就是處于idle狀態(tài)。
- prepared:準(zhǔn)備好狀態(tài)。想要讓mediaPlayer開始播放,不能直接start,必須要先prepareSync。這期間mediaPlayer會一直在準(zhǔn)備preparing,直到進(jìn)入prepared狀態(tài)。
- started:當(dāng)mediaPlayer準(zhǔn)備好,就可以調(diào)用mediaPlayer的start方法進(jìn)入started狀態(tài)。
- paused:當(dāng)調(diào)用pause方法,進(jìn)入paused狀態(tài)。
- completed:播放完成,進(jìn)入completed狀態(tài)。
- error:播放錯誤。
2、重要的方法
- prepareAsync:要想使用mediaPlayer,必須先調(diào)用prepareAsync。這是第一步。
- start:開始
- pause:暫停
- reset:播放完成后,如想重新開始,調(diào)用該方法。
3、重要的回調(diào)
- onSurfaceTextureAvailable:開始關(guān)聯(lián)mediaPlayer
- onPrepared:此處開始調(diào)用mediaPlayer.start()
- onInfo:播放開始后,視頻到底狀態(tài)如何,就是在onInfo中處理
- @Override
- public boolean onInfo(MediaPlayer mp, int what, int extra) {
- if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
- // 播放器渲染第一幀
- mCurrentState = STATE_PLAYING;
- mController.onPlayStateChanged(mCurrentState);
- } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
- // MediaPlayer暫時不播放,以緩沖更多的數(shù)據(jù)
- if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) {
- mCurrentState = STATE_BUFFERING_PAUSED;
- } else {
- mCurrentState = STATE_BUFFERING_PLAYING;
- }
- mController.onPlayStateChanged(mCurrentState);
- } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
- // 填充緩沖區(qū)后,MediaPlayer恢復(fù)播放/暫停
- if (mCurrentState == STATE_BUFFERING_PLAYING) {
- mCurrentState = STATE_PLAYING;
- mController.onPlayStateChanged(mCurrentState);
- }
- if (mCurrentState == STATE_BUFFERING_PAUSED) {
- mCurrentState = STATE_PAUSED;
- mController.onPlayStateChanged(mCurrentState);
- }
- } else {
- LogUtil.d("onInfo ——> what:" + what);
- }
- return true;
- }
4、MediaPlayer初始化和準(zhǔn)備播放
- mMediaPlayer = new MediaPlayer();
- mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- //設(shè)置準(zhǔn)備播放監(jiān)聽器,在onPrepared回調(diào)中開始播放
- mMediaPlayer.setOnPreparedListener(this);
- //...此處省去一系列監(jiān)聽設(shè)置
- //異步準(zhǔn)備
- mMediaPlayer.prepareAsync();
- /**
- * 播放器準(zhǔn)備好了
- * @param mp 解碼器
- */
- @Override
- public void onPrepared(MediaPlayer mp) {
- Logger.d(TAG,"onPrepared");
- if(null!=mSurfaceTexture){
- if(null!=mSurface){
- mSurface.release();
- mSurface=null;
- }
- mSurface =new Surface(mSurfaceTexture);
- mp.setSurface(mSurface);
- }
- //開始播放
- mp.start();
- }
四、封裝視頻播放器
1、封裝播放器
視頻播放控件應(yīng)該包含兩層:頂層是播放器的控制器mController,底層是播放視頻內(nèi)容的TextureView。這里將這兩層封裝在一個容器FrameLayout中;
- public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- if (mNetworkChangeReceiver == null) {
- mNetworkChangeReceiver = new NetworkChangeReceiver(this);
- }
- allow4GFlag = false;
- init();
- }
- private void init() {
- mContainer = new FrameLayout(mContext);
- mContainer.setBackgroundColor(Color.BLACK);
- LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- this.addView(mContainer, params);
- }
addTextureView
- private void addTextureView() {
- mContainer.removeView(mTextureView);
- LayoutParams params = new LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT,
- Gravity.CENTER);
- mContainer.addView(mTextureView, 0, params);
- }
setController
- public void setController(IVideoController controller) {
- mContainer.removeView(mController);
- mController = controller;
- mController.reset();
- mController.setVideoPlayer(this);
- LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- mContainer.addView(mController, params);
- }
播放,將TextureView、MediaPlayer、Controller進(jìn)行初始化。待TextureView的數(shù)據(jù)通道SurfaceTexture準(zhǔn)備就緒后,打開播放器
- private void openMediaPlayer() {
- // 屏幕常亮
- mContainer.setKeepScreenOn(true);
- // 設(shè)置監(jiān)聽
- mMediaPlayer.setOnPreparedListener(this);
- mMediaPlayer.setOnVideoSizeChangedListener(this);
- mMediaPlayer.setOnCompletionListener(this);
- mMediaPlayer.setOnErrorListener(this);
- mMediaPlayer.setOnInfoListener(this);
- mMediaPlayer.setOnBufferingUpdateListener(this);
- mCurrentNetworkState = NetworkChangeReceiver.getNetworkStatus(CtripBaseApplication.getInstance());
- mNetworkChangeReceiver.registerNetworkChangeBroadcast();
- // 設(shè)置dataSource
- try {
- mMediaPlayer.setDataSource(mUrl);
- if (mSurface == null) {
- mSurface = new Surface(mSurfaceTexture);
- }
- mMediaPlayer.setSurface(mSurface);
- mMediaPlayer.prepareAsync();
- mCurrentState = STATE_PREPARING;
- mController.onPlayStateChanged(mCurrentState);
- } catch (IOException e) {
- e.printStackTrace();
- LogUtil.e("打開播放器發(fā)生錯誤", e);
- }
- }
- private void initMediaPlayer() {
- if (mMediaPlayer == null) {
- mMediaPlayer = new MediaPlayer();
- mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- }
- }
- private void initTextureView() {
- if (mTextureView == null) {
- mTextureView = new TourTextureView(mContext);
- mTextureView.setSurfaceTextureListener(this);//此時回調(diào)onSurfaceTextureAvailable
- }
- }
- @Override
- public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
- if (mSurfaceTexture == null) {
- mSurfaceTexture = surfaceTexture;
- openMediaPlayer();
- } else {
- mTextureView.setSurfaceTexture(mSurfaceTexture);
- }
- }
播放邏輯寫完之后,具體UI展示邏輯在VideoPlayerController中。根據(jù)不同的狀態(tài)VideoPlayerController展示不同UI
- public static final int STATE_ERROR = -1; //播放錯誤
- public static final int STATE_IDLE = 0; //播放未開始
- public static final int STATE_PREPARING = 1; //播放準(zhǔn)備中
- public static final int STATE_PREPARED = 2; //播放準(zhǔn)備就緒
- public static final int STATE_PLAYING = 3; //正在播放
- public static final int STATE_PAUSED = 4; //暫停播放
- public static final int STATE_BUFFERING_PLAYING = 5; //正在緩沖
- public static final int STATE_BUFFERING_PAUSED = 6; //正在緩沖 播放器
- public static final int STATE_COMPLETED = 7; //播放完成
- public static final int STATE_NOTE_4G = 8; //提示4G
- public static final int STATE_NOTE_DISCONNECT = 9; //提示斷網(wǎng)
- public static final int MODE_NORMAL = 10; //普通模式
- public static final int MODE_FULL_SCREEN = 11; //全屏模式
- public static final int MODE_TINY_WINDOW = 13; //小窗口模式
2、全屏、小窗口播放的實現(xiàn)
實現(xiàn)全屏:將mContainer移除,并添加到android.R.content中,并設(shè)置成橫屏
- @Override
- public void enterFullScreen() {
- if (mCurrentMode == MODE_FULL_SCREEN) return;
- // 隱藏ActionBar、狀態(tài)欄,并橫屏
- TourVideoUtil.hideActionBar(mContext);
- TourVideoUtil.scanForActivity(mContext)
- .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- new Handler().post(new Runnable() {
- @Override
- public void run() {
- ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext)
- .findViewById(android.R.id.content);
- if (mCurrentMode == MODE_TINY_WINDOW) {
- contentView.removeView(mContainer);
- } else {
- TourVideoPlayer.this.removeView(mContainer);
- }
- LayoutParams params = new LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT);
- contentView.addView(mContainer, params);
- }
- });
- mCurrentMode = MODE_FULL_SCREEN;
- mController.onPlayModeChanged(mCurrentMode);
- }
實現(xiàn)小窗口:將mContainer移除,添加到android.R.content中,并設(shè)置寬高
- @Override
- public void enterTinyWindow() {
- if (mCurrentMode == MODE_TINY_WINDOW) return;
- this.removeView(mContainer);
- new Handler().post(new Runnable() {
- @Override
- public void run() {
- ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext)
- .findViewById(android.R.id.content);
- // 小窗口的寬度為屏幕寬度的60%,長寬比默認(rèn)為16:9,右邊距、下邊距為8dp。
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
- (int) (CommonUtil.getScreenWidth(mContext) * 0.6f),
- (int) (CommonUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f));
- params.gravity = Gravity.TOP | Gravity.START;
- params.topMargin = CommonUtil.dp2px(mContext, 48f);
- contentView.addView(mContainer, params);
- }
- });
- mCurrentMode = MODE_TINY_WINDOW;
- mController.onPlayModeChanged(mCurrentMode);
- }
總結(jié)
關(guān)于視頻播放器封裝的知識點還有很多,今天知識簡單的介紹了下封裝的步驟和思路;
大家如果想自己封裝可以參考網(wǎng)上NiceVieoPlayer;
以后會繼續(xù)講解關(guān)于視頻播放器的知識點;
本文轉(zhuǎn)載自微信公眾號「Android開發(fā)編程」