本文介绍的多路投屏直播,主要是智能交互会议、多人同时投屏的应用场景,但不限于此。现实生活中,早已经出现多路视频监控的应用领域。为了提高开会沟通效率,多人协同、多路投屏互动的场景应运而生。会议投屏对实时性要求非常高,目前可以做到1080P的视频流直播延时130ms左右,比游戏直播、主播直播的延时要求高很多。因此,需要基于IjkPlayer做二次修改,从缓冲队列、解码耗时、渲染队列三个方面优化。
关联文章:
RTSP直播延时的深度优化
从FFmpeg源码去解决IJKPlayer直播花屏问题
1、页面布局
采用水平、垂直两条分割线把整个画面分割为四画面,四个通道分别对应一个IjkVideoView。
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 clientMap = new HashMap<>();
//记录每个通道的投屏状态
private TreeMap 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时代到来,多屏互动会更加成熟,逐渐走入现实生活。