分组讨论

介绍如何通过音频分组接口实现分组讨论的应用场景。

典型场景

一个课堂,有老师,助教和学生。在讨论某个题目的时候,老师可以将学生分成若干个讨论组,每个讨论组中的学生只能在本组中讨论问题,他听不见其他组里面的声音。而老师和助教可以随意加入某个或者多个讨论组,和组内学生语音交流。

引入音频分组,大厅的概念

音频分组(group):允许频道内的成员建立工作组。User加入一个group后,有权向这个group发言,也可以收听这个group内的音频。

大厅(hall):如果一个user当前不在任何一个group中,那么他就留在大厅中。大厅是一个特殊的group,通过离开/加入一个group间接的达到加入/离开大厅的效果。和group类似,如果user在大厅中,那么他有权对大厅中的人发言,也可以订阅大厅内的音频。

权限约束:user无权对不在其中的group (含hall)发言,或者收听音频。

音频订阅约束:user不可以同时订阅多个group (含hall)的音频,这个是为了应对一个user出现在多个group中,以及过量的多路音频(比如6路音频)混音后造成理解困难的问题。

注意:音频分组不影响视频的publish和subscribe,即使2个user不在同一个group,也可以看见对方。

代码实现

下面用分组接口来实现分组讨论。以c++代码为例,其他平台接口是一致的。

老师侧,通过业务层,通知每个学生加入到指定的分组,比如张三加入group1, 李四加入group2, 王五加入group1, 赵六加入group2

1. 老师侧程序

// 老师加入所有的分组
void discuss_begin()
{
  engine.JoinGroup("group1", "");
  engine.JoinGroup("group2", "");

  // 通知学生加入 -
  // 张三加入group1, 李四加入group2, 王五加入group1, 赵六加入group2

  // 此时老师和学生都还在JoinGroup过程中,
  // 等所有的JoinGroup都成功(OnAudioGroupJoinResult)后,可以和任意
  // 分组中的学生交流了(discuss_group())。
}

// 当所有人加入分组完成后,老师和任意group中的学生交流
void discuss_group(string group, bool private_talk)
{
  // 老师一开始已经提前加入了所有group,无需再次加入group,
  // 只需要向这个group推流和拉流

    // sdk支持同时向多个group推流,如果老师不想其他group的
  // 学生听到,那么private_talk为true
  if (private_talk) {
    // 取消其他group的语音推流
    for (all_groups) {
      engine.MixAudioToGroup(true, ith_group);
    }
  }
  // 和指定的group双向交流
  engine.MixAudioToGroup(true, group);  
  engine.SwitchSubscriptionToGroup(group);
}

// 停止讨论
void discuss_end()
{
  // 解散所有分组,老师和所有学生都回到大厅
  engine.DismissGroup("group1");
  engine.DismissGroup("group2");

  // 注意:监控到自己回到大厅后,设置音频推流和拉流的目标为大厅
  //(在OnAudioGroupHallMembers中实现)
}

// 当老师回到大厅后,改变音频推流和拉流目标为大厅
void MyEngineEventListerner::OnAudioGroupHallMembers(ding::rtc::RtcEngineAudioGroupMember *hallMembers,
  int hallMemberCount)
{
  bool iAmInHall = false;
  for(int i = 0; i < memberCount; i++) {
    if (members[i].usrId 是我自己) {
      iAmInHall = true;
      break;
    }
  }
  // 如果在大厅
  if (iAmInHall) {
    // 音频推流到大厅
    engine.MixAudioToGroup(true, ding::rtc::RtcEngine::HallID);
    // 音频大厅拉流
    engine.SwitchSubscriptionToGroup(ding::rtc::RtcEngine::HallID);
  }
}

2. 学生侧程序

// 学生侧程序,业务层通知,加入某个分组
void on_teacher_notice_join_group(string group)
{
  // 加入指定的分组
  engine.JoinGroup(group, "");
  // 注意:音频推流和拉流的设置,等到OnAudioGroupJoinResult成功后
  // 再执行
}

// 当学生加入某个分组后,更新音频推流和拉流目标为该分组
void OnAudioGroupJoinResult(int result, const ding::rtc::String& errMsg,
  const ding::rtc::String& group,
  RtcEngineAudioGroupMember *members,
  int memberCount)
{
  if (result == 0) { // 0表示加入分组成功
    // 音频推流 (假定学生一开始已经推音频流了)
    engine.MixAudioToGroup(true, group);
    // 音频拉流
    engine.SwitchSubscriptionToGroup(group);
  }
  else {
    // 加入分组失败
  }
}

// 当学生回到大厅后,改变音频推流和拉流目标为大厅
void MyEngineEventListerner::OnAudioGroupHallMembers(ding::rtc::RtcEngineAudioGroupMember *hallMembers,
  int hallMemberCount)
{
  bool iAmInHall = false;
  for(int i = 0; i < memberCount; i++) {
    if (members[i].usrId 是我自己) {
      iAmInHall = true;
      break;
    }
  }
  // 如果在大厅
  if (iAmInHall) {
    // 音频推流到大厅
    engine.MixAudioToGroup(true, ding::rtc::RtcEngine::HallID);
    // 音频大厅拉流
    engine.SwitchSubscriptionToGroup(ding::rtc::RtcEngine::HallID);
  }
}