All Products
Search
Document Center

ApsaraVideo Live:Developer guide to streamer battles

Last Updated:Nov 04, 2025

ApsaraVideo Live supports the streamer battle feature. It allows multiple streamers across different live rooms to start a real-time competition, which enhances the live streaming experience for viewers. This topic describes how to implement streamer battles based on RTC+CDN bypass live streaming and provides relevant sample code to help you quickly integrate the streamer battle feature.

Solution overview

image

You can implement streamer battles across different rooms using the cross-room stream pulling capability provided by the ARTC SDK. This capability pulls real-time audio and video streams from streamers in different rooms. Each streamer in their respective live room then calls the UpdateLiveMPUTask - Update mixed stream forwarding task (new) interface to switch to mixed stream mode. By passing the target room's ChannelID and streamer's UserId, the video streams of both streamers can be mixed into one view. CDN viewers will see the display change from a single streamer view to a battle view with both streamers. The workflow for streamer broadcasting and battles is as follows:

Before battle

Streamer A: Uses ARTC SDK to join RTC Room A and pushes real-time audio/video stream.

Application server: Monitors stream change events in RTC Room A. When the streamer starts pushing stream, calls StartLiveMPUTask - Create mixed stream forwarding task (new) to start bypass forwarding task Task1, passing the CDN ingest URL to forward the stream from RTC Room A to CDN.

Live room A regular viewers: Use ApsaraVideo Player SDK with Live room A CDN streaming URL to pull and play the stream.

Streamer B: Uses ARTC SDK to join RTC Room B and pushes real-time audio/video stream.

Application server: Monitors stream change events in RTC Room B. When the streamer starts pushing stream, calls StartLiveMPUTask - Create mixed stream forwarding task (new) to start bypass forwarding task Task2, passing the CDN ingest URL to forward the stream from RTC Room B to CDN.

Live room B regular viewers: Use ApsaraVideo Player SDK with Live room B CDN streaming URL to pull and play the stream.

During battle

Streamer A: Calls the ARTC SDK cross-room stream pulling interface to start pulling stream, passing Room B and User B.

Streamer B: Calls the ARTC SDK cross-room stream pulling interface to start pulling stream, passing Room A and User A.

Application server:

  1. Updates Task1 to mixed stream mode, passing Room A and User A, Room B and User B, and mixed stream layout information.

  2. Updates Task2 to mixed stream mode, passing Room B and User B, Room A and User A, and mixed stream layout information.

End of battle

Streamer A: Calls the cross-room stream pulling interface to stop pulling stream.

Streamer B: Calls the cross-room stream pulling interface to stop pulling stream.

Application server:

  1. Updates task1 to bypass forwarding mode, forwarding only User A's stream from Room A.

  2. Updates task2 to bypass forwarding mode, forwarding only User B's stream from Room B.

In the streamer battle scenario, regular viewers do not need to perform any additional operations. The display automatically switches from a single streamer view to a mixed stream view.

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: Streamer cross-room battle

Basic workflow for Streamer A and Streamer B cross-room battle:

image

1. Streamer A and Streamer B start cross-room stream pulling

Streamer A and Streamer B each call the cross-room stream pulling interface, passing the target room ID and user ID to start cross-room stream pulling.

Android

mAliRtcEngine.subscribeRemoteDestChannelStream(channelId, userId, AliRtcVideoTrackCamera, AliRtcAudioTrackMic, true);

iOS

[self.rtcEngine subscribeRemoteDestChannelStream:channelId uid:userId videoTrack:AliRtcVideoTrackCamera audioTrack:AliRtcAudioTrackMic sub:YES];

2. Set up rendering views for Streamer A and Streamer B

Android

Set the corresponding callback mAliRtcEngine.setRtcEngineNotify when initializing the engine. You need to set the remote view for the remote user 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 starts or stops streaming, 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 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)
    }
}

3. Business service updates from bypass forwarding to mixed stream forwarding

After the business service receives the start streamer cross-room battle event from the business app, it calls UpdateLiveMPUTask - Update mixed stream forwarding task (new) to update the forwarding tasks for Live room A and Live room B. In the UserInfos field, pass the room numbers and user IDs of Streamer A and Streamer B respectively. In the Layout field, pass the mixed stream layout and other necessary fields to update the mixed stream.

Step 3: End battle

Basic workflow for Streamer A and Streamer B ending cross-room battle:

image

1. Streamer A and Streamer B stop cross-room stream pulling

Streamer A and Streamer B each call the cross-room stream pulling interface, passing the target room ID and user ID to stop cross-room stream pulling.

Android

mAliRtcEngine.subscribeRemoteDestChannelStream(channelId, userId, AliRtcVideoTrackCamera, AliRtcAudioTrackMic, false);

iOS

[self.rtcEngine subscribeRemoteDestChannelStream:channelId uid:userId videoTrack:AliRtcVideoTrackCamera audioTrack:AliRtcAudioTrackMic sub:NO];

2. Business service updates from mixed stream forwarding to bypass stream forwarding

After the business service receives the end streamer cross-room battle event from the business app, it calls UpdateLiveMPUTask to update the mixed stream forwarding tasks for Live room A and Live room B. Set MixMode to 0 along with other necessary fields to update the mixed stream task to a bypass forwarding task. For details on UpdateLiveMPUTask interface parameters, see: UpdateLiveMPUTask - Update mixed stream forwarding task (new)