TUILiveKit 产品动态
云直播推拉流 SDK 产品动态
CoGuestStore 和 LiveSeatStore在 iOS 应用中快速实现语音连麦功能。
CoGuestStore和LiveSeatStore支持以下几个主流的场景:applyForSeat 方法。import AtomicXCorelet liveID = "房间ID"var guestStore: CoGuestStore {CoGuestStore.create(liveID: liveID)}// 用户点击“申请连麦”func requestToConnect() {// timeout: 请求超时时间,例如 30 秒guestStore.applyForSeat(timeout: 30.0, extraInfo: nil) { result inswitch result {case .success():print("连麦申请已发送,等待主播处理...")case .failure(let error):print("申请发送失败: \\(error.message)")}}}
guestEventPublisher,您可以接收到主播的处理结果。import AtomicXCoreimport Combine// 在您的视图控制器初始化时订阅事件func subscribeGuestEvents() {guestStore.guestEventPublisher.sink { [weak self] event inif case let .onGuestApplicationResponded(isAccept, hostUser) = event {if isAccept {print("主播 \\(hostUser.userName) 同意了你的申请,准备上麦")// 1. 打开麦克风DeviceStore.shared.openLocalMicrophone(completion: nil)// 2. 在此更新 UI,例如关闭申请按钮,显示连麦中的状态} else {print("主播 \\(hostUser.userName) 拒绝了你的申请")// 弹窗提示用户申请被拒绝}}}.store(in: &cancellables) // 管理订阅生命周期}
disConnect 方法即可返回普通观众状态。// 用户点击“下麦”按钮func leaveSeat() {guestStore.disConnect { result inswitch result {case .success():print("已成功下麦")case .failure(let error):print("下麦失败: \\(error.message)")}}}
cancelApplication。// 用户在等待时,点击“取消申请”func cancelRequest() {guestStore.cancelApplication { result inswitch result {case .success():print("申请已取消")case .failure(let error):print("申请取消失败: \\(error.message)")}}}
hostEventPublisher,您可以在有新观众申请时立即收到通知,并给出提示。import AtomicXCoreimport Combinelet liveID = "房间ID"var guestStore: CoGuestStore {CoGuestStore.create(liveID: liveID)}// 订阅主播事件guestStore.hostEventPublisher.sink { [weak self] event inif case let .onGuestApplicationReceived(guestUser) = event {print("收到观众 \\(guestUser.userName) 的连麦申请")// 在此更新 UI,例如在“申请列表”按钮上显示红点}}.store(in: &cancellables)
CoGuestStore 的 state 会实时维护当前的申请者列表,您可以订阅它来刷新您的 UI。import AtomicXCoreimport Combine// 订阅状态变更guestStore.state.subscribe(StatePublisherSelector(keyPath: \\CoGuestState.applicants)) // 只关心申请者列表的变化.removeDuplicates().sink { applicants inprint("当前申请人数: \\(applicants.count)")// 在此刷新您的“申请者列表”UI// self.applicantListView.update(with: applicants)}.store(in: &cancellables)
// 主播点击“同意”按钮,传入申请者的 userIDfunc accept(userId: String) {guestStore.acceptApplication(userID: userId) { result inif case .success = result {print("已同意 \\(userId) 的申请,对方正在上麦")}}}// 主播点击“拒绝”按钮func reject(userId: String) {guestStore.rejectApplication(userID: userId) { result inif case .success = result {print("已拒绝 \\(userId) 的申请")}}}
inviteToSeat 方法。// 主播选择观众并发起邀请func invite(userId: String) {// timeout: 邀请超时时间guestStore.inviteToSeat(userID: userId, timeout: 30.0, extraInfo: nil) { result inif case .success = result {print("已向 \\(userId) 发出邀请,等待对方回应...")}}}
hostEventPublisher 监听 onHostInvitationResponded 事件。// 在 hostEventPublisher 的订阅中添加if case let .onHostInvitationResponded(isAccept, guestUser) = event {if isAccept {print("观众 \\(guestUser.userName) 接受了你的邀请")} else {print("观众 \\(guestUser.userName) 拒绝了你的邀请")}}
guestEventPublisher 监听 onHostInvitationReceived 事件。// 在 guestEventPublisher 的订阅中添加if case let .onHostInvitationReceived(hostUser) = event {print("收到主播 \\(hostUser.userName) 的连麦邀请")// 在此弹出一个对话框,让用户选择“接受”或“拒绝”// self.showInvitationDialog(from: hostUser)}
let inviterId = "发起邀请的主播ID" // 从 onHostInvitationReceived 事件中获取// 用户点击“接受”func accept() {guestStore.acceptInvitation(inviterID: inviterId) { result inif case .success = result {// 2. 打开麦克风DeviceStore.shared.openLocalMicrophone(completion: nil)}}}// 用户点击“拒绝”func reject() {guestStore.rejectInvitation(inviterID: inviterId) { result in// ...}}
LiveSeatStore 提供,它可以与 CoGuestStore 协同工作。LiveSeatStore 提供的接口来控制自己的麦克风静音状态。muteMicrophone() 方法。这是一个无回调的单向请求。unmuteMicrophone(completion:) 方法。let seatStore = LiveSeatStore.create(liveID: liveId)seatStore.muteMicrophone() //静音seatStore.unmuteMicrophone(completion: nil) //解除静音
参数 | 类型 | 描述 |
completion | CompletionClosure? | 操作完成后的回调。 |
closeRemoteMicrophone 会强制关闭对方的麦克风,并锁定对方的麦克风权限,被静音的用户会收到 liveSeatEventPublisher 中的 onLocalMicrophoneClosedByAdmin 事件,此时其本地的“打开麦克风”按钮应变为不可点击状态。openRemoteMicrophone 并非强制打开对方麦克风,而是解除对方的“锁定状态”,允许对方自行开麦。此时,被操作的用户会收到 onLocalMicrophoneOpenedByAdmin 事件,其本地的“打开麦克风”按钮应恢复为可点击状态,但用户仍处于静音中。LiveSeatStore 的 unmuteMicrophone()方法解除静音,才能让房间内其他人听到自己的声音。let targetUserId = "userD"// 1. 强制静音 userD 并锁定seatStore.closeRemoteMicrophone(userID: targetUserId) { result inif case .success = result {print("已将 \\(targetUserId) 静音并锁定")}}// 2. 解锁 userD 的麦克风权限(此时 userD 依然是静音状态)seatStore.openRemoteMicrophone(userID: targetUserId, policy: .unlockOnly) { result inif case .success = result {print("已邀请 \\(targetUserId) 打开麦克风(已解锁)")}}
// userD监听主播的操作seatStore.liveSeatEventPublisher.sink { event inswitch event {case .onLocalMicrophoneClosedByAdmin:print("被主播静音了")// self.muteButton.isEnabled = falsecase .onLocalMicrophoneOpenedByAdmin(policy: _):print("主播已解除静音锁定")// self.muteButton.isEnabled = true// self.muteButton.setTitle("打开麦克风")default:break}}.store(in: &cancellables)
参数 | 类型 | 描述 |
userID | String | 被操作的用户 userID。 |
completion | CompletionClosure? | 请求完成后的回调。 |
参数 | 类型 | 描述 |
userID | String | 被操作的用户 userID。 |
completion | CompletionClosure? | 请求完成后的回调。 |
kickUserOutOfSeat 方法,强制将指定用户踢下麦位。CoGuestStore.guestEventPublisher 中的 onKickedOffSeat 事件通知。import AtomicXCoreimport Combine// 假设要踢走 "userB"let targetUserId = "userB"seatStore.kickUserOutOfSeat(userID: targetUserId) { result inswitch result {case .success:print("已将 \\(targetUserId) 踢下麦位")case .failure(let error):print("踢人失败: \\(error.message)")}}// "userB"收到被踢下麦事件guestStore.guestEventPublisher.receive(on: RunLoop.main).sink { [weak self] event inguard let self = self else { return }switch event {case .onKickedOffSeat(seatIndex: _, hostUser: _):// 弹toast提示default:break}}.store(in: &cancellables)
参数 | 类型 | 描述 |
userID | String | 被踢下麦的用户 userID。 |
completion | CompletionClosure? | 请求完成后的回调。 |
lockSeat 方法锁定指定索引的麦位,麦位被锁定后,观众无法通过 applyForSeat 或takeSeat 占用该麦位。unlockSeat 可以解锁麦位,解锁后该麦位可以再次被占用。// 锁定2号麦位seatStore.lockSeat(seatIndex: 2) { result inif case .success = result {print("2号麦位已锁定")}}// 解锁2号麦位seatStore.unlockSeat(seatIndex: 2) { result inif case .success = result {print("2号麦位已解锁")}}
参数 | 类型 | 描述 |
seatIndex | Int | 需要锁定的麦位索引。 |
completion | CompletionClosure? | 请求完成后的回调。 |
参数 | 类型 | 描述 |
seatIndex | Int | 需要解锁的麦位索引。 |
completion | CompletionClosure? | 请求完成后的回调。 |
moveUserToSeat(userID:targetIndex:policy:completion:)移动麦位。userID 传入目标用户的 ID,targetIndex是目标麦位索引,policy 参数来指定如果目标麦位有人时的移动策略,详细参见 接口参数 说明。userID 必须传入用户自己的 ID,targetIndex 传入想去的新麦位索引,此时policy 参数无效,如果目标麦位有人时会报错并停止移动。seatStore.moveUserToSeat(userID: "userC",targetIndex: newSeatIndex,policy: .abortWhenOccupied) { result inif case .success = result {print("已成功移动到 \\(newSeatIndex) 号麦位")} else {print("换座失败,可能位置有人了")}}
参数 | 类型 | 描述 |
userID | String | 需要移麦的用户 userID。 |
targetIndex | Int | 目标麦位索引。 |
policy | MoveSeatPolicy? | 目标麦位有人时的移动策略枚举: abortWhenOccupied:目标麦位有人时放弃移动(默认策略)forceReplace:强制替换目标麦位上的用户,被替换的用户将会被踢下麦swapPosition:与目标麦位用户交换位置。 |
completion | CompletionClosure? | 请求完成后的回调。 |
LiveCoreView 作为核心组件来渲染主播和连麦观众的视频流。UI 的重点是处理视频画面的布局、大小,以及通过 VideoViewDelegate 在视频流上添加挂件(如昵称、占位图)。您可以同时打开摄像头,麦克风。LiveCoreView,而是会基于 LiveSeatStore 的 state(特别是 seatList)来构建一个网格 UI(例如 UICollectionView)。UI 的重点是实时显示每个麦位 SeatInfo 的状态:是否有人、是否静音、是否被锁定 ,以及是否正在说话。您只需要打开麦克风。LiveSeatState 中的 seatList 属性,它是一个[SeatInfo] 数组类型的响应式数据,每当数组变化时都会通知您重新渲染麦位列表。遍历此数组,您可以:seatInfo.userInfo 来获取麦位上的用户信息。seatInfo.isLocked 判断麦位是否被锁定。seatInfo.userInfo.microphoneStatus 判断麦上用户的麦克风状态。DeviceStore 管理的是物理设备,而 LiveSeatStore 管理的是麦位业务(即音频流)。DeviceStore:openLocalMicrophone:向系统请求权限并启动麦克风设备进行音频采集。这是一个“重”操作。closeLocalMicrophone:停止音频采集并释放麦克风设备。LiveSeatStore:muteMicrophone:静音。停止向远端发送本地的音频流,但麦克风设备本身仍在运行。unmuteMicrophone:解除静音。恢复向远端发送音频流。openLocalMicrophone来启动设备。unmuteMicrophone和 muteMicrophone来控制上行音频流的通断。disconnect),调用closeLocalMicrophone来释放设备。文档反馈