基于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时代到来,多屏互动会更加成熟,逐渐走入现实生活。

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