本文介绍如何实现秀场直播。
声网在 agora-ent-scenarios 仓库中提供秀场直播源代码供你参考。
本节展示声动语聊中常见的业务流程。
下图展示房主预览、创建、进入、退出直播的流程。
下图展示用户进入房主已创建好的直播间的流程。这里的用户可以有两种角色:
下图展示主播 PK 连麦的流程。在这个流程中,房主邀请另一个房间的房主开始 PK 连麦。两个房间内的观众都可以看到两个房主 PK 连麦直播的画面。
下图展示观众与主播连麦的流程。观众与主播连麦有两种方式:
在 Xcode 中进行以下操作,在你的 app 中实现秀场直播功能:
创建一个新的项目,Application 选择 App,Interface 选择 Storyboard,Language 选择 Swift。
为你的项目设置自动签名。
设置部署你的 app 的目标设备。
添加项目的设备权限。在项目导航栏中打开 info.plist
文件,编辑属性列表,添加以下属性:
key | type | value |
---|---|---|
Privacy - Microphone Usage Description | String | 使用麦克风的目的,例如 for a live interactive streaming |
Privacy - Camera Usage Description | String | 使用摄像头的目的,例如 for a live interactive streaming |
将声网 RTC SDK 集成到你的项目。开始前请确保你已安装 CocoaPods,如尚未安装 CocoaPods,参考 Getting Started with CocoaPods 安装说明。
在终端里进入项目根目录,并运行 pod init
命令。项目文件夹下会生成一个 Podfile
文本文件。
打开 Podfile
文件,修改文件为如下内容。注意将 Your App
替换为你的 Target 名称。
platform :ios, '9.0'
target 'Your App' do
# x.y.z 请填写具体的 SDK 版本号,如 4.0.1 或 4.0.0.4。
# 可通过互动直播发版说明获取最新版本号。
pod 'AgoraRtcEngine_iOS', 'x.y.z'
end
将商汤美颜 SDK 集成到你的项目中。请联系商汤技术支持获取美颜 SDK、测试证书、集成步骤。
在终端内运行 pod install
命令安装声网 SDK。成功安装后,Terminal 中会显示 Pod installation complete!
。
成功安装后,项目文件夹下会生成一个后缀为 .xcworkspace
的文件,通过 Xcode 打开该文件进行后续操作。
如下时序图中展示了展示如何创建直播间、加入直播间、PK 连麦、观众连麦、退出直播间。声网 RTC SDK 承担实时音视频的业务,声网云服务承担信令消息和数据存储的业务。本节会详细介绍如何调用 RTC SDK 的 API 完成这些逻辑,但是信令消息的逻辑需要你参考时序图和示例项目自行实现。
创建房间时,你需要初始化 RTC 引擎、注册原始视频数据以设置商汤美颜、为主播设置本地视图并进行预览。本节展示调用 sharedEngineWithConfig
初始化 RTC 引擎的示例代码。
fileprivate(set) lazy var agoraKit: AgoraRtcEngineKit = {
let kit = AgoraRtcEngineKit.sharedEngine(with: rtcEngineConfig, delegate: nil)
showLogger.info("load AgoraRtcEngineKit, sdk version: \(AgoraRtcEngineKit.getSdkVersion())", context: kShowLogBaseContext)
return kit
}()
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示加入频道的示例代码。
调用 joinChannelExByToken
加入频道。频道用于传输直播间中的音视频流,云服务用于传输直播间中的信令消息和存储数据。用户在频道内可以进行实时音视频互动。频道内的用户有两种角色:
private func joinChannel(needUpdateCavans: Bool = true) {
agoraKitManager.setRtcDelegate(delegate: self, roomId: roomId)
guard let channelId = room?.roomId, let ownerId = room?.ownerId else {
return
}
currentChannelId = channelId
self.joinStartDate = Date()
let uid: UInt = UInt(ownerId)!
agoraKitManager.joinChannelEx(currentChannelId: channelId,
targetChannelId: channelId,
ownerId: uid,
options: self.channelOptions,
role: role) { [weak self] in
guard let self = self else { return }
if needUpdateCavans {
if self.role == .audience {
self.agoraKitManager.setupRemoteVideo(channelId: channelId,
uid: uid,
canvasView: self.liveView.canvasView.localView)
} else {
self.agoraKitManager.setupLocalVideo(uid: uid, canvasView: self.liveView.canvasView.localView)
}
}
}
liveView.canvasView.setLocalUserInfo(name: room?.ownerName ?? "", img: room?.ownerAvatar ?? "")
self.muteLocalVideo = false
self.muteLocalAudio = false
}
// ShowAgoraKitManager.swift
private func _joinChannelEx(currentChannelId: String,
targetChannelId: String,
ownerId: UInt,
token: String,
options:AgoraRtcChannelMediaOptions,
role: AgoraClientRole) {
if exConnectionMap[targetChannelId] == nil {
let subscribeStatus = role == .audience ? false : true
let mediaOptions = AgoraRtcChannelMediaOptions()
mediaOptions.autoSubscribeAudio = subscribeStatus
mediaOptions.autoSubscribeVideo = subscribeStatus
mediaOptions.clientRoleType = role
// 对于观众,把延时等级设置为 lowLatency,以便体验低延时的音视频互动
if role == .audience {
mediaOptions.audienceLatencyLevel = .lowLatency
}else{
updateVideoEncoderConfigurationForConnenction(currentChannelId: currentChannelId)
}
let connection = AgoraRtcConnection()
connection.channelId = targetChannelId
connection.localUid = UInt(VLUserCenter.user.id) ?? 0
let proxy = delegateMap[currentChannelId]
let date = Date()
showLogger.info("try to join room[\(connection.channelId)] ex uid: \(connection.localUid)", context:
kShowLogBaseContext)
let ret =
agoraKit.joinChannelEx(byToken: token,
connection: connection,
delegate: proxy,
mediaOptions: mediaOptions) { channelName, uid, elapsed in
let cost = Int(-date.timeIntervalSinceNow * 1000)
showLogger.info("join room[\(channelName)] ex success uid: \(uid) cost \(cost) ms", context:
kShowLogBaseContext)
}
agoraKit.updateChannelEx(with: mediaOptions, connection: connection)
exConnectionMap[targetChannelId] = connection
if ret == 0 {
showLogger.info("join room ex: channelId: \(targetChannelId) ownerId: \(ownerId)",
context: "AgoraKitManager")
}else{
showLogger.error("join room ex fail: channelId: \(targetChannelId) ownerId: \(ownerId) token = \(token),
\(ret)",
context: kShowLogBaseContext)
}
}
}
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示调用 setupLocalVideo
在主播端设置并渲染主播视频的示例代码。
self.agoraKitManager.setupLocalVideo(uid: uid, canvasView: self.liveView.canvasView.localView)
// ShowAgoraKitManager.swift
func setupLocalVideo(uid: UInt, canvasView: UIView) {
canvas.view = canvasView
canvas.uid = uid
canvas.mirrorMode = .disabled
// 设置原始视频数据,以便后续设置商汤美颜
agoraKit.setVideoFrameDelegate(self)
// 设置耳返
agoraKit.setDefaultAudioRouteToSpeakerphone(true)
// 开启音频
agoraKit.enableAudio()
// 开启视频
agoraKit.enableVideo()
// 设置本地视图
agoraKit.setupLocalVideo(canvas)
// 开启本地视频预览
agoraKit.startPreview()
showLogger.info("setupLocalVideo target uid:\(uid), user uid\(UserInfo.userId)", context: kShowLogBaseContext)
}
加入房间时,你需要在主播和观众端都设置并渲染主播视频,再加入频道。本节展示调用 setupRemoteVideoEx
在观众端渲染远端视频(即主播的视频)的示例代码。
self.agoraKitManager.setupRemoteVideo(channelId: channelId,
uid: uid,
canvasView: self.liveView.canvasView.localView)
// ShowAgoraKitManager.swift
func setupRemoteVideo(channelId: String, uid: UInt, canvasView: UIView) {
guard let connection = exConnectionMap[channelId] else {
showLogger.error("_joinChannelEx fail: connection is empty")
return
}
let videoCanvas = AgoraRtcVideoCanvas()
videoCanvas.uid = uid
videoCanvas.view = canvasView
videoCanvas.renderMode = .hidden
let ret = agoraKit.setupRemoteVideoEx(videoCanvas, connection: connection)
showLogger.info("setupRemoteVideoEx ret = \(ret), uid:\(uid) localuid: \(UserInfo.userId) channelId: \(channelId)", context:
kShowLogBaseContext)
}
房主跨直播间 PK 连麦意味着不同频道内的主播加入对方频道进行连麦。当房间内用户收到房主 PK 连麦的信令消息后,房间内用户的代码逻辑如下:
joinChannelExByToken
加入频道 B,并且设置订阅频道 B 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 B 中主播的视频。joinChannelEx
加入频道 B,并且设置订阅频道 B 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 B 中主播的视频。joinChannelEx
加入频道 A,并且设置订阅频道 A 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 A 中主播的视频。joinChannelEx
加入频道 A,并且设置订阅频道 A 内音视频流,但不发送音视频流。同时通过 setupRemoteVideoEx
渲染频道 A 中主播的视频。完成这些逻辑后,观众可以同时接收频道 A 和 B 的音视频流,因此可以同时看到两个房间的房主。房主仅在自己的频道发流,在对方的频道内不发流仅收流,因此,房主可以(在对方频道)看到对方,达到连麦的效果。
结束 PK 连麦时,房间内用户都需要调用 leaveChannelEx
离开对方频道。
// 加入对方频道
agoraKitManager.joinChannelEx(currentChannelId: roomId,
targetChannelId: interactionRoomId,
ownerId: uid,
options: self.channelOptions,
role: role) {
showLogger.info("\(self.roomId) updateLoadingType _onStartInteraction---------- \(self.roomId)")
if self.role == .broadcaster {
self.agoraKitManager.setupRemoteVideo(channelId: interactionRoomId,
uid: uid,
canvasView: self.liveView.canvasView.remoteView)
}else{
self.updateLoadingType(loadingType: self.loadingType)
}
}
// ShowAgoraKitManager.swift
private func _joinChannelEx(currentChannelId: String,
targetChannelId: String,
ownerId: UInt,
token: String,
options:AgoraRtcChannelMediaOptions,
role: AgoraClientRole) {
if exConnectionMap[targetChannelId] == nil {
let subscribeStatus = role == .audience ? false : true
let mediaOptions = AgoraRtcChannelMediaOptions()
mediaOptions.autoSubscribeAudio = subscribeStatus
mediaOptions.autoSubscribeVideo = subscribeStatus
mediaOptions.clientRoleType = role
if role == .audience {
mediaOptions.audienceLatencyLevel = .lowLatency
}else{
updateVideoEncoderConfigurationForConnenction(currentChannelId: currentChannelId)
}
let connection = AgoraRtcConnection()
connection.channelId = targetChannelId
connection.localUid = UInt(VLUserCenter.user.id) ?? 0
let proxy = delegateMap[currentChannelId]
let date = Date()
showLogger.info("try to join room[\(connection.channelId)] ex uid: \(connection.localUid)", context: kShowLogBaseContext)
let ret =
agoraKit.joinChannelEx(byToken: token,
connection: connection,
delegate: proxy,
mediaOptions: mediaOptions) { channelName, uid, elapsed in
let cost = Int(-date.timeIntervalSinceNow * 1000)
showLogger.info("join room[\(channelName)] ex success uid: \(uid) cost \(cost) ms", context: kShowLogBaseContext)
}
agoraKit.updateChannelEx(with: mediaOptions, connection: connection)
exConnectionMap[targetChannelId] = connection
if ret == 0 {
showLogger.info("join room ex: channelId: \(targetChannelId) ownerId: \(ownerId)",
context: "AgoraKitManager")
}else{
showLogger.error("join room ex fail: channelId: \(targetChannelId) ownerId: \(ownerId) token = \(token), \(ret)",
context: kShowLogBaseContext)
}
}
}
// 退出对方频道
agoraKitManager.leaveChannelEx(roomId: self.roomId, channelId: interaction.roomId)
// ShowAgoraKitManager.swift
func leaveChannelEx(roomId: String, channelId: String) {
guard let connection = exConnectionMap[channelId] else { return }
let depMap: [String: ShowRTCLoadingType]? = exConnectionDeps[channelId]
guard depMap?.count ?? 0 == 0 else {
showLogger.info("leaveChannelEx break, depcount: \(depMap?.count ?? 0), roomId: \(roomId), channelId: \(channelId)", context:
kShowLogBaseContext)
return
}
showLogger.info("leaveChannelEx roomId: \(roomId), channelId: \(channelId)", context: kShowLogBaseContext)
agoraKit.leaveChannelEx(connection)
exConnectionMap[channelId] = nil
}
观众与主播连麦时,你可以通过信令让主播邀请观众连麦,或观众向主播申请连麦。让待上麦观众更新频道媒体选项、预览并设置本地视图。让其他用户收到观众连麦通知后,渲染该连麦观众的视频。完成这些逻辑后,直播间内观众可以看到主播和上麦观众的连麦直播。
结束连麦时,你需要让待下麦观众更新频道媒体选项、停止预览并取消本地试图。让其他用户收到该观众下麦通知后,取消渲染该观众的视频。完成这些逻辑后,直播间观众可以看到仅有主播的直播画面。
本节展示观众连麦和结束连麦时更新频道媒体选项、设置视图的示例代码。通过 updateChannelExWithMediaOptions
方法在观众加入频道后更新频道媒体选项,例如是否开启本地音频采集,是否发布本地音频流等。观众的用户角色为 audience
,因此无法在频道内发布音频流。如果观众想与主播连麦,需要将用户角色修改为 broadcaster
。
agoraKitManager.switchRole(role: role,
channelId: roomId,
options: self.channelOptions,
uid: interaction.userId,
canvasView: liveView.canvasView.remoteView)
// ShowAgoraKitManager.swift
func switchRole(role: AgoraClientRole,
channelId: String,
options:AgoraRtcChannelMediaOptions,
uid: String?,
canvasView: UIView?) {
guard let uid = UInt(uid ?? ""), let canvasView = canvasView else {
showLogger.error("switchRole fatel")
return
}
options.clientRoleType = role
updateChannelEx(channelId:channelId, options: options)
if "\(uid)" == VLUserCenter.user.id {
setupLocalVideo(uid: uid, canvasView: canvasView)
} else {
setupRemoteVideo(channelId: channelId, uid: uid, canvasView: canvasView)
}
}
func updateChannelEx(channelId: String, options: AgoraRtcChannelMediaOptions) {
guard let connection = exConnectionMap[channelId] else {
showLogger.error("updateChannelEx fail: connection is empty")
return
}
agoraKit.updateChannelEx(with: options, connection: connection)
}
直播结束时,主播和观众离开房间,你可以离开频道并销毁 RTC 引擎。
本节展示调用 destroy
销毁 RTC 引擎的示例代码。
// ShowAgoraKitManager.swift
deinit {
AgoraRtcEngineKit.destroy()
showLogger.info("deinit-- ShowAgoraKitManager")
}
下图展示实现本文全部流程所需的 API 调用时序。