本文介紹連麥互動的操作步驟和範例程式碼,助您快速接入。
方案介紹
使用即時音視頻+CDN旁路直播的方式,實現超低延時、更多人數的直播即時互動。
主播和連麥觀眾通過ARTC SDK向RTC房間推流;
商務服務器監聽推流事件並通過調用StartLiveMPUTask - 建立混流轉推任務(新)介面將RTC房間內的流轉推至直播CDN;
一般觀眾則使用播放器SDK從CDN拉流觀看。
主播開播和連麥的基本流程如下:
連麥前 | 主播端:使用ARTC SDK加入RTC 房間,推送音視頻即時資料流。 商務服務器:監聽RTC房間內流變化事件,當主播推流後,調用StartLiveMPUTask - 建立混流轉推任務(新)啟動旁路轉推任務Task1,傳入直播CDN推流地址,將RTC房間內流轉推到直播CDN上。 一般觀眾:使用播放器SDK傳入直播CDN拉流地址,拉流播放。 |
連麥中 | 連麥觀眾:
主播:拉取RTC房間內連麥觀眾的即時音視頻流,設定渲染視圖,即時渲染。 商務服務器:監聽RTC房間內流變化事件,當連麥觀眾推流後,更新旁路轉推任務Task1,將模式從旁路切換成混流,傳入主播和連麥觀眾的混流布局。 一般觀眾:不需要額外操作,直播畫面自動從主播一個人畫面切換成主播和連麥觀眾兩個人畫面。 |
結束連麥 | 連麥觀眾:
主播: 停止拉取連麥觀眾的音視頻流。 商務服務器:監聽RTC房間內流變化事件,當連麥觀眾停止推流後,更新混流任務task1,將模式從混流切換成旁路。 一般觀眾:不需要額外操作,直播畫面自動從主播和連麥觀眾兩個人畫面切換成主播一個人畫面。 |
監聽RTC房間內流變化事件回調相關API可以參考CreateEventSub - 建立訂閱房間訊息回調。
啟動旁路轉推任務相關API可以參考:StartLiveMPUTask - 建立混流轉推任務(新)。
實現步驟
步驟一 主播開播
主播開播的基本流程:
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];
步驟二 觀眾連麥
主播和觀眾連麥的基本流程:
1.連麥觀眾端停止播放CDN流,推流到RTC房間
連麥觀眾端停止阿里雲播放器CDN流播放,銷毀播放器引擎。
Android
aliPlayer.stop(); aliPlayer = nul;iOS
[self.cdnPlayer stop]; [self.cdnPlayer clearScreen]; self.cdnPlayer.playerView = nil;連麥觀眾端向RTC房間推流:連麥觀眾端推流到RTC房間和上面的主播端向RTC房間推流調用流程一樣,具體可參見向RTC房間推流。
連麥觀眾設定主播渲染畫面。
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) } }
2.主播端設定連麥觀眾渲染畫面
主播端渲染連麥觀眾畫面和連麥觀眾端渲染主播端畫面邏輯一致,可以參考上面的步驟。
3.商務服務更新旁路轉推到混流轉推
商務服務收到RTC房間內連麥觀眾推流後,調用UpdateLiveMPUTask - 更新混流轉推任務(新)更新原先旁路轉推的taskID, 將旁路更新成混流,MixMode設定成1,並設定主播和連麥觀眾的混流布局。
步驟三 觀眾下麥
主播和觀眾結束連麥的基本流程:
1.連麥觀眾端停止RTC房間推流,切換到播放器拉CDN流
連麥觀眾退出RTC房間,銷毀RTC引擎。
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建立播放器引擎,播放CDN流,步驟和上面一般觀眾端使用阿里雲播放器SDK拉流播放一致。
2.主播端停止拉取連麥觀眾的音視頻流
主播端停止播放連麥觀眾的音視頻流,在onRemoteTrackAvailableNotify中移除遠端視圖。
3.商務服務更新混流轉推到旁路轉推
商務服務收到RTC房間內連麥觀眾退出訊息回調後,調用UpdateLiveMPUTask - 更新混流轉推任務(新)更新原先混流轉推的taskID, 將混流更新成旁路,MixMode設定成0,CDN直播畫面從主播和觀眾連麥畫面切換成單主播畫面。