WebRTC、会话状态与延迟预算
正在读取学习记录…
实时语音对话需要声音连续传送,也需要应用知道当前处于收音、思考还是播放。WebRTC是一组适合浏览器实时音视频的技术,负责媒体连接与传输;会话状态负责当前一轮对话;延迟预算负责把等待时间拆开。像视频会议里声音偶尔先后颠倒,播放器会短暂攒几个包再按顺序播放,这段等待叫缓冲。
假设音频包每20ms一块,编号0、2、1乱序到达,播放器按编号恢复成0、1、2。准备40ms缓冲后,编号1在30ms到达,预定60ms播放,仍能赶上。缓冲太短会让迟到包错过播放时刻,太长又会让回答更晚。
从输入到可核对结果
输入是人工给定的音频包到达时间与三个阶段预算,程序实际计算重排与播放截止时刻,并通过给定控制事件推进会话状态。seq代表音频连续序号,stamp代表到达毫秒数;两者含义不同。
我们以buffer_ms作为第一包播放时刻,后续每包加20ms。arrival不晚于scheduled时可播放。首音预算把端点100ms、网络40ms、生成180ms、缓冲40ms相加,得到360ms。这个相加成立于串行的示例路径;真实系统有并发时应测每个时间戳,不能把阶段相加当真实耗时。WebRTC接入文档说明浏览器媒体与控制事件连接。
| 包序号 | 到达时刻 | 40ms缓冲时播放时刻 | 能否赶上 |
|---|---|---|---|
| 0 | 0ms | 40ms | 能 |
| 1 | 30ms | 60ms | 能 |
| 2 | 10ms | 80ms | 能 |
保存并运行这两个文件
在 PowerShell 创建练习目录:
New-Item -ItemType Directory -Force l9-realtime
cd l9-realtimemacOS 终端:
mkdir -p l9-realtime
cd l9-realtime把下面第一段完整代码保存为 session_lab.py,第二段保存为 test_session_lab.py,两份文件放在这个目录。
BUFFER_MS = 40
ARRIVALS = [(0, 0), (2, 10), (1, 30)]
STAGES = {"端点等待": 100, "网络": 40, "生成": 180}
def playback(arrivals, buffer_ms):
if buffer_ms < 0:
raise ValueError("缓冲时间不能为负数")
if len({seq for seq, _ in arrivals}) != len(arrivals):
raise ValueError("音频包序号重复")
if any(seq < 0 or stamp < 0 for seq, stamp in arrivals):
raise ValueError("序号和到达时间不能为负数")
# 固定每包20ms,按序号安排播放时刻;超过时刻的包计为迟到。
ordered = sorted(arrivals)
if [seq for seq, _ in ordered] != list(range(len(ordered))):
raise ValueError("缺少音频包")
result = []
for seq, arrival_ms in ordered:
scheduled = buffer_ms + seq * 20
result.append((seq, scheduled, "播放" if arrival_ms <= scheduled else "迟到"))
return result
class Session:
NAMES = {"new": "连接", "listening": "收音", "generating": "生成",
"playing": "播放", "closed": "关闭"}
def __init__(self):
self.state = "new"
self.trace = ["连接"]
self.current = None
self.counter = 0
self.queue = []
def event(self, action):
# 控制事件合法迁移;取消与断线都会清理尚未播放的媒体包。
if action == "disconnect" and self.state != "closed":
self.queue.clear()
self.current = None
self.state = "closed"
elif action == "connect" and self.state == "new":
self.state = "listening"
elif action == "commit" and self.state == "listening":
self.counter += 1
self.current = "r" + str(self.counter)
self.state = "generating"
elif action == "cancel" and self.state in ("generating", "playing"):
self.queue.clear()
self.current = None
self.state = "listening"
else:
raise ValueError("当前状态不接受事件:" + action)
self.trace.append(self.NAMES[self.state])
return self.current
def audio(self, request_id, packet):
# 旧响应尾包不进入新会话;合法包让生成状态进入播放状态。
if request_id != self.current or self.state not in ("generating", "playing"):
return False
self.queue.append(packet)
if self.state == "generating":
self.state = "playing"
self.trace.append("播放")
return True
if __name__ == "__main__":
packets = playback(ARRIVALS, BUFFER_MS)
session = Session()
session.event("connect")
request = session.event("commit")
for packet in ARRIVALS:
session.audio(request, packet)
print("会话:" + " → ".join(session.trace))
print("包序号:" + ",".join(str(seq) for seq, _, _ in packets))
print("迟到包:" + str(sum(state == "迟到" for _, _, state in packets)))
print("首音预算:" + str(sum(STAGES.values()) + BUFFER_MS) + "ms")
session.event("cancel")
print("取消后:" + session.NAMES[session.state] + ",待播包=" + str(len(session.queue)))
print("旧响应尾包:" + str(session.audio(request, (3, 90))))
session.event("disconnect")
print("断线后:" + session.NAMES[session.state])import unittest
import session_lab as lab
class MechanismTests(unittest.TestCase):
# 正常、边界和失败输入都由这里提供,直接运行即可。
def test_normal_reorder(self):
self.assertEqual([x[0] for x in lab.playback(lab.ARRIVALS, 40)], [0, 1, 2])
session = lab.Session()
session.event("connect")
r1 = session.event("commit")
self.assertTrue(session.audio(r1, (0, 0)))
session.event("cancel")
r2 = session.event("commit")
self.assertFalse(session.audio(r1, (1, 20)))
self.assertTrue(session.audio(r2, (0, 0)))
self.assertEqual(session.trace, ["连接", "收音", "生成", "播放", "收音", "生成", "播放"])
def test_boundary_deadline(self):
self.assertEqual(lab.playback([(0, 40)], 40)[0][2], "播放")
self.assertEqual(lab.playback([], 0), [])
session = lab.Session()
session.event("connect")
r1 = session.event("commit")
session.audio(r1, (0, 0))
session.event("disconnect")
self.assertEqual(session.queue, [])
self.assertFalse(session.audio(r1, (1, 20)))
def test_failure_late_and_missing(self):
self.assertEqual(lab.playback([(0, 41)], 40)[0][2], "迟到")
with self.assertRaises(ValueError):
lab.playback([(1, 0)], 40)
session = lab.Session()
with self.assertRaises(ValueError):
session.event("commit")
session.event("disconnect")
with self.assertRaises(ValueError):
session.event("connect")
if __name__ == "__main__":
unittest.main()打开 PowerShell,逐行输入下列命令。cd 是进入文件夹,dir 是列出文件;确认列表里有刚保存的两个 .py 文件,再运行 Python。
# 在刚才创建的练习目录检查文件,再运行程序。
ls
python -X utf8 session_lab.py
python -X utf8 -m unittest -v test_session_lab.py程序输出:
会话:连接 → 收音 → 生成 → 播放
包序号:0,1,2
迟到包:0
首音预算:360ms
取消后:收音,待播包=0
旧响应尾包:False
断线后:关闭测试末尾应显示 Ran 3 tests 与 OK。若找不到文件,检查当前目录与扩展名;若导入失败,确认测试和主程序在同一文件夹。测试描述的三组材料就在上方代码,不需要另外准备输入。
改一处,再看结果
输出恢复了包序号顺序,迟到包为0,首音预算为360ms。会话状态那一行来自 Session 逐次处理 connect、commit和audio后的轨迹。connect进入收音,commit表示提交这一轮收音并开始生成,第一块合法响应音频进入播放。cancel清空队列、清除当前响应ID并回到收音;disconnect清空队列并关闭会话。旧响应尾包的ID与当前响应不同,会返回False。
| 当前状态 | 事件 | 下一状态 | 实际副作用 |
|---|---|---|---|
| 连接 | connect | 收音 | 等待输入 |
| 收音 | commit | 生成 | 创建响应ID |
| 生成 | audio | 播放 | 音频包入队 |
| 生成或播放 | cancel | 收音 | 清队列并清响应ID |
| 未关闭状态 | disconnect | 关闭 | 清队列并拒收尾包 |
非法事件会抛出ValueError,例如还没有连接就提交输入。新响应有新ID,取消后旧响应包不会混进新一轮。
WebRTC媒体流通常通过RTP及浏览器媒体栈处理包序号、抖动缓冲和播放;data channel传递应用控制消息,如响应开始与取消。这里的playback重排是可运行的缓冲算法练习,Session是应用控制事件练习;两者分别处理媒体与对话,应用不能把控制消息队列当作浏览器已经处理好的媒体缓冲。
把 BUFFER_MS = 40 改成 BUFFER_MS = 0,保存并重跑。预算降为320ms,但包1在30ms到达,错过20ms播放时刻,因此迟到包变成1。恢复成40后重跑。延迟降低与连续性下降发生在同一组输入上,选择缓冲量需要看用户能接受的两种代价。
音频包丢失时要确定丢弃、补偿或重传策略;等待所有缺包会把实时变成无限等待。浏览器真实接入还需要麦克风权限、安全连接和会话认证,长期API密钥应放在服务器,前端使用短期会话凭据。接入远端媒体和模型服务前设置会话时长与费用预算。
改动后仍在刚才的 PowerShell 目录运行:
python -X utf8 session_lab.py确认上述变化后,把参数恢复成原值,保存并再次执行这条命令。
把机制接回 Agent
接入WebRTC时,先由服务器创建有期限的会话凭据,浏览器请求麦克风授权,再通过RTCPeerConnection协商媒体连接,并创建数据通道接收控制事件。应用为会话分配ID,维护收音、响应、播放状态;断线重连后新会话需明确是否恢复历史,旧包不得混入新轮次。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“WebRTC、会话状态与延迟预算”,哪种做法更稳妥?
对应知识点:L9-realtime
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L9-realtime
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L9-realtime
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L9-realtime
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。