AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

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缓冲时播放时刻能否赶上
00ms40ms能
130ms60ms能
210ms80ms能
媒体包按序缓冲,控制事件推进生成播放取消关闭状态
上半部比较媒体播放缓冲,下半部展示应用控制事件;取消和断线都清队列并隔离旧响应。

保存并运行这两个文件

在 PowerShell 创建练习目录:

PowerShell
New-Item -ItemType Directory -Force l9-realtime
cd l9-realtime

macOS 终端:

Bash
mkdir -p l9-realtime
cd l9-realtime

把下面第一段完整代码保存为 session_lab.py,第二段保存为 test_session_lab.py,两份文件放在这个目录。

Python
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])
Python
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。

Bash
# 在刚才创建的练习目录检查文件,再运行程序。
ls
python -X utf8 session_lab.py
python -X utf8 -m unittest -v test_session_lab.py

程序输出:

TXT
会话:连接 → 收音 → 生成 → 播放
包序号: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 目录运行:

Bash
python -X utf8 session_lab.py

确认上述变化后,把参数恢复成原值,保存并再次执行这条命令。

把机制接回 Agent

接入WebRTC时,先由服务器创建有期限的会话凭据,浏览器请求麦克风授权,再通过RTCPeerConnection协商媒体连接,并创建数据通道接收控制事件。应用为会话分配ID,维护收音、响应、播放状态;断线重连后新会话需明确是否恢复历史,旧包不得混入新轮次。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“WebRTC、会话状态与延迟预算”,哪种做法更稳妥?

对应知识点:L9-realtime

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L9-realtime

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L9-realtime

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L9-realtime

图片查看器
100%

文章配图 · 按 Esc 关闭