对于一个音视频通话,你可以将其中的多路音视频流合为一路,并将合并得到的音视频流推送到指定的推流地址(通常是 CDN 地址)。你可以在应用服务端和应用客户端启动合流转推,本文介绍如何通过调用客户端 API,在 RTC 服务端发起和完成合流转推任务。
关于如何调用 Open API,在服务端完成合流转推,参见 通过 OpenAPI 使用合流转推功能。
你已经集成 RTC SDK,实现了基本的音视频通话。
支持发起合流转推的 SDK 详见API 及回调。
创建和初始化一个音视频引擎类。
参考 构建 RTC 应用 获取详细步骤。
// 创建引擎
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, videoEventHandler, null, null);
// 开启音视频采集
rtcVideo.startVideoCapture();
rtcVideo.startAudioCapture();
创建房间实例后,你可以加入房间发布和订阅音视频流。建议设置房间回调接口,以便监听房间和音视频流的状态回调。
private void joinRoom(String roomId) {
// 创建房间
rtcRoom = rtcVideo.createRTCRoom(roomId);
rtcRoom.setRTCRoomEventHandler(roomEventHandler);
String token = requestRoomToken(roomId);
// 用户信息
UserInfo userInfo = new UserInfo(localUid, "");
// 房间配置
boolean isAutoPublish = true;
boolean isAutoSubscribeAudio = true;
boolean isAutoSubscribeVideo = true;
RTCRoomConfig roomConfig = new RTCRoomConfig(ChannelProfile.CHANNEL_PROFILE_CHAT_ROOM, isAutoPublish, isAutoSubscribeAudio, isAutoSubscribeVideo);
// 加入房间
rtcRoom.joinRoom(token, userInfo, roomConfig);
}
onRoomStateChanged
回调,进入 RTC 房间成功后调用 startPushMixedStreamToCDN
。private void startPushCDNStream() {
String cdnAddr = cdnAddressInput.getText().toString();
if (cdnAddr.isEmpty()) {
ToastUtil.showAlert(this, "cdn address is null");
return;
}
mixedStreamConfig.setUserID(localUid);
mixedStreamConfig.setRoomID(roomID);
// RTMP 推流地址
mixedStreamConfig.setPushURL(cdnAddr);
mixedStreamConfig.setPushURL(cdnAddr);mixedStreamConfig.setExpectedMixingType(ByteRTCStreamMixingType.STREAM_MIXING_BY_SERVER);
MixedStreamConfig.MixedStreamLayoutConfig layoutConfig = new MixedStreamConfig.MixedStreamLayoutConfig();
// 背景色
layoutConfig.setBackgroundColor(layoutColorInput.getText().toString());
// 设置合流布局
layoutConfig.setRegions(getLayoutRegions());
mixedStreamConfig.setLayout(layoutConfig);
// 开始推流到 CDN
rtcVideo.startPushMixedStreamToCDN(CDN_TASK_ID, mixedStreamConfig, mixedStreamObserver);
}
// 监听任务回调
IMixedStreamObserver mixedStreamObserver = new IMixedStreamObserver() {
@Override
public boolean isSupportClientPushStream() {
ToastUtil.showShortToast(CDNStreamActivity.this, "isSupportClientPushStream");
//应用层是否具备推流能力, false:不具备,使用 RTC 进行合流转推
return false;
}
@Override
public void onMixingEvent(ByteRTCStreamMixingEvent eventType, String taskId, ByteRTCTranscoderErrorCode error, MixedStreamType mixType) {
String msg = String.format("onMixingEvent, type:%s, taskId:%s, error:%s, mixType:%s", eventType.toString(), taskId, error.toString(), mixType.toString());
Log.d(TAG, msg);
ToastUtil.showLongToast(CDNStreamActivity.this, msg);
}
@Override
public void onMixingAudioFrame(String taskId, byte[] audioFrame, int frameNum, long timeStampMs) {
String msg = String.format("onMixingEvent, taskId:%s, frameNum:%d, timeStampMs:%l", taskId, frameNum, timeStampMs);
Log.d(TAG, msg);
ToastUtil.showLongToast(CDNStreamActivity.this, msg);
}
@Override
public void onMixingVideoFrame(String taskId, VideoFrame videoFrame) {
String msg = String.format("onMixingVideoFrame, taskId:%s", taskId);
Log.d(TAG, msg);
ToastUtil.showLongToast(CDNStreamActivity.this, msg);
}
@Override
public void onMixingDataFrame(String taskId, byte[] dataFrame, long time) {
String msg = String.format("onMixingDataFrame, taskId:%s", taskId);
Log.d(TAG, msg);
ToastUtil.showLongToast(CDNStreamActivity.this, msg);
}
@Override
public void onCacheSyncVideoFrames(String taskId, String[] userIds, VideoFrame[] videoFrame, byte[][] dataFrame, int count) {
String msg = String.format("onCacheSyncVideoFrames, taskId:%s", taskId);
Log.d(TAG, msg);
ToastUtil.showLongToast(CDNStreamActivity.this, msg);
}
};
private MixedStreamConfig.MixedStreamLayoutRegionConfig[] getLayoutRegions() {
int width = 360;
int height = 640;
int userNum = userNameList.size();
MixedStreamConfig.MixedStreamLayoutRegionConfig[] regions = new MixedStreamConfig.MixedStreamLayoutRegionConfig[userNum];
int index = 0;
String mode = layoutMode.getSelectedItem().toString();
if ("1x4".equals(mode)) {
for (String uid : userNameList) {
MixedStreamConfig.MixedStreamLayoutRegionConfig region = new MixedStreamConfig.MixedStreamLayoutRegionConfig();
region.setRoomID(roomID);
region.setUserID(uid);
region.setLocationX(index * width / userNum);
// 留出部分背景区域
region.setLocationY(50);
region.setWidth(width / userNum);
region.setHeight(height);
region.setAlpha(1);
region.setZOrder(0);
region.setRenderMode(MixedStreamConfig.MixedStreamRenderMode.MIXED_STREAM_RENDER_MODE_HIDDEN);
region.setStreamType(MixedStreamConfig.MixedStreamLayoutRegionConfig.MixedStreamVideoType.MIXED_STREAM_VIDEO_TYPE_MAIN);
region.setMediaType(MixedStreamConfig.MixedStreamMediaType.MIXED_STREAM_MEDIA_TYPE_AUDIO_AND_VIDEO);
regions[index] = region;
index ++;
}
} else if ("2x2".equals(mode)) {
for (String uid : userNameList) {
MixedStreamConfig.MixedStreamLayoutRegionConfig region = new MixedStreamConfig.MixedStreamLayoutRegionConfig();
region.setRoomID(roomID);
region.setUserID(uid);
region.setLocationX((index % 2) * width / userNum);
region.setLocationY((index / 2) * height / 2 + 50);
region.setWidth(width / 2);
// 为展示部分背景
region.setHeight(height / 2);
region.setAlpha(1);
region.setZOrder(0);
region.setRenderMode(MixedStreamConfig.MixedStreamRenderMode.MIXED_STREAM_RENDER_MODE_HIDDEN);
region.setStreamType(MixedStreamConfig.MixedStreamLayoutRegionConfig.MixedStreamVideoType.MIXED_STREAM_VIDEO_TYPE_MAIN);
region.setMediaType(MixedStreamConfig.MixedStreamMediaType.MIXED_STREAM_MEDIA_TYPE_AUDIO_AND_VIDEO);
regions[index] = region;
index ++;
}
}
return regions;
}
在收到远端用户视频流后,才可以更新合流布局。
在合流转推进行时,部分设置可以更新,详见 API 文档。
private void updateCDNStreamConfig() {
String cdnAddr = cdnAddressInput.getText().toString();
if (cdnAddr.isEmpty()) {
ToastUtil.showAlert(this, "cdn address is null");
return;
}
mixedStreamConfig.setPushURL(cdnAddr);
MixedStreamConfig.MixedStreamLayoutConfig layoutConfig = new MixedStreamConfig.MixedStreamLayoutConfig();
layoutConfig.setBackgroundColor(layoutColorInput.getText().toString());
layoutConfig.setRegions(getLayoutRegions());
mixedStreamConfig.setLayout(layoutConfig);
rtcVideo.updatePushMixedStreamToCDN(CDN_TASK_ID, mixedStreamConfig);
}
在音视频互动中,你可以随时启动或停止合流转推。
private void stopPushCDNStream() {
rtcVideo.stopPushStreamToCDN(CDN_TASK_ID);
}
private void leaveRoom() {
if (rtcRoom != null) {
rtcRoom.leaveRoom();
rtcRoom.destroy();
rtcRoom = null;
}
}
RTCVideo.destroyRTCVideo();
说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。
API | Android | iOS | macOS | Windows | Electron | Fluter | Web |
---|---|---|---|---|---|---|---|
开启转推 |
| 使用 |
|
|
|
| |
更改音视频参数和视频布局 | updatePushMixedStreamToCDN | updatePushMixedStreamToCDN:mixedConfig: | updatePushMixedStreamToCDN:mixedConfig: | updatePushMixedStreamToCDN | updatePushMixedStreamToCDN | updatePushMixedStreamToCDN | updateLiveTranscoding |
停止合流转推 | stopPushStreamToCDN | stopPushStreamToCDN: | stopPushStreamToCDN: | stopPushStreamToCDN | stopPushStreamToCDN | stopPushStreamToCDN | stopLiveTranscoding |
SEI 是视频编码格式中的补充增强信息,和视频编码帧一起打包发送,因此可以达到和视频帧发送和解析同步的效果。转推任务发起成功后,画面布局和背景等信息作为 SEI 透传到 RTMP 流中。拉流端需要自行提取和解析 SEI,例如,更新画面布局。
合流接口中传递到直播流中的信息,会在合流 I 帧前重复发送。例如,合流布局不变更,重复发送相同 SEI 数据,当合流布局变更,触发一个最新的 SEI。
在开启/更新合流时,可以通过设置 layoutConfig.userConfigExtraInfo
来设置自定义 SEI 信息。详见 发送和接收媒体补充增强信息。比如在直播答题场景中,在 SEI 中打包题目信息,每个人听到主播讲题时,同时看到对应的题目,不会因为不同延时导致题目出现的时间与讲解不匹配。
合流的 SEI 结构示例如下,其中,自定义消息为 app_data
的值。
{
"app_data": "自定义消息",
"canvas": {
"bgnd": "#000000",
"h": 640,
"w": 360
},
"regions": [
{
"alpha": 1.0,
"contentControl": 0,
"height": 640,
"locationX": 0,
"locationY": 50,
"renderMode": 1,
"uid": "user_343",
"width": 360,
"zorder": 0
}
],
"ts": 1705994199709
}
当 APP 需要开启多个视频合流时,可以通过 task_id 来区分多个合流 ID。如果同时只有一个合流视频数据可以使用空字符串代替。startPushMixedStreamToCDN
和 stopPushStreamToCDN
的 task_id
需要成对出现。 如果 task_id
不同,会导致合流不会关闭。
在开启转推任务后,如果因为进行刷新页面等操作,造成应用端进程异常终止,则转推任务会在空闲时间超过设定值后自动停止,默认空闲超时时间为180s
。
重启客户端重新登录后,需先调用 stopPushStreamToCDN
结束上一个任务,再开启新的转推任务,以免造成多个任务同时操作一个推流地址,导致新的转推任务失败。
合流转推过程中返回的错误码详见各端 API 文档。
TaskId
:TaskId
: