全部產品
Search
文件中心

ApsaraVideo Live:主播PK互動開發指南

更新時間:Jul 18, 2025

阿里雲ApsaraVideo for Live提供主播PK互動功能,該功能允許兩位或多位主播在直播中展開PK,增強觀眾的觀看體驗。本文為您介紹基於RTC+CDN旁路直播的方式來實現主播PK互動的操作步驟和相關範例程式碼,協助使用者快速接入主播PK互動情境。

方案介紹

使用ARTC SDK提供的跨房間拉流能力可以實現主播與主播之間跨房間PK的情境。 從不同的房間拉取主播的即時音視頻流,同時再各自直播間分別調用UpdateLiveMPUTask - 更新混流轉推任務(新)介面,模式切換成混流模式, 傳入目標房間的ChannelID和主播UserId, 便可以將兩個主播的視頻畫面混流到一個畫面中, CDN側觀眾畫面從單主播畫面切換成主播與主播PK畫面。其主播開播和PK的流程如下:

PK前

主播端A:使用ARTC SDK加入RTC 房間A,推送音視頻即時資料流。

商務服務器:監聽RTC房間A內流變化事件,當主播推流後,調用StartLiveMPUTask - 建立混流轉推任務(新),啟動旁路轉推任務Task1,傳入直播CDN推流地址,將RTC房間A內流轉推到直播CDN上。

直播間A一般觀眾:使用阿里雲播放器SDK傳入直播間A的直播CDN拉流地址,拉流播放。

主播端B:使用ARTC SDK加入RTC 房間B,推送音視頻即時資料流。

商務服務器:監聽RTC房間B內流變化事件,當主播推流後,調用StartLiveMPUTask - 建立混流轉推任務(新),啟動旁路轉推任務Task2,傳入直播CDN推流地址,將RTC房間內B流轉推到直播CDN上。

直播間B一般觀眾:使用阿里雲播放器SDK傳入直播間B的直播CDN拉流地址,拉流播放。

PK中

主播端A:調用ARTC SDK的跨房間拉流介面開始拉流,傳入房間B使用者B

主播端B:調用ARTC SDK的跨房間拉流介面開始拉流,傳入房間A使用者A

商務服務器

  1. 更新Task1成混流,分別傳入房間A使用者A房間B使用者B、混流布局資訊。

  2. 更新Task2成混流,分別傳入房間B使用者B房間A使用者A、混流布局資訊。

PK結束

主播端A:調用跨房間拉流介面停止拉流。

主播端B:調用跨房間拉流介面停止拉流。

商務服務器

  1. 更新task1成旁路轉推,轉推房間A內使用者A的流。

  2. 更新task2成旁路轉推,轉推房間B內使用者B的流。

主播PK情境,一般觀眾側不需要任何額外操作,觀看畫面自動從單主播畫面切換成混流畫面。

實現步驟

步驟一 主播開播

主播開播的基本流程:

1. 主播端向RTC房間推流

主播端使用ARTC SDK,向RTC房間推流。

Android

使用ARTC SDK 加入RTC房間及推流詳細步驟可參考:實現步驟

// 匯入ARTC相關類
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);
}
// 設定頻道模式
mAliRtcEngine.setChannelProfile(AliRtcEngine.AliRTCSdkChannelProfile.AliRTCSdkInteractiveLive);
mAliRtcEngine.setClientRole(AliRtcEngine.AliRTCSdkClientRole.AliRTCSdkInteractive);
mAliRtcEngine.setAudioProfile(AliRtcEngine.AliRtcAudioProfile.AliRtcEngineHighQualityMode, AliRtcEngine.AliRtcAudioScenario.AliRtcSceneMusicMode);

//設定視頻編碼參數
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);

//設定相關回調
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: 務必處理;建議業務提示客戶,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
                    ToastHelper.showToast(VideoChatActivity.this, R.string.video_chat_connection_failed, Toast.LENGTH_SHORT);
                } else {
                    /* TODO: 可選處理;增加業務代碼,一般用於資料統計、UI變化 */
                }
            }
        });
    }
    @Override
    public void OnLocalDeviceException(AliRtcEngine.AliRtcEngineLocalDeviceType deviceType, AliRtcEngine.AliRtcEngineLocalDeviceExceptionType exceptionType, String msg){
        super.OnLocalDeviceException(deviceType, exceptionType, msg);
        /* TODO: 務必處理;建議業務提示裝置錯誤,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
        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: 務必處理;Token即將到期,需要業務觸發重新擷取當前channel,user的鑒權資訊,然後設定refreshAuthInfo即可 */
    }

    @Override
    public void onRemoteUserOnLineNotify(String uid, int elapsed){
        super.onRemoteUserOnLineNotify(uid, elapsed);
    }

    //在onRemoteUserOffLineNotify回調中解除遠端視頻流渲染控制項的設定
    @Override
    public void onRemoteUserOffLineNotify(String uid, AliRtcEngine.AliRtcUserOfflineReason reason){
        super.onRemoteUserOffLineNotify(uid, reason);
    }

    //在onRemoteTrackAvailableNotify回調中設定遠端視頻流渲染控制項
    @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);
                        }
                    }
                }
            }
        });
    }

    /* 業務可能會觸發同一個UserID的不同裝置搶佔的情況,所以這個地方也需要處理 */
    @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);

//本地預覽
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();

//加入RTC房間
mAliRtcEngine.joinChannel(token, null, null, null);
iOS

使用ARTC SDK 加入RTC房間及推流詳細步驟可參考:實現步驟

// 匯入ARTC相關類
import AliVCSDK_ARTC

private var rtcEngine: AliRtcEngine? = nil
// 建立引擎並設定回調
let engine = AliRtcEngine.sharedInstance(self, extras:nil)
...
self.rtcEngine = engine
// 設定頻道模式
engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
engine.setClientRole(AliRtcClientRole.roleInteractive)
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)

//設定視頻編碼參數
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)

//設定相關回調
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) {
        // 遠端使用者的上線
        "onRemoteUserOlineNotify uid: \(uid)".printLog()
    }

    func onRemoteUserOffLineNotify(_ uid: String, offlineReason reason: AliRtcUserOfflineReason) {
        // 遠端使用者的下線
        "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: 務必處理;Token即將到期,需要業務觸發重新擷取當前channel,user的鑒權資訊,然後設定refreshAuthInfo即可 */
    }

    func onAuthInfoExpired() {
        "onAuthInfoExpired".printLog()

        /* TODO: 務必處理;提示Token失效,並執行離會與釋放引擎 */
    }

    func onBye(_ code: Int32) {
        "onBye code: \(code)".printLog()

        /* TODO: 務必處理;業務可能會觸發同一個UserID的不同裝置搶佔的情況 */
    }

    func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
        "onLocalDeviceException deviceType: \(deviceType)  exceptionType: \(exceptionType)".printLog()

        /* TODO: 務必處理;建議業務提示裝置錯誤,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
    }

    func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
        "onConnectionStatusChange status: \(status)  reason: \(reason)".printLog()

        if status == .failed {
            /* TODO: 務必處理;建議業務提示使用者,此時SDK內部已經嘗試了各種恢複策略已經無法繼續使用時才會上報 */
        }
        else {
            /* TODO: 可選處理;增加業務代碼,一般用於資料統計、UI變化 */
        }
    }
}


//本地預覽
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()

//加入RTC房間
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. 客戶商務服務發起轉推任務,將RTC房間流轉推到CDN

  • 客戶業務Server建立訂閱RTC房間訊息的回調,監聽房間內主播推流事件,訂閱RTC房間訊息的回調詳細介面參考:CreateEventSub - 建立訂閱房間訊息回調

  • 當收到RTC房間內主播推流後,調用旁路轉推OpenAPI介面StartLiveMPUTask,將RTC房間內的流轉推到CDN上,旁路轉推介面詳情參考:StartLiveMPUTask - 建立混流轉推任務(新)

    說明

    主播開播時可以將MixMode設定成0,表示單路轉推不轉碼。介面需要傳入直播推流地址,僅支援 RTMP協議,建置規則請參見產生推流地址和播放地址

  • 客戶業務Server監聽CDN推流回調,當流轉推到CDN後,下發直播拉流地址,通知一般觀眾端播放。CDN 推流回調詳情參考:回調設定

3. 一般觀眾端使用阿里雲播放器SDK拉流播放

當一般觀眾端接收到商務服務拉流通知後,建立阿里雲播放器執行個體,使用直播拉流地址進行播放。詳細的播放器介面及使用請參考:播放器SDK

說明

建議將一般觀眾的CDN播放地址由RTMP格式改為HTTP-FLV格式。兩者內容一致,但傳輸協議不同。HTTP作為互連網主流協議,具備更成熟的網路最佳化基礎,且預設使用80/443連接埠,更易通過防火牆;而RTMP協議較舊,常用連接埠1935可能被限制,影響播放穩定性。綜合來看,HTTP-FLV在通用性和播放體驗(如卡頓、延遲)上優於RTMP,推薦優先使用。

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");  //一般觀眾(非連麥觀眾)的CDN播放地址
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];

步驟二 主播跨房間PK

主播A和主播B跨房間PK的基本流程:

1.主播A和主播B開始跨房間拉流

主播A和主播B分別調用跨房間拉流介面,傳入目標的房間ID和使用者ID,開始跨房間拉流。

Android

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

iOS

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

2.分別設定主播A和主播B渲染畫面

Android

在初始化引擎的時候設定對應回調mAliRtcEngine.setRtcEngineNotify,需要在onRemoteTrackAvailableNotify回調中,為遠端使用者佈建遠端視圖,範例程式碼如下:

@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

遠端使用者並進行推流或停止推流時,會觸發onRemoteTrackAvailableNotify回調,在回調會設定或移除遠端視圖,範例程式碼如下:

func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
    "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack)  videoTrack: \(videoTrack)".printLog()
    // 遠端使用者的流狀態
    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.商務服務更新旁路轉推到混流轉推

商務服務收到業務App通知過來的開始主播跨房間PK的事件後,分別調用UpdateLiveMPUTask - 更新混流轉推任務(新)更新直播間A和直播間B的轉推任務,在UserInfos欄位傳入分別傳入主播A和主播B的房間號及使用者ID,Layout欄位傳入混流布局以及其他必要欄位,更新混流。

步驟三 結束PK

主播A和主播B結束跨房間PK的基本流程:

1.主播A和主播B停止跨房間拉流

主播A和主播B分別調用跨房間拉流介面,傳入目標的房間ID和使用者ID,停止跨房間拉流。

Android

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

iOS

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

2.商務服務更新旁路轉推到旁路流轉推

商務服務收到業務App通知過來的結束主播跨房間PK的事件後,分別調用UpdateLiveMPUTask更新直播間A和直播間B的混流轉推任務,MixMode設定成0及必要欄位,將混流任務更新成旁路轉推任務,其UpdateLiveMPUTask介面參數詳見:UpdateLiveMPUTask - 更新混流轉推任務(新)