本文介紹的多路投屏直播,主要是智能交互會議、多人同時投屏的應用場景,但不限於此。現實生活中,早已經出現多路視頻監控的應用領域。爲了提高開會溝通效率,多人協同、多路投屏互動的場景應運而生。會議投屏對實時性要求非常高,目前可以做到1080P的視頻流直播延時130ms左右,比遊戲直播、主播直播的延時要求高很多。因此,需要基於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時代到來,多屏互動會更加成熟,逐漸走入現實生活。