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

增强Twilio Voice JavaScript SDK通话持久性:浏览器关闭后维持会话

基于Node.js的Twilio通话会话管理方案实现

完全可行,通过服务端跟踪通话状态结合Twilio的Calls API,就能实现客户端意外断开时保持通话、重连恢复的会话管理能力。具体实现步骤如下:

1. 服务端维护通话会话映射

需要在服务端建立通话SID与用户标识、客户端在线状态的关联存储,生产环境推荐用Redis或数据库,开发阶段可以先用内存Map快速验证:

// 内存存储示例(生产环境替换为Redis)
const activeCalls = new Map();
const userHeartbeats = new Map();

// 在通话创建的Webhook中绑定用户与通话
app.post('/webhooks/voice', (req, res) => {
  const callSid = req.body.CallSid;
  const userId = req.body.From; // 替换为你的自定义用户标识字段
  const callStatus = req.body.CallStatus;

  if (callStatus === 'ringing' || callStatus === 'in-progress') {
    activeCalls.set(callSid, { userId, status: callStatus, clientConnected: true });
  } else if (callStatus === 'completed') {
    activeCalls.delete(callSid);
  }

  // 处理Twiml响应...
  const twiml = new twilio.twiml.VoiceResponse();
  // 你的Twiml逻辑
  res.type('text/xml');
  res.send(twiml.toString());
});

2. 客户端心跳检测与断开标记

通过心跳机制实时感知客户端在线状态,当客户端超时未发送心跳时,标记对应通话的客户端连接状态为断开:

// 客户端心跳上报接口
app.post('/heartbeat', (req, res) => {
  const userId = req.body.userId;
  userHeartbeats.set(userId, Date.now());
  res.sendStatus(200);
});

// 定时检查心跳(每30秒执行一次)
setInterval(() => {
  const now = Date.now();
  const timeoutThreshold = 60000; // 60秒无心跳判定为断开

  userHeartbeats.forEach((timestamp, userId) => {
    if (now - timestamp > timeoutThreshold) {
      // 找到该用户对应的活跃通话,标记客户端断开
      activeCalls.forEach((callData, callSid) => {
        if (callData.userId === userId && callData.status === 'in-progress') {
          callData.clientConnected = false;
          // 自动将通话置为保持状态
          holdCall(callSid);
        }
      });
      userHeartbeats.delete(userId);
    }
  });
}, 30000);

3. 利用Twilio API管理通话状态

通过Twilio的Calls API实现通话保持与恢复,支持用户重连时继续通话:

const twilio = require('twilio')(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);

// 保持通话
async function holdCall(callSid) {
  try {
    await twilio.calls(callSid).update({ hold: true });
  } catch (err) {
    console.error('保持通话失败:', err);
  }
}

// 恢复通话
async function unholdCall(callSid) {
  try {
    await twilio.calls(callSid).update({ hold: false });
  } catch (err) {
    console.error('恢复通话失败:', err);
  }
}

// 用户重连时恢复通话的接口
app.post('/reconnect-call', async (req, res) => {
  const userId = req.body.userId;
  let targetCallSid = null;

  // 查找用户对应的未完成通话
  activeCalls.forEach((callData, callSid) => {
    if (callData.userId === userId && callData.status === 'in-progress' && !callData.clientConnected) {
      targetCallSid = callSid;
    }
  });

  if (targetCallSid) {
    await unholdCall(targetCallSid);
    // 返回通话SID给客户端,用于重新建立媒体连接
    res.json({ callSid: targetCallSid });
  } else {
    res.status(404).json({ message: '当前无可用的活跃通话' });
  }
});

4. 客户端重连逻辑

客户端重新打开浏览器/PWA后,调用/reconnect-call接口获取通话SID,然后初始化Twilio Voice SDK并恢复媒体流:

// 客户端重连示例(前端代码)
async function reconnectCall(userId) {
  const response = await fetch('/reconnect-call', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userId })
  });
  
  const data = await response.json();
  if (data.callSid) {
    // 初始化Twilio Voice SDK,使用返回的callSid恢复通话
    const device = new Twilio.Device(token);
    device.on('ready', () => {
      device.connect({ callSid: data.callSid });
    });
  }
}

注意事项

  • 生产环境必须使用Redis或数据库替代内存存储,避免服务重启丢失会话数据
  • 心跳间隔和超时阈值需根据业务场景调整,避免因网络波动误判客户端断开
  • 需确认Twilio账号所在区域支持通话保持功能,部分地区可能有额外限制
  • 通话长时间保持可能产生额外费用,需结合业务需求设置自动挂断逻辑

内容的提问来源于stack exchange,提问作者Parneet kaur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 22:46:27