CoGuestStore and LiveSeatStore.
CoGuestStore and LiveSeatStore support the following core live interaction scenarios:applyForSeat method.import AtomicXCorelet liveId = "Room ID"var guestStore: CoGuestStore {CoGuestStore.create(liveID: liveID)}// User taps "Request Mic-Link"func requestToConnect() {// timeout: Request timeout, e.g., 30 secondsguestStore.applyForSeat(timeout: 30.0, extraInfo: nil) { result inswitch result {case .success():print("Mic-link request sent, waiting for host response...")case .failure(let error):print("Failed to send request: \\(error.message)")}}}
guestEventPublisher to receive the host’s response.import AtomicXCoreimport Combine// Subscribe to events during view controller initializationfunc subscribeGuestEvents() {guestStore.guestEventPublisher.sink { [weak self] event inif case let .onGuestApplicationResponded(isAccept, hostUser) = event {if isAccept {print("Host \\(hostUser.userName) accepted your request, preparing to join mic")// 1. Open microphoneDeviceStore.shared.openLocalMicrophone(completion: nil)// 2. Update UI, e.g., disable request button, show "on mic" status} else {print("Host \\(hostUser.userName) rejected your request")// Show popup to inform user of rejection}}}.store(in: &cancellables) // Manage subscription lifecycle}
disConnect to return to regular audience status.// User taps "Leave Mic" buttonfunc leaveSeat() {guestStore.disConnect { result inswitch result {case .success():print("Successfully left mic")case .failure(let error):print("Failed to leave mic: \\(error.message)")}}}
cancelApplication.// User taps "Cancel Request" while waitingfunc cancelRequest() {guestStore.cancelApplication { result inswitch result {case .success():print("Request cancelled")case .failure(let error):print("Failed to cancel request: \\(error.message)")}}}
hostEventPublisher to be notified immediately when a new audience request arrives.import AtomicXCoreimport Combinelet liveId = "Room ID"var guestStore: CoGuestStore {CoGuestStore.create(liveID: liveID)}// Subscribe to host eventsguestStore.hostEventPublisher.sink { [weak self] event inif case let .onGuestApplicationReceived(guestUser) = event {print("Received mic-link request from audience \\(guestUser.userName)")// Update UI, e.g., show a red dot on the "Request List" button}}.store(in: &cancellables)
CoGuestStore state maintains the current list of applicants in real time. Subscribe to it to refresh your UI.import AtomicXCoreimport Combine// Subscribe to state changesguestStore.state.subscribe(StatePublisherSelector(keyPath: \\CoGuestState.applicants)) // Only care about applicant list changes.removeDuplicates().sink { applicants inprint("Current number of applicants: \\(applicants.count)")// Refresh your "Applicant List" UI here// self.applicantListView.update(with: applicants)}.store(in: &cancellables)
// Host taps "Accept" button, passing applicant's userIDfunc accept(userId: String) {guestStore.acceptApplication(userID: userId) { result inif case .success = result {print("Accepted \\(userId)'s request, they are joining the mic")}}}// Host taps "Reject" buttonfunc reject(userId: String) {guestStore.rejectApplication(userID: userId) { result inif case .success = result {print("Rejected \\(userId)'s request")}}}
inviteToSeat method.// Host selects audience and initiates invitationfunc invite(userId: String) {// timeout: Invitation timeoutguestStore.inviteToSeat(userID: userId, timeout: 30.0, extraInfo: nil) { result inif case .success = result {print("Invitation sent to \\(userId), waiting for response...")}}}
onHostInvitationResponded event via hostEventPublisher.// Add to hostEventPublisher subscriptionif case let .onHostInvitationResponded(isAccept, guestUser) = event {if isAccept {print("Audience \\(guestUser.userName) accepted your invitation")} else {print("Audience \\(guestUser.userName) rejected your invitation")}}
onHostInvitationReceived event via guestEventPublisher.// Add to guestEventPublisher subscriptionif case let .onHostInvitationReceived(hostUser) = event {print("Received mic-link invitation from host \\(hostUser.userName)")// Show a dialog for the user to choose "Accept" or "Reject"// self.showInvitationDialog(from: hostUser)}
let inviterId = "Host ID who sent the invitation" // Get from onHostInvitationReceived event// User taps "Accept"func accept() {guestStore.acceptInvitation(inviterID: inviterId) { result inif case .success = result {// 2. Open microphoneDeviceStore.shared.openLocalMicrophone(completion: nil)}}}// User taps "Reject"func reject() {guestStore.rejectInvitation(inviterID: inviterId) { result in// ...}}
LiveSeatStore, which functions with CoGuestStore.LiveSeatStore interface.muteMicrophone(). This is a one-way request with no callback.unmuteMicrophone(completion:).let seatStore = LiveSeatStore.create(liveID: liveId)seatStore.muteMicrophone() // MuteseatStore.unmuteMicrophone(completion: nil) // Unmute
Parameter | Type | Description |
completion | CompletionClosure? | Callback after the operation completes. |
closeRemoteMicrophone to forcibly mute the target user's microphone and lock their mic control. The muted user will receive the onLocalMicrophoneClosedByAdmin event via liveSeatEventPublisher, and their local "Open Microphone" button should become disabled.openRemoteMicrophone—this does not forcibly open the user's mic, but unlocks their mic control, allowing them to unmute themselves. The target user receives the onLocalMicrophoneOpenedByAdmin event, their "Open Microphone" button should become enabled, but they remain muted until they unmute themselves.LiveSeatStore's unmuteMicrophone() to actually unmute and be heard in the room.let targetUserId = "userD"// 1. Force mute and lock userDseatStore.closeRemoteMicrophone(userID: targetUserId) { result inif case .success = result {print("\\(targetUserId) has been muted and locked")}}// 2. Unlock userD’s microphone control (userD remains muted)seatStore.openRemoteMicrophone(userID: targetUserId, policy: .unlockOnly) { result inif case .success = result {print("Invited \\(targetUserId) to open microphone (unlocked)")}}
// userD listens for host actionsseatStore.liveSeatEventPublisher.sink { event inswitch event {case .onLocalMicrophoneClosedByAdmin:print("Muted by host")// self.muteButton.isEnabled = falsecase .onLocalMicrophoneOpenedByAdmin(policy: _):print("Host unlocked mute control")// self.muteButton.isEnabled = true// self.muteButton.setTitle("Open Microphone")default:break}}.store(in: &cancellables)
Parameter | Type | Description |
userID | String | The userID of the target user. |
completion | CompletionClosure? | Callback after the request completes. |
Parameter | Type | Description |
userID | String | The userID of the target user. |
completion | CompletionClosure? | Callback after the request completes. |
kickUserOutOfSeat to forcibly remove a specified user from the mic seat.onKickedOffSeat event via CoGuestStore.guestEventPublisher.// Suppose you want to remove "userB"let targetUserId = "userB"seatStore.kickUserOutOfSeat(userID: targetUserId) { result inswitch result {case .success:print("\\(targetUserId) has been removed from the mic seat")case .failure(let error):print("Failed to remove user: \\(error.message)")}}// "userB" receives the removal eventguestStore.guestEventPublisher.receive(on: RunLoop.main).sink { [weak self] event inguard let self = self else { return }switch event {case .onKickedOffSeat(seatIndex: _, hostUser: _):// Show toast notificationdefault:break}}.store(in: &cancellables)
Parameter | Type | Description |
userID | String | The userID of the user to be removed from the mic seat. |
completion | CompletionClosure? | Callback after the request completes. |
lockSeat to lock the seat at the specified index. Once locked, audience members cannot occupy this seat via applyForSeat or takeSeat.unlockSeat to unlock the seat, making it available again.// Lock seat #2seatStore.lockSeat(seatIndex: 2) { result inif case .success = result {print("Seat #2 locked")}}// Unlock seat #2seatStore.unlockSeat(seatIndex: 2) { result inif case .success = result {print("Seat #2 unlocked")}}
Parameter | Type | Description |
seatIndex | Int | Index of the seat to lock. |
completion | CompletionClosure? | Callback after the request completes. |
Parameter | Type | Description |
seatIndex | Int | Index of the seat to unlock. |
completion | CompletionClosure? | Callback after the request completes. |
moveUserToSeat(userID:targetIndex:policy:completion:) to move users between mic seats.userID, the target seat index as targetIndex, and use the policy parameter to specify the seat movement strategy if the target seat is occupied (see parameter details below).userID must be the user's own ID, targetIndex is the desired new seat index, and the policy parameter is ignored. If the target seat is occupied, the move fails with an error.seatStore.moveUserToSeat(userID: "userC",targetIndex: newSeatIndex,policy: .abortWhenOccupied) { result inif case .success = result {print("Successfully moved to seat #\\(newSeatIndex)")} else {print("Move failed, seat may be occupied")}}
Parameter | Type | Description |
userID | String | The userID of the user to move. |
targetIndex | Int | Target seat index. |
policy | MoveSeatPolicy? | Seat movement policy if the target seat is occupied:<br>- abortWhenOccupied: Abort move if seat is occupied (default)<br>- forceReplace: Force replace the user on the target seat; replaced user will be removed<br>- swapPosition: Swap positions with the user on the target seat. |
completion | CompletionClosure? | Callback after the request completes. |
Store/Component | Feature Description | API Documentation |
CoGuestStore | Audience mic-link management: mic-link requests/invitations/accept/reject, member permission control (microphone/camera), state synchronization. | |
LiveSeatStore | Mic seat management: mute/unmute, lock/unlock seats, remove users from mic, remote mic control, mic seat list state monitoring |
Video Live: The core is the video feed. Use LiveCoreView as the main component to render video streams for the host and mic-linked viewers. The UI focuses on video layout and sizing, and you can add overlays (nickname, placeholder image) via VideoViewDelegate. Both camera and microphone can be enabled.Voice Room: The core is the mic seat grid. Do not use LiveCoreView; instead, build a grid UI (e.g., UICollectionView) based on the LiveSeatStore's state (especially seatList). The UI focuses on real-time display of each seat’s SeatInfo status: occupied, muted, locked, and speaking status. Only the microphone needs to be enabled.seatList property in LiveSeatState, which is a reactive [SeatInfo] array. Any changes will notify you to re-render the mic seat list. Iterate through this array to:seatInfo.userInfo.seatInfo.isLocked.seatInfo.userInfo.microphoneStatus.DeviceStore manages the physical device, while LiveSeatStore manages mic seat business logic (audio stream).DeviceStore:openLocalMicrophone: Requests system permission and starts the microphone hardware for audio capture. This is a time-consuming operation.closeLocalMicrophone: Stops audio capture and releases the microphone device.LiveSeatStore:muteMicrophone: Mute. Stops sending local audio stream to remote, but the microphone hardware remains running.unmuteMicrophone: Unmute. Resumes sending audio stream to remote.openLocalMicrophone once to start the device.muteMicrophone and unmuteMicrophone to control the audio stream.disconnect), call closeLocalMicrophone to release the device.Feedback