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

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

前提条件

你已经集成 RTC SDK,实现了基本的音视频通话。

调用时序

alt

示例代码
开启任务

开启合流方法,需要在收到 onRoomStateChanged 回调,进入 RTC 房间成功后执行。

/**
 * 开启合流转推
 * @param rtcRoomId 房间id
 * @param rtcUserId 用户id
 * @param pushUrl rtmp 推流地址
 */
- (void)startLiveTranscodingWithPushUrl:(NSString *)pushUrl
                              rtcUserId:(NSString *)rtcUserId
                              rtcRoomId:(NSString *)rtcRoomId {
    ByteRTCVideoCompositingRegion *region = [[ByteRTCVideoCompositingRegion alloc] init];
    region.uid = rtcUserId; // 参与合流的媒体流所属用户的 ID
    region.roomId = rtcRoomId;
    region.localUser = YES;
    region.renderMode = ByteRTCRenderModeHidden;
    region.x = 0.0; //
    region.y = 0.0;
    region.width = 1.0;
    region.height = 1.0;
    region.zOrder = 1;
    region.alpha = 1.0;
    
    ByteRTCLiveTranscoding *transcodingSetting = [ByteRTCLiveTranscoding defaultTranscoding];
    transcodingSetting.layout.regions = @[region];
    transcodingSetting.roomId = rtcRoomId;
    transcodingSetting.userId = rtcUserId;
    transcodingSetting.url = pushUrl;
    
    // 设置为服务器合流
    transcodingSetting.expectedMixingType = ByteRTCStreamMixingTypeByServer;
    
    // 示例参数,具体参数可以根据业务修改。
    // 合流音频采样率,单位 Hz
    transcodingSetting.audio.sampleRate = 44100;
    
    // 合流音频声道数
    transcodingSetting.audio.channels = 2;
    
    // 合流视频帧率
    transcodingSetting.video.fps = 15;
    
    // 合流视频码率
    transcodingSetting.video.kBitRate = 1600;
    
    // 合流视频分辨率
    transcodingSetting.video.width = 720;
    transcodingSetting.video.height = 1280;
    
    self.transcodingSetting = transcodingSetting;
    // 开启合流
    int result = [self.rtcEngineKit startLiveTranscoding:@""
                                             transcoding:transcodingSetting
                                                observer:self];
    
    if (result != 0) {
        // Start live transcoding error
    }
}
objectivec
private final ILiveTranscodingObserver mILiveTranscodingObserver = new ILiveTranscodingObserver() {
    // ILiveTranscodingObserver相关方法
};
/**
 * 开启合流转推
 * @param roomId 房间id
 * @param userId 用户id
 * @param liveUrl rtmp 推流地址
 */
private void startLiveTranscoding(String roomId, String userId, String liveUrl) {
    LiveTranscoding liveTranscoding = LiveTranscoding.getDefualtLiveTranscode();
    // 设置房间id
    liveTranscoding.setRoomId(roomId);
    // 设置用户id
    liveTranscoding.setUserId(userId);
    // 设置推流的直播地址
    liveTranscoding.setUrl(liveUrl);
    // 设置合流模式,0 代表服务端合流
    liveTranscoding.setMixType(ByteRTCStreamMixingType.STREAM_MIXING_BY_SERVER);

    // 设置合流视频参数,具体参数根据情况而定
    LiveTranscoding.VideoConfig videoConfig = liveTranscoding.getVideo();
    videoConfig.setWidth(720);
    videoConfig.setHeight(1280);
    videoConfig.setFps(15);
    videoConfig.setKBitRate(1600);
    liveTranscoding.setVideo(videoConfig);

    // 设置合流音频参数,具体参数根据情况而定
    LiveTranscoding.AudioConfig audioConfig = liveTranscoding.getAudio();
    audioConfig.setSampleRate(44100);
    audioConfig.setChannels(2);
    liveTranscoding.setAudio(audioConfig);

    // 设置合流视频布局参数
    LiveTranscoding.Layout.Builder layoutBuilder = new LiveTranscoding.Layout.Builder();
    LiveTranscoding.Region selfRegion = new LiveTranscoding.Region();
    selfRegion.uid(userId);
    selfRegion.roomId(roomId);
    selfRegion.position(0, 0);
    selfRegion.size(1, 1);
    selfRegion.alpha(1);
    selfRegion.zorder(0);
    selfRegion.renderMode(LiveTranscoding.TranscoderRenderMode.RENDER_HIDDEN);
    layoutBuilder.addRegion(selfRegion);
    liveTranscoding.setLayout(layoutBuilder.builder());
    
    mLiveTranscoding = liveTranscoding;
    // 开始服务端合流任务,taskid使用空字符串即可
    mRTCVideo.startLiveTranscoding("", liveTranscoding, mILiveTranscodingObserver);
}
java
更新任务

在收到远端用户视频流后,才可以更新合流布局。

- (void)rtcRoom:(ByteRTCRoom *)rtcRoom onUserPublishStream:(NSString *)userId type:(ByteRTCMediaStreamType)type {
    if (type == ByteRTCMediaStreamTypeBoth ||
        type == ByteRTCMediaStreamTypeVideo) {
        // 在收到远端用户视频流后,才可以更新合流布局
    }
}
objectivec

private final IRTCRoomEventHandler mRTCRoomEventHandler = new IRTCRoomEventHandler() {
    @Override
    public void onUserPublishStream(String uid, MediaStreamType type) {
        if (type == MediaStreamType.RTC_MEDIA_STREAM_TYPE_BOTH || 
            type == MediaStreamType.RTC_MEDIA_STREAM_TYPE_VIDEO) {
            // 在收到远端用户视频流后,才可以更新合流布局
        }
    }
};
java

/**
 * @param localUserId 本地用户ID
 * @param remoteUserId 远端用户ID
 * @param roomId 房间ID
 */
- (void)updateLiveTranscoding:(NSString *)localUserId
                 remoteUserId:(NSString *)remoteUserId
                    rtcRoomId:(NSString *)rtcRoomId {
    // 当前主播的合流布局
    ByteRTCVideoCompositingRegion *localRegion = [[ByteRTCVideoCompositingRegion alloc] init];
    localRegion.uid = localUserId;
    localRegion.roomId = rtcRoomId;
    localRegion.localUser = YES;
    localRegion.renderMode = ByteRTCRenderModeHidden;
    localRegion.x = 0.0;
    localRegion.y = 0.25;
    localRegion.width = 0.5;
    localRegion.height = 0.5;
    localRegion.zOrder = 0;
    localRegion.alpha = 1.0;
    
    // 远端主播的合流布局
    ByteRTCVideoCompositingRegion *remoteRegion = [[ByteRTCVideoCompositingRegion alloc] init];
    remoteRegion.uid = remoteUserId;
    remoteRegion.roomId = rtcRoomId;
    remoteRegion.localUser = NO;
    remoteRegion.renderMode = ByteRTCRenderModeHidden;
    remoteRegion.x = 0.5;
    remoteRegion.y = 0.25;
    remoteRegion.width = 0.5;
    remoteRegion.height = 0.5;
    remoteRegion.zOrder = 0;
    remoteRegion.alpha = 1.0;
    
    self.transcodingSetting.layout.regions = @[localRegion, remoteRegion];
    [self.rtcEngineKit updateLiveTranscoding:@""
                                 transcoding:self.transcodingSetting];
}
objectivec

/**
 * @param localUserId 本地用户ID
 * @param remoteUserId 远端用户ID
 * @param roomId 房间ID
 */
private void updateLiveTranscoding(String localUserId, String remoteUserId, String roomId) {
    LiveTranscoding.Layout.Builder layoutBuilder = new LiveTranscoding.Layout.Builder();

    LiveTranscoding.Region selfRegion = new LiveTranscoding.Region();
    // 当前主播合流布局
    selfRegion.uid(localUserId);
    selfRegion.setLocalUser(true);
    selfRegion.renderMode(LiveTranscoding.TranscoderRenderMode.RENDER_HIDDEN);
    selfRegion.roomId(roomId);
    selfRegion.position(0, 0.25); 
    selfRegion.size(0.5, 0.5);
    selfRegion.alpha(1);
    selfRegion.zorder(0);
    layoutBuilder.addRegion(selfRegion);
    // 远端主播的合流布局
    LiveTranscoding.Region hostRegion = new LiveTranscoding.Region();
    hostRegion.uid(remoteUserId);
    hostRegion.setLocalUser(false);
    hostRegion.renderMode(LiveTranscoding.TranscoderRenderMode.RENDER_HIDDEN);
    hostRegion.roomId(roomId);
    hostRegion.position(0.5, 0.25);
    hostRegion.size(0.5, 0.5);
    hostRegion.alpha(1);
    hostRegion.zorder(0);
    layoutBuilder.addRegion(hostRegion);

    mLiveTranscoding.setLayout(layoutBuilder.builder());
    mRTCVideo.updateLiveTranscoding("", mLiveTranscoding);
}
java
结束任务

/**
 * 停止合流转推或者退出房间的时候调用
 */
- (void)stopLiveTranscoding {
    [self.rtcEngineKit stopLiveTranscoding:@""];
    self.transcodingSetting = nil;
}
objectivec

/**
 * 停止合流转推或者退出房间的时候调用
 */
public void stopLiveTranscoding() {
    if (mRTCVideo != null) {
        mRTCVideo.stopLiveTranscoding("");
    }
    mLiveTranscoding = null;
}
java
常见问题

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

SEI 是视频编码格式中的补充增强信息,用于传输一些用户自定义的信息。
使用场景:因为可以和视频编码帧打包到一起发送,可以和视频帧达到发送和解析同步的效果,比如在直播答题场景中,可以用 SEI 打包题目信息,这样每个人听到主播讲那道题时,可以同步看到题目,不会因为延时不同导致题目出现的时间不一样。

- (void)startLiveTranscodingWithPushUrl:(NSString *)pushUrl
                              rtcUserId:(NSString *)rtcUserId
                              rtcRoomId:(NSString *)rtcRoomId {
    self.transcodingSetting.layout.appData = @"json";
    int result = [self.rtcEngineKit startLiveTranscoding:@""
                                             transcoding:self.transcodingSetting
                                                observer:self];
}

- (void)updateLiveTranscoding:(NSString *)localUserId
                 remoteUserId:(NSString *)remoteUserId
                    rtcRoomId:(NSString *)rtcRoomId {
    self.transcodingSetting.layout.appData = @"json";
    [self.rtcEngineKit updateLiveTranscoding:@""
                                 transcoding:self.transcodingSetting];
}
ObjectC
2. 如何设置 task_id

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

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

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

4. 错误码

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

AndroidiOSMacWindowsLinuxWeb
错误码ByteRTCTranscoderErrorCodeByteRTCTranscoderErrorCodeByteRTCTranscoderErrorCodeStreamMixingErrorCodeStreamMixingErrorCodeStreamMixingEventErrorCode
客户端 API 参考
Android
iOS
Mac
Windows
Linux
Web