一、幽灵麦介绍
幽灵麦问题的本质在于业务侧麦位状态与 RTC 实际音频上行状态不一致,或业务权限控制被绕过。常见原因可归纳为以下三类:
·麦位状态同步异常
用户下麦后,业务侧虽已更新麦位状态,但由于信令回调未正常触达、消息被拦截、状态同步异常等原因,客户端未及时执行切换观众角色、关闭麦克风或停止推流等操作,导致用户实际仍在进行音频上行。
·RTC 状态切换失败
客户端已正确收到下麦通知,并开始执行 TRTC 角色切换或停止推流操作,但由于接口调用失败、状态机异常、网络问题等原因,导致 RTC 实际推流状态未成功关闭,从而出现“已下麦但仍能发言”的现象。
·权限控制被绕过或恶意攻击
攻击者通过 App 破解、外挂、Hook、协议模拟、UserSig 泄露等方式,绕过业务侧上麦鉴权或客户端状态控制,伪造主播身份进入房间并强行进行音频上行,进而实施噪音干扰、违规内容传播、恶意刷屏等破坏房间秩序的行为。
针对幽灵麦问题,可通过主动检测 RTC 实际音频状态与业务侧麦位状态的一致性,实现幽灵麦的快速识别与处理。下面将介绍几种常用的幽灵麦预防和处理方案,建议根据业务需求自行选择合适的方案。
二、手动订阅音频流方案
方案原理:通过 TRTC SDK setDefaultStreamRecvMode 设置音频流手动订阅模式,当收到远端用户发布音频流的回调,先通过对比业务侧麦位列表,再决策是否订阅并播放该用户的音频流。该方案具备较好的幽灵麦预防效果,参考示例代码如下。
KotlinSwiftDart// 进房前:关闭自动订阅,改为手动按需拉流trtcCloud.setDefaultStreamRecvMode(false, false)trtcCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_LIVE)// 麦位列表:业务侧自行维护,决定哪些用户的音频需要被订阅private val seatUserSet = mutableSetOf<String>()// SDK 回调:远端用户发布/取消音频流 → 对比麦位列表决定是否订阅override fun onUserAudioAvailable(userId: String, available: Boolean) { if (available && userId in seatUserSet) { trtcCloud.muteRemoteAudio(userId, false) }}// 麦位变更补偿:防止音频回调与麦位信令时序不一致导致漏订或多订fun onSeatListUpdated(newSet: Set<String>) { (newSet – seatUserSet).forEach { trtcCloud.muteRemoteAudio(it, false) } (seatUserSet – newSet).forEach { trtcCloud.muteRemoteAudio(it, true) } seatUserSet.clear(); seatUserSet.addAll(newSet)}// 进房前:关闭自动订阅,改为手动按需拉流trtcCloud.setDefaultStreamRecvMode(false, video: false)trtcCloud.enterRoom(params, appScene: .LIVE)// 麦位列表:业务侧自行维护,决定哪些用户的音频需要被订阅private var seatUserSet: Set<String> = []// SDK 回调:远端用户发布/取消音频流 → 对比麦位列表决定是否订阅func onUserAudioAvailable(_ userId: String, available: Bool) { if available && seatUserSet.contains(userId) { trtcCloud.muteRemoteAudio(userId, mute: false) }}// 麦位变更补偿:防止音频回调与麦位信令时序不一致导致漏订或多订func onSeatListUpdated(_ newSet: Set<String>) { newSet.subtracting(seatUserSet).forEach { trtcCloud.muteRemoteAudio($0, mute: false) } seatUserSet.subtracting(newSet).forEach { trtcCloud.muteRemoteAudio($0, mute: true) } seatUserSet = newSet}// 进房前:关闭自动订阅,改为手动按需拉流await trtcCloud.setDefaultStreamRecvMode(false, false);await trtcCloud.enterRoom(params, TRTCAppScene.LIVE);// 麦位列表:业务侧自行维护,决定哪些用户的音频需要被订阅final _seatUserSet = <String>{};// SDK 回调:远端用户发布/取消音频流 → 对比麦位列表决定是否订阅void _onUserAudioAvailable(String userId, bool available) { if (available && _seatUserSet.contains(userId)) { trtcCloud.muteRemoteAudio(userId, false); }}// 麦位变更补偿:防止音频回调与麦位信令时序不一致导致漏订或多订void onSeatListUpdated(Set<String> newSet) { newSet.difference(_seatUserSet).forEach((id) => trtcCloud.muteRemoteAudio(id, false)); _seatUserSet.difference(newSet).forEach((id) => trtcCloud.muteRemoteAudio(id, true)); _seatUserSet..clear()..addAll(newSet);}
三、音量大小回调方案
方案原理:通过 TRTC SDK enableAudioVolumeEvaluation 启用音量大小提示,当收到远端用户的音量大小回调,比对该音频推流用户和业务侧麦位列表,从而识别出不在麦位上却有音频推流的幽灵麦。整体实现流程如下所示。
当检测出幽灵麦后,客户端本地执行 muteRemoteAudio 静音该远端用户的音频流,同时上报到业务服务端,业务服务端决策是否将该用户禁言或踢出房间 RemoveUser。
四、业务侧进房校验方案
方案原理:幽灵麦用户通常会通过非法入侵手段获取 TRTC 鉴权凭证 UserSig,然后利用 Web 等平台直接绕过业务逻辑,通过 TRTC SDK 进入任意房间实施干扰或攻击。这种情况下,可以通过在 TRTC 服务端进房事件回调中增加业务侧校验逻辑,检查用户是否同时登录了业务房间,若仅进入了 TRTC 房间则视为幽灵麦用户并进行禁言处理或踢出房间。
1. 实时音视频 TRTC 控制台支持自助配置回调信息,配置完成后即可接收事件回调通知。详情请参见 回调配置。
2. 接收并解析回调事件包体,监听103: 进入房间事件,并在事件回调中检查用户是否同时登录了业务房间。详情请参见 事件回调。
3. 如果检测出用户仅进入 TRTC 房间并未登录业务房间,则将该用户视为幽灵麦用户并进行禁言或踢出房间 RemoveUser。
Node.jsGoPythonapp.post('/trtc/callback', async (req, res) => { const { EventType, EventInfo } = req.body; // 仅处理 103 进房事件 if (EventType === 103) { const { RoomId, UserId } = EventInfo; // 查询业务侧:该用户是否在业务房间中 const inBusinessRoom = await db.checkUserInRoom(RoomId, UserId); if (!inBusinessRoom) { // 幽灵麦 → 踢出房间 await trtcApi.removeUser(RoomId, UserId); } } res.json({ code: 0 });});func handleCallback(w http.ResponseWriter, r *http.Request) { var cb CallbackBody json.NewDecoder(r.Body).Decode(&cb) // 仅处理 103 进房事件 if cb.EventType == 103 { info := cb.EventInfo // 查询业务侧:该用户是否在业务房间中 if !db.CheckUserInRoom(info.RoomId, info.UserId) { // 幽灵麦 → 踢出房间 trtcApi.RemoveUser(info.RoomId, info.UserId) } } w.Write([]byte(`{"code":0}`))}@app.route('/trtc/callback', methods=['POST'])def trtc_callback(): data = request.get_json() # 仅处理 103 进房事件 if data['EventType'] == 103: info = data['EventInfo'] # 查询业务侧:该用户是否在业务房间中 if not db.check_user_in_room(info['RoomId'], info['UserId']): # 幽灵麦 → 踢出房间 trtc_api.remove_user(info['RoomId'], info['UserId']) return jsonify(code=0)