AtomicXCore 提供了 CoGuestStore 和 LiveSeatStore 模块,用于管理观众连麦的完整业务流程。用户无需关心复杂的状态同步和信令交互,只需调用几个简单的方法,即可为您的直播添加强大的观众与主播音视频互动功能。本文档将指导您如何使用
CoGuestStore 和 LiveSeatStore 在 Flutter 应用中快速实现语音连麦功能。
核心场景
CoGuestStore 和 LiveSeatStore 支持以下几个主流的场景:观众申请上麦:观众主动发起连麦请求,主播在收到请求后进行同意或拒绝。
主播邀请上麦:主播可以主动向直播间内的任意一位观众发起连麦邀请。
主播管理麦位:主播可以对麦位上的用户进行强制下麦、闭麦、锁麦等操作。
实现步骤
步骤1:组件集成
说明:
在开始实现连麦功能之前,您需要获取
liveId(直播间唯一标识),用于区分不同的直播房间。获取方式如下主播端:通过
LiveListStore.shared.createLive(liveInfo) 创建直播时指定。观众端:通过
LiveListState.liveList.value中的 liveInfo 获取,或由主播分享直播间 ID 获取。步骤2:实现观众申请上麦
观众端实现
观众端的核心任务是发起申请、接收结果和主动下麦。
1. 发起连麦申请
当用户点击界面上的"申请连麦"按钮时,调用
applyForSeat 方法。import 'package:atomic_x_core/atomicxcore.dart';final String liveId = "房间ID";late CoGuestStore guestStore;@overridevoid initState() {super.initState();guestStore = CoGuestStore.create(liveId);}// 用户点击"申请连麦"Future<void> requestToConnect() async {// seatIndex: 麦位索引,从 0 开始,表示要申请的麦位位置。0 表示第一个麦位。可通过 LiveSeatState.seatList.value 查看麦位状态,选择空闲的麦位// timeout: 请求超时时间(秒),在超时时间内观众可调用 cancelApplication 取消上麦请求,// 到达超时时间后如该请求未被处理则会失效final result = await guestStore.applyForSeat(seatIndex: 0,timeout: 30,extraInfo: null,);if (result.isSuccess) {debugPrint('连麦申请已发送,等待主播处理...');} else {debugPrint('申请发送失败: ${result.message}');}}
2. 监听主播的处理结果
通过添加
GuestListener,您可以接收到主播的处理结果。late GuestListener _guestListener;// 在您的 Widget 初始化时订阅事件void subscribeGuestEvents() {_guestListener = GuestListener(onGuestApplicationResponded: (isAccept, hostUser) {if (isAccept) {debugPrint('主播 ${hostUser.userName} 同意了您的申请,准备上麦');// 1. 打开麦克风// DeviceStore 是 AtomicXCore 提供的设备管理模块,用于管理麦克风、摄像头等物理设备。// 当主播同意连麦后,需要打开麦克风开始采集音频。DeviceStore.shared.openLocalMicrophone();// 2. 在此更新 UI,例如关闭申请按钮,显示连麦中的状态} else {debugPrint('主播 ${hostUser.userName} 拒绝了您的申请');// 弹窗提示用户申请被拒绝}},);guestStore.addGuestListener(_guestListener);}@overridevoid dispose() {guestStore.removeGuestListener(_guestListener);super.dispose();}
3. 主动下麦
当连麦观众想结束互动时,调用
disconnect 方法即可返回普通观众状态。// 用户点击"下麦"按钮Future<void> leaveSeat() async {final result = await guestStore.disconnect();if (result.isSuccess) {debugPrint('已成功下麦');} else {debugPrint('下麦失败: ${result.message}');}}
4. (可选) 取消申请
如果观众在主播处理前想撤回申请,可以调用
cancelApplication。// 用户在等待时,点击"取消申请"Future<void> cancelApplication() async {final result = await guestStore.cancelApplication();if (result.isSuccess) {debugPrint('申请已取消');} else {debugPrint('申请取消失败: ${result.message}');}}
主播端实现
作为主播,您的核心任务是接收申请、展示申请列表和处理申请。
1. 监听新的连麦申请
通过添加
HostListener,您可以在有新观众申请时立即收到通知,并给出提示。import 'package:atomic_x_core/atomicxcore.dart';final String liveId = "房间ID";late CoGuestStore guestStore;late HostListener _hostListener;@overridevoid initState() {super.initState();guestStore = CoGuestStore.create(liveId);// 订阅主播事件_hostListener = HostListener(onGuestApplicationReceived: (guestUser) {debugPrint('收到观众 ${guestUser.userName} 的连麦申请');// 在此更新 UI,例如在"申请列表"按钮上显示红点},);guestStore.addHostListener(_hostListener);}@overridevoid dispose() {guestStore.removeHostListener(_hostListener);super.dispose();}
2. 展示申请列表
CoGuestStore 的 coGuestState 会实时维护当前的申请者列表,您可以订阅它来刷新您的 UI。late final VoidCallback _applicantsListener = _onApplicantsChanged;// 订阅状态变更void subscribeApplicants() {guestStore.coGuestState.applicants.addListener(_applicantsListener);}void _onApplicantsChanged() {final applicants = guestStore.coGuestState.applicants.value;debugPrint('当前申请人数: ${applicants.length}');// 在此刷新您的"申请者列表" UI}@overridevoid dispose() {guestStore.coGuestState.applicants.removeListener(_applicantsListener);super.dispose();}
3. 处理连麦申请
当您在列表中选择一位观众并点击"同意"或"拒绝"时,调用相应的方法。
// 主播点击"同意"按钮,传入申请者的 userIDFuture<void> accept(String userId) async {final result = await guestStore.acceptApplication(userId);if (result.isSuccess) {debugPrint('已同意 $userId 的申请,对方正在上麦');}}// 主播点击"拒绝"按钮Future<void> reject(String userId) async {final result = await guestStore.rejectApplication(userId);if (result.isSuccess) {debugPrint('已拒绝 $userId 的申请');}}
步骤3:实现主播邀请上麦
主播端实现
1. 向观众发起邀请
当主播在观众列表中选择某人并点击"邀请连麦"时,调用
inviteToSeat 方法。// 主播选择观众并发起邀请Future<void> invite(String userId) async {// inviteeID: 被邀请者 ID,seatIndex: 麦位索引,timeout: 邀请超时时间final result = await guestStore.inviteToSeat(inviteeID: userId,seatIndex: 0,timeout: 30,extraInfo: null,);if (result.isSuccess) {debugPrint('已向 $userId 发出邀请,等待对方回应...');}}
2. 监听观众的回应
通过
HostListener 监听 onHostInvitationResponded 事件。// 在 HostListener 的配置中添加_hostListener = HostListener(onHostInvitationResponded: (isAccept, guestUser) {if (isAccept) {debugPrint('观众 ${guestUser.userName} 接受了您的邀请');} else {debugPrint('观众 ${guestUser.userName} 拒绝了您的邀请');}},);
观众端实现
1. 接收主播的邀请
通过
GuestListener 监听 onHostInvitationReceived 事件。// 在 GuestListener 的配置中添加_guestListener = GuestListener(onHostInvitationReceived: (hostUser) {debugPrint('收到主播 ${hostUser.userName} 的连麦邀请');// 在此弹出一个对话框,让用户选择"接受"或"拒绝"// showInvitationDialog(hostUser);},);
2. 响应邀请
当用户在弹出的对话框中做出选择后,调用相应的方法。
final String inviterId = "发起邀请的主播 ID"; // 从 onHostInvitationReceived 事件中获取// 用户点击"接受"Future<void> accept() async {final result = await guestStore.acceptInvitation(inviterId);if (result.isSuccess) {// 打开麦克风// DeviceStore 是 AtomicXCore 提供的设备管理模块,用于管理麦克风、摄像头等物理设备。// 接受邀请后,需要打开麦克风开始采集音频DeviceStore.shared.openLocalMicrophone();}}// 用户点击"拒绝"Future<void> reject() async {await guestStore.rejectInvitation(inviterId);}
功能进阶
当用户上麦后,主播可能需要对麦位进行管理。以下功能主要由
LiveSeatStore 提供,它可以与 CoGuestStore 协同工作。DeviceStore 与 LiveSeatStore 的麦克风控制流程
在使用麦位功能时,您需要了解
DeviceStore 和 LiveSeatStore 在麦克风控制上的区别:DeviceStore:管理物理设备。
openLocalMicrophone 启动麦克风设备进行音频采集,closeLocalMicrophone 停止音频采集并释放麦克风设备。LiveSeatStore:管理麦位业务(即音频流)。
muteMicrophone 静音,停止向远端发送本地的音频流,但麦克风设备本身仍在运行;unmuteMicrophone 解除静音,恢复向远端发送音频流。推荐的工作流程:您应该遵循"设备只开一次,麦上只用静音切换"的原则
1. 上麦时:当观众成功上麦,调用一次
openLocalMicrophone 来启动设备。2. 在麦上时:用户在麦位上所有的"开麦"和"闭麦"操作,都应该调用
unmuteMicrophone 和 muteMicrophone 来控制上行音频流的通断。3. 下麦时:当用户下麦(例如调用
disconnect),调用 closeLocalMicrophone 来释放设备。麦上用户自行开/关麦
麦上用户(包括主播自己)可以通过
LiveSeatStore 提供的接口来控制自己的麦克风静音状态。实现方式:
1. 静音:调用
muteMicrophone() 方法。这是一个无回调的单向请求。2. 解除静音:调用
unmuteMicrophone() 方法。示例代码:
final seatStore = LiveSeatStore.create(liveId);seatStore.muteMicrophone(); // 静音await seatStore.unmuteMicrophone(); // 解除静音
主播远程控制麦上用户开麦或关麦
主播可以对其他麦上用户进行"强制静音"或"邀请开麦"的操作。
实现方式:
1. 强制静音(锁定):主播调用
closeRemoteMicrophone 会强制关闭对方的麦克风,并锁定对方的麦克风权限,被静音的用户会收到 LiveSeatListener 中的 onLocalMicrophoneClosedByAdmin 事件,此时其本地的"打开麦克风"按钮应变为不可点击状态。2. 邀请开麦(解锁):主播调用
openRemoteMicrophone 并非强制打开对方麦克风,而是解除对方的"锁定状态",允许对方自行开麦。此时,被操作的用户会收到 onLocalMicrophoneOpenedByAdmin 事件,其本地的"打开麦克风"按钮应恢复为可点击状态,但用户仍处于静音中。3. 用户自行开麦:用户在收到"解除锁定"的通知后,必须主动调用
LiveSeatStore 的 unmuteMicrophone() 方法解除静音,才能让房间内其他人听到自己的声音。示例代码:
主播端:
final seatStore = LiveSeatStore.create(liveId);final String targetUserId = "userD";// 1. 强制静音 userD 并锁定final result1 = await seatStore.closeRemoteMicrophone(targetUserId);if (result1.isSuccess) {debugPrint('已将 $targetUserId 静音并锁定');}// 2. 解锁 userD 的麦克风权限(此时 userD 依然是静音状态)final result2 = await seatStore.openRemoteMicrophone(userID: targetUserId,policy: DeviceControlPolicy.unlockOnly,);if (result2.isSuccess) {debugPrint('已解除 $targetUserId 的麦克风锁定,用户可自行开麦');}
观众端:
late LiveSeatListener _seatListener;// userD 监听主播的操作void subscribeSeatEvents() {_seatListener = LiveSeatListener(onLocalMicrophoneClosedByAdmin: () {debugPrint('被主播静音了');// muteButton.isEnabled = false;},onLocalMicrophoneOpenedByAdmin: (policy) {debugPrint('主播已解除静音锁定');// muteButton.isEnabled = true;// muteButton.text = "打开麦克风";},);seatStore.addLiveSeatEventListener(_seatListener);}@overridevoid dispose() {seatStore.removeLiveSeatEventListener(_seatListener);super.dispose();}
closeRemoteMicrophone 接口参数:
参数 | 类型 | 描述 |
userID | String | 被操作的用户 userID。 |
openRemoteMicrophone 接口参数:
参数 | 类型 | 描述 |
userID | String | 被操作的用户 userID。 |
policy | DeviceControlPolicy | 打开麦克风的策略。 |
主播将麦上用户踢下麦位
实现方式:
1. 踢人下麦:主播可以调用
kickUserOutOfSeat 方法,强制将指定用户踢下麦位。2. 监听事件通知:被踢下麦的用户会收到
GuestListener 中的 onKickedOffSeat 事件通知。示例代码:
// 假设要踢走 "userB"final String targetUserId = "userB";final result = await seatStore.kickUserOutOfSeat(targetUserId);if (result.isSuccess) {debugPrint('已将 $targetUserId 踢下麦位');} else {debugPrint('踢人失败: ${result.message}');}// "userB" 收到被踢下麦事件_guestListener = GuestListener(onKickedOffSeat: (seatIndex, hostUser) {// 弹 toast 提示debugPrint('您已被主播踢下麦位');},);
kickUserOutOfSeat 接口参数:
参数 | 类型 | 描述 |
userID | String | 被踢下麦的用户 userID。 |
主播锁定和解锁麦位
主播可以锁定或解锁某个麦位。
实现方式:
1. 锁定麦位:主播可调用
lockSeat 方法锁定指定索引的麦位,麦位被锁定后,观众无法通过 applyForSeat 或 takeSeat 占用该麦位,但主播可通过 inviteToSeat 邀请观众上锁定麦位。2. 解锁麦位:调用
unlockSeat 可以解锁麦位,解锁后该麦位可以再次被占用。示例代码:
// 锁定 2 号麦位final result1 = await seatStore.lockSeat(2);if (result1.isSuccess) {debugPrint('2 号麦位已锁定');}// 解锁 2 号麦位final result2 = await seatStore.unlockSeat(2);if (result2.isSuccess) {debugPrint('2 号麦位已解锁');}
lockSeat 接口参数:
参数 | 类型 | 描述 |
seatIndex | int | 需要锁定的麦位索引。 |
unlockSeat 接口参数:
参数 | 类型 | 描述 |
seatIndex | int | 需要解锁的麦位索引。 |
移动麦位
主播和普通上麦用户可以调用
moveUserToSeat 移动麦位。实现方式:
1. 主播移动麦位:主播可以调用此接口将任意用户移动到指定麦位。此时
userID 传入目标用户的 ID,targetIndex 是目标麦位索引,policy 参数来指定如果目标麦位有人时的移动策略,详细参见 接口参数 说明。2. 普通上麦用户移动自己麦位:普通上麦用户也可以调用此接口来移动自己。此时
userID 必须传入用户自己的 ID,targetIndex 传入想去的新麦位索引,此时 policy 参数无效,如果目标麦位有人时会报错并停止移动。示例代码:
final result = await seatStore.moveUserToSeat(userID: "userC",targetIndex: newSeatIndex,policy: MoveSeatPolicy.abortWhenOccupied,);if (result.isSuccess) {debugPrint('已成功移动到 $newSeatIndex 号麦位');} else {debugPrint('换座失败,可能位置有人了');}
moveUserToSeat 接口参数:
参数 | 类型 | 描述 |
userID | String | 需要移麦的用户 userID。 |
targetIndex | int | 目标麦位索引。 |
policy | MoveSeatPolicy | 目标麦位已被占用时的处理策略: abortWhenOccupied:目标麦位有人时放弃移动(默认策略)forceReplace:强制替换目标麦位上的用户,被替换的用户将会被踢下麦。swapPosition:与目标麦位用户交换。 |
API 文档
关于 CoGuestStore 和 LiveSeatStore 及其相关类的所有公开接口、属性和方法的详细信息,请参阅 AtomicXCore 框架的官方 API 文档。本指南使用到的相关 Store 如下:
常见问题
语音房连麦和视频直播连麦的实现有何不同?
两者的主要区别在于业务形态与 UI 展现:
视频直播:核心是视频画面。您会使用
LiveCoreWidget 作为核心组件来渲染主播和连麦观众的视频流。UI 的重点是处理视频画面的布局、大小,以及通过 VideoViewDelegate 在视频流上添加挂件(如昵称、占位图)。您可以同时打开摄像头、麦克风。语音房(语聊房):核心是麦位网格。您不会使用
LiveCoreWidget,而是会基于 LiveSeatStore 的 liveSeatState(特别是 seatList)来构建一个网格 UI(例如 GridView)。UI 的重点是实时显示每个麦位 SeatInfo 的状态:是否有人、是否静音、是否被锁定,以及是否正在说话。您只需要打开麦克风。如何在 UI 上实时刷新麦位信息(如是否有人、是否静音)?
您应该订阅
LiveSeatState 中的 seatList 属性,它是一个 ValueListenable<List<SeatInfo>> 类型的响应式数据,每当数组变化时都会通知您重新渲染麦位列表。遍历此数组,您可以:通过
seatInfo.userInfo 来获取麦位上的用户信息。通过
seatInfo.isLocked 判断麦位是否被锁定。通过
seatInfo.userInfo.microphoneStatus 判断麦上用户的麦克风状态。