You need to enable JavaScript to run this app.
导航
客户端发起转推直播
最近更新时间:2024.04.28 18:30:41首次发布时间:2021.07.18 15:06:10

对于一个音视频通话,你可以将其中的多路音视频流合为一路,并将合并得到的音视频流推送到指定的推流地址(通常是 CDN 地址)。你可以在应用服务端和应用客户端启动合流转推,本文介绍如何通过调用客户端 API,在 RTC 服务端发起和完成合流转推任务。

alt

关于如何调用 Open API,在服务端完成合流转推,参见 通过 OpenAPI 使用合流转推功能

前提条件

你已经集成 RTC SDK,实现了基本的音视频通话
支持发起合流转推的 SDK 详见API 及回调

调用时序

api

1. 创建引擎类

创建和初始化一个音视频引擎类。

参考 构建 RTC 应用 获取详细步骤。

// 创建引擎
rtcVideo = RTCVideo.createRTCVideo(this, Constants.APP_ID, videoEventHandler, null, null);
// 开启音视频采集
rtcVideo.startVideoCapture();
rtcVideo.startAudioCapture();

2. 进房

创建房间实例后,你可以加入房间发布和订阅音视频流。建议设置房间回调接口,以便监听房间和音视频流的状态回调。

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);
}

3. 开启任务

  1. 发起合流转推任务,在收到 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);
  }
};
  1. 合流视频的布局设置。分别以 1x4 和 2x2 两种布局模式为例。
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;
}

4. 更新任务

在收到远端用户视频流后,才可以更新合流布局。
在合流转推进行时,部分设置可以更新,详见 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);
}

5. 结束任务

在音视频互动中,你可以随时启动或停止合流转推。

private void stopPushCDNStream() {
    rtcVideo.stopPushStreamToCDN(CDN_TASK_ID);
  }

6. 离房

private void leaveRoom() {
    if (rtcRoom != null) {
        rtcRoom.leaveRoom();
        rtcRoom.destroy();
        rtcRoom = null;
    }
}

7. 销毁引擎

RTCVideo.destroyRTCVideo();

示例项目

API 及回调

说明:表格中的 macOS API 接口为 Objective-C,而示例项目中的 macOS 项目使用的是 Windows SDK 中的 API 接口。

APIAndroidiOSmacOSWindowsElectronFluterWeb

开启转推

startPushMixedStreamToCDN
任务参数为 MixedStreamConfig 结构体
使用 defaultMixedStreamConfig 进行参数初始化。

使用 startPushMixedStreamToCDN:mixedConfig:observer:
任务参数为 ByteRTCMixedStreamConfig 结构体,
使用 defaultMixedStreamConfig 进行参数初始化。

startPushMixedStreamToCDN:mixedConfig:observer: 开启转推。
任务参数为 ByteRTCMixedStreamConfig 结构体,
使用 defaultMixedStreamConfig 进行参数初始化。

startPushMixedStreamToCDN
任务参数为 IMixedStreamConfig 结构体,
使用 defaultMixedStreamConfig 进行参数初始化。

startPushMixedStreamToCDN
任务参数为 IMixedStreamConfig 结构体

startPushMixedStreamToCDN
任务参数为 MixedStreamConfig 结构体

startLiveTranscoding

更改音视频参数和视频布局updatePushMixedStreamToCDNupdatePushMixedStreamToCDN:mixedConfig:updatePushMixedStreamToCDN:mixedConfig:updatePushMixedStreamToCDNupdatePushMixedStreamToCDNupdatePushMixedStreamToCDNupdateLiveTranscoding
停止合流转推stopPushStreamToCDNstopPushStreamToCDN:stopPushStreamToCDN:stopPushStreamToCDNstopPushStreamToCDNstopPushStreamToCDNstopLiveTranscoding

常见问题

1. 如何在合流转推流中使用 SEI

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
}

2. 如何设置 task_id

当 APP 需要开启多个视频合流时,可以通过 task_id 来区分多个合流 ID。如果同时只有一个合流视频数据可以使用空字符串代替。
startPushMixedStreamToCDNstopPushStreamToCDNtask_id 需要成对出现。 如果 task_id 不同,会导致合流不会关闭。

3. 如何处理发起端意外掉线后重新登录

在开启转推任务后,如果因为进行刷新页面等操作,造成应用端进程异常终止,则转推任务会在空闲时间超过设定值后自动停止,默认空闲超时时间为180s
重启客户端重新登录后,需先调用 stopPushStreamToCDN 结束上一个任务,再开启新的转推任务,以免造成多个任务同时操作一个推流地址,导致新的转推任务失败。

4. 错误码

合流转推过程中返回的错误码详见各端 API 文档。

AndroidiOSMacWindowsElectronFlutterWeb
ByteRTCTranscoderErrorCodeByteRTCStreamMixingErrorCodeByteRTCStreamMixingErrorCodeStreamMixingErrorCodeStreamMixingErrorCodeStreamMixingErrorCodeStreamMixingEventErrorCode