This topic describes the procedure and sample code for co-streaming to help you quickly integrate this feature.
Solution overview
Use real-time communication and CDN bypass streaming to achieve ultra-low latency and real-time interaction with more participants.
The streamer and co-streamer push streams to the RTC room through the ARTC SDK.
The application server monitors stream change events and calls the StartLiveMPUTask - Create a stream mixing task (new) API to forward streams from the RTC room to the CDN.
Regular viewers use the ApsaraVideo Player SDK to pull and watch streams from the CDN.
The basic process for streaming and co-streaming is as follows:
Before co-streaming | Streamer side: Uses the ARTC SDK to join the RTC room and push real-time audio and video streams. Application server: Monitors stream change events in the RTC room. After the streamer pushes a stream, calls StartLiveMPUTask - Create a stream mixing task (new) to start a bypass streaming task (Task1), passes the CDN ingest URL, and forwards streams from the RTC room to the CDN. Viewers: Use the ApsaraVideo Player SDK with the CDN playback URL to pull and play streams. |
During co-streaming | Co-streamer:
Streamer: Pulls the co-streamer's real-time audio and video stream from the RTC room, sets up the rendering view, and renders in real time. Application server: Monitors stream change events in the RTC room. After the co-streamer pushes a stream, updates the bypass streaming task (Task1), changes the mode from bypass to stream mixing, and passes the layout for the streamer and co-streamer. Viewers: No additional operations needed. The live stream automatically switches from showing only the streamer to showing both the streamer and co-streamer. |
End co-streaming | Co-streamer:
Streamer: Stops pulling the co-streamer's audio and video stream. Application server: Monitors stream change events in the RTC room. After the co-streamer stops pushing the stream, updates the stream mixing task (Task1) and changes the mode from stream mixing to bypass. Viewers: No additional operations needed. The live stream automatically switches from showing both the streamer and co-streamer to showing only the streamer. |
For APIs related to monitoring RTC room stream change event callbacks, see CreateEventSub - Create a subscription for room message callbacks.
For APIs related to starting bypass streaming tasks, see StartLiveMPUTask - Create a stream mixing task (new).
Implementation steps
Step 1: Streamer starts broadcasting
The basic process for a streamer to start broadcasting:
1. Streamer pushes stream to RTC room
The streamer uses the ARTC SDK to push a stream to the RTC room.
Android
For detailed steps on using the ARTC SDK to join an RTC room and push streams, see: Implementation steps
// Import ARTC related classes
import com.alivc.rtc.AliRtcEngine;
import com.alivc.rtc.AliRtcEngineEventListener;
import com.alivc.rtc.AliRtcEngineNotify;
private AliRtcEngine mAliRtcEngine = null;
if(mAliRtcEngine == null) {
mAliRtcEngine = AliRtcEngine.getInstance(this);
}
// Set channel mode
mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);
//Set video encoding parameters
AliRtcEngine.AliRtcVideoEncoderConfiguration aliRtcVideoEncoderConfiguration = new AliRtcEngine.AliRtcVideoEncoderConfiguration();
aliRtcVideoEncoderConfiguration.dimensions = new AliRtcEngine.AliRtcVideoDimensions(
720, 1280);
aliRtcVideoEncoderConfiguration.frameRate = 20;
aliRtcVideoEncoderConfiguration.bitrate = 1200;
aliRtcVideoEncoderConfiguration.keyFrameInterval = 2000;
aliRtcVideoEncoderConfiguration.orientationMode = AliRtcVideoEncoderOrientationModeAdaptive;
mAliRtcEngine.setVideoEncoderConfiguration(aliRtcVideoEncoderConfiguration);
mAliRtcEngine.publishLocalAudioStream(true);
mAliRtcEngine.publishLocalVideoStream(true);
mAliRtcEngine.setDefaultSubscribeAllRemoteAudioStreams(true);
mAliRtcEngine.subscribeAllRemoteAudioStreams(true);
mAliRtcEngine.setDefaultSubscribeAllRemoteVideoStreams(true);
mAliRtcEngine.subscribeAllRemoteVideoStreams(true);
//Set related callbacks
private AliRtcEngineEventListener mRtcEngineEventListener = new AliRtcEngineEventListener() {
@Override
public void onJoinChannelResult(int result, String channel, String userId, int elapsed) {
super.onJoinChannelResult(result, channel, userId, elapsed);
handleJoinResult(result, channel, userId);
}
@Override
public void onLeaveChannelResult(int result, AliRtcEngine.AliRtcStats stats){
super.onLeaveChannelResult(result, stats);
}
@Override
public void onConnectionStatusChange(AliRtcEngine.AliRtcConnectionStatus status, AliRtcEngine.AliRtcConnectionStatusChangeReason reason){
super.onConnectionStatusChange(status, reason);
handler.post(new Runnable() {
@Override
public void run() {
if(status == AliRtcEngine.AliRtcConnectionStatus.AliRtcConnectionStatusFailed) {
/* TODO: Be sure to handle the exception. The SDK has tried various recovery policies but still cannot recover. */
ToastHelper.showToast(VideoChatActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
} else {
/* TODO: Handle the exception as needed. Business code is added, usually for data statistics and UI changes. */
}
}
});
}
@Override
public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
super.OnLocalDeviceException(deviceType, exceptionType, msg);
/* TODO: Be sure to handle the exception. It is recommended to notify users of device errors when the SDK has tried all recovery policies but still cannot use the device. */
handler.post(new Runnable() {
@Override
public void run() {
String str = "OnLocalDeviceException deviceType: " + deviceType + " exceptionType: " + exceptionType + " msg: " + msg;
ToastHelper.showToast(VideoChatActivity.this, str, Toast.LENGTH_SHORT);
}
});
}
};
private AliRtcEngineNotify mRtcEngineNotify = new AliRtcEngineNotify() {
@Override
public void onAuthInfoWillExpire() {
super.onAuthInfoWillExpire();
/* TODO: Be sure to handle this. The token is about to expire. The business needs to trigger obtaining new authentication information for the current channel and user, then set refreshAuthInfo. */
}
@Override
public void onRemoteUserOnLineNotify(String uid, int elapsed){
super.onRemoteUserOnLineNotify(uid, elapsed);
}
//Remove the remote video stream rendering control settings in the onRemoteUserOffLineNotify callback
@Override
public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
super.onRemoteUserOffLineNotify(uid, reason);
}
//Set remote video stream rendering controls in the onRemoteTrackAvailableNotify callback
@Override
public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
handler.post(new Runnable() {
@Override
public void run() {
if(videoTrack == AliRtcVideoTrackCamera) {
SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
surfaceView.setZOrderMediaOverlay(true);
FrameLayout view = getAvailableView();
if (view == null) {
return;
}
remoteViews.put(uid, view);
view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
remoteVideoCanvas.view = surfaceView;
mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera);
} else if(videoTrack == AliRtcVideoTrackNo) {
if(remoteViews.containsKey(uid)) {
ViewGroup view = remoteViews.get(uid);
if(view != null) {
view.removeAllViews();
remoteViews.remove(uid);
mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera);
}
}
}
}
});
}
/* The business might trigger a situation where different devices compete for the same UserID, so this also needs to be handled */
@Override
public void onBye(int code){
handler.post(new Runnable() {
@Override
public void run() {
String msg = "onBye code:" + code;
}
});
}
};
mAliRtcEngine.setRtcEngineEventListener(mRtcEngineEventListener);
mAliRtcEngine.setRtcEngineNotify(mRtcEngineNotify);
//Local preview
mLocalVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
SurfaceView localSurfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this);
localSurfaceView.setZOrderOnTop(true);
localSurfaceView.setZOrderMediaOverlay(true);
FrameLayout fl_local = findViewById(R.id.fl_local);
fl_local.addView(localSurfaceView, layoutParams);
mLocalVideoCanvas.view = localSurfaceView;
mAliRtcEngine.setLocalViewConfig(mLocalVideoCanvas, AliRtcVideoTrackCamera);
mAliRtcEngine.startPreview();
//Join RTC room
mAliRtcEngine.joinChannel(token, null, null, null);
iOS
For detailed steps on using the ARTC SDK to join an RTC room and push streams, see: Implementation steps
// Import ARTC related classes
import AliVCSDK_ARTC
private var rtcEngine: AliRtcEngine? = nil
// Create engine and set callbacks
let engine = AliRtcEngine.sharedInstance(self, extras:nil)
...
self.rtcEngine = engine
// Set channel mode
engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
engine.setClientRole(AliRtcClientRole.roleInteractive)
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)
//Set video encoding parameters
let config = AliRtcVideoEncoderConfiguration()
config.dimensions = CGSize(width: 720, height: 1280)
config.frameRate = 20
config.bitrate = 1200
config.keyFrameInterval = 2000
config.orientationMode = AliRtcVideoEncoderOrientationMode.adaptive
engine.setVideoEncoderConfiguration(config)
engine.setCapturePipelineScaleMode(.post)
engine.publishLocalVideoStream(true)
engine.publishLocalAudioStream(true)
engine.setDefaultSubscribeAllRemoteAudioStreams(true)
engine.subscribeAllRemoteAudioStreams(true)
engine.setDefaultSubscribeAllRemoteVideoStreams(true)
engine.subscribeAllRemoteVideoStreams(true)
//Set related callbacks
extension VideoCallMainVC: AliRtcEngineDelegate {
func onJoinChannelResult(_ result: Int32, channel: String, elapsed: Int32) {
"onJoinChannelResult1 result: \(result)".printLog()
}
func onJoinChannelResult(_ result: Int32, channel: String, userId: String, elapsed: Int32) {
"onJoinChannelResult2 result: \(result)".printLog()
}
func onRemoteUser(onLineNotify uid: String, elapsed: Int32) {
// Remote user comes online
"onRemoteUserOlineNotify uid: \(uid)".printLog()
}
func onRemoteUserOffLineNotify(_ uid: String, offlineReason reason: AliRtcUserOfflineReason) {
// Remote user goes offline
"onRemoteUserOffLineNotify uid: \(uid) reason: \(reason)".printLog()
}
func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
"onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack) videoTrack: \(videoTrack)".printLog()
}
func onAuthInfoWillExpire() {
"onAuthInfoWillExpire".printLog()
/* TODO: Be sure to handle this. The token is about to expire. The business needs to trigger obtaining new authentication information for the current channel and user, then set refreshAuthInfo. */
}
func onAuthInfoExpired() {
"onAuthInfoExpired".printLog()
/* TODO: Be sure to handle this. Notify that the token is invalid, and perform leaving the meeting and releasing the engine. */
}
func onBye(_ code: Int32) {
"onBye code: \(code)".printLog()
/* TODO: Be sure to handle this. The business might trigger a situation where different devices compete for the same UserID. */
}
func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
"onLocalDeviceException deviceType: \(deviceType) exceptionType: \(exceptionType)".printLog()
/* TODO: Be sure to handle this. It is recommended to notify users of device errors when the SDK has tried all recovery policies but still cannot use the device. */
}
func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
"onConnectionStatusChange status: \(status) reason: \(reason)".printLog()
if status == .failed {
/* TODO: Be sure to handle this. It is recommended to notify users when the SDK has tried all recovery policies but still cannot recover. */
}
else {
/* TODO: Handle this as needed. Add business code, usually for data statistics and UI changes. */
}
}
}
//Local preview
let videoView = self.createVideoView(uid: self.userId)
let canvas = AliVideoCanvas()
canvas.view = videoView.canvasView
canvas.renderMode = .auto
canvas.mirrorMode = .onlyFrontCameraPreviewEnabled
canvas.rotationMode = ._0
self.rtcEngine?.setLocalViewConfig(canvas, for: AliRtcVideoTrack.camera)
self.rtcEngine?.startPreview()
//Join RTC room
let ret = self.rtcEngine?.joinChannel(joinToken, channelId: nil, userId: nil, name: nil) { [weak self] errCode, channelId, userId, elapsed in
if errCode == 0 {
// success
}
else {
// failed
}
let resultMsg = "\(msg) \n CallbackErrorCode: \(errCode)"
resultMsg.printLog()
UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self!)
}
let resultMsg = "\(msg) \n ReturnErrorCode: \(ret ?? 0)"
resultMsg.printLog()
if ret != 0 {
UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self)
}
2. Application server initiates forwarding task to forward RTC room streams to CDN
The application server creates a subscription for RTC room message callbacks to monitor streamer push stream events in the room. For detailed API information about subscribing to RTC room messages, see CreateEventSub - Create a subscription for room message callbacks.
After receiving notification that the streamer has pushed a stream to the RTC room, call the bypass streaming OpenAPI StartLiveMPUTask to forward streams from the RTC room to the CDN. For details about the bypass streaming API, see StartLiveMPUTask - Create a stream mixing task (new).
NoteWhen the streamer starts broadcasting, you can set
MixModeto 0, indicating single-stream forwarding without transcoding. The API requires a live streaming ingest URL, which only supports the RTMP protocol. For information about how to generate this URL, see Generate ingest and playback URLs.The application server monitors CDN stream pushing callbacks. After streams are forwarded to the CDN, it distributes the live streaming playback URL to notify viewers to start playback. For details about CDN stream pushing callbacks, see Callback settings.
3. Viewers use ApsaraVideo Player SDK to pull and play streams
When viewers receive the stream pulling notification from the application server, they create an ApsaraVideo Player instance and use the live streaming playback URL for playback. For detailed player API and usage information, see ApsaraVideo Player SDK.
It is recommended to change the CDN playback URL for regular viewers from RTMP format to HTTP-FLV format. Both contain the same content but use different transmission protocols. HTTP, as a mainstream Internet protocol, has a more mature network optimization foundation and uses default ports 80/443, making it easier to pass through firewalls. The RTMP protocol is older, and its common port 1935 may be restricted, affecting playback stability. Overall, HTTP-FLV is superior to RTMP in terms of compatibility and playback experience (such as stuttering and latency), so it is recommended to use HTTP-FLV as the first choice.
Android
AliPlayer aliPlayer = AliPlayerFactory.createAliPlayer(context);
aliPlayer.setAutoPlay(true);
UrlSource urlSource = new UrlSource();
urlSource.setUri("http://test.alivecdn.com/live/streamId.flv?auth_key=XXX"); // The CDN streaming URL of viewers.
aliPlayer.setDataSource(urlSource);
aliPlayer.prepare();iOS
self.cdnPlayer = [[AliPlayer alloc] init];
self.cdnPlayer.delegate = self;
self.cdnPlayer.autoPlay = YES;
AVPUrlSource *source = [[AVPUrlSource alloc] urlWithString:@""http://test.alivecdn.com/live/streamId.flv?auth_key=XXX"];
[self.cdnPlayer setUrlSource:source];
[self.cdnPlayer prepare];
Step 2: Viewer co-streaming
The basic process for streamer and viewer co-streaming:
1. Co-streamer stops playing CDN stream and pushes stream to RTC room
The co-streamer stops ApsaraVideo Player CDN stream playback and destroys the player engine.
Android
aliPlayer.stop(); aliPlayer = nul;iOS
[self.cdnPlayer stop]; [self.cdnPlayer clearScreen]; self.cdnPlayer.playerView = nil;The co-streamer pushes a stream to the RTC room: The process for a co-streamer to push a stream to the RTC room is the same as the process for a streamer described above. For details, see Push stream to RTC room.
The co-streamer sets up the streamer's rendering view.
Android
When initializing the engine, set the corresponding callback
mAliRtcEngine.setRtcEngineNotify. You need to set the remote view for remote users in theonRemoteTrackAvailableNotifycallback. Sample code is as follows:@Override public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){ handler.post(new Runnable() { @Override public void run() { if(videoTrack == AliRtcVideoTrackCamera) { SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(VideoChatActivity.this); surfaceView.setZOrderMediaOverlay(true); FrameLayout fl_remote = findViewById(R.id.fl_remote); if (fl_remote == null) { return; } fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); AliRtcEngine.AliRtcVideoCanvas remoteVideoCanvas = new AliRtcEngine.AliRtcVideoCanvas(); remoteVideoCanvas.view = surfaceView; mAliRtcEngine.setRemoteViewConfig(remoteVideoCanvas, uid, AliRtcVideoTrackCamera); } else if(videoTrack == AliRtcVideoTrackNo) { FrameLayout fl_remote = findViewById(R.id.fl_remote); fl_remote.removeAllViews(); mAliRtcEngine.setRemoteViewConfig(null, uid, AliRtcVideoTrackCamera); } } }); }iOS
When a remote user pushes or stops pushing a stream, the
onRemoteTrackAvailableNotifycallback is triggered. In this callback, you set or remove the remote view. Sample code is as follows:func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) { "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack) videoTrack: \(videoTrack)".printLog() // Remote user's stream status if audioTrack != .no { let videoView = self.videoViewList.first { $0.uidLabel.text == uid } if videoView == nil { _ = self.createVideoView(uid: uid) } } if videoTrack != .no { var videoView = self.videoViewList.first { $0.uidLabel.text == uid } if videoView == nil { videoView = self.createVideoView(uid: uid) } let canvas = AliVideoCanvas() canvas.view = videoView!.canvasView canvas.renderMode = .auto canvas.mirrorMode = .onlyFrontCameraPreviewEnabled canvas.rotationMode = ._0 self.rtcEngine?.setRemoteViewConfig(canvas, uid: uid, for: AliRtcVideoTrack.camera) } else { self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera) } if audioTrack == .no && videoTrack == .no { self.removeVideoView(uid: uid) self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera) } }
2. Streamer sets up co-streamer's rendering view
The process for the streamer to render the co-streamer's view is the same as the process for the co-streamer to render the streamer's view. You can refer to the steps above.
3. Application server updates from bypass streaming to stream mixing
After the application server receives notification that the co-streamer has pushed a stream to the RTC room, it calls UpdateLiveMPUTask - Update a stream mixing task (new) to update the original bypass streaming taskID, changing it from bypass to stream mixing by setting MixMode to 1, and configuring the stream mixing layout for the streamer and co-streamer.
Step 3: Viewer ends co-streaming
The basic process for ending co-streaming between streamer and viewer:
1. Co-streamer stops pushing stream to RTC room and switches to pulling CDN stream with player
The co-streamer exits the RTC room and destroys the RTC engine.
Android
mAliRtcEngine.stopPreview(); mAliRtcEngine.setLocalViewConfig(null, AliRtcVideoTrackCamera); mAliRtcEngine.leaveChannel(); mAliRtcEngine.destroy(); mAliRtcEngine = null;iOS
self.rtcEngine?.stopPreview() self.rtcEngine?.leaveChannel() AliRtcEngine.destroy() self.rtcEngine = nilCreate a player engine and play the CDN stream. The steps are the same as those for regular viewers using ApsaraVideo Player SDK to pull and play streams described above.
2. Streamer stops pulling co-streamer's audio and video stream
The streamer stops playing the co-streamer's audio and video stream and removes the remote view in onRemoteTrackAvailableNotify.
3. Application server updates from stream mixing to bypass streaming
After the application server receives the callback notification that the co-streamer has left the RTC room, it calls UpdateLiveMPUTask - Update a stream mixing task (new) to update the original stream mixing taskID, changing it from stream mixing to bypass by setting MixMode to 0. The CDN live stream view changes from showing both the streamer and co-streamer to showing only the streamer.