语聊房里,用户从听众变成”开麦说话的人”,这个操作叫上麦。
在 SDK 层面,上麦的核心是调用 setClientRole 把角色从 audience 切换为 broadcaster,但”上麦功能”不只是这一行代码,它还包括:服务端原子写麦位状态(保证并发安全)、通过信令广播变更给所有客户端、以及用户意外断线时自动释放麦位。把这几件事做完整,上麦功能才算可靠。
这篇文章覆盖上麦的完整实现流程:SDK 角色切换 API、服务端协调机制、静音与下麦的区别,以及各平台的 API 对比。

一. setClientRole 是做什么的
声网 RTC 频道有两种模式,语聊房必须用直播模式(Live Broadcasting)。直播模式下用户有两种角色:
- broadcaster(主播):可以发布音频流,频道内所有人能听到
- audience(观众):只能接收,不发布音频
上麦 = 从 audience 切换为 broadcaster。下麦 = 从 broadcaster 切换回 audience。切换是即时生效的,不需要重新加入频道。
各平台的角色切换 API:
| 平台 | 上麦(切换为主播) | 下麦(切换为观众) |
|---|---|---|
| Android | engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER) |
engine.setClientRole(Constants.CLIENT_ROLE_AUDIENCE) |
| iOS | updateChannel(with: options) // clientRoleType = .broadcaster |
updateChannel(with: options) // clientRoleType = .audience |
| Web | client.setClientRole("host") |
client.setClientRole("audience") |
| 小程序 | client.setRole("broadcaster") |
client.setRole("audience") |
Web SDK 里主播叫 "host",不是 "broadcaster",这是 Web SDK 和原生 SDK 命名上最容易混淆的地方。
二. 只调 SDK 为什么不够
如果在客户端直接调用 setClientRole(BROADCASTER) 而不经过服务端,会有几个问题:
- 并发冲突:两个用户同时点击上麦,都调了
setClientRole(BROADCASTER),两人都能开始发音频,但麦位只有一个。服务端不知道哪个用户占了这个麦位。 - 状态不同步:某个用户上麦后,其他客户端不知道这件事(除非依赖 RTC 的
onUserJoined,但这个回调只说明用户加入了频道,不携带麦位信息)。 - 断线麦位不释放:用户突然断线,麦位一直被占用,其他人无法上麦。
这三个问题都需要服务端参与才能解决。
三. 完整上麦流程
服务端协调的完整上麦序列:
1. 客户端 → 服务端:申请上麦(roomId + seatIndex + uid)
2. 服务端:原子操作写麦位状态
- 麦位为空 → 写入成功,继续
- 麦位有人 / 被锁定 → 返回失败,客户端提示
3. 服务端 → 客户端:通知该用户切换 broadcaster 角色
4. 客户端:调用 setClientRole(BROADCASTER)
5. 客户端:onClientRoleChanged 回调确认切换成功
6. 服务端 → 信令频道:广播麦位变更给房间内所有用户
7. 所有客户端:收到广播,更新麦位 UI
步骤 3-5 是客户端行为,步骤 6-7 确保所有人的界面同步。步骤 2 的原子操作是防并发冲突的关键,两个人同时申请同一个空麦位时,只有一个能写入成功。
Android 代码示例(客户端部分):
// 收到服务端通知后,切换角色
public void onServerApproveOnMic() {
engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
}
// 角色切换结果回调
@Override
public void onClientRoleChanged(int oldRole, int newRole) {
runOnUiThread(() -> {
if (newRole == Constants.CLIENT_ROLE_BROADCASTER) {
// 上麦成功,更新 UI 显示自己在麦位上
}
});
}
四. 完整下麦流程
1. 客户端 → 服务端:请求下麦(roomId + seatIndex + uid)
2. 服务端:清空该麦位状态(status = empty,uid = null)
3. 服务端 → 客户端:通知该用户切换为 audience
4. 客户端:调用 setClientRole(AUDIENCE)
5. 服务端 → 信令频道:广播麦位变更
6. 所有客户端:更新麦位 UI
房主强制下麦的流程和用户主动下麦一样,只是发起方是房主:服务端收到房主的踢人请求后,向被踢用户的客户端发信令,通知其切换为 audience。
五. 静音和下麦是不同的操作
这两个操作经常被混淆:
| 操作 | 角色变化 | 麦位占用 | 适用场景 |
|---|---|---|---|
| 下麦 | broadcaster → audience | 释放麦位,变空位 | 用户离开麦位 |
| 静音 | 仍是 broadcaster | 继续占用麦位 | 暂时不说话但不离开麦位 |
静音通过 muteLocalAudioStream 实现,不改变角色,麦位仍然显示有人:
// 静音自己(仍是 broadcaster,麦位显示有人)
engine.muteLocalAudioStream(true);
// 取消静音
engine.muteLocalAudioStream(false);
房主对他人静音,SDK 层面没有直接 API。正确做法:服务端下发禁言信令 → 目标用户客户端收到后调用 muteLocalAudioStream(true) → 服务端更新该麦位的 mutedByHost = true 状态。禁言状态必须在服务端记录,否则用户刷新后状态会丢失。
六. 使用声网语聊房 SDK 的简化方式
如果不想自己实现麦位管理的服务端逻辑,声网提供了语聊房场景化 SDK,封装了上麦申请、审批、踢人等业务逻辑:
// 用户申请上麦(指定麦位序号)
voiceRoom.startMicSeatApply(seatIndex)
// 用户主动下麦
voiceRoom.leaveMic()
// 房主禁言某个麦位
voiceRoom.forbidMic(seatIndex, isMute = true)
// 房主锁定某个麦位(禁止任何人上)
voiceRoom.lockMic(seatIndex)
// 房主解锁麦位
voiceRoom.unLockMic(seatIndex)
// 回调:麦位状态更新
// onSeatUpdated(seatList) — 房间内所有用户都会收到
语聊房 SDK 把 RTC 角色切换和业务状态管理都封装了,适合想快速上线的团队。代价是定制空间有限,如果产品有特殊的麦位规则,可能需要自己基于 RTC SDK 实现。
七. 断线时麦位的自动释放
用户意外断线(强杀 App、手机断电),SDK 会在超时后触发 onUserOffline 回调(reason = USER_OFFLINE_DROPPED)。
@Override
public void onUserOffline(int uid, int reason) {
if (reason == Constants.USER_OFFLINE_DROPPED) {
// 用户意外断线,通知服务端释放该 uid 占用的麦位
notifyServerUserDropped(uid);
}
}
服务端收到通知后,清空该麦位状态,再广播变更。这个机制需要在房间内有其他用户在线时才能触发——如果房间只剩一个用户而且该用户断线,需要依赖服务端自己的超时检测(通过心跳包判断用户是否还在线)。
八. 常见问题
上麦后对方听不到声音
确认 setClientRole(BROADCASTER) 调用成功(onClientRoleChanged 回调有触发),且没有调用 muteLocalAudioStream(true)。通过声网控制台的实时数据监控可以看到当前频道的发布状态。
下麦后麦位显示还有人
通常是信令广播没有收到或处理延迟。检查信令频道是否正常,客户端收到广播后是否及时更新 UI。如果用户断线重连,客户端需要重新拉取麦位全量状态,不能依赖之前缓存的状态。
onClientRoleChanged 没有触发
检查频道是否以 CHANNEL_PROFILE_LIVE_BROADCASTING 模式创建。通话模式(COMMUNICATION)下 setClientRole 无效,也不会触发这个回调。
