You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将基于WebRTC与Ably API的一对一语音通话代码改造为群组通话

多人语音房间改造思路

基于你现有的一对一WebRTC+Ably代码,要改成单频道多人语音房间,核心是把主动呼叫的一对一P2P模式改成自动Mesh组网的多人P2P模式,以下是具体改造步骤:

一、移除一对一呼叫的交互与逻辑

现有代码里的呼叫请求、应答逻辑都是针对一对一场景的,需要全部移除:

  • 删除call()函数,以及incoming-call/${clientId}、call-details/${clientId}的订阅逻辑
  • 移除currentCall变量(不再需要跟踪单个通话对象)
  • 删除initiateCall()里的currentCall = client_id赋值逻辑

二、改为用户加入房间后自动建立全员连接

利用Ably的Presence功能,在用户进入/离开时自动处理连接:

  1. 用户加入时自动连接所有在线成员

    • 在AblyRealtime.presence.subscribe('enter')的回调中,除了更新成员列表,对新加入的非当前用户的成员,直接创建Connection连接:
      AblyRealtime.presence.subscribe('enter', function(member) {
          AblyRealtime.presence.get((err, members) => {
              membersList = members
              renderMembers()
              // 主动连接新加入的成员(排除自己)
              if (member.clientId !== clientId && !connections[member.clientId]) {
                  // 当前用户作为initiator发起连接
                  connections[member.clientId] = new Connection(member.clientId, AblyRealtime, true, localStream)
              }
          })
      })
      
    • 同时,当前用户自己进入房间时,要主动连接房间内已有的所有其他成员:
      AblyRealtime.presence.enter((err) => {
          if (!err) {
              AblyRealtime.presence.get((err, members) => {
                  membersList = members
                  renderMembers()
                  // 连接所有已在线的非自己成员
                  members.forEach(member => {
                      if (member.clientId !== clientId && !connections[member.clientId]) {
                          connections[member.clientId] = new Connection(member.clientId, AblyRealtime, true, localStream)
                      }
                  })
              })
          }
      })
      
  2. 处理新用户的连接请求

    • 保留rtc-signal/${clientId}的订阅逻辑,但修改connect()函数,确保每个远程用户只创建一个Connection实例,无需确认直接建立连接:
      function connect(data, stream) {
          if (!connections[data.user]) {
              connections[data.user] = new Connection(data.user, AblyRealtime, false, stream)
          }
          connections[data.user].handleSignal(data.signal)
          document.getElementById('call').style.display = 'block'
      }
      

三、调整成员列表与UI逻辑

  • 修改renderMembers()函数,移除"call now"按钮,仅显示在线用户ID:
    function renderMembers() {
        var list = document.getElementById('memberList')
        var online = document.getElementById('online')
        online.innerHTML = 'Users online (' + (membersList.length === 0 ? 0 : membersList.length - 1) + ')'
        var html = ''
        if (membersList.length === 1) {
            html += '<li> No member online </li>'
            list.innerHTML = html
            return
        }
        for (var index = 0; index < membersList.length; index++) {
            var element = membersList[index]
            if (element.clientId !== clientId) {
                html += '<li><small>' + element.clientId + '</small></li>'
            }
        }
        list.innerHTML = html
    }
    
  • 调整call元素的显示逻辑,只要有连接就显示,或者改成显示房间状态。

四、修改连接管理与流处理

  1. 本地流提前初始化

    • 在页面加载或用户点击"加入房间"按钮时,提前获取本地音频流,无需等到呼叫时:
      // 页面加载时初始化本地流
      window.addEventListener('load', () => {
          navigator.mediaDevices.getUserMedia(constraints)
              .then(function(stream) {
                  localStream = stream
                  // 注意:原来设置volume=0会听不到自己的声音,多人场景下可以保留echoCancellation,但不需要静音自己
                  localStream.getAudioTracks().forEach(track => {
                      track.enabled = true;
                      // track.volume = 0; // 移除这行,否则自己听不到本地声音(如果需要监听自己可以保留,但通常不需要)
                  });
              })
              .catch(function(err) {
                  alert('Could not get audio stream from source')
              })
      })
      
  2. 更新Connection类的关闭逻辑

    • 修改Connection的_onClose方法,不再调用handleEndCall(),而是直接清理对应连接和音频元素:
      _onClose() {
          console.log(`connection to ${this._remoteClient} closed`)
          // 移除对应的音频元素
          const audioElement = document.getElementById(`audio-${this._remoteClient}`)
          if (audioElement) {
              audioElement.remove()
          }
          // 删除连接记录
          delete connections[this._remoteClient]
      }
      
  3. 修改receiveStream函数,为每个远程用户创建独立音频元素

    • 避免重复创建,给每个音频元素设置唯一ID,并添加到页面中:
      function receiveStream(client_id, stream) {
          // 检查是否已存在该用户的音频元素
          let audio = document.getElementById(`audio-${client_id}`);
          if (!audio) {
              audio = new Audio();
              audio.id = `audio-${client_id}`;
              audio.srcObject = stream;
              audio.autoplay = true;
              document.getElementById('call').appendChild(audio);
          }
          renderMembers()
      }
      
  4. 重构handleEndCall为离开房间函数

    • 新增leaveRoom()函数,用于关闭所有连接并停止本地流:
      function leaveRoom() {
          // 关闭所有P2P连接
          Object.values(connections).forEach(connection => {
              connection.destroy()
          })
          connections = {}
          // 停止本地流
          if (localStream) {
              for (var track of localStream.getTracks()) {
                  track.stop()
              }
              localStream = undefined
          }
          // 离开Ably频道的presence
          AblyRealtime.presence.leave()
          document.getElementById('call').style.display = 'none'
          renderMembers()
      }
      

五、处理用户离开的清理逻辑

  • 在AblyRealtime.presence.subscribe('leave')的回调中,销毁对应该用户的连接并清理音频元素:
    AblyRealtime.presence.subscribe('leave', member => {
        AblyRealtime.presence.get((err, members) => {
            membersList = members
            renderMembers()
            // 清理离开用户的连接
            if (connections[member.clientId]) {
                connections[member.clientId].destroy()
                delete connections[member.clientId]
            }
        })
    })
    

内容的提问来源于stack exchange,提问作者Anita Aksentowicz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 04:54:50