本文介绍如何实现秀场直播。
声网在 agora-ent-scenarios 仓库中提供秀场直播源代码供你参考。
本节展示声动语聊中常见的业务流程。
下图展示房主预览、创建、进入、退出直播的流程。
下图展示用户进入房主已创建好的直播间的流程。这里的用户可以有两种角色:
下图展示主播 PK 连麦的流程。在这个流程中,房主邀请另一个房间的房主开始 PK 连麦。两个房间内的观众都可以看到两个房主 PK 连麦直播的画面。
下图展示观众与主播连麦的流程。观众与主播连麦有两种方式:
按照以下步骤准备开发环境:
如需创建新项目,在 Android Studio 里,依次选择 Phone and Tablet > Empty Activity,创建 Android 项目。
使用 Maven Central 将声网 RTC SDK 集成到你的项目中。
a. 在 /Gradle Scripts/build.gradle(Project: <projectname>)
文件中添加如下代码,添加 Maven Central 依赖:
buildscript {
repositories {
...
mavenCentral()
}
...
}
allprojects {
repositories {
...
mavenCentral()
}
}
b. 在 /Gradle Scripts/build.gradle(Module: <projectname>.app)
文件中添加如下代码,将声网 RTC SDK 集成到你的 Android 项目中:
...
dependencies {
...
// x.y.z,请填写具体的 SDK 版本号,如:4.0.0 或 4.1.0-1。
// 通过互动直播产品发版说明获取最新版本号。
implementation 'io.agora.rtc:full-sdk:x.y.z'
}
将商汤美颜 SDK 集成到你的项目中。请联系商汤技术支持获取美颜 SDK、测试证书、集成步骤。
添加网络及设备权限。
在 /app/Manifests/AndroidManifest.xml
文件中,在 </application>
后面添加如下权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<!-- 对于 Android 12.0 及以上且集成 v4.1.0 以下声网 SDK 的设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- 对于 Android 12.0 及以上设备,还需要添加以下权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
在 /Gradle Scripts/proguard-rules.pro
文件中添加如下行,以防止声网 SDK 的代码被混淆:
-keep class io.agora.**{*;}
-dontwarn javax.**
-dontwarn com.google.devtools.build.android.**
如下时序图展示如何创建直播间、加入直播间、PK 连麦、观众连麦、退出直播间。声网 RTC SDK 承担实时音视频的业务,声网云服务承担信令消息和数据存储的业务。本节会详细介绍如何调用 RTC SDK 的 API 完成这些逻辑,但是信令消息的逻辑需要你参考时序图和示例项目自行实现。
创建房间时,你需要初始化 RTC 引擎、注册原始视频数据以设置商汤美颜、为主播设置本地视图并进行预览。本节展示初始化 RTC 引擎和注册原始视频数据的示例代码。
你需要调用 create
方法创建 RTC 引擎,并在 config
参数中配置上下文 Context、项目的 App ID、注册事件回调。调用 registerVideoFrameObserver
注册原始视频数据 beautyProcessor
,用于后续设置商汤美颜。再调用 enableVideo
开启视频。
val rtcEngine: RtcEngineEx
get() {
if (innerRtcEngine == null) {
val config = RtcEngineConfig()
config.mContext = AgoraApplication.the()
config.mAppId = io.agora.scene.base.BuildConfig.AGORA_APP_ID
config.mEventHandler = object : IRtcEngineEventHandler() {
override fun onError(err: Int) {
super.onError(err)
ToastUtils.showToast(
"Rtc Error code:$err, msg:" + RtcEngine.getErrorDescription(err)
)
}
}
innerRtcEngine = (RtcEngine.create(config) as RtcEngineEx).apply {
registerVideoFrameObserver(beautyProcessor)
enableVideo()
}
}
return innerRtcEngine!!
}
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示加入频道的示例代码。
调用 joinChannelEx
加入频道。频道用于传输直播间中的音视频流,云服务用于传输直播间中的信令消息和存储数据。用户在频道内可以进行实时音视频互动。频道内的用户有两种角色:
private fun joinChannel(eventListener: VideoSwitcher.IChannelEventListener) {
val rtcConnection = mMainRtcConnection
val uid = UserManager.getInstance().user.id
val channelName = mRoomInfo.roomId
val channelMediaOptions = ChannelMediaOptions()
channelMediaOptions.clientRoleType =
if (isRoomOwner) Constants.CLIENT_ROLE_BROADCASTER else Constants.CLIENT_ROLE_AUDIENCE
channelMediaOptions.autoSubscribeVideo = true
channelMediaOptions.autoSubscribeAudio = true
// 对于房主,发布音视频流
// 对于观众,不发布音视频流
channelMediaOptions.publishCameraTrack = isRoomOwner
channelMediaOptions.publishMicrophoneTrack = isRoomOwner
// 对于观众,把延时等级设置为 LOW_LATENCY,以便体验低延时的音视频互动
if (!isRoomOwner) {
channelMediaOptions.audienceLatencyLevel = AUDIENCE_LATENCY_LEVEL_LOW_LATENCY
}
mRtcVideoSwitcher.joinChannel(
rtcConnection,
channelMediaOptions,
eventListener
)
}
// class VideoSwitcherImpl
override fun joinChannel(
connection: RtcConnection,
mediaOptions: ChannelMediaOptions,
eventListener: VideoSwitcher.IChannelEventListener
) {
connectionsJoined.firstOrNull{ it.isSameChannel(connection)}
?.let {
ShowLogger.d(tag, "joinChannel joined connection=$it")
it.rtcEventHandler?.setEventListener(eventListener)
return
}
connectionsPreloaded.firstOrNull { it.isSameChannel(connection) }
?.let {
ShowLogger.d(tag, "joinChannel preloaded connection=$it")
it.rtcEventHandler?.setEventListener(eventListener)
it.rtcEventHandler?.subscribeMediaTime = SystemClock.elapsedRealtime()
it.mediaOptions = mediaOptions
rtcEngine.updateChannelMediaOptionsEx(mediaOptions, it)
connectionsPreloaded.remove(it)
connectionsJoined.add(it)
return
}
val connectionWrap = RtcConnectionWrap(connection)
connectionWrap.mediaOptions = mediaOptions
ShowLogger.d(tag, "joinChannel connection=$connectionWrap")
joinRtcChannel(connectionWrap, eventListener)
connectionsJoined.add(connectionWrap)
mainHandler.removeCallbacks(preLoadRun)
if (connectionsJoined.size == 1 || connectionsPreloaded.size <= 0) {
mainHandler.postDelayed(preLoadRun, 500)
}
}
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示调用 setupLocalVideo
在主播端设置并渲染主播视频的示例代码。
mRtcVideoSwitcher.setupLocalVideo(
VideoSwitcher.VideoCanvasContainer(
it,
mBinding.videoLinkingLayout.videoContainer,
0
)
)
// class VideoSwitcherImpl
override fun setupLocalVideo(container: VideoSwitcher.VideoCanvasContainer) {
localVideoCanvas?.let {
if (it.lifecycleOwner == container.lifecycleOwner && it.renderMode == container.renderMode && it.uid == container.uid) {
val videoView = it.view
val viewIndex = container.container.indexOfChild(videoView)
if (viewIndex == container.viewIndex) {
return
}
(videoView.parent as? ViewGroup)?.removeView(videoView)
container.container.addView(videoView, container.viewIndex)
return
}
}
var videoView = container.container.getChildAt(container.viewIndex)
if (!(videoView is TextureView)) {
videoView = TextureView(container.container.context)
container.container.addView(videoView, container.viewIndex)
}
val local = LocalVideoCanvasWrap(
container.lifecycleOwner,
videoView, container.renderMode, container.uid
)
local.mirrorMode = Constants.VIDEO_MIRROR_MODE_DISABLED
rtcEngine.setupLocalVideo(local)
}
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示调用 setupRemoteVideoEx
在观众端渲染远端视频(即主播的视频)的示例代码。
mRtcVideoSwitcher.setupRemoteVideo(
rtcConnection,
VideoSwitcher.VideoCanvasContainer(it, mBinding.videoLinkingLayout.videoContainer, mRoomInfo.ownerId.toInt())
)
// class VideoSwitcherImpl
override fun setupRemoteVideo(
connection: RtcConnection,
container: VideoSwitcher.VideoCanvasContainer
) {
remoteVideoCanvasList.firstOrNull {
it.connection.isSameChannel(connection) && it.uid == container.uid && it.renderMode == container.renderMode && it.lifecycleOwner == c
}?.let {
val videoView = it.view
val viewIndex = container.container.indexOfChild(videoView)
if (viewIndex == container.viewIndex) {
if (it.connection.rtcEventHandler?.isJoinChannelSuccess == true) {
rtcEngine.setupRemoteVideoEx(
it,
it.connection
)
}
return
}
it.release()
}
var videoView = container.container.getChildAt(container.viewIndex)
if (videoView !is TextureView) {
videoView = TextureView(container.container.context)
container.container.addView(videoView, container.viewIndex)
} else {
container.container.removeViewInLayout(videoView)
videoView = TextureView(container.container.context)
container.container.addView(videoView, container.viewIndex)
}
var connectionWrap = connectionsJoined.firstOrNull { it.isSameChannel(connection) }
if(connectionWrap == null){
connectionWrap = connectionsPreloaded.firstOrNull { it.isSameChannel(connection) }
}
if(connectionWrap == null){
connectionWrap = RtcConnectionWrap(connection)
}
val remoteVideoCanvasWrap = RemoteVideoCanvasWrap(
connectionWrap,
container.lifecycleOwner,
videoView,
container.renderMode,
container.uid
)
if(connectionWrap.rtcEventHandler?.isJoinChannelSuccess == true){
rtcEngine.setupRemoteVideoEx(
remoteVideoCanvasWrap,
connectionWrap
)
}
}
房主跨直播间 PK 连麦意味着不同频道内的主播加入对方频道进行连麦。当房间内用户收到房主 PK 连麦的信令消息后,房间内用户的代码逻辑如下:
joinChannelEx
加入频道 B,并且设置订阅频道 B 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 B 中主播的视频。joinChannelEx
加入频道 B,并且设置订阅频道 B 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 B 中主播的视频。joinChannelEx
加入频道 A,并且设置订阅频道 A 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 A 中主播的视频。joinChannelEx
加入频道 A,并且设置订阅频道 A 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 A 中主播的视频。完成这些逻辑后,观众可以同时接收频道 A 和 B 的音视频流,因此可以同时看到两个房间的房主。房主仅在自己的频道发流,在对方的频道内不发流仅收流,因此,房主可以(在对方频道)看到对方,达到连麦的效果。
结束 PK 连麦时,房间内用户都需要调用 leaveChannelEx
离开对方频道。
本节展示用户加入对方频道和离开对方频道的示例代码,如需查阅 setupRemoteVideoEx
方法调用逻辑,请参考 GitHub 示例项目。
// 加入对方频道
mRtcVideoSwitcher.joinChannel(
pkRtcConnection, channelMediaOptions, eventListener
)
// class VideoSwitcherImpl
override fun joinChannel(
connection: RtcConnection,
mediaOptions: ChannelMediaOptions,
eventListener: VideoSwitcher.IChannelEventListener
) {
connectionsJoined.firstOrNull{ it.isSameChannel(connection)}
?.let {
ShowLogger.d(tag, "joinChannel joined connection=$it")
it.rtcEventHandler?.setEventListener(eventListener)
return
}
connectionsPreloaded.firstOrNull { it.isSameChannel(connection) }
?.let {
ShowLogger.d(tag, "joinChannel preloaded connection=$it")
it.rtcEventHandler?.setEventListener(eventListener)
it.rtcEventHandler?.subscribeMediaTime = SystemClock.elapsedRealtime()
it.mediaOptions = mediaOptions
rtcEngine.updateChannelMediaOptionsEx(mediaOptions, it)
connectionsPreloaded.remove(it)
connectionsJoined.add(it)
return
}
val connectionWrap = RtcConnectionWrap(connection)
connectionWrap.mediaOptions = mediaOptions
ShowLogger.d(tag, "joinChannel connection=$connectionWrap")
joinRtcChannel(connectionWrap, eventListener)
connectionsJoined.add(connectionWrap)
mainHandler.removeCallbacks(preLoadRun)
if (connectionsJoined.size == 1 || connectionsPreloaded.size <= 0) {
mainHandler.postDelayed(preLoadRun, 500)
}
}
// 退出对方频道
mRtcVideoSwitcher.leaveChannel(
RtcConnection(
interactionInfo!!.roomId,
UserManager.getInstance().user.id.toInt()
)
)
// class VideoSwitcherImpl
override fun leaveChannel(connection: RtcConnection): Boolean {
connectionsJoined.firstOrNull { it.isSameChannel(connection) }
?.let { conn ->
val options = conn.mediaOptions
options.clientRoleType = Constants.CLIENT_ROLE_AUDIENCE
options.audienceLatencyLevel = Constants.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY
options.autoSubscribeVideo = false
options.autoSubscribeAudio = false
rtcEngine.updateChannelMediaOptionsEx(options, conn)
conn.rtcEventHandler?.setEventListener(null)
connectionsJoined.remove(conn)
connectionsPreloaded.add(conn)
conn.audioMixingPlayer?.stop()
return true
}
connectionsPreloaded.firstOrNull { it.isSameChannel(connection) }
?.let {
leaveRtcChannel(it)
connectionsPreloaded.remove(it)
return true
}
return false
}
观众与主播连麦时,你可以通过信令让主播邀请观众连麦,或观众向主播申请连麦。让待上麦观众更新频道媒体选项、预览并设置本地视图。让其他用户收到观众连麦通知后,渲染该连麦观众的视频。完成这些逻辑后,直播间内观众可以看到主播和上麦观众的连麦直播。
结束连麦时,你需要让待下麦观众更新频道媒体选项、停止预览并取消本地试图。让其他用户收到该观众下麦通知后,取消渲染该观众的视频。完成这些逻辑后,直播间观众可以看到仅有主播的直播画面。
本节展示观众连麦和结束连麦时更新频道媒体选项的示例代码。通过 updateChannelMediaOptionsEx
方法在观众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。观众的用户角色为 CLIENT_ROLE_AUDIENCE
,因此无法在频道内发布音频流。如果观众想与主播连麦,需要将用户角色修改为 CLIENT_ROLE_BROADCASTER
。
// 观众上麦时,用户角色从 AUDIENCE 切换成 BROADCASTER
val channelMediaOptions = ChannelMediaOptions()
// 发布音视频流
channelMediaOptions.publishCameraTrack = true
channelMediaOptions.publishMicrophoneTrack = true
channelMediaOptions.publishCustomAudioTrack = false
channelMediaOptions.enableAudioRecordingOrPlayout = true
channelMediaOptions.autoSubscribeVideo = true
channelMediaOptions.autoSubscribeAudio = true
channelMediaOptions.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
mRtcEngine.updateChannelMediaOptionsEx(channelMediaOptions, rtcConnection)
// 连麦观众下麦时,用户角色从 BROADCASTER 切换成 AUDIENCE
val channelMediaOptions = ChannelMediaOptions()
val rtcConnection = mMainRtcConnection
// 不发布音视频流
channelMediaOptions.publishCameraTrack = false
channelMediaOptions.publishMicrophoneTrack = false
channelMediaOptions.publishCustomAudioTrack = false
channelMediaOptions.enableAudioRecordingOrPlayout = true
channelMediaOptions.autoSubscribeVideo = true
channelMediaOptions.autoSubscribeAudio = true
// 注意:角色为观众时,即使 publishCameraTrack 和 publishMicrophoneTrack 设为 true,也无法发音视频流。如需发布音视频流,必须将角色设为主播。
channelMediaOptions.clientRoleType = Constants.CLIENT_ROLE_AUDIENCE
channelMediaOptions.audienceLatencyLevel = Constants.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY
mRtcEngine.updateChannelMediaOptionsEx(channelMediaOptions, rtcConnection)
直播结束时,主播和观众离开房间,你可以离开频道并销毁 RTC 引擎。
本节展示调用 destroy
销毁 RTC 引擎的示例代码。
fun destroy() {
// 移除所有消息和定时任务
innerVideoSwitcher?.let {
it.unloadConnections()
innerVideoSwitcher = null
}
// 销毁 RTC 引擎
innerRtcEngine?.let {
workingExecutor.execute { RtcEngine.destroy() }
innerRtcEngine = null
}
// 释放 beautyProcessor
innerBeautyProcessor?.let { processor ->
processor.release()
innerBeautyProcessor = null
}
}
// innerVideoSwitcher
// class VideoSwitcherImpl
// unloadConnections 函数中执行的具体操作
override fun unloadConnections() {
mainHandler.removeCallbacksAndMessages(null)
connectionsJoined.forEach {
leaveRtcChannel(it)
}
connectionsPreloaded.forEach {
leaveRtcChannel(it)
}
localVideoCanvas?.release()
connectionsForPreloading.clear()
connectionsPreloaded.clear()
connectionsJoined.clear()
ShowLogger.d(tag, "unloadConnections")
}
下图展示实现本文全部流程所需的 API 调用时序。