AtomicXCore 提供了 CoGuestState 模块,专门用于管理观众连麦的完整业务流程。您无需关心复杂的状态同步和信令交互,只需调用几个简单的方法,即可为您的直播添加强大的观众与主播音视频互动功能。CoGuestState 支持以下两种最主流的连麦场景:applyForSeat 方法。import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前加入直播间的 liveID// 通过 liveID 获取 CoGuestState 的实例const { applyForSeat } = useCoGuestState(liveID);// 用户点击"申请连麦"const handleRequestToConnect = () => {applyForSeat({liveID,seatIndex: -1, // 申请的麦位,申请上麦传 -1, 随机分配麦位timeout: 30, // 请求超时时间,例如 30s});};
addCoGuestGuestListener 订阅 onGuestApplicationResponded 事件,您可以接收到主播的处理结果。import { useEffect } from 'react';import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';import { useDeviceState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/DeviceState';const liveID = 'xxx'; // 您当前加入直播间的 liveIDconst { addCoGuestGuestListener, removeCoGuestGuestListener } = useCoGuestState(liveID);const { openLocalMicrophone, openLocalCamera } = useDeviceState(liveID);// 在页面初始化时订阅事件const handleGuestApplicationResponded = (event) => {if (event.isAccept) {console.log('上麦申请被同意');openLocalMicrophone();openLocalCamera({ isFront: true });// 在此更新 UI,例如变更申请按钮状态,显示为连麦中} else {console.log('上麦申请被拒绝');// 弹窗提示用户申请被拒绝}};useEffect(() => {addCoGuestGuestListener('onGuestApplicationResponded', handleGuestApplicationResponded);return () => {removeCoGuestGuestListener('onGuestApplicationResponded', handleGuestApplicationResponded);};}, []);
disconnect 方法即可返回普通观众状态。import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前加入直播间的 liveIDconst { disconnect } = useCoGuestState(liveID);// 用户点击"下麦"按钮const handleDisconnect = () => {disconnect({liveID,onSuccess: () => { console.log('下麦成功'); },onError: (error) => { console.log('下麦失败', error); },});};
cancelApplication。import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前加入的直播间的 liveIDconst { cancelApplication } = useCoGuestState(liveID);// 用户在等待时,点击"取消申请"const handleCancelRequest = () => {cancelApplication({liveID,onSuccess: () => { console.log('取消申请成功'); },onError: (error) => { console.log('取消申请失败', error); },});};
CoGuestHostListener 订阅 onGuestApplicationReceived 事件后,您可以在有新观众申请时立即收到通知,并给出提示。import { useEffect } from 'react';import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前直播间的 liveIDconst { addCoGuestHostListener, removeCoGuestHostListener } = useCoGuestState(liveID);// 在页面初始化时订阅事件const handleGuestApplicationReceived = (event) => {console.log('收到观众的连麦申请', event);// 在此更新 UI,例如在"申请列表"按钮上显示红点};useEffect(() => {addCoGuestHostListener('onGuestApplicationReceived', handleGuestApplicationReceived);return () => {removeCoGuestHostListener('onGuestApplicationReceived', handleGuestApplicationReceived);};}, []);
CoGuestState 会实时维护当前的申请者列表,数据本身是响应式数据,您可以直接在 UI 上使用。import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前直播间的 liveIDconst { applicants } = useCoGuestState(liveID);// 在这里绘制您的 "申请者列表" UI<FlatListdata={applicants || []}keyExtractor={(item) => item.userID}renderItem={({ item }) => (<View><Text>{item.userName}</Text><Text>{item.userID}</Text></View>)}/>
import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前直播间的 liveIDconst { acceptApplication, rejectApplication } = useCoGuestState(liveID);// 主播点击"同意"按钮,传入申请者的 userIDconst handleAccept = (userID) => {acceptApplication({liveID,userID,onSuccess: () => { console.log('同意上麦申请成功'); },onError: (error) => { console.log('同意上麦申请失败', error); },});};// 主播点击"拒绝"按钮,传入申请者的 userIDconst handleReject = (userID) => {rejectApplication({liveID,userID,onSuccess: () => { console.log('拒绝上麦申请成功'); },onError: (error) => { console.log('拒绝上麦申请失败', error); },});};
inviteToSeat 方法。import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前直播间的 liveIDconst { inviteToSeat } = useCoGuestState(liveID);// 主播选择观众并发起邀请const handleInviteToSeat = (inviteeID) => {inviteToSeat({liveID,inviteeID,seatIndex: -1, // 邀请的麦位,传 -1, 随机分配麦位timeout: 30, // 超时时间onSuccess: () => { console.log('已向观众发送上麦邀请'); },onError: (error) => { console.log('发送上麦邀请失败', error); },});};
CoGuestHostListener 订阅 onHostInvitationResponded 事件。import { useEffect } from 'react';import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';const liveID = 'xxx'; // 您当前直播间的 liveIDconst { addCoGuestHostListener, removeCoGuestHostListener } = useCoGuestState(liveID);// 在页面初始化时订阅事件const handleHostInvitationResponded = (event) => {if (event.isAccept) {console.log('观众接受了您的邀请');} else {console.log('观众拒绝了您的邀请');}};useEffect(() => {addCoGuestHostListener('onHostInvitationResponded', handleHostInvitationResponded);return () => {removeCoGuestHostListener('onHostInvitationResponded', handleHostInvitationResponded);};}, []);
addCoGuestGuestListener 订阅 onHostInvitationReceived 事件。import { useEffect } from 'react';import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';import { useLiveListState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/LiveListState';const liveID = 'xxx'; // 您当前加入直播间的 liveIDconst { addCoGuestGuestListener, removeCoGuestGuestListener } = useCoGuestState(liveID);const { currentLive } = useLiveListState();// 在页面初始化时订阅事件const handleHostInvitationReceived = (event) => {console.log('收到主播邀请连麦', event);const inviterID = currentLive?.liveOwner?.userID || '';// 在此弹出一个对话框,让用户选择"接受"或"拒绝"};useEffect(() => {addCoGuestGuestListener('onHostInvitationReceived', handleHostInvitationReceived);return () => {removeCoGuestGuestListener('onHostInvitationReceived', handleHostInvitationReceived);};}, [currentLive]);
import { useCoGuestState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/CoGuestState';import { useDeviceState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/DeviceState';import { useLiveListState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/LiveListState';const liveID = 'xxx'; // 您当前加入直播间的 liveIDconst { acceptInvitation, rejectInvitation } = useCoGuestState(liveID);const { openLocalMicrophone, openLocalCamera } = useDeviceState(liveID);const { currentLive } = useLiveListState();// 通过 currentLive 获取 inviterIDconst inviterID = currentLive?.liveOwner?.userID || '';// 用户点击"接受"const handleAcceptInvitation = () => {acceptInvitation({liveID,inviterID,onSuccess: () => {console.log('接受邀请成功');openLocalMicrophone();openLocalCamera({ isFront: true });},onError: (error) => { console.log('接受邀请失败', error); },});};// 用户点击"拒绝"const handleRejectInvitation = () => {rejectInvitation({liveID,inviterID,onSuccess: () => { console.log('拒绝邀请成功'); },onError: (error) => { console.log('拒绝邀请失败', error); },});};


import React, { useMemo } from 'react';import { View, Text, Image, StyleSheet, Dimensions } from 'react-native';const DEFAULT_AVATAR = 'https://liteav-test-1252463788.cos.ap-guangzhou.myqcloud.com/voice_room/voice_room_cover1.png';const { width: SCREEN_WIDTH } = Dimensions.get('window');// 接收来自父组件的核心数据:seatList 和 canvasexport default function ParticipantOverlay({ seatList, canvas }) {if (!seatList || seatList.length === 0) return null;// 根据 canvas 计算缩放比例const scale = useMemo(() => {if (!canvas?.w || !canvas?.h) return { scaleX: 1, scaleY: 1 };const displayWidth = SCREEN_WIDTH;const displayHeight = SCREEN_WIDTH * (canvas.h / canvas.w);return {scaleX: displayWidth / canvas.w,scaleY: displayHeight / canvas.h,};}, [canvas]);// 计算每个成员 UI 容器的精确位置和大小const getParticipantStyle = (participant) => {if (!participant?.region) return {};return {position: 'absolute',left: participant.region.x * scale.scaleX,top: participant.region.y * scale.scaleY,width: participant.region.w * scale.scaleX,height: participant.region.h * scale.scaleY,};};return (// overlay-container<View style={styles.overlayContainer} pointerEvents="none">{/* 遍历 seatList,为每个麦位成员创建独立的 UI 容器 */}{seatList.map((participant) => {if (!participant?.userInfo?.userID) return null;const isCameraOff = participant.userInfo.cameraStatus === 'OFF';return (// participant-ui-container<View key={participant.userInfo.userID} style={getParticipantStyle(participant)}>{/* 条件渲染:根据摄像头状态显示不同 UI */}{isCameraOff ? (// 1. 当摄像头关闭时,显示居中的头像和昵称<View style={styles.avatarPlaceholder}><Imagestyle={styles.avatarImage}source={{ uri: participant.userInfo.userAvatar || DEFAULT_AVATAR }}/><Text style={styles.avatarName}>{participant.userInfo.userName || participant.userInfo.userID}</Text></View>) : (// 2. 当摄像头开启时,显示左下角的昵称条<View style={styles.nicknameBar}><Text style={styles.nicknameText}>{participant.userInfo.userName || participant.userInfo.userID}</Text></View>)}</View>);})}</View>);}const styles = StyleSheet.create({overlayContainer: {position: 'absolute',top: 0,left: 0,right: 0,bottom: 0,},avatarPlaceholder: {flex: 1,backgroundColor: '#2E323A',justifyContent: 'center',alignItems: 'center',},avatarImage: {width: 60,height: 60,borderRadius: 30,},avatarName: {marginTop: 8,fontSize: 13,color: '#fff',},nicknameBar: {position: 'absolute',left: 6,bottom: 6,backgroundColor: 'rgba(0, 0, 0, 0.5)',paddingHorizontal: 8,paddingVertical: 3,borderRadius: 10,},nicknameText: {color: '#fff',fontSize: 11,},});
import React from 'react';import { StyleSheet, View } from 'react-native';import { LiveCoreView } from 'react-native-tuikit-atomic-x/lib/module/components/LiveCoreView';import { useLiveSeatState } from 'react-native-tuikit-atomic-x/lib/module/atomic-x/state/LiveSeatState';import ParticipantOverlay from '../../components/ParticipantOverlay';export default function YourAnchorScreen({ route, navigation }) {const { liveID } = route.params || {};const { seatList, canvas } = useLiveSeatState(liveID);return (// page-container<View style={styles.pageContainer}>{/* live-container */}<View style={styles.liveContainer}>{/* 底层:视频渲染层 */}<LiveCoreViewliveID={liveID}coreViewType="pushView" // 主播端: pushView, 观众端: playViewstyle={styles.videoLayer}/>{/* 上层:自定义 UI 覆盖层 */}<ParticipantOverlay seatList={seatList} canvas={canvas} /></View>{/* 页面的其他 UI,例如底部的操作栏 */}{/* <View style={styles.bottomControls}>...</View> */}</View>);}const styles = StyleSheet.create({pageContainer: {flex: 1,backgroundColor: '#000',},liveContainer: {flex: 1,},videoLayer: {flex: 1,},});
ParticipantOverlay 组件后,显示的昵称条或头像占位图没有精确地覆盖在对应的视频小窗上,存在偏移或尺寸不匹配。ParticipantOverlay 需要接收 useLiveSeatState(liveID) 返回的 canvas 对象(服务端画布尺寸),组件内部会根据 canvas.w / canvas.h 与屏幕实际宽度自动计算缩放比例:scaleX = 屏幕宽度 / canvas.w。applyForSeat, acceptApplication, inviteToSeat 等 CoGuestState 提供的方法后,没有任何效果,也没有收到预期的回调。CoGuestState 的所有功能都与特定的直播间绑定。最常见的原因是初始化 CoGuestState 实例或调用其方法时,传入的 liveID 不正确、为 null 或与当前用户实际所在的直播间不符。liveID:确保在调用 useCoGuestState(liveID) 和所有相关方法(例如 applyForSeat({ liveID, ... }))时,使用的 liveID 是当前直播间真实且有效的 ID。liveID 一致:在整个页面的生命周期中,所有与 CoGuestState 相关的操作都必须使用同一个 liveID。CoGuestState 只负责将用户“搬上”麦位,但不会自动打开用户的媒体设备。onGuestApplicationResponded 回调中 isAccept 为 true 时,或调用 acceptInvitation 的 success 回调时),必须手动调用 DeviceState 提供的方法来开启媒体设备。文档反馈