Gemini 3.8 Live 聊到一半没声了 / 是网络抖动还是会话被截断?

文章导读
聊到一半突然安静,先不要直接归因于网络抖动。实时语音链路里的“没声”至少对应两种状态:一种是采集端还在工作,但上行音频字节已经不发了;另一种是上行一直正常,只是这一轮会话被某一端结束,页面不再播放。可行的排查顺序是:先看网络面板里音频相关请求的字节增长在什么时候停,再看控制台里音频轨道的 readyState,然后用换网络、换耳机的方式做一次对照,最后把控制台报错时间戳和听到静音的时间对齐。
📋 目录
  1. 一 在网络面板里筛出音频相关请求,看掉线前字节是否停止增长
  2. 二 在控制台执行麦克风设备与轨道状态检查
  3. 三 换一条网络和一副耳机,重复同一段话术
  4. 四 对齐控制台报错时刻与断声时刻
  5. 五 整理成一张现象对照清单
A A

聊到一半突然安静,先不要直接归因于网络抖动。实时语音链路里的“没声”至少对应两种状态:一种是采集端还在工作,但上行音频字节已经不发了;另一种是上行一直正常,只是这一轮会话被某一端结束,页面不再播放。可行的排查顺序是:先看网络面板里音频相关请求的字节增长在什么时候停,再看控制台里音频轨道的 readyState,然后用换网络、换耳机的方式做一次对照,最后把控制台报错时间戳和听到静音的时间对齐。

静音不等于断线,也不等于会话结束。先记录网络面板中音频上行字节停止增长的时刻,再在控制台确认音频轨道是否仍为 live,接着用换网络、换耳机做一次单变量对照,最后把报错时间与静音时间对齐。若上行字节在静音前就停了,优先查采集与链路;若字节正常但轨道 ended,或先出现关闭、超时类报错,更可能是设备被占用或会话已被结束。这些判断要结合自己页面的具体实现确认,不宜直接当作服务端行为的定论。

在网络面板里筛出音频相关请求,看掉线前字节是否停止增长

这一步只回答一个问题:上行音频有没有在静音之前就停了。操作路径通常是打开开发者工具(F12 或右键“检查”),切到 Network 面板,先点一次清空记录,再开始说话,让整段对话都留在面板里。

筛选的关键字可以先用 ws 或 wss 找长连接,再用 audio、live、stream、media 之类的词找媒体相关请求;Chrome 里勾上 WS 分类可以展开帧列表看单帧大小和时间。如果音频是走 WebRTC,那么 Network 面板里不一定有逐帧记录,这时改看 chrome://webrtc-internals 中对应的 outbound 音频统计曲线,判断逻辑是一样的。

需要记录两个时间点:

  • T1:音频相关请求最后一次出现字节增长或新帧的时间;
  • T2:你自己听到静音、或页面状态提示变化的挂钟时间。

正常与异常的差别通常体现在这两个时间点的先后。如果字节一直增长到 T2 之后才停,说明采集和上行都还在,问题更可能出在播放侧、音频输出设备切换或这一轮回复本身没有产生音频;如果字节在 T2 之前明显提前停止,说明上行已经断了,先按采集和链路方向查,不要继续怀疑会话超时。

在控制台执行麦克风设备与轨道状态检查

这一步确认采集端是否还活着,同时排除麦克风被其他程序占用。注意 enumerateDevices() 只有在页面已经获得过麦克风授权后才会返回可读的 label,否则 label 是空字符串。

// 在页面控制台执行,需先授权麦克风
const devices = await navigator.mediaDevices.enumerateDevices();
console.table(
  devices
    .filter(d => d.kind === 'audioinput')
    .map(d => ({ id: d.deviceId.slice(0, 8), label: d.label }))
);

// 若页面自己拿到了流,先保留引用,例如 window.__stream = stream
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
window.__stream = stream;
const track = stream.getAudioTracks()[0];

console.log('readyState:', track.readyState); // live / ended
console.log('enabled:', track.enabled);
console.log('muted:', track.muted);
console.log('settings:', track.getSettings()); // deviceId / sampleRate / channelCount

track.addEventListener('ended', () => console.warn('音频轨道 ended', performance.now()));

对照的判断比较直接:readyState 为 live 说明轨道还在,采集端没有主动结束;变成 ended 说明设备被拔出、被系统或别的程序抢占,或者标签页被回收。muted 为 true 且长时间不恢复,通常指向系统输入设备被切换或被独占。getSettings().deviceId 在过程中发生变化,说明输入设备被换掉了。

如果页面没有把流保存到可访问的变量上,可以在开始对话前先执行一遍带 window.__stream 赋值的片段,之后再回来查,不必重启会话。

换一条网络和一副耳机,重复同一段话术

这一步的目的是把链路问题和会话本身的问题分开,所以要控制变量:每次只换一个东西。第一次用原来的网络、原来的耳机;第二次只换网络(例如从 Wi-Fi 切到有线,或换一个接入点);第三次只换耳机或输入设备,网络保持第二次的状态。浏览器、账号、页面版本尽量保持一致。

Gemini 3.8 Live 聊到一半没声了 / 是网络抖动还是会话被截断?

复现用的话术建议固定成一段 30 到 60 秒、包含停顿的内容,比如持续数数加几句固定短句,说到某个固定位置时故意停两秒,这样每次复现的位置可以对齐。

结果是否一致的判断标准可以这样用:如果两次或三次都在同一句话、相似时长后安静,控制台报错也一致,更偏向会话或服务侧的行为;如果只有某一条网络下复现,换回原来的网络就正常,方向在链路上;如果换了耳机才复现,先把输入设备与系统音频占用查清楚,别急着改代码。

对齐控制台报错时刻与断声时刻

这一步区分“正常超时结束”和“被动断开”。先在控制台保留日志,并把 Preserve log 勾上,避免页面跳转把记录清掉。

需要留意的报错类型大致有这几类:长连接的 close 事件及其 code(1000 属正常关闭,1001 表示一端离开,1006 是异常断开且没有关闭帧);WebRTC 相关的连接状态变化,如 iceconnectionstatechange 转为 disconnected 或 failed;音频轨道触发的 ended;NotAllowedError 一类的权限错误;HTTP 层面的 4xx、5xx 或请求超时。

记录时间差可以这样做:在感觉静音的那一刻,手动在控制台执行一条带时间戳的标记,或用 Performance 面板打一个时间标记,然后和报错自带的时间戳相减,得到大致差值。

三种组合分别指向不同方向:

  • 报错明显早于静音:说明连接或轨道先被断开,静音是结果,属于被动断开;
  • 报错与静音几乎同时:优先看这次报错的具体 code 和文本,通常能直接指向原因;
  • 静音在前、之后一段时间才有报错或根本没有报错:更像这一轮会话按预期结束或回复侧没有产出音频,而不是链路中途断掉。

整理成一张现象对照清单

把上面的信号固定成三列,下次遇到先按现象定位,不用再从头试一遍。下表的结论是方向性判断,仍要结合自己页面的具体实现和日志确认。

现象先看哪里通常结论
网络面板字节在静音前就停止增长,轨道 readyState 仍是 liveNetwork 面板 + 音频轨道状态采集还在但上行没发出去,查发送侧逻辑与链路
字节持续增长,但听不到声音播放侧、AudioContext 状态、系统输出设备更可能是回放链路或输出设备被切换
轨道 readyState 变成 ended,或输入设备列表里原设备消失enumerateDevices 结果 + 系统音频占用麦克风被其他程序抢占或设备被拔出
控制台先出现连接 close 或 4xx、5xx,随后才静音Console 与 Network 的请求状态会话被某一端结束,不是单纯的抖动
同一段话术只在某一条网络或某一副耳机下复现两次对照的结果是否一致更像本地链路或本地设备问题

排查时建议一次只改一个变量,并把时间点、报错文本、设备信息记在同一份记录里。只凭“感觉卡了一下”很难区分这两类原因,留下字节曲线和轨道状态,判断会稳定得多。