最开始观看直播是主播在那边又唱又跳,而你想与女神互动,只能简单的刷刷弹幕送送礼物。直到有了连麦,你才能用音视频的方式和主播互动,让女神看到你的画面,一起诉说风花雪月。
其实连麦简单说就是直播场景下,观众需要与主播音视频互动的功能。其中有三个角色,直播间里最开始的主播我们称为大主播,请求连麦的称为小主播,然后就是第三方观众。大致流程是,大主播端推一路自己的画面,拉一路小主播的画面;小主播端推一路自己的画面,拉一路大主播的画面;第三方观众拉一路大小主播混流后的画面。
主要流程就是这样简单,但是实际过程中还需要考虑一些细节,比如请求和接受连麦通信怎么做、大小主播怎样实现低延时交流、连麦前后不同流状态的处理。考虑到这些因素,腾讯云针对这部分逻辑进行了封装,提供了一套前后端完整的解决方案(MLVBLiveRoom)。
低延时流/加速流(ACC):区别于普通的直播流走的是CDN,延迟大概3秒左右;低延时流采用超级节点和内网专线构建的超级链路将大小主播之间地域的传输延迟降至最低,延迟可以控制在500ms以内。生成低延时流地址的方法和生成推流地址类似,通过rtmp拉流地址后面加上推流防盗链key计算的防盗链就可以了。
注意:防盗链签名计算使用默认初始的推流防盗链Key,不受自定义域名鉴权key影响。可重新添加推流域名查询初始key。
回音消除(AEC):对于大主播和小主播端,由于需要一边采集本地音视频数据推流出去,一边播放对方的音视频,这样就可能重复采集音频数据,导致回音现象,所以连麦场景需要打开回音消除。
云端混流:对于第三方观众,如果想同时看到大主播和小主播的画面,最简单的办法就是拉两路流。但是这里的缺点是这两条流的延时不好控制,以及多拉一条流产生多一路流的费用。所以通过在云端把这两条流混成一路流分发,就是云端混流。
以下代码以iOS为例,其中涉及的原理和接口名在Android端也基本一致。iOS和Android相关具体代码可直接下载TXLiteAVSDK,参考压缩包TXLiteAVDemo里面的MLVBLiveRoom封装类。
主播 A 从您的业务后台获取推流防盗链地址 streamA,之后可以用 TXLivePusher 进行推流。
TXLivePushConfig* _config = [[TXLivePushConfig alloc] init];
_config.audioSampleRate = AUDIO_SAMPLE_RATE_48000; //音频采样率默认就是48K,不要设为其它值
_txLivePush = [[TXLivePush alloc] initWithConfig: _config];
_txLivePush.delegate = _pushDelegate;
[_txLivePush setVideoQuality:VIDEO_QUALITY_HIGH_DEFINITION]; //非连麦模式:高清
[_txLivePush startPreview:previewView];
[_txLivePush startPush:rtmpUrl];
在连麦开始前,推流的 setVideoQuality 要切换为 VIDEO_QUALITY_LINKMIC_MAIN_PUBLISHER。该模式中会开启回声抑制(AEC),避免连麦中有回音。
setVideoQuality 支持推流中直接改变场景模式。
主播 B 从您的业务后台获取推流防盗链地址 streamB,之后可以用 TXLivePusher 进行推流。
TXLivePushConfig* _config = [[TXLivePushConfig alloc] init];
_config.audioSampleRate = AUDIO_SAMPLE_RATE_48000; //音频采样率默认就是48K,不要设为其它值
_txLivePush = [[TXLivePush alloc] initWithConfig: _config];
_txLivePush.delegate = _pushDelegate;
[_txLivePush setVideoQuality:VIDEO_QUALITY_LINKMIC_SUB_PUBLISHER]; //连麦模式:小主播
[_txLivePush startPreview:previewView];
[_txLivePush startPush:rtmpUrl];
在连麦开始前,推流的 setVideoQuality 要切换为 VIDEO_QUALITY_LINKMIC_SUB_PUBLISHER。该模式中会开启回声抑制(AEC),避免连麦中有回音。
SUB_PUBLISHER 的分辨率和码率都要低于 MAIN_PUBLISHER,毕竟那么小的窗口,用很高的分辨率是浪费的。
主播 B 向主播 A 发起连麦请求,请求可以由您的业务服务器中转,也可以使用腾讯云的 IM 云通讯解决方案。请求中要带上主播 B 的推流直播码,否则主播 A 无法去播放主播 B 的视频流。下面示例代码就是通过IMSDK发送C2C自定义消息实现信令的传输:
- (void)sendCCCustomMessage:(NSString *)userID data:(NSData *)data {
TIMCustomElem *elem = [[TIMCustomElem alloc] init];
[elem setData:data];
TIMMessage *msg = [[TIMMessage alloc] init];
[msg addElem:elem];
TIMConversation *conversation = [[TIMManager sharedInstance] getConversation:TIM_C2C receiver:userID];
if (conversation) {
[conversation sendMessage:msg succ:^{
NSLog(@"sendCCCustomMessage success");
} fail:^(int code, NSString *msg) {
NSLog(@"sendCCCustomMessage failed, data[%@]", data);
}];
}
}
SDK Demo源码中使用了腾讯云的 IM 云通讯解决方案实现了连麦请求和响应逻辑,详情参考Demo里面的RoomUtil封装组件。
主播 A 如果接受主播 B 的连麦请求,可以进行应答,这样主播 B 就知道连麦请求是否已经被同意了。
主播 A 此时需要使用 TXLivePlayer 播放 streamB 的 低延时 地址,<font color='red'>特别注意</font>,这里不能播放普通的 CDN 观看地址。区别在于前者的延迟一般在 500ms 以内,而 CDN 的延迟一般在 2s 以上,CDN 地址只能给普通观众观看,不能用于主播之间的连麦。
所以,要得到 500ms 左右的低延迟播放效果,需要:
低延时链路使用的是腾讯云核心机房的BGP资源,需要有带防盗链签名的 rtmp-liveplay 地址才能访问,所以主播 A 和 主播 B 都要给播放地址加上防盗链签名(txTime 和 txSecret)才能低延迟播放,如下是一个正确的低延迟播放地址:
| rtmp://8888.liveplay.myqcloud.com/live/8888_streamB?bizid=8888&txSecret=xxxxx&txTime=5C2A3CFF |
|:----|
LIVE_RTMP_ACC 的模式会开启播放器自带的精准延迟控制模块,该模式下的缓冲处理和音画同步技术相比于普通直播要求高很多。
NSString * playUrl = @"rtmp://8888.liveplay.myqcloud.com/live/8888_test?bizid=8888&txSecret=xxxx&txTime=xxx"; //加速拉流地址必须带防盗链key
TXLivePlayConfig * playConfig = [[TXLivePlayConfig alloc] init];
_livePlayer = [[TXLivePlayer alloc] init];
_livePlayer.deletate = _playDelegate;
[_livePlayer setConfig: playConfig];
[_livePlayer setupVideoWidget:CGRectMake(0, 0, 0, 0) containView: videoView insertIndex:0];
[_livePlayer setRenderMode:RENDER_MODE_FILL_SCREEN];
[_livePlayer startPlay:playUrl type:PLAY_TYPE_LIVE_RTMP_ACC]; //开始播放,type参数必须设置为PLAY_TYPE_LIVE_RTMP_ACC
由于低延时流使用腾讯云核心机房的BGP资源,所以需要购买计费套餐才能使用,如果您拉流报<font color='red'>获取加速拉流地址失败</font>错误,请先检查是否购买套餐包,腾讯云提供了1元套餐包方便开发者体验测试。
1.涉及业务功能:直播连麦(MLVBLiveRoom)功能、视频通话(RTCRoom)功能、低延时播放(RTMP_ACC)功能。
2.涉及终端包括:微信小程序端、Windows端、Web端、iOS端、Android端。
3.目前低延时拉流支持的最高并发数为10
路。
主播 B 在接到主播 A 同意连麦的请求后,可以开始播放 streamA 的低延时地址,同样需要:
低延时链路使用的是腾讯云核心机房的BGP资源,如果用于普通观众观看,延迟是挺低的,但是费用也挺高的。所以,普通观众观看还是要使用普通的 CDN 地址。拼接混流参数在SDK Demo里面使用的是下面接口,建议开发者先按照下面示例代码调通再根据自己需求自定义修改:
{
"timestamp":int(time.time()), # UNIX时间戳,即从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数
"eventId":int(time.time()), # 混流事件ID,取时间戳即可,后台使用
"interface":
{
"interfaceName":"Mix_StreamV2", # 固定取值"Mix_StreamV2"
"para":
{
"app_id": appid, # 填写直播APPID
"interface": "mix_streamv2.start_mix_stream_advanced", # 固定取值"mix_streamv2.start_mix_stream_advanced"
"mix_stream_session_id" : "3891_zachary1", # 填大主播的流ID
"output_stream_id": "3891_zachary1", # 填大主播的流ID
"input_stream_list":
[
# 主播1
{
"input_stream_id":"3891_zachary1", # 填大主播的流ID
"layout_params":
{
"image_layer": 1, # 图层标识号
}
},
# 主播2
{
"input_stream_id":"3891_zachary2", #填小主播的流ID
"layout_params":
{
"image_layer": 2,
"image_width": 160, # 小主播画面宽度
"image_height": 240, # 小主播画面高度
"location_x": 380, # x偏移:相对于大主播背景画面左上角的横向偏移
"location_y": 630 # y偏移:相对于大主播背景画面左上角的纵向偏移
}
}
]
}
}
}
{"code":0, "message":"Success!", "timestamp":1490079362}
具体使用方案可以参考 云端混流 API。
iOS示例
// 只有在推流启动前设置启动纯音频推流才会生效,推流过程中设置不会生效。
txLivePush.config.enablePureAudioPush = YES; // true 为启动纯音频推流,而默认值是 false;
[_txLivePublisher setConfig:_config]; // 重新设置 config
NSString* rtmpUrl = @"rtmp://2157.livepush.myqcloud.com/live/xxxxxx";
[_txLivePush startPush:rtmpUrl];
Android示例
// 只有在推流启动前设置启动纯音频推流才会生效,推流过程中设置不会生效。
mLivePushConfig.enablePureAudioPush(true); // true 为启动纯音频推流,而默认值是 false;
mLivePusher.setConfig(mLivePushConfig); // 重新设置 config
String rtmpUrl = "rtmp://2157.livepush.myqcloud.com/live/xxxxxx";
mLivePusher.startPusher(rtmpUrl);
iOS端示例代码来源于LiveRoom.m文件里面连麦合流参数拼接的接口createLinkMicMergeParams,在原有基础上修改了下面12行和45行,设置了input_type输入源类型为4表示输入源为音频:
// 连麦合流参数
- (NSDictionary*)createLinkMicMergeParams:(NSArray<NSString *> *)playUrlArray {
NSString *mainStreamId = [self getStreamIDByStreamUrl:_pushUrl];
NSMutableArray * inputStreamList = [NSMutableArray new];
//大主播
NSDictionary * mainStream = @{
@"input_stream_id": mainStreamId,
@"layout_params": @{
@"image_layer": [NSNumber numberWithInt:1],
@"input_type": [NSNumber numberWithInt:4]
}
};
[inputStreamList addObject:mainStream];
NSString * streamInfo = [NSString stringWithFormat:@"mainStream: %@", mainStreamId];
int mainStreamWidth = 540;
int mainStreamHeight = 960;
int subWidth = 160;
int subHeight = 240;
int offsetHeight = 90;
if (mainStreamWidth < 540 || mainStreamHeight < 960) {
subWidth = 120;
subHeight = 180;
offsetHeight = 60;
}
int subLocationX = mainStreamWidth - subWidth;
int subLocationY = mainStreamHeight - subHeight - offsetHeight;
NSMutableArray *subStreamIds = [[NSMutableArray alloc] init];
for (NSString *playUrl in playUrlArray) {
[subStreamIds addObject:[self getStreamIDByStreamUrl:playUrl]];
}
//小主播
int index = 0;
for (NSString * item in subStreamIds) {
NSDictionary * subStream = @{
@"input_stream_id": item,
@"layout_params": @{
@"image_layer": [NSNumber numberWithInt:(index + 2)],
@"input_type": [NSNumber numberWithInt:4],
@"image_width": [NSNumber numberWithInt: subWidth],
@"image_height": [NSNumber numberWithInt: subHeight],
@"location_x": [NSNumber numberWithInt:subLocationX],
@"location_y": [NSNumber numberWithInt:(subLocationY - index * subHeight)]
}
};
++index;
[inputStreamList addObject:subStream];
streamInfo = [NSString stringWithFormat:@"%@ subStream%d: %@", streamInfo, index, item];
}
NSLog(@"MergeVideoStream: %@", streamInfo);
//para
NSDictionary * para = @{
@"app_id": [NSNumber numberWithInt:[_appID intValue]] ,
@"interface": @"mix_streamv2.start_mix_stream_advanced",
@"mix_stream_session_id": mainStreamId,
@"output_stream_id": mainStreamId,
@"input_stream_list": inputStreamList
};
//interface
NSDictionary * interface = @{
@"interfaceName":@"Mix_StreamV2",
@"para":para
};
//mergeParams
NSDictionary * mergeParams = @{
@"timestamp": [NSNumber numberWithLong: (long)[[NSDate date] timeIntervalSince1970]],
@"eventId": [NSNumber numberWithLong: (long)[[NSDate date] timeIntervalSince1970]],
@"interface": interface
};
return mergeParams;
}
Android端示例代码来源于LiveRoom文件里面连麦合流参数拼接的接口createRequestParam,在原有基础上修改了下面13行和38行,设置了input_type输入源类型为4表示输入源为音频:
private JSONObject createRequestParam() {
JSONObject requestParam = null;
try {
// input_stream_list
JSONArray inputStreamList = new JSONArray();
// 大主播
{
JSONObject layoutParam = new JSONObject();
layoutParam.put("image_layer", 1);
layoutParam.put("input_type", 4);
JSONObject mainStream = new JSONObject();
mainStream.put("input_stream_id", mMainStreamId);
mainStream.put("layout_params", layoutParam);
inputStreamList.put(mainStream);
}
int subWidth = 160;
int subHeight = 240;
int offsetHeight = 90;
if (mMainStreamWidth < 540 || mMainStreamHeight < 960) {
subWidth = 120;
subHeight = 180;
offsetHeight = 60;
}
int subLocationX = mMainStreamWidth - subWidth;
int subLocationY = mMainStreamHeight - subHeight - offsetHeight;
// 小主播
int layerIndex = 0;
for (String item : mSubStreamIds) {
JSONObject layoutParam = new JSONObject();
layoutParam.put("image_layer", layerIndex + 2);
layoutParam.put("input_type", 4);
layoutParam.put("image_width", subWidth);
layoutParam.put("image_height", subHeight);
layoutParam.put("location_x", subLocationX);
layoutParam.put("location_y", subLocationY - layerIndex * subHeight);
JSONObject subStream = new JSONObject();
subStream.put("input_stream_id", item);
subStream.put("layout_params", layoutParam);
inputStreamList.put(subStream);
++layerIndex;
}
// para
JSONObject para = new JSONObject();
para.put("app_id", Long.valueOf(mAppID));
para.put("interface", "mix_streamv2.start_mix_stream_advanced");
para.put("mix_stream_session_id", mMainStreamId);
para.put("output_stream_id", mMainStreamId);
para.put("input_stream_list", inputStreamList);
// interface
JSONObject interfaceObj = new JSONObject();
interfaceObj.put("interfaceName", "Mix_StreamV2");
interfaceObj.put("para", para);
// requestParam
requestParam = new JSONObject();
requestParam.put("timestamp", System.currentTimeMillis() / 1000);
requestParam.put("eventId", System.currentTimeMillis() / 1000);
requestParam.put("interface", interfaceObj);
}
catch (Exception e) {
e.printStackTrace();
}
return requestParam;
}
混流后输出的画面有黑边一般是大小主播推流实际分辨率与混流参数layout_params里面设置的image_width和image_height不一致导致的,服务端对流画面进行了裁切所以出现黑边现象。解决办法:
1.设置image_width和image_height与推流分辨率比例保持一致,比如推流使用SDK接口设置了VIDEO_QUALITY_LINKMIC_SUB_PUBLISHER类型,分辨率为320_480,那么混流参数设置160_240就没问题
"input_stream_id":"3891_zachary2", #填小主播的流ID
"layout_params":
{
"image_layer": 2,
"image_width": 160, # 小主播画面宽度
"image_height": 240, # 小主播画面高度
"location_x": 380, # x偏移:相对于大主播背景画面左上角的横向偏移
"location_y": 630 # y偏移:相对于大主播背景画面左上角的纵向偏移
}
2.混流的输入流宽image_width和高image_height,不仅支持像素类型(建议在0-3000以内),也支持百分比类型(建议0.01-0.99)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。