Android 直播播放器+彈幕使用總結

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布

又來寫文章了,懶癌晚期拖啊拖總抽出點時間來,直播算是現在比較火了,公司最近也要開發直播的功能。在這里分享下開發過程遇到的一些問題以及解決方案。

項目地址https://github.com/Hemumu/HLiveDemo/tree/master

筆誤,JieCaoVideoPlayer是基于MediaPlayer的寫的,不是基于ijkplayer封裝的,在此修正

現在有很多的開源播放器,本文所選的是基于MediaPlayer封裝的開源播放器JieCaoVideoPlayer,彈幕使用的也是B站的開源項目https://github.com/Bilibili/DanmakuFlameMaster

JieCaoVideoPlayer默認提供了基本的UI界面,但是肯定滿足不了每個人的界面要求,所以我們就需要在JieCaoVideoPlayer上簡單的封裝一下。首先新建一個類繼承JCVideoPlayerStandard


public class HVideoPlayer extends JCVideoPlayerStandard {

    @Override
    public void init(final Context context) {
        super.init(context);
        this.mContext = context;
        mEditText = (EditText) findViewById(R.id.msg_edittext);
        mSendImage = (ImageView) findViewById(R.id.send_img);
        mRewardBtn = (ImageView) findViewById(R.id.reward_img);

        mSendImage.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                String content = mEditText.getText().toString();
                mSendListener.sendMsg(content);
            }
        });
        mRewardBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mShowPay.showPay();
            }
        });
    }

    @Override
    public int getLayoutId() {
        return R.layout.custom_video_player;
    }
}

JCVideoPlayerStandard對一些基本的界面操作以及頁面邏輯做了封裝,我們只需要繼承這個類,然后自定義自己的布局。如果有你不需要的控件就隱藏,刪除可能會報錯。重寫init方法初始化一些你自定義的控件和按鈕的點擊事件。

JieCaoVideoPlayer是通過setUp方法來初始化播放器參數,所以我們也需要來重寫這個方法來初始化我們自己的一些參數

    @Override
    public void setUp(String url, int screen, Object... objects) {
        //強制全屏
        FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
        //添加全屏下的參數
        if (objects.length != 1) {
            mSendListener = (OnSendMsgListener) objects[1];
            mShowPay = (OnPayListener) objects[2];
            mOnFullScreenListener = (OnFullScreenListener) objects[3];
        }
        super.setUp(url, screen, objects[0], mSendListener, mShowPay, mOnFullScreenListener);
        //全屏下展示彈幕
        if (currentScreen == SCREEN_WINDOW_FULLSCREEN) {
            initDanmu();
            mEditText.setVisibility(View.VISIBLE);
            mSendImage.setVisibility(View.VISIBLE);
            mOnFullScreenListener.onFullScreen(this);
        } else if (currentScreen == SCREEN_LAYOUT_NORMAL
                || currentScreen == SCREEN_LAYOUT_LIST) {
            mEditText.setVisibility(View.INVISIBLE);
            mSendImage.setVisibility(View.INVISIBLE);
        }

        //點擊返回按鈕隱藏彈幕
        backButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                hideDanmu();
                backPress();
            }
        });

        //重寫全屏按鈕點擊事件
        fullscreenButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (currentState == CURRENT_STATE_AUTO_COMPLETE) return;
                if (currentScreen == SCREEN_WINDOW_FULLSCREEN) {
                    hideDanmu();
                    backPress();
                } else {
                    //全屏
                    startWindowFullscreen();
                }
            }
        });
    }

需要注意一點的就是播放器器全屏,這里修改了FULLSCREEN_ORIENTATION 參數為ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE使播放器點擊全屏后強制全屏并且是橫屏的,默認情況點擊全屏后是豎屏的,并且根據重力感應調整屏幕方向。需要注意的是使用播放器的Activity需要設置為豎屏

android:screenOrientation="portrait"

否則調用橫屏后整個Activity會整個橫屏。

需要注意播放器橫屏后會創建一個新的播放器實例和當前的播放器不是同一個實例,也就是說點擊全屏后會重新初始化當前類,并重新調用setUp方法。那怎么拿到前面小屏模式下一些必須的參數呢?查看下JCVideoPlayerStandard全屏的源碼

      public void startWindowFullscreen() {
        Log.i(TAG, "startWindowFullscreen " + " [" + this.hashCode() + "] ");
        hideSupportActionBar(getContext());
        JCUtils.getAppCompActivity(getContext()).setRequestedOrientation(FULLSCREEN_ORIENTATION);

        ViewGroup vp = (ViewGroup) (JCUtils.scanForActivity(getContext()))//.getWindow().getDecorView();
                .findViewById(Window.ID_ANDROID_CONTENT);
        View old = vp.findViewById(FULLSCREEN_ID);
        if (old != null) {
            vp.removeView(old);
        }
        textureViewContainer.removeView(JCMediaManager.textureView);
        try {
            Constructor<JCVideoPlayer> constructor = (Constructor<JCVideoPlayer>) JCVideoPlayer.this.getClass().getConstructor(Context.class);
            JCVideoPlayer jcVideoPlayer = constructor.newInstance(getContext());
            jcVideoPlayer.setId(FULLSCREEN_ID);
            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            vp.addView(jcVideoPlayer, lp);
            jcVideoPlayer.setUp(url, JCVideoPlayerStandard.SCREEN_WINDOW_FULLSCREEN, objects);
            jcVideoPlayer.setUiWitStateAndScreen(currentState);
            jcVideoPlayer.addTextureView();
            JCVideoPlayerManager.putSecondFloor(jcVideoPlayer);
            R.anim.start_fullscreen);
            CLICK_QUIT_FULLSCREEN_TIME = System.currentTimeMillis();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    }

可以看到在全屏的時候重新創建了JCVideoPlayer的實例,并且調用了setUp方法傳入了url以及全屏,后面這個objects是干嘛的呢?查看源碼

   public void setUp(String url, int screen, Object... objects) {
        if (!TextUtils.isEmpty(this.url) && TextUtils.equals(this.url, url)) {
            return;
        }
        this.url = url;
        this.objects = objects;
        this.currentScreen = screen;
}

可以看到這個objects是在父類的setUp中賦值的,說明我們在調setUp傳入的objects會相應的傳入全屏播放器實例中,這也就有了上面的代碼

   if (objects.length != 1) {
            mSendListener = (OnSendMsgListener) objects[1];
            mShowPay = (OnPayListener) objects[2];
            mOnFullScreenListener = (OnFullScreenListener) objects[3];
        }
    super.setUp(url, screen, objects[0], mSendListener, mShowPay, mOnFullScreenListener);

默認的objects的第一個參數是標題,后面就可以傳遞自己的一些字段,比如我們在全屏實例中需要回調一些方法,就要將這些接口傳到全屏播放器示例中,否則在全屏中使用這些字段會報空指針。

setUp中如果當前是全屏那么我們需要去加載彈幕,currentScreen字段是當前的狀態,如果是全屏就顯示彈幕否則就隱藏彈幕相關的東西。關于彈幕庫的使用可以參考郭神的文章http://blog.csdn.net/guolin_blog/article/details/51933728這里我就不再細講了

   /**
     * 初始化彈幕
     */
    private void initDanmu() {
        ViewGroup vp = (ViewGroup) (JCUtils.scanForActivity(getContext()))//.getWindow().getDecorView();
                .findViewById(Window.ID_ANDROID_CONTENT);

        LayoutParams lp = new LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        lp.setMargins(0, sp2px(48), 0, sp2px(48));
        danmakuView = new DanmakuView(mContext);
        vp.addView(danmakuView, lp);
        danmakuView.enableDanmakuDrawingCache(true);
        danmakuView.setCallback(new DrawHandler.Callback() {
            @Override
            public void prepared() {
                showDanmaku = true;
                danmakuView.start();
                generateSomeDanmaku();
            }

            @Override
            public void updateTimer(DanmakuTimer timer) {

            }

            @Override
            public void danmakuShown(BaseDanmaku danmaku) {

            }

            @Override
            public void drawingFinished() {

            }
        });
        danmakuContext = DanmakuContext.create();
        danmakuView.prepare(parser, danmakuContext);

    }

在當直播流異常或者的或者網絡異常我們需要做一些操作,但JCVideoPlayer并沒有提供這方面的回調。又只有發揚我們的探索精神去探索源碼了


    @Override
    public void onError(int what, int extra) {
        Log.e(TAG, "onError " + what + " - " + extra + " [" + this.hashCode() + "] ");
        if (what != 38 && what != -38) {
            setUiWitStateAndScreen(CURRENT_STATE_ERROR);
        }
    }

在流異常或者網絡異常會打印onError日志,所以找到了這個方法,這下就簡單了重寫這個方法就行了

   @Override
    public void onError(int what, int extra) {
        super.onError(what, extra);
        //重寫onError 視頻播放錯誤的時候隱藏彈幕

         hideDanmu();

    }

默認播放上下有一個工具欄,在3秒后會自動隱藏,可是我們不需要自動隱藏可以重寫這個方法

    @Override
    public void startDismissControlViewTimer() {
        //重寫父類方法,防止自動隱藏播放器工具欄。如需要自動隱藏請刪除此方法或調用super.startDismissControlViewTimer();
    }

可以通過代碼的方式自動開始播放,如果在播放就暫停播放

jcVideoPlayer.startButton.performClick();

默認的JieCaoVideoPlayer還支持重力感應進入全屏,只需要在Activity中加入如下代碼

JCVideoPlayer.JCAutoFullscreenListener sensorEventListener;
SensorManager                          sensorManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    sensorEventListener = new JCVideoPlayer.JCAutoFullscreenListener();
}
@Override
protected void onResume() {
    super.onResume();
    Sensor accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
    sensorManager.registerListener(sensorEventListener, accelerometerSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
@Override
protected void onPause() {
    super.onPause();
    sensorManager.unregisterListener(sensorEventListener);
}

JieCaoVideoPlayer還支持浮層小窗播放,能在ListViewViewPagerListViewViewPagerFragment等多重嵌套模式下全屏工作,源碼的類大部分方法都是public需要什么重寫就行了。

使用
<com.helin.hlivedemo.view.HVideoPlayer
                android:id="@+id/custom_videoplayer_standard"
                android:layout_width="match_parent"
                android:layout_height="200dp" />

Acitivity中生命周期中加入對播放器的管理

    @Override
    public void onBackPressed() {
        if (JCVideoPlayer.backPress()) {
            //隱藏彈幕
            if (mFullScreenPlayer != null) {
                mFullScreenPlayer.hideDanmu();
                mFullScreenPlayer=null;
            }
            return;
        }
        super.onBackPressed();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mVideoPlayerStandard.danmaDes();
    }

    @Override
    protected void onPause() {
        super.onPause();
        JCVideoPlayer.releaseAllVideos();
        if (mFullScreenPlayer != null) {
            mFullScreenPlayer.hideDanmu();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        mVideoPlayerStandard.danmaResume();
    }

還可以添加UserAction對播放器的各種狀態監聽

mVideoPlayerStandard.setJcUserAction(new JCUserAction() {
            @Override
            public void onEvent(int type, String url, int screen, Object... objects) {
                switch (type){
                    //開始播放
                    case JCVideoPlayer.CURRENT_STATE_PLAYING:

                        break;
                     //暫停播放
                    case JCVideoPlayer.CURRENT_STATE_PAUSE:

                        break;

                }
            }
        });

最后效果如下

GIF.gif
demo中的直播流不太穩定大家可以替換成自己覺得穩定的直播流,或者換成一個視頻也可以。有什么問題歡迎交流!
Thanks

https://github.com/Bilibili/DanmakuFlameMaster
https://github.com/Bilibili/ijkplayer
https://github.com/lipangit/JieCaoVideoPlayer

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容