天天看点

超详细的《使用腾讯云移动直播开发连麦》

前言

之前有一篇文章讲了:使用腾讯开发基本的直播功能, 后来随着公司业务发展,又做了连麦功能,说句实话,这个连麦做的人真是蛋疼,腾讯的文档给的实在是无语😓,连麦功能嘛,大家经常看直播的都熟悉,下面就开始讲讲作为一个后端,我是们大概怎么处理连麦功能的。

贴一下百度对连麦对解释:

超详细的《使用腾讯云移动直播开发连麦》

先贴一下我找到的腾讯云连麦功能的文档

如何实现连麦:https://cloud.tencent.com/document/product/267/8108

云端混流:https://cloud.tencent.com/document/product/267/8832

连麦混流方案:https://github.com/tencentyun/qcloud-documents/blob/master/product/%E8%A7%86%E9%A2%91%E6%9C%8D%E5%8A%A1/%E7%A7%BB%E5%8A%A8%E7%9B%B4%E6%92%AD/to-del/%E8%BF%9E%E9%BA%A6-%E5%8D%87%E7%BA%A7%E6%96%B9%E6%A1%88(Android).md

选择连麦方案

首先腾讯云的连麦有两个方案,一是客户端混流,一是服务端混流,这两个的区别在文档里面也有,我在这里贴一下,方便大家理解:

补充一个点,腾讯云做直播的产品不止一个,我们使用的是移动云直播,腾讯云还有另一个可以做直播的产品叫:实时音视频,这个可以用于直播内容实时性要求较高的场景,比如视频会议等

超详细的《使用腾讯云移动直播开发连麦》

这个图应该很明确的说明了连麦混流的2个方式,混流其实就是把两个推流的画面混在一起,生成一个叠加的画面,然后用户就可以同时看到“两个主播的”直播,我们实现的连麦是使用的服务端混流,为啥选择服务端混流呢?在我们这边的原因是:

  1. 我们业务的功能就是一对一连麦,并且连麦的小画面流位置不需要变
  2. 服务端混流可以减少客户端压力,因为我们app并不是单纯的直播app,有时候开播或者看播会造成app的cpu较高、发热等,服务端混流,对于普通的观看用户来说,就是普通的观看,并不需要做任何其他处理
  3. 服务端混流可以少花钱,因为腾讯云直播计费是根据下行流量计费的,如果客户端混流的话,每个看播的用户都要同时拉两个流,那就相当于双倍计费

上述原因,我们选择服务端混流,反正我花钱买的云直播,干嘛自己搞。。。

了解连麦 并花钱

首先你要实现连麦,就得买腾讯云的连麦包,首次开发体验的可以买体验包,也不贵,购买位置:

超详细的《使用腾讯云移动直播开发连麦》

为什么要买这个呢,因为我们都知道,普通的直播呢,主播播的画面在观众那边看到的时候,是有一定延时的,大概就是3-5秒,那么连麦功能其实就是跟微信视频通话差不多,大主播推流,拉小主播的流,小主播推流,拉大主播的流也就是相互看对方的直播,但是在普通直播下,延时都3-5秒,那两个人相互看对方直播,这延时就不能接受了,所以就要买连麦包了,花钱是万能的,至于腾讯云直播是怎么处理的呢,大概就是把连麦的两个人的推流视频搞到各自超低延时的加速流下面,然后相互拉取对方的低延时流,这样两人就像视频通话一样,感觉不到明显的延时了,在后面的看法中,我们也会说怎么获取加速拉流地址,也就是低延时流、低延时播放,大家只要先明白,买连麦包,就是为了让连麦的两个人看对方的直播没有明显的延时

实现大小主播(或主播和观众)连麦

说半天买连麦包,我真不是腾讯的托。。。买完之后呢,当然是看文档啦。。。。通过上面那段话,大家应该明白了连麦就是两个人相互看对方的直播,那么连麦的两个人就需要都推流、都拉流,推流的话呢,推流地址和普通的推流地址没啥区别,假如两个主播要连麦,那么他们两个本来应该就是推流的,如果是主播和观众连麦,那么主播本来就是推流的,观众要和主播连麦的时候,给观众一个普通的推流地址,流名称根据业务定就行,别和其他的主播重复就行,推流地址咋生成呢?请看文章开头的那些文档哈,记得rtmp推流哦,毕竟如果你能看来这个文章

然后两个人拿到自己的推流地址之后,那他们只能推流,看不到对方的视频流啊,这个时候,连麦的关键就来了,加速拉流地址:

超详细的《使用腾讯云移动直播开发连麦》

就是这个图片了,对!没错,就是这个坑爹的图片。。。这文档,你告诉我,我怎么看出来这个是怎么生成的?!这文档首先不好找,其次是太难懂,无力吐槽,所以当时做的时候,拉着腾讯云技术支持调了很久,才调出来,这里就无私的给大家了,接下来说怎么生成加速拉流地址:

上面这个就是java生成加速拉流地址的方式,接下来解释一下这些参数里面的变量的意义:

  • PLAYURL : 就是你们在腾讯云直播的直播播放域名
  • APPNAME : 就是你自己的APPNAME,和推流地址里面的一样就行
  • streamId : 流id
  • bizid=12345 : 这个就跟恶心了,恶心到爆炸,其实很简单,你们开通腾讯云直播之后,他会给你们默认生成一个推流域名,就是腾讯云直播控制台里面-域名管理 这里面的那个删不了的那个域名,这个域名前面有一串数字,比如是:12345.livepush.myqcloud.com 那么这个12345 就是你们的这个腾讯云直播的bizId
  • KEY : 点进你们的推流域名下面,就有一个API key,就是这个
  • 然后最后面的LivePushAddress.getSafeUrl 这个是腾讯云提供的生成鉴权码的,可以在腾讯云文档找到,我的上一篇:使用腾讯开发基本的直播功能中有具体的代码,可以去看

到了这里,加速拉流地址就生成好了,然后大主播拉小主播的加速拉流地址、小主播拉大主播的加速拉流地址,分别把对方的加速拉流地址给到大主播小主播,客户端一边让他们推自己的流,同时又拉对方的加速拉流,就实现了大主播小主播的连麦。

在这里顺带说一下,这些加速拉流地址啊、推流地址啊、主播和用户、主播和主播如何知道对方请求连麦,肯定是一种类似实时通讯或者请求服务端接口的方式获取的,这篇文章我就不说这个具体细节了,在文章开头贴出的文档中,腾讯云给出了一个小直播的客户端例子,里面有连麦9步,研究一下客户端代码就会发现,其实腾讯云给出的例子中连麦的对方都是通过IM实现信息传输和接受的,但是具体实现呢,还是要看各位的实际情况,不一定要走例子中的9步,比如我们就没有完全按照例子中的9步去实现,这一部分需要客户端同学有一定的功底,研究清楚腾讯云提供的dome,这段想告诉大家的是连麦用户之间是需要实时通讯的,光靠给服务端发起请求是不够的,要主动把消息发送到客户端,具体实现方式大家根据自己的情况定

实现服务端混流

那么大主播小主播之间已经能看到对方的视频画面了,但是这个时候,也只是连麦的人在连麦,那直播的时候,不止连麦的两个人要看的对方的画面,对于普通观众来说,要同时能看到连麦的两个人的画面,如果使用客户端混流,那么客户端就需要同时拉连麦两人的流,然后把两个流画面重叠,这也就是前面说的客户端混流造成费用较高的原因。那么我这里只讲服务端混流,服务端混流,其实就是拿到连麦的两个人的推流流id,然后调用混流的Http接口,腾讯云会把你传过去的参数解析,然后把两个流混成一个流,混好之后的视频流会输出到你指定的流的输出流上面,就实现了服务端混流。

那么对于我们的连麦来说,混流的两个流其实就是主播的推流和观众的推流,混流之后的输出流其实就是主播流的输出流,理解了上面这个,那么其实恶心人的就来了,这个小画面大画面、小画面在大画面上的位置,这就非常恶心,很难调,我先列一下文档中的说法:

超详细的《使用腾讯云移动直播开发连麦》
超详细的《使用腾讯云移动直播开发连麦》

特别说明一下上面这个图,他这个图,要把它理解成一个正向拿着的手机,但是这个手机的宽比高长,千万别理解成他这个是把手机横着拿的

超详细的《使用腾讯云移动直播开发连麦》

上面这三个图在文档中都有,特地列出来给大家看看,不知道你们能不理解哈,说起来比较费口舌,我都不知道怎么说,这个东西涉及到了客户端的一些知识以及对UI位置、图层的裁剪、缩放的知识,所以建议拉着客户端一块研究,我这里呢就直接把我做好的贴出来了,我是调了好久才调出来的一个比较完善的,这个做法需要客户端传给后端连麦双方的推流的宽高,注意是推流的流的宽高,而不是手机的像素宽高,这个流的宽高应该是客户端在腾讯云sdk中获取,我在做的时候,我们客户端说这个不一定准确,所以在这里唠叨一下,客户端如果不确定的时候,写死一个固定的流的宽高,宁可写固定的不精确的,也不要传错误的,下面贴代码:

说明一下,我下面这个代码的大画面是连麦观众的画面,小画面是主播画面,别搞混了哈,你们自己看需求定,理解了就知道怎么调了

再说明一个点:我们的服务端混流画面,相对于正常竖屏手机小画面在大画面的右上角,距离屏幕上边框、右边框有一定间隔,如果对位置要求不一样的,只需要修改下面参数的:location_x、location_y、image_width、image_width等参数就行

int subWidth  = 134;
int subHeight = 201;
int offsetWidth = 80;
if(param.getWidth() == null || param.getWidth() == 0 || param.getHeight() == null || param.getHeight() == 0){
    param.setWidth(540);
    param.setHeight(960);
}
if (param.getWidth() < 540 || param.getHeight() < 960) {
    subWidth  = 100;
    subHeight = 150;
    offsetWidth = 50;
}
Integer DvalueWidth = null;     //多出来的宽
Integer DvalueHeight = null;     //多出来的高
int proportionWidth = param.getAnchorWidth();
int proportionHeight = param.getAnchorHeight();
if(param.getAnchorHeight() != null && param.getAnchorHeight() > 0
        && param.getAnchorWidth() != null && param.getAnchorWidth() > 0){
    //首先高不变裁宽
    proportionWidth = param.getAnchorHeight() * 2 / 3;
    if(proportionWidth > param.getAnchorWidth()){     //如果计算的宽比实际的宽大,那就宽不变,裁高
        proportionHeight = param.getAnchorWidth() * 3 / 2;
        if(param.getAnchorHeight() > proportionHeight){       //如果实际的高大于正常比例的高,裁剪高
            DvalueHeight = param.getAnchorHeight() - proportionHeight;
        }
    }else if(proportionWidth < param.getAnchorWidth()){
        DvalueWidth = param.getAnchorWidth() - proportionWidth;
    }
}
int subLocationX = param.getWidth() - subWidth - offsetWidth;
int subLocationY = (int) (param.getHeight() * 0.15);

Integer t = TimeUtil.getNowTimestamp() + 60;
String s = MD5Util.md5(KEY + t);
String url = "http://fcgi.video.qcloud.com/common_access?" +
        "cmd=" + APPID +
        "&inter +
        "&t=" + t +
        "&sign=" + s;
JSONObject bodyObject = new JSONObject();
bodyObject.put("timestamp", t - 60);
bodyObject.put("eventId", t - 60);

JSONObject interfaceObj = new JSONObject();
interfaceObj.put("interfaceName", "Mix_StreamV2");

JSONObject para = new JSONObject();
para.put("app_id", APPID);
para.put("interface", "mix_streamv2.start_mix_stream_advanced");
para.put("mix_stream_session_id", streamId + "_" + bizId);		
//这个mix_stream_session_id很重要,他表示一次混流,启用混流到后面结束混流,要用同一个mix_stream_session_id,也就是对于一次连麦(启用混流到结束混流),
//mix_stream_session_id值要相同,不然启用混流之后,没法结束混流,对于主播来说他没发正常进行下次连麦,对于观众来说可能看到的会长时间是混流画面,或者产生其他不可控问题,所以我是用流id加业务id,这个你们根据自己的情况定
para.put("output_stream_id", streamId + "");	//这个是你混流之后的流要输出到哪个流到输出流上面,一般来说就输出到直播的流的输出流,对于普通的观众来说就不用切换拉流地址了
//对于这个输出流到底用新的流地址还是用老的主播的流地址,其实是有一定争议的,输出到老的主播流上面,好处是不用通知所有用户切换流,而且用户刷新直播间,不需要担心拉流地址的问题,因为如果连麦过程中,新进来的用户和刷新直播间的用户,你返回给用户的流,要是新的连麦流,还有就是对于直播的录制(腾讯云点播api)等功能,有一定的不方便,
//但是如果输出到新的混流地址之后,不方便的点是要通知所有用户切换流,如果通知失败等等异常,那就只能用户端刷新看播了,但是这样做的好处是,可以较好的实现用户端看播平滑的切换连麦和连麦结束画面,
//因为你可以在用户端收到切换请求的时候,预加载要切换的流,等流拉成功后,在平滑的由客户端切换,而且用新的流对于录制连麦这段的视频,就很方便,具体看取舍,我这段话写的时候,属于后知后觉

JSONArray inputStreamList = new JSONArray();//要混合的流的集合

JSONObject streamObj = new JSONObject();            //背景流
streamObj.put("input_stream_id", streamId1 + "");		//连麦观众的流streamId1
JSONObject imageLayer = new JSONObject();
imageLayer.put("image_layer", 1);           //1表示背景流、大画面
streamObj.put("layout_params", imageLayer);
inputStreamList.add(streamObj);     //背景流加入list

JSONObject streamObj1 = new JSONObject();            //小画面流
streamObj1.put("input_stream_id", streamId + "");	//小窗口流
JSONObject imageLayer1 = new JSONObject();
imageLayer1.put("image_layer", 2);
imageLayer1.put("image_width", subWidth);            //小主播画面宽度
imageLayer1.put("image_height", subHeight);           //小主播画面高度
imageLayer1.put("location_x", subLocationX);             //x偏移:相对于大主播背景画面左上角的横向偏移
imageLayer1.put("location_y", subLocationY);               //y偏移:相对于大主播背景画面左上角的纵向偏移
streamObj1.put("layout_params", imageLayer1);

if(DvalueWidth != null || DvalueHeight != null){
    JSONObject cropParams = new JSONObject();
    cropParams.put("crop_width", proportionWidth);            //裁剪宽度
    if(DvalueWidth != null){
        cropParams.put("crop_x", DvalueWidth);         //裁剪x偏移:相对于原画面左上角的横向偏移
    }else{
        cropParams.put("crop_x", 0);
    }
    cropParams.put("crop_height", proportionHeight);          //裁剪高度
    if(DvalueWidth != null){
        cropParams.put("crop_y", DvalueHeight);         //裁剪y偏移:相对于原画面左上角的纵向偏移
    }else{
        cropParams.put("crop_y", 0);
    }
    streamObj1.put("crop_params", cropParams);
}

inputStreamList.add(streamObj1);    //图层流加入list

para.put("input_stream_list", inputStreamList);

interfaceObj.put("para", para);

bodyObject.put("interface", interfaceObj);
//最多重试5次,每次间隔2秒
for(int i = 0; i < 5; i++){
    String s1 = HttpClientUtils.sendPost(url, bodyObject);
    if(StringUtils.isNotBlank(s1)){
        JSONObject result = JSON.parseObject(s1);
        if(result.getInteger("code").equals(new Integer(0))){
            return true;
        }
    }
    Thread.sleep(2000L);
}
           

这个代码,有的时候小画面下面还是会有很细的黑边,但是不影响,我调了很多次才调出来的,里面最难调的就是混流之后小画面的黑边,供大家参考。

当上面这个http请求返回成功之后,那么普通观众那边的画面就会变成连个人连麦的画面。

结束连麦

连麦成功后肯定是要结束连麦的,结束连麦的时候也是发起http请求,但是注意的是,这个http请求的时候,其中的参数:mix_stream_session_id和output_stream_id一定要和混流的时候的参数值一样,才能正确取消混流,这个上面已经强调过了。取消混流没啥好说的,先贴一下文档图片:

超详细的《使用腾讯云移动直播开发连麦》

这个挺简单的,没啥难度,给大家贴一下我的代码:

Integer t = TimeUtil.getNowTimestamp() + 60;
String s = MD5Util.md5(KEY + t);
String url = "http://fcgi.video.qcloud.com/common_access?" +
        "cmd=" + APPID +
        "&inter +
        "&t=" + t +
        "&sign=" + s;
JSONObject bodyObject = new JSONObject();
bodyObject.put("timestamp", t - 60);
bodyObject.put("eventId", t - 60);

JSONObject interfaceObj = new JSONObject();
interfaceObj.put("interfaceName", "Mix_StreamV2");

JSONObject para = new JSONObject();
para.put("app_id", APPID);
para.put("interface", "mix_streamv2.cancel_mix_stream");
para.put("mix_stream_session_id", streamId + "_" + bizId);
para.put("output_stream_id", streamId) + "");

interfaceObj.put("para", para);

bodyObject.put("interface", interfaceObj);
String s1 = HttpClientUtils.sendPost(url, bodyObject);
           

到这里,连麦的基本功能就开发完成了。

最后跟大家说一下,不理解的多看文档、多试试,是在搞不出来那就给腾讯云提供单,谁让他文档写的那么辣鸡!

我上面那些代码仅限于我自己的开发经验,有不对的地方欢迎大家评论反馈,over!

2020-01-07记:最近随着使用的经验积累,发现还是由客户端混流的方式最好,服务端混流对于连麦异常断开的处理不友好,异常断开立马做下一次链接的话,需要等待1分钟左右才能正常连麦下一个;客户端混流就不需要,而且客户端混流支持的样式灵活,可以平滑的处理连麦画面切换,但是对于客户端要求更多

2020年6月1日记:https://www.agora.io/cn/ 声网的连麦产品听说很牛逼,腾讯有TRTC,也更适合连麦或者教育网课之类的场景

继续阅读