摘要:在线课堂对音频有严格的声音主权要求,老师的声音必须始终突出,学生只有在被点名时才获得发言权。本文提出了一套基于 ZEGO 手动混流 + 焦点语音的分层方案:老师为永久焦点(isAudioFocus = true),学生默认非焦点,点名时由老师主动触发焦点切换。文章涵盖了大班课(极简混流)与小班课(焦点频繁切换+分组讨论)的差异化架构、课堂混流管理器(Node.js)的完整实现、学生静音策略与回声处理、录制回放的配合等教学场景特有的工程细节。适合开发在线课堂、辅导教学、远程培训等产品的开发者。

一、背景
做过在线课堂的开发者对这些问题不陌生:
- “老师在讲 PPT,有学生忘记关麦,键盘声、家人说话声全进来了。”
- “小班课上,老师在逐个纠正学生发音,但学生的声音被其他孩子的环境音盖住了。”
- “大班课千人同时在线,老师讲得声嘶力竭,学生却说’老师你声音好小’。”
在线教学场景和会议、直播有一个关键差异:存在严格的声音主权逻辑。课堂的主角永远是老师,学生的声音只有在被授权(举手、提问、被点名)时才应该进入”可听频道”。但现实开发中,如果只是把所有推流的声音简单叠加,任何一个学生的环境噪声都可能破坏整堂课的体验。
ZEGO 实时音视频 SDK (ZEGO Express SDK) 的混流焦点语音可以解决这个问题。
二、教学场景的独特性
在线教学和会议/直播在混流需求上有本质不同:
第一,声音主权是层级化的。 课堂上存在一个明确的权力结构:老师 > 被点名的学生 > 其他学生。老师的声音必须始终在最上层;被点名的学生临时获得”焦点许可”;其他学生的声音要么静音,要么压到最低。
第二,焦点切换是老师主动触发的。 会议中焦点切换可能是自动的(谁说话切给谁);直播中可能由运营事件驱动(PK 开始)。但在课堂上,焦点切换的唯一触发器是老师——”小明你来读这段””小红你有什么问题”。这意味着混流参数更新需要和课堂交互信令打通。
第三,大班课和小班课的方案不同。 大班课(1000+人)学生基本不发声,焦点永远在老师一人身上,方案最简单。小班课(6~20 人)师生互动频繁,焦点需要在老师和不同学生之间来回切换。1 对 1 教学则完全不需要混流,直接拉两路独立流即可。
第四,录制回放也是一个消费端。 课堂混流的输出不仅要给实时在线的学生看,还要存下来给课后回放。混流输出就是最终录制内容,焦点语音效果会被永久保存到回放文件中。
三、方案架构:手动混流 + 焦点语音 + 信令联动
教学场景推荐手动混流而非自动混流。原因很简单:课堂内的流池是确定的,老师应该能精确控制哪些学生的声音进入混流、以什么优先级进入。
架构示意
小班课 room_class_001:
老师推流 (stream_teacher) ────┐ isAudioFocus = true (永久焦点)
学生 A 推流 (stream_stu_a) ───┤ isAudioFocus = false (已被静音)
学生 B 推流 (stream_stu_b) ───┤ isAudioFocus = true (被老师点名,临时焦点)
学生 C 推流 (stream_stu_c) ───┤ isAudioFocus = false (已被静音)
↓
ZEGO 手动混流引擎
mixMode = focused
↓
输出混流 → 所有学生端拉流 + 云端录制存储
↓
回放文件中:老师声音清晰,学生 B 回答问题声音突出
焦点层级模型
层级 1:老师 — isAudioFocus 始终 true,不可被覆盖
层级 2:被点名学生 — isAudioFocus 临时 true,回答完毕后恢复 false
层级 3:其他学生 — isAudioFocus false,或被 mutePlayStream 静音
四、关键 API 与参数
教学场景使用手动混流(startMixerTask / StartMix),核心参数和会议篇一致,但使用方式有差异。
4.1 混流音频配置
ZegoMixerAudioConfig *audioConfig = [[ZegoMixerAudioConfig alloc] init];
audioConfig.mixMode = ZegoMixModeFocused;
4.2 老师流(永久焦点)
ZegoMixerInput *teacherInput = [[ZegoMixerInput alloc] init];
teacherInput.streamID = @"stream_teacher";
teacherInput.contentType = ZegoMixerInputContentTypeAudio;
teacherInput.isAudioFocus = YES; // 老师永远是焦点
teacherInput.soundLevelID = @"teacher"; // 用于音量回调追踪
4.3 学生流(按需焦点)
// 默认状态:学生在混流中存在但不为焦点
ZegoMixerInput *studentInput = [[ZegoMixerInput alloc] init];
studentInput.streamID = @"stream_stu_b";
studentInput.contentType = ZegoMixerInputContentTypeAudio;
studentInput.isAudioFocus = NO; // 默认不是焦点
五、实现步骤
5.1 课堂开始:创建初始混流
老师进入房间后,创建混流任务,初始只有老师为焦点:
// 混流音频配置
ZegoMixerAudioConfig *audioConfig = [[ZegoMixerAudioConfig alloc] init];
audioConfig.mixMode = ZegoMixModeFocused;
// 老师输入
ZegoMixerInput *teacherInput = [self createTeacherInput];
// 所有学生输入(初始非焦点)
NSMutableArray<ZegoMixerInput *> *allInputs = [NSMutableArray array];
[allInputs addObject:teacherInput];
for (NSString *studentStreamID in enrolledStudentStreams) {
ZegoMixerInput *stuInput = [[ZegoMixerInput alloc] init];
stuInput.streamID = studentStreamID;
stuInput.contentType = ZegoMixerInputContentTypeAudio;
stuInput.isAudioFocus = NO; // 初始都非焦点
[allInputs addObject:stuInput];
}
// 混流任务
ZegoMixerTask *task = [[ZegoMixerTask alloc] init];
task.taskID = [NSString stringWithFormat:@"class_%@", classRoomID];
task.audioConfig = audioConfig;
task.inputList = allInputs;
task.outputList = @[output];
[[ZegoExpressEngine sharedInstance] startMixerTask:task callback:^(int errorCode, NSDictionary *extendedData) {
if (errorCode == 0) {
NSLog(@"课堂混流创建成功,老师为焦点");
}
}];
5.2 老师点名:动态切换焦点
老师点击”请小明回答”按钮 → 客户端或服务端触发混流参数更新:
- (void)teacherCalledOnStudent:(NSString *)studentStreamID {
// 遍历 inputList,将除老师外的所有学生 isAudioFocus 设为 NO
for (ZegoMixerInput *input in self.currentTask.inputList) {
if ([input.streamID isEqualToString:studentStreamID]) {
input.isAudioFocus = YES; // 被点名的学生设为焦点
} else if (![input.streamID isEqualToString:self.teacherStreamID]) {
input.isAudioFocus = NO; // 其他学生取消焦点
}
}
// 重新调用 startMixerTask 更新混流
[[ZegoExpressEngine sharedInstance] startMixerTask:self.currentTask
callback:^(int errorCode, NSDictionary *extendedData) {
if (errorCode == 0) {
NSLog(@"焦点已切换到学生:%@", studentStreamID);
}
}];
}
5.3 学生回答完毕:恢复默认
- (void)studentFinishedSpeaking {
for (ZegoMixerInput *input in self.currentTask.inputList) {
if (![input.streamID isEqualToString:self.teacherStreamID]) {
input.isAudioFocus = NO; // 所有学生取消焦点
}
}
// 老师恢复为唯一焦点
[[ZegoExpressEngine sharedInstance] startMixerTask:self.currentTask callback:nil];
}
5.4 分组讨论模式
小班课的分组讨论是一个特殊场景:老师不再讲,每组内学生互相讨论。此时有两种处理方式:
方式一(推荐):每组独立房间。 每组学生进入子房间,子房间内的混流以”当前发言人”为焦点。老师可以在各组之间巡视(加入对应子房间)。
方式二:主房间内切换焦点。 将所有讨论组的学生都设为焦点(isAudioFocus = true),取消焦点模式(mixMode 恢复默认),大家的音量平等。讨论结束后重新启用焦点模式。
// 进入分组讨论:关闭焦点模式
audioConfig.mixMode = ZegoMixModeDefault; // 恢复默认混流,音量平等
// 讨论结束:重新开启焦点模式,老师为主
audioConfig.mixMode = ZegoMixModeFocused;
// teacherInput.isAudioFocus 继续保持 YES
六、完整调用示例(Node.js 服务端)
教学场景建议在服务端管理混流任务,和课堂管理后台打通:
// 课堂混流管理器
class ClassroomMixer {
constructor(appId, roomId, teacherStreamId) {
this.appId = appId;
this.roomId = roomId;
this.teacherStreamId = teacherStreamId;
this.mixTaskId = `class_mix_${roomId}`;
this.studentStreams = new Map(); // streamId -> { isFocus, studentName }
}
// 课堂开始:创建混流
async startClass() {
const params = {
AppId: this.appId,
RoomId: this.roomId,
MixTaskId: this.mixTaskId,
Audio: { MixMode: 'focused' },
Input: [
{
StreamId: this.teacherStreamId,
ContentType: 'audio',
IsAudioFocus: true, // 老师永久焦点
},
],
Output: [{ StreamId: `output_${this.roomId}`, ContentType: 'audio' }],
};
// 调用 StartMix API...
console.log(`课堂混流创建:${this.roomId},老师为焦点`);
}
// 学生加入课堂
async studentJoin(streamId, studentName) {
this.studentStreams.set(streamId, { isFocus: false, studentName });
await this.updateMix();
}
// 老师点名
async callOnStudent(streamId) {
for (const [sid, info] of this.studentStreams) {
info.isFocus = (sid === streamId);
}
await this.updateMix();
console.log(`焦点切换到学生:${this.studentStreams.get(streamId).studentName}`);
}
// 学生回答完毕
async studentDone(streamId) {
this.studentStreams.get(streamId).isFocus = false;
await this.updateMix();
console.log(`学生 ${this.studentStreams.get(streamId).studentName} 回答完毕,焦点回到老师`);
}
// 重建混流 Input 列表并更新
async updateMix() {
const inputs = [
{ StreamId: this.teacherStreamId, ContentType: 'audio', IsAudioFocus: true },
];
for (const [streamId, info] of this.studentStreams) {
inputs.push({
StreamId: streamId,
ContentType: 'audio',
IsAudioFocus: info.isFocus,
});
}
const params = {
AppId: this.appId,
RoomId: this.roomId,
MixTaskId: this.mixTaskId,
Audio: { MixMode: 'focused' },
Input: inputs,
Output: [{ StreamId: `output_${this.roomId}`, ContentType: 'audio' }],
};
// 调用 StartMix 更新混流...
}
// 课堂结束
async endClass() {
// 调用 StopMix API...
console.log(`课堂混流停止:${this.roomId}`);
}
}
七、大班课 vs 小班课方案差异
| 维度 | 大班课(1000+人) | 小班课(6~20 人) |
|---|---|---|
| 学生推流 | 通常不推流(仅拉流) | 所有学生推流 |
| 混流输入 | 只含老师流(或极少数举手学生) | 老师 + 所有学生 |
| 焦点策略 | 老师唯一焦点,基本不变 | 老师 + 被点名学生,频繁切换 |
| 混流方式 | 自动混流足够(流稳定) | 手动混流(精确控制) |
| 分组讨论 | 不支持 | 支持(切换到子房间或取消焦点模式) |
| 复杂度 | 低 | 中高 |
大班课方案(极简版)
大班课学生基本不推流,混流输入只有老师一路。这种场景严格来说不太需要焦点语音(单流没有”对比”),但保持混流的好处是:
- 如果以后需要加入举手学生的语音,架构不需要变
- 录制输出走混流统一管理
# 大班课:最简单的混流——只有老师一人
curl -X POST "https://rtc-api.zego.im/?Action=StartMix" \
-H "Content-Type: application/json" \
-d '{
"AppId": 1234567890,
"RoomId": "large_class_001",
"MixTaskId": "mix_large_class_001",
"Audio": { "MixMode": "focused" },
"Input": [
{
"StreamId": "stream_teacher",
"ContentType": "audio",
"IsAudioFocus": true
}
],
"Output": [{
"StreamId": "mix_output_large_class",
"ContentType": "audio"
}]
}'
八、注意事项
8.1 学生端静音 ≠ 不参与混流
很多开发者习惯让学生端直接不推音频(mutePublishStreamAudio),这在教学场景中有个问题:老师想点名学生发言时,学生需要重新推音频流,有 1~3 秒的恢复延迟,体验很差。
更好的做法:学生始终推流,但在混流中控制。学生的流参与混流但 isAudioFocus = false(或搭配静音逻辑),点名时直接改 isAudioFocus,无切换延迟。
8.2 录制与回放
混流输出通常也是录制输入源。确保录制的是混流输出流而非老师的独立流——这样回放时学生提问的声音(当被设为焦点时)也能被清晰听到。
8.3 学生回声问题
被点名的学生如果在用外放,其设备采集到的老师声音会通过混流再次输出,产生回声。建议:
- 学生端强制开启 AEC(回声消除)
- 或在点名时提示学生佩戴耳机
8.4 焦点模式与默认模式的切换开销
mixMode 的切换(focused ↔ 默认)在混流引擎内部是即时生效的,不需要重启混流任务。分组讨论时切换模式不会造成额外的延迟或中断。
8.5 白板和屏幕共享的配合
教学场景通常同时有视频流(白板/PPT/屏幕共享),混流时需要处理视频布局。焦点语音只影响音频,不影响视频布局参数(layout),两者独立配置。
九、总结
在线教学场景突出老师声音的核心方案:
手动混流 +
MixMode = focused+ 老师流IsAudioFocus = true(永久) + 学生流按需切换
和直播场景的关键差异:
| 维度 | 直播 | 教学 |
|---|---|---|
| 混流方式 | 自动混流 | 手动混流 |
| 焦点变更 | 事件驱动(PK/连麦) | 老师主动触发(点名) |
| 焦点对象 | 主播(可能切换) | 老师永久焦点 + 学生临时焦点 |
| 复杂度 | 中 | 中高(焦点切换+分组讨论+录制) |
一句话:老师永远是焦点,学生被点名时才获得焦点许可,回答完毕立刻归还。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/info/70048.html