语聊房上麦有两种入口模式:一种是”自由上麦”,有空位就能直接点,不需要审批;另一种是”申请上麦”,用户提交申请,房主审批通过后才能上。大多数社交娱乐类语聊房用申请模式,防止麦位被随意抢占,维持房间秩序。
这篇文章聚焦申请上麦的实现,覆盖完整的状态流转、等待队列设计、房主邀请模式、超时处理和并发场景。语聊房SDK 角色切换的基础用法见:《语聊房上麦功能怎么做》
一. 两种入麦模式对比
产品形态决定用哪种模式:
| 模式 | 触发方 | 适用场景 |
|---|---|---|
| 申请上麦 | 用户主动申请,房主审批 | 社交娱乐、KTV,需要有秩序的麦位管理 |
| 邀请上麦 | 房主主动邀请,用户接受/拒绝 | 房主主导的场景,如语音相亲、嘉宾连麦 |
| 自由上麦 | 有空位即可上,无需审批 | 游戏语音、熟人语聊,参与者之间相互信任 |
声网语聊房 SDK 把这三种模式都封装了,用不同的 API 方法对应不同的入麦流程。
二. 申请上麦的完整状态流转
用户点击"申请上麦"
│
▼
客户端 → 服务端:startMicSeatApply(seatIndex)
│
▼
服务端:创建申请记录,加入等待队列
│
├─ 房主在线 → 通过信令推送通知给房主
└─ 房主离线 / 无响应 → 超时后自动取消(见第五节)
│
▼
房主收到通知(onReceiveSeatRequest)
│
├─ 审批通过(acceptMicSeatApply)→ 服务端写麦位 → 通知用户切换 broadcaster
└─ 拒绝(rejectMicSeatApply) → 服务端删除申请 → 通知用户申请被拒
用户在申请待审批期间,可以撤回申请:
// 用户撤回申请
voiceRoom.cancelMicSeatApply()
// 房主审批通过
voiceRoom.acceptMicSeatApply(userId)
// 房主拒绝申请
// (具体方法名以当前官方文档为准)
voiceRoom.rejectMicSeatApply(userId)
批准后,服务端再通知申请用户执行角色切换。切换成功后广播麦位变更。
三. 等待队列设计
多个用户同时申请上麦时,需要一个等待队列。队列里的信息:
// 等待队列的数据结构(伪代码)
queue = [
{ uid: "user_A", seatIndex: 2, applyAt: 1717000001, status: "pending" },
{ uid: "user_B", seatIndex: 2, applyAt: 1717000005, status: "pending" },
{ uid: "user_C", seatIndex: 0, applyAt: 1717000010, status: "pending" }
]
队列的几个设计决策:
是否允许指定麦位:有些产品允许用户选择申请哪个空麦位,有些产品让房主分配。如果允许指定,queue 里要记录 seatIndex;如果不指定,审批时由房主决定分配给哪个位置。
同一个用户能否同时有多个申请:通常不允许。服务端收到申请时检查该用户是否已在队列里,有则拒绝新申请或覆盖旧的。
队列长度上限:防止恶意刷申请。一般设置上限(比如最多 20 个申请在等待),超出上限时拒绝新申请,在 UI 上提示”等待人数过多”。
审批顺序:通常按申请时间排序,先申请先审批。房主也可以跳过队列顺序直接批准某个用户。
四. 房主邀请上麦的流程
邀请模式由房主主动发起,用户可以接受或拒绝:
// 房主邀请某个用户上麦
voiceRoom.startMicSeatInvitation(userId, seatIndex)
// 被邀请的用户收到通知(回调)
// onReceiveSeatInvitation(invitation)
// 用户接受邀请
voiceRoom.acceptMicSeatInvitation()
// 用户拒绝邀请
voiceRoom.refuseInvite()
邀请流程和申请流程的主要区别:发起方不同。邀请是房主发起、用户决定;申请是用户发起、房主决定。两种流程结束时都需要写麦位状态和广播变更,后续处理是一样的。
邀请和申请可以同时存在于一个房间:房主既可以等待用户申请,也可以主动邀请某个感兴趣的用户。
五. 超时处理
用户申请上麦后,如果房主长时间没有响应,申请需要自动超时取消。超时不处理会导致队列积压,后续用户的申请可能一直排不上。
超时机制在服务端实现:
// 服务端伪代码:创建申请时设置超时
function createMicSeatRequest(uid, seatIndex, roomId) {
const request = {
uid,
seatIndex,
roomId,
status: "pending",
expireAt: Date.now() + 30_000 // 30 秒后超时
}
queue.add(request)
// 定时任务:超时时自动取消
scheduleJob(30_000, () => {
if (request.status === "pending") {
request.status = "expired"
queue.remove(request)
// 通知申请用户:申请超时
notifyUser(uid, "mic_apply_expired")
}
})
}
超时时长通常 30-60 秒。太短,房主还没看到通知就超时了;太长,用户等待体验差。产品可以根据实际情况调整,也可以在 UI 上给申请用户显示倒计时。
邀请同样需要超时处理。房主发出邀请后,如果被邀请用户 30 秒内没有响应,邀请自动失效。
六. 并发申请的冲突处理
最常见的并发场景:一个空麦位,同时有多个用户的申请被房主批准了(比如房主快速点了两次)。服务端写麦位时必须用原子操作:
-- 服务端原子操作(Redis Lua 脚本伪代码)
local seatKey = "seat:" .. roomId .. ":" .. seatIndex
local current = redis.call("HGET", seatKey, "status")
if current == "empty" then
redis.call("HMSET", seatKey, "status", "occupied", "uid", uid)
return 1 -- 成功
else
return 0 -- 已被占用,拒绝
end
写入失败时,向发起批准操作的房主返回错误,提示”该麦位已被占用”。
另一种并发场景:用户 A 申请了 2 号麦位,队列里排着,房主此时手动把 2 号麦位锁定了。这时房主对 A 的申请批准应该失败——服务端批准前需要再检查一次麦位状态是否仍然可用,不能假设申请创建时的状态会一直保持。
七. 信令传递申请消息
申请上麦的通知(房主收到申请提醒、用户收到审批结果)通过信令通道传递。使用声网 RTM 时,房主和申请用户在同一个信令频道里,服务端向频道发送消息,相关用户客户端收到后更新 UI。
几类需要通过信令传递的消息:
- 新申请通知:广播给房主(或管理员),携带申请用户 uid 和申请麦位
- 审批结果:定向发给申请用户,携带批准/拒绝结果
- 申请超时:定向发给申请用户,提示申请已超时
- 邀请通知:定向发给被邀请用户,携带房主 uid 和目标麦位
- 队列更新:广播给所有人(可选),显示当前等待申请数量
消息格式建议带上 type 字段区分消息类型,客户端根据 type 做对应处理,方便后续扩展。
八. 边界情况
用户申请后断线
用户提交了申请然后断线,申请记录仍在队列里。房主批准后,服务端发通知但对方已离线,角色切换会失败。服务端需要在通知失败或超时后自动清除申请并释放麦位。
房主离开房间
房主离开时,等待审批的申请队列怎么处理?有几种策略:1)新房主继承待审批队列;2)清空队列,所有申请自动拒绝。产品选择哪种,服务端和客户端都要对应处理。
麦位已满时的申请
所有麦位都有人时,是否允许用户申请排队?如果允许,审批通过时麦位可能仍然满的(没人下麦)。服务端批准前要再次验证有空位。
申请数量的 UI 展示
等待队列里的申请数量,建议在房主的管理界面实时显示(通过信令推送变化),避免房主需要手动刷新才能看到新申请。
