基於IjkPlayer的多路投屏直播

本文介紹的多路投屏直播,主要是智能交互會議、多人同時投屏的應用場景,但不限於此。現實生活中,早已經出現多路視頻監控的應用領域。爲了提高開會溝通效率,多人協同、多路投屏互動的場景應運而生。會議投屏對實時性要求非常高,目前可以做到1080P的視頻流直播延時130ms左右,比遊戲直播、主播直播的延時要求高很多。因此,需要基於IjkPlayer做二次修改,從緩衝隊列、解碼耗時、渲染隊列三個方面優化。

關聯文章:

RTSP直播延時的深度優化

從FFmpeg源碼去解決IJKPlayer直播花屏問題

 

1、頁面佈局

採用水平、垂直兩條分割線把整個畫面分割爲四畫面,四個通道分別對應一個IjkVideoView。

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/divider1"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/ijk_color_blue_800"
        android:layout_centerVertical="true"/>

    <View
        android:id="@+id/divider2"
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:background="@color/ijk_color_blue_800"
        android:layout_centerHorizontal="true"/>

    <com.frank.living.widget.IjkVideoView
        android:id="@+id/video_view1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/divider1"
        android:layout_toStartOf="@id/divider2"/>

    <com.frank.living.widget.IjkVideoView
        android:id="@+id/video_view2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/divider1"
        android:layout_toEndOf="@id/divider2"/>

    <com.frank.living.widget.IjkVideoView
        android:id="@+id/video_view3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/divider1"
        android:layout_toStartOf="@id/divider2"/>

    <com.frank.living.widget.IjkVideoView
        android:id="@+id/video_view4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/divider1"
        android:layout_toEndOf="@id/divider2"/>

</RelativeLayout>

2、初始化播放器參數

IjkPlayer播放器的參數分爲:PLAYER、FORMAT、CODEC、SWS四大類。包括探測數據包數量、分析碼流時長、TCP/UDP連接、環路濾波、網絡卡頓丟幀、硬解碼配置、緩衝區大小設置等等。

private void setOptions(IjkMediaPlayer ijkPlayer){
    if (ijkPlayer == null)
        return;
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fast", 1);//不額外優化
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 200);
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);//是否開啓緩衝
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
    //0:代表關閉,1:代表開啓
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);//開啓硬解
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);//自動旋屏
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);//處理分辨率變化

    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", 0);//最大緩存數
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 2);//默認最小幀數2
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 30);//最大緩存時長
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);//是否限制輸入緩存數
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
    //設置播放前的最大探測時間,分析碼流時長:默認1024*1000
    ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100);
    //ijkPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");//tcp傳輸數據
}

3、初始化播放器

創建播放器,並初始化。單路投屏時,我們默認爲全屏顯示。

private void setupView(){
    //第一路投屏默認全屏
    enterFullScreen(1);
    mVideoView1.setVideoPath(url);
    mVideoView1.setIjkPlayerListener(new IjkPlayerListener() {
        @Override
        public void onIjkPlayer(IjkMediaPlayer ijkMediaPlayer) {
            //設置播放器option
            setOptions(ijkMediaPlayer);
        }
    });
    mVideoView1.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
        @Override
        public void onPrepared(IMediaPlayer iMediaPlayer) {

            ijkPlayer = iMediaPlayer;
        }
    });
    mVideoView1.start();
}

4、投屏通道處理

我們使用HashMap來保存投屏客戶端ip與通道數對應關係,另外使用TreeMap來記錄每個通道投屏狀態。

//四分屏模式還是全屏模式
private boolean isMultiScreen;
//保存客戶端ip與通道數對應關係
private HashMap<String, Integer> clientMap = new HashMap<>();
//記錄每個通道的投屏狀態
private TreeMap<Integer, Boolean> channelMap = new TreeMap<>();

5、增加投屏

在接收到增加投屏廣播後,選擇空閒通道來投屏。需要特別注意的是,當第二路來投屏時,自動把全屏切換爲多屏模式。

  int clientNum = intent.getIntExtra("clientNum", 0);
  String otherUrl = intent.getStringExtra("url");
  String ipAddress = intent.getStringExtra("ip");
  //選擇空閒通道
  int channel = selectIdleChannel(clientNum);
  clientMap.put(ipAddress, channel);
  channelMap.put(channel, true);
  addClient(channel, otherUrl);
  //單屏變爲兩路投屏時,自動切換爲多屏模式
  if (clientNum == 2){
      exitFullScreen();
  }
/**
 * 選擇空閒通道
 * @param clientNum clientNum
 * @return idleChannel
 */
private int selectIdleChannel(int clientNum){
    for (int channel = 1; channel < clientNum; channel++){
        if (!channelMap.get(channel)){
            return channel;
        }
    }
    return clientNum;
}

 

6、移除投屏

當接收到移除投屏廣播後,根據待移除客戶端的ip地址,從HashMap遍歷找到需要移除的客戶端。如果當前投屏總數是兩路,移除其中一路後,剩下最後一路投屏自動切換爲全屏。

int num = intent.getIntExtra("clientNum", 0);
if (num == 0){
    Process.killProcess(Process.myPid());
}else if (num > 0){
    String ipAddress = intent.getStringExtra("ipAddress");
    int target = clientMap.get(ipAddress);
    removeClient(target);
    clientMap.remove(ipAddress);
    channelMap.put(target, false);
    //多屏變爲單屏時,自動切換爲全屏
    if (num == 1){
        int castingChannel = getCastingChannel();
        enterFullScreen(castingChannel);
    }
}

7、全屏與分屏切換

雙擊某路投屏通道時,實現全屏與多屏的互相切換。

/**
 * 進入全屏模式
 * @param channel channel
 */
private void enterFullScreen(int channel){
    hideDivider();
    switch (channel){
        case 1:
            mVideoView1.setVisibility(View.VISIBLE);
            mVideoView2.setVisibility(View.GONE);
            mVideoView3.setVisibility(View.GONE);
            mVideoView4.setVisibility(View.GONE);
            break;
        case 2:
            mVideoView1.setVisibility(View.GONE);
            mVideoView2.setVisibility(View.VISIBLE);
            mVideoView3.setVisibility(View.GONE);
            mVideoView4.setVisibility(View.GONE);
            break;
        case 3:
            mVideoView1.setVisibility(View.GONE);
            mVideoView2.setVisibility(View.GONE);
            mVideoView3.setVisibility(View.VISIBLE);
            mVideoView4.setVisibility(View.GONE);
            break;
        case 4:
            mVideoView1.setVisibility(View.GONE);
            mVideoView2.setVisibility(View.GONE);
            mVideoView3.setVisibility(View.GONE);
            mVideoView4.setVisibility(View.VISIBLE);
            break;
        default:
            break;
    }
}
/**
 * 退出全屏模式
 */
private void exitFullScreen(){
    showDivider();
    mVideoView1.setVisibility(View.VISIBLE);
    mVideoView2.setVisibility(View.VISIBLE);
    mVideoView3.setVisibility(View.VISIBLE);
    mVideoView4.setVisibility(View.VISIBLE);
}

通過標誌位來記錄當前是全屏模式還是分屏模式,再根據標誌位來切換全屏/分屏。

/**
 * 切換分屏模式
 * @param channel channel
 */
private void changeScreenMode(int channel){
    isMultiScreen = !isMultiScreen;
    if (isMultiScreen){
        enterFullScreen(channel);
    }else {
        exitFullScreen();
    }
}

至此,可以實現多路投屏直播的基本功能。等待5G時代到來,多屏互動會更加成熟,逐漸走入現實生活。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章