All Products
Search
Document Center

ApsaraVideo Live:Developer guide to co-streaming

Last Updated:Dec 15, 2025

This topic describes the procedure and sample code for co-streaming to help you quickly integrate this feature.

Solution overview

image

Use real-time communication and CDN bypass streaming to achieve ultra-low latency and real-time interaction with more participants.

  1. The streamer and co-streamer push streams to the RTC room through the ARTC SDK.

  2. 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.

  3. 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:

  1. Stops CDN stream playback using ApsaraVideo Player and destroys the player instance.

  2. Uses the ARTC SDK to join the RTC room and push real-time audio and video streams.

  3. Pulls the streamer's real-time audio and video stream from the RTC room, sets up the rendering view, and renders in real time.

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:

  1. Uses the ARTC SDK to leave the RTC room and destroys the RTC instance.

  2. Uses ApsaraVideo Player SDK with the CDN playback URL to switch back to CDN stream playback.

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.

Note

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:

image

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).

    Note

    When the streamer starts broadcasting, you can set MixMode to 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.

Note

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:

image

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 the onRemoteTrackAvailableNotify callback. 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 onRemoteTrackAvailableNotify callback 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:

image

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 = nil
  • Create 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.