1 背景
直播带货最近一年来最火的业务了,但是长期以来,在直播间里都是以主播主动推流,观众被动拉流观看的方式维系一场直播。为了调动直播气氛,很多直播间也逐步开通了用户弹幕、点赞和行为提示等三大宝剑,从而达到主播和观众之间的互动反馈。的确这些功能都可以起到氛围烘托的作用,但是说到底仍然还是单向的数据交互。为了本质上提升互动性,还有这样的两个利器存在:
- 主播PK,即主播可以邀请另一个直播间的主播进行实时连麦,从而直播间内的观众可以同时观看两个房主互动,活跃气氛。
- 观众连麦,这就是主播邀请观众也加入到直播间中,成为主播,从而面对面交流,增强互动。
上述的这两功能,都是基于直播连麦这一基础能力来实现的,本文就讲介绍直播连麦的解决方案。
2 原理
在继续阅读下文之前,如果对于直播基础不是很清楚的朋友们,可以先去看看前文实战分享手把手教你直播解决方案,文中介绍了如何快速搭建一套直播解决方案,会有一些直播基础概念的讲解。
连麦,与日常直播最大的一个不同就是,实时性要求高。无论是主播之间的连麦PK,还是主播与观众之间的连麦,他们的角色都会提升成为主播。在我们典型的直播场景下,直播数据流都会经过CDN,再达到一般观众端,这个过程会经历多个网络节点,最终会有3-5s左右的时延,对于秀场、带货、游戏直播等等场景,这些都是完全可以接受的。但是一旦进入了连麦模式,连麦者与原主播之间的实时对话是无法接受这样的3-5s的时延的。
腾讯云采用了两种传输通道才实现了直播 连麦功能:
- 直播采用标准的 HTTP-FLV 协议,使用标准 CDN 线路,没有并发观看人数限制,且带宽成本很低,但延迟一般3s以上。
- 连麦采用 UDP 协议,使用专用加速线路,延迟一般在500ms以内,但由于线路成本较高,因此采用连麦时长进行计费。
连麦时会走UDP加速线路,所以费用较高,因此普通用户观看的时候,我们依然需要采用CDN的线路来观看3s时延的直播。
综合上面的分析,因此在这个地方,我们需要的是:
- 原主播A的数据流要超低时延的到达连麦者;
- 连麦者要成为新主播B角色进行推流;
- 连麦新主播B的数据流要超低时延到达原主播;
- 主播A和主播B的画面需要混合到一个画面;
- 普通用户仍然是3-5s的一般时延观看混合画面的直播流;
3 方案
在了解原理之后,我们来拆分一下,看具体需要做那些事情。
3.1 非连麦的典型直播架构
在具体拆分之前,我们先来看一下在连麦之前是一个什么样的架构:
主播通过互联网就近推流到上行加速OC点保证直播质量,再通过互联网进入腾讯云的云直播集群进行系列视频处理后,观众客户端通过互联网访问就近的腾讯云直播CDN(具备RTMP、HLS、HTTP FLV能力的CDN)从本地域的云直播拉流到观众侧进行观看(从观众到云直播方向是拉流操作),一般来说,如果主播和观众分布在不同地域,则跨地域推流由云直播内部完成。各地域直播CDN只会从本地域云直播拉流。
我们可以看到,在这种模式下,从主播推流到观众拉流观看,视频数据流一直是单向的,互动性较差。
3.2 多路RTMP流的连麦架构方案
当有连麦存在时,我们优先会想到的方案是,主播端和连麦者端,都分别推一路RTMP流到CDN,CDN再将这两路RTMP流发送给观众端,观众端在终端侧将两路RTMP流合成为一个画面。如下图所示:
这种实现方式的优点是,简单,但缺点也比较多:
- 主播与连麦者如果要进行交互,则考虑到上面分析的延时问题,在这里延时需要至少加大一倍,这样对于实时交互来说,完全无法接受;
- 主播与连麦者交互时,声音会产生干扰,形成回音;
- 观众端要接收两条视频流,带宽、流量消耗过大,并且两路视频流解码播放,耗费CPU等资源也非常多;
这样分析下来,基本上这种方案完全是利大于弊,是不可取的了。
3.3 服务端合流的连麦方案
该方案中,我们目标是在云端做更多的事。
首先需要增加拉流加速服务器,主播和连麦者RTMP推流数据通过就近OC节点依旧会发送给CDN侧,同时也需要推一份数据流到加速拉流服务器侧。
主播和连麦者不再是从CDN获取对方的拉流数据,而是直接从加速拉流服务器拉取。这样的设计,使得不需要通过中间几个节点的中转,缩短了拉流路径以及去除CDN的时延,可以直接降低时延到500ms左右,大大提升了实时性。
这个过程需要注意的是,加速拉流服务器的拉流地址,与原先CDN的拉流地址会不同,业务逻辑中需要做拉流地址的变更处理。
上面的方案总来来说还是很复杂的,好在依托于腾讯云的直播能力,上面的这些架构在腾讯云后台都是已有的,云帮我们解决了最大的难题,那么梳理下来,我们还需要做的就是:
- 获取蓝色拉流加速地址,供主播B拉主播A的低时延直播流*;
- 连麦时,生成一个新的推流地址,供主播B推流;
- 获取红色拉流加速地址,供主播A拉主播B的低时延直播流*;
- 云端进行样式混流;
- 保证用户端尽量0改动;
*由于低延时流使用腾讯云核心机房的BGP资源,所以需要购买计费套餐才能使用,如果拉加速流报获取加速拉流地址失败错误,请先检查是否购买套餐包。
4 协议设计
上面一直在说的,都是在方案上或者原理上,下面我们来看看具体落地时候怎么做。首先第一步需要考虑的就是,设计连麦协议。协议设计依托于总体的方案选择,我们先来看下基于腾讯与直播,我们实现连麦有哪些方案。
4.1 方案选型
总的来看,我们是依托于腾讯云直播服务来构建的,所以在协议设计上,就会有两个方案。
1. 完全基于SDK
基于腾讯云提供移动直播SDK,有着一个MLVBLiveRoom组件,该组件开源,基于了原有的LiteAVSDK 和 TIMSDK 搭建一个支持连麦互动和消息互动的“直播间”。
所属平台 | LiteAVSDK | TIMSDK | MLVBLiveRoom 组件 | 示例代码 |
---|---|---|---|---|
iOS | MLVBSDK | TIMSDK | MLVBLiveRoom | SimpleCode |
Android | MLVBSDK | TIMSDK | MLVBLiveRoom | SimpleCode |
基于MLVBLiveRoom组件,可以直接来实现。至于具体如何基于该组件来一步步的实现连麦,大家可以移步官网查看。
组件的存在会对于我们实现连麦方便很多,但是,如果引入 mlvb-live-room,在我们的项目中会带来下面几个问题:
- 小程序会增加直播间350KB体积(而当下直播间才600KB);
- mlvb-live-room中包含了IM组件,而我们项目中弹幕模块和消息通知已经使用了腾讯云的IM组件,这样就会有两个不同版本的IM SDK在跑,小程序和app的不稳定性会高很多;
- 该方案会额外增加2条长链接,在中低端设备下,会导致十分卡顿;
2. 自研上层协议
第二种方案,是根据我们对连麦原理的理解,自研连麦协议和通信方案来实现。
在上面第2、第3节中,我们已经详细的分析了连麦的原理、方案了,其实自己来实现这一套上层逻辑,并不是十分复杂,主要是需要后台自行实现大量逻辑开发。我们的项目中,最终是选择了自研来实现,对于为何选择采用了方案2自研,主要原因首先是我们的项目背景早期已经引入了IM组件等,而且自研实现可以更方便我们在后端来主动控制连麦的一些资源等。
4.2 连麦协议
确定方案之后,我们就需要着手设计协议了,我们可以梳理连麦流程如下:
- 主播A向观众1发送一个连麦请求;
- 观众1收到连麦请求,可以接受连麦,也可以拒绝连麦;
- 如果2中接受,观众端会停止当前普通拉流播放,换成连麦请求中的主播A加速拉流地址播放,同时根据连麦请求给到的推流地址进行推流,成为主播B角色。UI播放主播B画面和自己的摄像头画面。操作完毕后,调用响应连麦;
- 如果2中拒绝,回复拒绝连麦;
- 如果主播A收到接受连麦响应,获取主播B的加速拉流地址,播放主播B画面和自己的摄像头画面;
- 如果主播A收到拒绝连麦响应,回到初始状态;
- 云端会把主播A和主播B两个流混合成一个流,同时,以主播A的拉流地址为混流后拉流地址;
- 普通观众端画面自动展现为连麦画面;
根据上面提到的步骤,我们可以列出以下几个接口:
- 【主播】发起连麦请求,createJoinAnchor
- 【主播】取消连麦请求,cancelJoinAnchor
- 【主播】结束连麦请求,finishJoinAnchor
- 【连麦者】应答连麦请求,replyJoinAnchor
- 【连麦者】拒绝连麦请求,refuseJoinAnchor
- 【连麦者】退出连麦请求,quitJoinAnchor
需要注意的是,上面的流程中,省略了服务端的存在,我们需要认识到主播和观众之间永远不会直接交互。也就是所有的请求(无论是主播发给观众,还是观众发给主播)都是需要经过服务端中转的。因此,我们还需要提供一套服务端通知C端的机制,这儿就用到了我们前面提到的腾讯云IM组件。
4.3 通信方案
我们依托于腾讯云的IM组件设计了一套通信机制,因为这儿的消息都是需要高可靠的,为了提高可用性,我们使用的是C2C的IM消息。其实IM消息和上面提到的连麦接口是一一对应的,也就是可以理解在这儿服务端作为一个消息中转站的角色而存在着。之所以要这么做,是整个连麦过程是需要依靠服务端来做一系列的鉴权、管控和资源分配/释放等事务。
Type | 作用 | 消息触发 | 消息接受 |
---|---|---|---|
CreateJoinAnchor | 请求连麦 | 主播 | 连麦者 |
CancelJoinAnchor | 取消连麦 | 主播 | 连麦者 |
FinishJoinAnchor | 结束连麦 | 主播 | 连麦者 |
ReplyJoinAnchor | 应答连麦 | 连麦者 | 主播 |
RefuseJoinAnchor | 拒绝连麦 | 连麦者 | 主播 |
QuitJoinAnchor | 退出连麦 | 连麦者 | 主播 |
5 云端混流
当我们处理完主播端和连麦者端的交流之后,接下来需要处理的就是普通观众端了。我们希望普通观众能看到两个主播的画面,最直白的想法当然是把两个主播的拉流地址都给到普通观众,然后在C侧做展现。但是前文也提到,这样最大的问题就是带宽和费用直接双倍了,显然是不可取的。所以我们需要在云端做混流转码。
好在腾讯云直播服务为我们提供了直播混流功能,可以根据设定好的混流布局,同步的将各路输入源混流成一个新的流。具体的混流参数就不在这儿赘述了,主要是涉及了包括布局(LayoutParams)、剪切(CropParams)、码率帧率等等。
针对混流,我们需要实现创建混流CreateMixStream和取消混流两个接口CancelMixStream:
5.1 CreateMixStream的demo代码
代码语言:javascript复制private void CreateMixStream(String mixStreamSessionId, String anchorStreamName, String haozhuStreamName) throws TencentCloudSDKException {
CommonMixOutputParams outP = new CommonMixOutputParams();
outP.setOutputStreamName(anchorStreamName);
CommonMixInputParam[] inputList = new CommonMixInputParam[3];
CommonMixInputParam inputBackground = new CommonMixInputParam();
inputBackground.setInputStreamName(anchorStreamName);
CommonMixLayoutParams layoutBackground = new CommonMixLayoutParams();
layoutBackground.setImageLayer(1L);
layoutBackground.setInputType(3L);
layoutBackground.setImageWidth(720F);
layoutBackground.setImageHeight(1280F);
layoutBackground.setColor("0x000000");
inputBackground.setLayoutParams(layoutBackground);
CommonMixInputParam inputAnchor = new CommonMixInputParam();
inputAnchor.setInputStreamName(anchorStreamName);
CommonMixLayoutParams layoutAnchor = new CommonMixLayoutParams();
layoutAnchor.setImageLayer(2L);
layoutAnchor.setImageWidth(360F);
layoutAnchor.setImageHeight(640F);
layoutAnchor.setLocationX(0F);
layoutAnchor.setLocationY(220F);
CommonMixCropParams cropAnchor = new CommonMixCropParams();
cropAnchor.setCropWidth(360F);
cropAnchor.setCropHeight(640F);
cropAnchor.setCropStartLocationX(0F);
cropAnchor.setCropStartLocationY(0F);
inputAnchor.setLayoutParams(layoutAnchor);
inputAnchor.setCropParams(cropAnchor);
CommonMixInputParam inputHaozhu = new CommonMixInputParam();
inputHaozhu.setInputStreamName(haozhuStreamName);
CommonMixLayoutParams layoutHaozhu = new CommonMixLayoutParams();
layoutHaozhu.setImageLayer(3L);
layoutHaozhu.setImageWidth(360F);
layoutHaozhu.setImageHeight(640F);
layoutHaozhu.setLocationX(360F);
layoutHaozhu.setLocationY(220F);
CommonMixCropParams cropHaozhu = new CommonMixCropParams();
cropHaozhu.setCropWidth(360F);
cropHaozhu.setCropHeight(640F);
cropHaozhu.setCropStartLocationX(0F);
cropHaozhu.setCropStartLocationY(0F);
inputHaozhu.setLayoutParams(layoutHaozhu);
inputHaozhu.setCropParams(cropHaozhu);
inputList[0] = inputBackground;
inputList[1] = inputAnchor;
inputList[2] = inputHaozhu;
//发起混流
CreateCommonMixStreamRequest req = new CreateCommonMixStreamRequest();
req.setMixStreamSessionId(mixStreamSessionId);
req.setOutputParams(outP);
req.setInputStreamList(inputList);
Credential cred = new Credential(cloudVideoConfig.getSecretId(), cloudVideoConfig.getSecretKey());
ClientProfile clientProfile = new ClientProfile();
clientProfile.setSignMethod(ClientProfile.SIGN_TC3_256);
LiveClient client = new LiveClient(cred, cloudVideoConfig.getRegion(), clientProfile);
CreateCommonMixStreamResponse resp = client.CreateCommonMixStream(req);
}
创建混流,只有在当连麦者响应接受连麦请求发出后,服务端在收到请求后会后台去执行创建混流。这时候会同时生成一个唯一的混流sessionId以供识别当前混流。
需要注意的是,混流的前提是需要混的输入流必须是已经存在于云端了。这儿就会存在一个时间差问题:连麦者开始推流并且收到了推流成功,然后调用接受连麦的接口,服务端收到接受连麦之后便会去尝试创建混流,但是这时候可能流还没有到云端,那就会出现混流失败的错误了。那么解决这个时间差的方案是,目前会在连麦者端推流成功后,通过UI一般等待1-2s,然后再去调用接受连麦响应会比较更安全些。
5.2 CancelMixStream的demo代码
代码语言:javascript复制private void CancelMixStream(String mixStreamSessionId) throws TencentCloudSDKException {
//取消混流
CancelCommonMixStreamRequest req = new CancelCommonMixStreamRequest();
req.setMixStreamSessionId(mixStreamSessionId);
Credential cred = new Credential(cloudVideoConfig.getSecretId(), cloudVideoConfig.getSecretKey());
ClientProfile clientProfile = new ClientProfile();
clientProfile.setSignMethod(ClientProfile.SIGN_TC3_256);
LiveClient client = new LiveClient(cred, cloudVideoConfig.getRegion(), clientProfile);
CancelCommonMixStreamResponse resp = client.CancelCommonMixStream(req);
}
取消混流,则是有三种情况下都会发生:主播退出连麦、连麦者退出连麦和异常连麦状态检测触发。取消的时候,就需要用到前文我们提到的混流sessionId来唯一指定取消一场混流。
5.3 混流地址优化
需要单独提一下的是,因为在混流后是一个新流了,所以会生成一个新的流地址,如果不做任何特殊处理,这就会需要我们服务端在混流成功后,通过消息新到下发通知给到普通观众侧,这样做会有两个弊端:
- 当用户量大的时候,性能会是很大的挑战,
- 这个切换地址的时候,用户可能感受会不好。
为了解决这个问题,在这儿我们做了一个小trick,我们使用了原始主播A的拉流地址作为新的混流拉流地址。通过这样的替换,可以带来两个好处:
- 解决了前面的两个弊端,性能和体验的问题
- 兼容了以往的直播录制逻辑,无需改动即可实现混流视频的回放
5.4 混流画面黑边
混流后输出的画面有黑边一般是大小主播推流实际分辨率与混流参数layout_params里面设置的image_width和image_height不一致导致的,服务端对流画面进行了裁切所以出现黑边现象。解决办法:
- 设置image_width和image_height与推流分辨率比例保持一致,比如推流使用SDK接口设置了VIDEO_QUALITY_LINKMIC_SUB_PUBLISHER类型,分辨率为320_480,那么混流参数设置160_240就没问题。
- 混流的输入流宽image_width和高image_height,不仅支持像素类型(建议在0-3000以内),也支持百分比类型(建议0.01-0.99)。
6 可用性
连麦由于是一个实时性更高的场景,我们需要考虑其中的一些异常情况的出现,其中最大的问题就是连麦异常中断了,比如主播或连麦者的网络异常,或者一方设备故障、关机等等。
针对于这些异常情况,我们需要对于连麦中的双方设计一个心跳机制,心跳作用比较简单,就是为了当一方故障无法通知另一方的时候,由服务端来兜底通知,从而释放资源,并实现最终的一致性。下面是一个连麦的时序图设计:
需要注意的是,由于我们的服务都是采用的集群部署,所以会需要一个分布式锁去保证定时器的准确性。这儿我们采用了redis来实现我们的分布式锁,具体实现就不再赘述了。
7 总结
以上就是关于直播连麦的解决方案,时间仓促,写作水平也比较一般,如果有疑问的、或者描述有误的欢迎一起沟通探讨。