CoGuestStore supports the two most common guest interaction scenarios:applyForSeat method.import 'package:atomic_x_core/atomicxcore.dart';final String liveId = "room ID";final CoGuestStore guestStore = CoGuestStore.create(liveId);// User clicks "apply for mic connection"void requestToConnect() async {// seatIndex: The microphone slot index, value range is -1 (auto select an empty slot) and [0, maximum number of microphone slots in the current room -1], timeout: request timeout period (seconds)final result = await guestStore.applyForSeat(seatIndex: 0,timeout: 30,extraInfo: null,);if (result.isSuccess) {print("Mic connection request sent, wait for anchor processing...");} else {print("Failed to send request: ${result.errorMessage}");}}
addGuestListener to receive the host’s response.late GuestListener _guestListener;// Subscribe to events when your Widget is initializedvoid subscribeGuestEvents() {_guestListener = GuestListener(onApplicationAccepted: (hostUser) {print("Anchor ${hostUser.userName} agreed to your request, preparing to connect mic");// 1. Turn on the camera, microphoneDeviceStore.shared.openLocalCamera(isFront: true);DeviceStore.shared.openLocalMicrophone();// 2. Update the UI here, such as disabling the apply button and displaying the mic connection state},onApplicationRejected: (hostUser) {print("Anchor ${hostUser.userName} refused your request");// Pop-up prompts user that the request was rejected},onInvitationReceived: (hostUser) {print("Received mic connection invitation from Anchor ${hostUser.userName}");// Pop up a dialog box herein for users to choose "Accept" or "Reject"_showInvitationDialog(hostUser);},onKickedOffSeat: () {print("You have been kicked off the mic by Anchor");// Update UI, turn off the camera and microphone},);guestStore.addGuestListener(_guestListener);}@overridevoid dispose() {guestStore.removeGuestListener(_guestListener);super.dispose();}
disconnect method to return to the regular audience state.// User clicks "leave mic" buttonvoid leaveSeat() async {final result = await guestStore.disconnect();if (result.isSuccess) {print("Left mic successfully");} else {print("Failed to leave mic: ${result.errorMessage}");}}
cancelApplication.// While waiting, the user clicks "Cancel Application"void cancelRequest() async {final result = await guestStore.cancelApplication();if (result.isSuccess) {print("Application canceled");} else {print("Failed to cancel request: ${result.errorMessage}");}}
addHostListener, you can receive immediate notification when a new audience applies and get prompted.import 'package:atomic_x_core/atomicxcore.dart';final String liveId = "room ID";final CoGuestStore guestStore = CoGuestStore.create(liveId);late HostListener _hostListener;late final VoidCallback _applicantsChangedListener = _onApplicantsChanged;// Subscribe to anchor eventvoid subscribeHostEvents() {_hostListener = HostListener(onApplicationReceived: (guestUser) {print("Received join microphone application from audience ${guestUser.userName}");// Update UI herein, for example display a red dot on the "application list" button},onInvitationAccepted: (guestUser) {print("Audience ${guestUser.userName} accepted your invitation");},onInvitationRejected: (guestUser) {print("Audience ${guestUser.userName} refused your invite");},);guestStore.addHostListener(_hostListener);}@overridevoid dispose() {guestStore.removeHostListener(_hostListener);super.dispose();}
coGuestState in CoGuestStore maintains the current list of applicants in real time. You can listen to it to update your UI.// Subscription status updatevoid subscribeApplicants() {guestStore.coGuestState.applicants.addListener(_applicantsChangedListener);}void _onApplicantsChanged() {final applicants = guestStore.coGuestState.applicants.value;print("Current number of applicants: ${applicants.length}");// Refresh your "applicant list" UI hereinsetState(() {_applicantList = applicants;});}@overridevoid dispose() {guestStore.coGuestState.applicants.removeListener(_applicantsChangedListener);super.dispose();}
// Anchor clicks the "Grant" button and imports the applicant's userIDvoid accept(String userId) async {final result = await guestStore.acceptApplication(userId);if (result.isSuccess) {print("Approved $userId's request, the other party is connecting mic");}}// Anchor clicked the "Reject" buttonvoid reject(String userId) async {final result = await guestStore.rejectApplication(userId);if (result.isSuccess) {print("Denied $userId's request");}}
inviteToSeat method.// Anchor selects audience and initiates invitationvoid invite(String userId) async {// inviteeID: The invitee ID, seatIndex: The microphone slot index, value range is -1 (auto select an empty slot) and [0, maximum number of microphone slots in the current room -1]// timeout: invitation timeout durationfinal result = await guestStore.inviteToSeat(inviteeID: userId,seatIndex: 0,timeout: 30,extraInfo: null,);if (result.isSuccess) {print("Invitation sent to $userId, waiting for peer response...");}}
HostListener to listen for invitation response events.// Callback configured in HostListener_hostListener = HostListener(onInvitationAccepted: (guestUser) {print("Audience ${guestUser.userName} accepted your invitation");},onInvitationRejected: (guestUser) {print("Audience ${guestUser.userName} refused your invite");},);
GuestListener.// Callback configured in GuestListener_guestListener = GuestListener(onInvitationReceived: (hostUser) {print("Received mic connection invitation from Anchor ${hostUser.userName}");// Pop up a dialog box herein for users to choose "Accept" or "Reject"_showInvitationDialog(hostUser);},);
String inviterId = "Anchor ID initiating invitation"; // Obtain from onInvitationReceived callback// User click "Accept"void acceptInvitation() async {final result = await guestStore.acceptInvitation(inviterId);if (result.isSuccess) {// Turn on the camera, microphoneDeviceStore.shared.openLocalCamera(isFront: true);DeviceStore.shared.openLocalMicrophone();}}// User click "Reject"void rejectInvitation() async {await guestStore.rejectInvitation(inviterId);}

VideoWidgetBuilder parameter of LiveCoreWidget to add custom views on top of the guest video streams. This allows you to display nicknames, avatars, and other information, or provide placeholder images when the camera is off, enhancing the overall visual experience.
import 'package:flutter/material.dart';import 'package:rtc_room_engine/rtc_room_engine.dart';/// Custom user information floating view (foreground)class CustomSeatForegroundView extends StatelessWidget {final SeatFullInfo seatInfo;const CustomSeatForegroundView({Key? key,required this.seatInfo,}) : super(key: key);@overrideWidget build(BuildContext context) {return Container(color: Colors.transparent,child: Align(alignment: Alignment.bottomLeft,child: Container(margin: const EdgeInsets.all(5),padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),decoration: BoxDecoration(color: Colors.black.withOpacity(0.5),borderRadius: BorderRadius.circular(12),),child: Text(seatInfo.userInfo.userName,style: const TextStyle(color: Colors.white,fontSize: 14,),),),),);}}
import 'package:flutter/material.dart';import 'package:rtc_room_engine/rtc_room_engine.dart';/// Custom avatar placeholder view (background)class CustomSeatBackgroundView extends StatelessWidget {final SeatFullInfo seatInfo;const CustomSeatBackgroundView({Key? key,required this.seatInfo,}) : super(key: key);@overrideWidget build(BuildContext context) {final avatarUrl = seatInfo.userInfo.avatarUrl;return Container(decoration: BoxDecoration(color: Colors.grey[800],),child: Center(child: Column(mainAxisSize: MainAxisSize.min,children: [ClipOval(child: avatarUrl.isNotEmpty? Image.network(avatarUrl,width: 60,height: 60,fit: BoxFit.cover,errorBuilder: (context, error, stackTrace) {return _buildDefaultAvatar();},): _buildDefaultAvatar(),),const SizedBox(height: 8),Text(seatInfo.userInfo.userName,style: const TextStyle(color: Colors.white,fontSize: 12,),),],),),);}Widget _buildDefaultAvatar() {return Container(width: 60,height: 60,color: Colors.grey,child: const Icon(Icons.person, size: 40, color: Colors.white),);}}
VideoWidgetBuilder's coGuestWidgetBuilder callback, and return the corresponding view based on the viewLayer value.import 'package:flutter/material.dart';import 'package:atomic_x_core/atomicxcore.dart';import 'package:rtc_room_engine/rtc_room_engine.dart';/// Live streaming page with custom connection viewclass CustomCoGuestLiveWidget extends StatefulWidget {final String liveId;const CustomCoGuestLiveWidget({Key? key,required this.liveId,}) : super(key: key);@overrideState<CustomCoGuestLiveWidget> createState() => _CustomCoGuestLiveWidgetState();}class _CustomCoGuestLiveWidgetState extends State<CustomCoGuestLiveWidget> {late LiveCoreController _controller;@overridevoid initState() {super.initState();_controller = LiveCoreController.create();_controller.setLiveID(widget.liveId);}@overridevoid dispose() {_controller.dispose();super.dispose();}/// Build a custom view for the mic-connecting audienceWidget _buildCoGuestWidget(BuildContext context,SeatFullInfo seatFullInfo,ViewLayer viewLayer,) {if (viewLayer == ViewLayer.foreground) {// Foreground layer: Always display on top of the video image, used for Nickname, information, etc.return CustomSeatForegroundView(seatInfo: seatFullInfo);} else {// Background layer: Displayed only when the corresponding user has no video stream, used for displaying avatar placeholder imagesreturn CustomSeatBackgroundView(seatInfo: seatFullInfo);}}@overrideWidget build(BuildContext context) {return Scaffold(body: LiveCoreWidget(controller: _controller,videoWidgetBuilder: VideoWidgetBuilder(coGuestWidgetBuilder: _buildCoGuestWidget,),),);}}
Parameter | Type | Note |
seatFullInfo | SeatFullInfo | Seat information object, containing the Anchor's detailed information. |
seatFullInfo.userInfo.userId | String | Anchor's user ID. |
seatFullInfo.userInfo.userName | String | On-mic user's nickname. |
seatFullInfo.userInfo.avatarUrl | String | On-mic user's profile photo URL. |
viewLayer | ViewLayer | View layer enumeration: ViewLayer.foreground represents the pendant view, always displayed on top of the video image.ViewLayer.background means the background widget, below the foreground layer, only displayed when the user has no video stream (e.g., camera is off), usually used to show the user’s default avatar or placeholder image. |
Store/Component | Feature Description | API Documentation |
LiveCoreWidget | Core view component for live video stream display and interaction: responsible for video rendering and widget handling, supports host streaming, guest interaction, host connection, and other scenarios. | |
LiveCoreController | Controller for LiveCoreWidget: used to set the live ID, control preview, and other operations. | |
VideoWidgetBuilder | Video view adapter: used to customize video stream widgets for guest interaction, host connection, PK, and other scenarios. | |
DeviceStore | Audio/video device control: microphone (on/off / volume), camera (on/off / switch / quality), screen sharing, real-time device status monitoring. | |
CoGuestStore | Guest interaction management: guest request/invite/accept/reject, guest member permissions (microphone/camera), state synchronization. |
coGuestWidgetBuilder callback, so you don't need to handle them manually. If necessary to process user interaction (such as click events) in a custom view, just add corresponding event handling when creating the view.ViewLayer is used to distinguish between foreground and background widgets.ViewLayer.foreground: The foreground layer, always displayed on top of the video image.ViewLayer.background: The background layer, displayed only when the corresponding user has no video stream (for example, when the camera is not turned on), usually used for displaying the default avatar or placeholder image of the user.LiveCoreWidget's videoWidgetBuilder parameter is correctly set.coGuestWidgetBuilder callback function is correctly implemented.Widget instance.Feedback