语音麦序控制本质是「状态同步 + 请求排队」,即服务端用Redis原子命令强一致管理发言权队列与持麦者,禁止前端绕过调度直连媒体,Workerman仅负责信令调度而非音频传输。

语音麦序控制本质是「状态同步 + 请求排队」,不是实时音频转发
Workerman 本身不处理音频编解码或 WebRTC 信令,它只负责在 PHP 层维护玩家发言权的状态机。所谓“麦序”,就是让 player_id 按顺序获得 is_mic_open 权限,且同一时间最多 1 人(或预设 N 人)可发言。关键不在“传声音”,而在“谁被允许发”——这个决策必须由服务端强一致控制,客户端只能申请、等待、接收指令。
常见错误现象:前端直接调用 navigator.mediaDevices.getUserMedia() 后就推流,服务端没拦截;多个玩家同时说话导致混音爆炸;抢麦后状态不同步,A 看到自己已开麦,B 却收不到 A 的音频。这些都不是网络或音频问题,而是状态没管住。
- 所有麦序操作必须走 Workerman 的
onMessage回调,禁止前端绕过服务端直连媒体服务器 - 使用
$_SESSION或Redis存储房间级麦序队列(推荐 Redis,因需跨进程共享),不能仅靠 PHP 进程内数组 - 每个房间对应一个独立的
mic_queue队列(如 Redis key:room:1001:mic_queue)和一个mic_holder(当前持麦者player_id) - 客户端发起
request_mic消息时,服务端检查是否已在队列中;若未在,则LPUSH到队列尾部,并返回排队位置(如 “你在第3位”)
用 Redis 实现原子性麦序调度,避免并发冲突
PHP 进程间无法共享内存,而 Workerman 默认多进程运行,array_shift() 这类操作在多个 Worker 进程里并发执行会丢状态。必须用 Redis 的原子命令做队列管理,否则会出现“两人同时被分配到麦”或“跳过某人”的情况。
示例流程(服务端收到 request_mic):
if ($data['type'] === 'request_mic') {
$room_id = $data['room_id'];
$player_id = $data['player_id'];
// 原子性:只有未在队列中才入队
$queue_key = "room:{$room_id}:mic_queue";
$in_queue = $redis->sIsMember("room:{$room_id}:mic_set", $player_id);
if (!$in_queue) {
$redis->rPush($queue_key, $player_id);
$redis->sAdd("room:{$room_id}:mic_set", $player_id);
$pos = $redis->lLen($queue_key);
$connection->send(json_encode(['type' => 'mic_queued', 'position' => $pos]));
}
}
- 用
sAdd+sIsMember防止重复入队,比单纯rPush更可靠 - 释放麦时(
release_mic),先LPOP获取下一位,再DEL当前mic_holder,并广播mic_granted给新持麦者 - 超时自动释放:给每个
mic_holder设置EXPIRE(比如 60 秒),避免玩家断线后麦一直被占
前端必须严格遵循「申请 → 等待 → 授权 → 开始采集」四步,不能提前调用 getUserMedia
很多开发者让前端一进房间就执行 navigator.mediaDevices.getUserMedia(),结果服务端还没批准就已在本地采集音频,造成资源浪费和隐私风险。正确做法是:只在收到服务端 mic_granted 后才启动采集,并立刻绑定到指定 RTCPeerConnection 或 WebSocket 二进制通道。
- 收到
{type: "mic_granted", player_id: "p203"}后,再调用getUserMedia({audio: true}),并立即创建MediaRecorder或编码后发往媒体中继节点 - 前端需监听自身连接断开、页面隐藏等事件,主动发
release_mic,否则服务端依赖心跳或超时清理,延迟高 - UI 上的“举手”按钮点击后,应禁用并显示“已申请(第X位)”,而不是反复发送请求
- 不要在
setInterval里轮询麦序状态——用服务端主动推送(Workerman 的$connection->send())替代
音频实际传输必须交给专门媒体服务,Workerman 只做调度中枢
Workerman 是 IO 多路复用模型,适合长连接信令,但不适合持续收发音频帧(尤其 Opus 编码后的二进制流)。强行用它做音频中继会导致 CPU 暴涨、延迟飙升、Worker 进程卡死。
- 语音流走独立路径:WebRTC 直连(P2P 或 SFU)、或专用媒体服务(如 Mediasoup、Janus)——Workerman 只负责把
player_id和room_id透传给它们的信令接口 - Workerman 要做的唯一音频相关事,是当
mic_granted时,调用外部 API 触发媒体服务为该玩家开启上行流权限(例如 POST 到/api/room/1001/player/p203/enable_audio) - 如果坚持全栈 PHP,至少把音频帧转成 Base64 后通过 WebSocket 发送——但这仅适用于极低码率、演示用途,不可用于真实狼人杀场景
真正难的不是代码怎么写,而是厘清边界:Workerman 管“谁能说”,不管“怎么说”;音频格式、编解码、抖动缓冲、NAT 穿透,这些都得交出去。一旦混淆,后面全是坑。


















