【P2P音视频通信系统】webrtc 之 SDP 详解

1. SDP 概述

1.1 什么是 SDP

SDP (Session Description Protocol) 全称是"会话描述协议"。简单来说,SDP 就像是两个人打电话之前互相交换的"名片",告诉对方自己支持什么功能、用什么方式通信。

生活中的类比

想象一下你要和一个新认识的朋友建立视频通话:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                              生活中的"SDP交换"类比                                           │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  你(A)                                                    朋友(B)
    │                                                          │
    │  "你好,我支持以下功能:"                                 │
    │  - 可以视频通话                                          │
    │  - 可以语音通话                                          │
    │  - 我的网络地址是 xxx                                    │
    │  - 我使用 VP8 视频编码                                   │
    │ ────────────────────────────────────────────────────────>│
    │                                                          │
    │                               "收到!我支持以下功能:"    │
    │                               - 视频通话 OK              │
    │                               - 我也用 VP8               │
    │                               - 我的地址是 yyy           │
    │ <────────────────────────────────────────────────────────│
    │                                                          │
    │  ═════════════════════════════════════════════════════════
    │  双方确认了共同的能力,开始通话
    │  ═════════════════════════════════════════════════════════

SDP 就是这个"功能介绍名片"的标准化格式,让不同厂商、不同平台的设备能够互相理解对方的能力。

为什么需要 SDP?

在 WebRTC 视频通话中,双方需要协商很多事情:

需要协商的内容 通俗解释 SDP 中的体现
音频编解码器 双方用什么格式压缩音频 Opus、PCMA、PCMU 等
视频编解码器 双方用什么格式压缩视频 VP8、VP9、H.264 等
网络地址 双方通过什么地址连接 ICE 候选者
加密方式 如何保证通话安全 DTLS 指纹
媒体方向 是发送、接收还是双向 sendrecv、sendonly 等

1.2 SDP 在 WebRTC 中的作用

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 在 WebRTC 通话中的位置                                │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     呼叫方 A                                          信令服务器                    被呼叫方 B
        │                                                │                               │
        │                                                │                               │
        │  ┌─────────────────────┐                       │                               │
        │  │ 第一步:创建 Offer  │                       │                               │
        │  │                     │                       │                               │
        │  │ Offer 就像是 A 的   │                       │                               │
        │  │ "简历",告诉 B:    │                       │                               │
        │  │ - 我支持哪些编码    │                       │                               │
        │  │ - 我的网络地址      │                       │                               │
        │  │ - 我的安全证书      │                       │                               │
        │  └─────────────────────┘                       │                               │
        │                                                │                               │
        │  发送 Offer ──────────────────────────────────>│                               │
        │                                                │                               │
        │                                                │  转发 Offer ─────────────────>│
        │                                                │                               │
        │                                                │                    ┌──────────┴──────────┐
        │                                                │                    │ 第二步:创建 Answer │
        │                                                │                    │                     │
        │                                                │                    │ Answer 是 B 的回复:│
        │                                                │                    │ - 我选择 VP8 编码   │
        │                                                │                    │ - 我的网络地址      │
        │                                                │                    │ - 确认安全连接      │
        │                                                │                    └─────────────────────┘
        │                                                │                               │
        │                                                │  返回 Answer                  │
        │  接收 Answer <─────────────────────────────────│ <─────────────────────────────│
        │                                                │                               │
        │  ┌─────────────────────┐                       │                               │
        │  │ 第三步:确认连接    │                       │                               │
        │  │                     │                       │                               │
        │  │ A 收到 Answer 后    │                       │                               │
        │  │ 知道了 B 的选择     │                       │                               │
        │  │ 开始建立 P2P 连接   │                       │                               │
        │  └─────────────────────┘                       │                               │
        │                                                │                               │
        ▼                                                ▼                               ▼

简单总结:SDP 是 WebRTC 通话的"握手协议",双方通过交换 SDP 来达成一致,然后才能开始真正的音视频传输。

1.3 SDP 的四大核心作用

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 的四大核心作用                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ 媒体协商 - "我们用什么格式通话?"                                                      │
│                                                                                             │
│     想象两个人要对话,首先得确定用什么语言:                                                │
│     - A 说:我会说中文、英文、日文                                                          │
│     - B 说:我会中文和英文,那我们用中文吧                                                  │
│                                                                                             │
│     SDP 中:A 列出所有支持的编解码器,B 选择一个双方都支持的                               │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  2️⃣ 网络协商 - "我们怎么找到对方?"                                                        │
│                                                                                             │
│     就像寄快递需要知道地址:                                                                │
│     - A 告诉 B:我的地址是 xxx,你可以通过这些路径找到我                                    │
│     - B 告诉 A:我的地址是 yyy,你可以这样连接我                                            │
│                                                                                             │
│     SDP 中:通过 ICE 候选者交换网络地址信息                                                │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  3️⃣ 安全协商 - "我们怎么保证通话安全?"                                                    │
│                                                                                             │
│     就像两个人约定暗号:                                                                    │
│     - A 发送自己的"指纹"(证书哈希值)                                                      │
│     - B 验证指纹后,建立加密连接                                                            │
│                                                                                             │
│     SDP 中:通过 DTLS 指纹确保通信安全                                                     │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  4️⃣ 能力协商 - "我们能做什么?"                                                            │
│                                                                                             │
│     确定双方的能力边界:                                                                    │
│     - 是否支持视频?                                                                        │
│     - 是否只接收不发送?                                                                    │
│     - 带宽限制是多少?                                                                      │
│                                                                                             │
│     SDP 中:通过媒体方向属性和带宽参数描述                                                  │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2. SDP 结构详解

2.1 SDP 整体结构

SDP 文件看起来像一堆乱码,但其实结构很清晰。可以把 SDP 想象成一份"会议议程":

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 结构类比                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

一份会议议程的结构:

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
│  │                           会议基本信息(会话级别)                                   │   │
│  │                                                                                     │   │
│  │  会议名称:产品讨论会                                                               │   │
│  │  发起人:张三                                                                       │   │
│  │  时间:2024年1月1日                                                                 │   │
│  │                                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────────────────────┘   │
│                                        │                                                    │
│                    ┌───────────────────┴───────────────────┐                               │
│                    ▼                                       ▼                               │
│  ┌─────────────────────────────────────┐   ┌─────────────────────────────────────┐        │
│  │     议题一:音频讨论(媒体级别)     │   │     议题二:视频演示(媒体级别)     │        │
│  │                                     │   │                                     │        │
│  │  讨论内容:音频编码选择             │   │  演示内容:视频编码选择             │        │
│  │  参与方式:双向交流                 │   │  参与方式:双向交流                 │        │
│  │                                     │   │                                     │        │
│  └─────────────────────────────────────┘   └─────────────────────────────────────┘        │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

对应的 SDP 结构:

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
│  │                              Session Level (会话级别)                               │   │
│  │                                                                                     │   │
│  │  v= (协议版本)           → 会议格式版本                                             │   │
│  │  o= (会话发起者)         → 发起人信息                                               │   │
│  │  s= (会话名称)           → 会议名称                                                 │   │
│  │  t= (会话时间)           → 会议时间                                                 │   │
│  │                                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────────────────────┘   │
│                                        │                                                    │
│                    ┌───────────────────┴───────────────────┐                               │
│                    ▼                                       ▼                               │
│  ┌─────────────────────────────────────┐   ┌─────────────────────────────────────┐        │
│  │      Media Level (媒体级别) #1      │   │      Media Level (媒体级别) #2      │        │
│  │                                     │   │                                     │        │
│  │  m=audio ... (音频媒体描述)         │   │  m=video ... (视频媒体描述)         │        │
│  │  a=rtpmap:... (编解码器)            │   │  a=rtpmap:... (编解码器)            │        │
│  │  a=sendrecv (双向通信)              │   │  a=sendrecv (双向通信)              │        │
│  │                                     │   │                                     │        │
│  └─────────────────────────────────────┘   └─────────────────────────────────────┘        │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

关键理解:

  • 会话级别:描述整个通话的基本信息,对所有媒体流都适用
  • 媒体级别:描述具体的音频或视频流,每个媒体流有自己的配置

2.2 SDP 行类型详解

SDP 的每一行都有特定含义,格式为 类型=值。下面用通俗的方式解释每一行:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 行类型通俗解释                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

会话级别行 (Session Level) - 描述整个通话的基本信息:

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  类型  │ 名称            │ 通俗解释                        │ 是否必需                       │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  v=   │ 版本            │ "这是第几版的格式?"            │ 必需(目前固定为 0)           │
│  o=   │ 发起者          │ "谁发起的这个通话?"            │ 必需                           │
│  s=   │ 会话名称        │ "这次通话叫什么名字?"          │ 必需(通常用 "-" 表示无名称)  │
│  t=   │ 时间            │ "通话什么时候开始和结束?"      │ 必需(0 0 表示永久)           │
│  c=   │ 连接信息        │ "用什么地址连接?"              │ 可选                           │
│  a=   │ 属性            │ "有什么特殊要求?"              │ 可选                           │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

媒体级别行 (Media Level) - 描述具体的音频或视频流:

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  类型  │ 名称            │ 通俗解释                        │ 是否必需                       │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  m=   │ 媒体描述        │ "这是什么媒体?用什么编码?"    │ 必需                           │
│  c=   │ 连接信息        │ "这个媒体用什么地址?"          │ 可选                           │
│  a=   │ 属性            │ "这个媒体有什么特性?"          │ 可选                           │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2.3 完整 SDP 示例解析

下面是一个真实的 SDP 示例,我们逐行解读:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    完整 SDP 示例及逐行解读                                   │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════════════════════════════════════
会话级别部分 - 描述整个通话的基本信息
═══════════════════════════════════════════════════════════════════════════════════════════════

v=0
└── 【协议版本】SDP 格式版本号,目前固定为 0,表示使用标准 SDP 格式
    通俗理解:就像文件的版本号,告诉接收方"这是第几版的格式"

o=- 1234567890 1234567890 IN IP4 192.168.1.100
└── 【会话发起者】
    ├── - : 用户名(WebRTC 中通常为空,用 - 表示)
    ├── 1234567890 : 会话 ID(唯一标识这次通话,相当于"会议编号")
    ├── 1234567890 : 会话版本(每次 SDP 变更递增,相当于"修订版本号")
    ├── IN : 网络类型(IN = Internet)
    ├── IP4 : 地址类型(IPv4)
    └── 192.168.1.100 : 发起者的 IP 地址
    
    通俗理解:就像会议邀请函上的"发起人信息"

s=-
└── 【会话名称】WebRTC 中通常没有具体名称,用 - 表示
    通俗理解:这次通话的名字,WebRTC 不需要命名,所以用 - 占位

t=0 0
└── 【会话时间】0 0 表示永久会话,没有固定的开始和结束时间
    通俗理解:通话什么时候开始和结束?0 0 表示"随时可以开始,没有截止时间"

a=group:BUNDLE 0 1
└── 【BUNDLE 分组】将音频(mid:0)和视频(mid:1)复用到同一个连接
    好处:只需要一个端口,简化 NAT 穿透
    
    通俗理解:把音频和视频"打包"在一起传输,就像把多个文件打包成一个 ZIP

a=msid-semantic: WMS localStream
└── 【MSID 语义】定义媒体流的标识方式
    WMS = WebRTC Media Stream
    localStream = 媒体流的 ID
    
    通俗理解:告诉对方"我的媒体流叫什么名字"

═══════════════════════════════════════════════════════════════════════════════════════════════
媒体级别部分 - 音频媒体描述
═══════════════════════════════════════════════════════════════════════════════════════════════

m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126
└── 【音频媒体描述】
    ├── audio : 媒体类型是音频
    ├── 9 : 端口号(ICE 场景中为占位符,实际端口由 ICE 确定)
    │         通俗理解:9 是一个特殊的"丢弃"端口,表示"实际端口待定"
    ├── UDP/TLS/RTP/SAVPF : 传输协议(安全的 RTP over UDP)
    │         通俗理解:用 UDP 传输,加上 TLS 加密,使用 RTP 协议
    └── 111 63 103... : 支持的音频格式列表(Payload Type)
              通俗理解:这些数字是"菜单编号",对应不同的音频编码

c=IN IP4 0.0.0.0
└── 【连接信息】IP 地址为 0.0.0.0,表示由 ICE 协商确定实际地址
    通俗理解:地址还没确定,等 ICE 协商后再填

a=rtcp:9 IN IP4 0.0.0.0
└── 【RTCP 端口】RTCP 控制通道的端口
    通俗理解:RTCP 是用来传输控制信息的通道,比如"我收到了多少数据包"

a=ice-ufrag:abcd
└── 【ICE 用户名】用于 ICE 连接认证,相当于"登录名"
    通俗理解:ICE 连接需要验证身份,这是用户名

a=ice-pwd:abcdefghijklmnopqrstuvwx
└── 【ICE 密码】用于 ICE 连接认证,相当于"登录密码"
    通俗理解:配合用户名使用,验证对方身份

a=ice-options:trickle
└── 【ICE 选项】支持 Trickle ICE(逐步发送 ICE 候选者)
    通俗理解:不用等所有地址都收集完再发送,收集到一个就发一个

a=fingerprint:sha-256 12:34:56:78:90:AB:CD:EF...
└── 【DTLS 指纹】DTLS 证书的哈希值,用于验证连接安全
    通俗理解:这是"数字身份证"的指纹,确保连接不被冒充

a=setup:actpass
└── 【DTLS 角色】actpass 表示可以接受任何角色
    Offer 中用 actpass,Answer 中会确定为 active 或 passive
    通俗理解:"我可以主动连接你,也可以等你连接我"

a=mid:0
└── 【媒体标识】这个音频流的 ID 是 0,用于 BUNDLE 关联
    通俗理解:给音频流一个编号,方便在 BUNDLE 中识别

a=sendrecv
└── 【媒体方向】sendrecv 表示既能发送也能接收音频
    通俗理解:双向通话,既能说也能听

a=rtcp-mux
└── 【RTCP 复用】RTP 和 RTCP 使用同一个端口
    通俗理解:数据和控制信息走同一个通道,减少端口占用

a=rtpmap:111 opus/48000/2
└── 【编解码器映射】Payload Type 111 对应 Opus 编解码器
    ├── 48000 : 采样率 48kHz(每秒采样 48000 次)
    └── 2 : 双声道(立体声)
    
    通俗理解:编号 111 对应 Opus 音频编码,音质很好

a=fmtp:111 minptime=10;useinbandfec=1
└── 【编解码器参数】Opus 的具体配置
    ├── minptime=10 : 最小打包时间 10ms
    │                 通俗理解:每 10ms 打包一次音频数据
    └── useinbandfec=1 : 启用带内前向纠错
                         通俗理解:丢包时可以恢复部分数据,不需要重传

a=ssrc:12345678 cname:user1
a=ssrc:12345678 msid:localStream audioTrack
└── 【SSRC 标识】同步源标识符,用于标识这个音频流
    12345678 是随机生成的唯一 ID
    通俗理解:这是音频流的"身份证号"

═══════════════════════════════════════════════════════════════════════════════════════════════
媒体级别部分 - 视频媒体描述
═══════════════════════════════════════════════════════════════════════════════════════════════

m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 125
└── 【视频媒体描述】
    ├── video : 媒体类型是视频
    └── 96 97 98... : 支持的视频格式列表
              通俗理解:这些是支持的视频编码"菜单编号"

a=mid:1
└── 【媒体标识】这个视频流的 ID 是 1
    通俗理解:给视频流一个编号,方便在 BUNDLE 中识别

a=rtpmap:96 VP8/90000
└── 【编解码器映射】Payload Type 96 对应 VP8 编解码器
    90000 是视频的标准时钟频率
    通俗理解:编号 96 对应 VP8 视频编码,Google 开发的免费编码

a=rtpmap:100 H264/90000
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
└── 【H.264 编解码器】
    ├── profile-level-id=42e01f : 使用 Constrained Baseline Profile
    │   通俗理解:这是硬件加速支持最好的 H.264 配置
    └── packetization-mode=1 : 非交错模式
        通俗理解:最常用的打包方式,兼容性最好

a=ssrc:87654321 cname:user1
a=ssrc:87654321 msid:localStream videoTrack
└── 【SSRC 标识】视频流的唯一标识符
    通俗理解:这是视频流的"身份证号"

3. 关键 SDP 字段详解

3.1 会话级别字段详解

v= (Version) - 协议版本
复制代码
v=0

通俗解释: 这就像文件的版本号。目前 SDP 协议只有一个版本,所以永远写 v=0

为什么需要: 如果将来 SDP 格式升级,接收方可以根据版本号知道如何解析。


o= (Origin) - 会话发起者
复制代码
o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>

示例:o=- 1234567890 1234567890 IN IP4 192.168.1.100

通俗解释: 这就像会议邀请函上的"发起人信息"。

字段 示例值 通俗解释
username - 发起者用户名,WebRTC 中通常为空
sess-id 1234567890 会话唯一标识,相当于"会议编号"
sess-version 1234567890 会话版本,每次 SDP 变更递增
nettype IN 网络类型,IN = Internet
addrtype IP4 地址类型,IP4 或 IP6
unicast-address 192.168.1.100 发起者的 IP 地址

重要提示: sess-version 每次重新协商时必须递增,这样对方才知道这是新的 SDP。


s= (Session Name) - 会话名称
复制代码
s=-

通俗解释: 会话的名称。WebRTC 中通常没有具体名称,用 - 表示。


t= (Timing) - 会话时间
复制代码
t=<start-time> <stop-time>

示例:t=0 0

通俗解释: 会话的开始和结束时间。

  • 0 0 表示永久会话,没有固定的开始和结束时间
  • WebRTC 视频通话通常是 t=0 0

a=group:BUNDLE - BUNDLE 分组
复制代码
a=group:BUNDLE 0 1

通俗解释: 把音频和视频"打包"在一起传输。

为什么需要 BUNDLE?

复制代码
没有 BUNDLE 时:
┌─────────────────┐     ┌─────────────────┐
│     音频端口     │     │     视频端口     │
│    50000        │     │    50002        │
└────────┬────────┘     └────────┬────────┘
         │                       │
         │  需要两个端口,NAT 穿透更复杂
         │                       │
         ▼                       ▼

使用 BUNDLE 后:
┌─────────────────────────────────────────┐
│           音频和视频共用一个端口          │
│               50000                      │
└────────────────────┬────────────────────┘
                     │
                     │  只需要一个端口,NAT 穿透更简单
                     │
                     ▼

通俗理解: BUNDLE 就像把多个快递打包成一个包裹,只需要一个地址就能送达。


3.2 媒体级别字段详解

m= (Media) - 媒体描述
复制代码
m=<media> <port> <proto> <fmt> ...

示例:m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8
      m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101

通俗解释: 这一行告诉对方"我要发送什么类型的媒体,用什么协议,支持哪些编码格式"。

字段 示例值 通俗解释
media audio/video 媒体类型:音频或视频
port 9 端口号(ICE 场景中为占位符)
proto UDP/TLS/RTP/SAVPF 传输协议(安全 RTP)
fmt 111 63 103... 支持的格式列表(Payload Type)

关于端口号 9:

  • 在 WebRTC 中,实际端口由 ICE 协商确定
  • SDP 中的端口号只是一个占位符
  • 9 是一个特殊的"丢弃"端口,表示"待定"

a=ice-ufrag / a=ice-pwd - ICE 认证
复制代码
a=ice-ufrag:abcd
a=ice-pwd:abcdefghijklmnopqrstuvwx

通俗解释: ICE 连接的"用户名和密码"。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 认证类比                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  ICE 连接检查就像"敲门":

  客户端 A                          客户端 B
      │                                │
      │  "你好,我是 abcd              │
      │   密码是 abcdefgh...           │
      │   我可以进来吗?"              │
      │ ───────────────────────────────>│
      │                                │
      │              "密码正确,请进!" │
      │ <───────────────────────────────│
      │                                │

  ice-ufrag = 用户名
  ice-pwd = 密码

a=fingerprint - DTLS 指纹
复制代码
a=fingerprint:sha-256 12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF...

通俗解释: 这是 DTLS 证书的"指纹",用于验证连接的安全性。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    DTLS 指纹类比                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  就像验证身份证真伪:

  ┌─────────────────┐                    ┌─────────────────┐
  │   客户端 A       │                    │   客户端 B       │
  │                 │                    │                 │
  │  我的证书指纹:  │                    │                 │
  │  12:34:56:78... │ ──────────────────>│  收到指纹       │
  │                 │                    │                 │
  │                 │                    │  验证证书       │
  │                 │                    │  计算哈希值     │
  │                 │                    │  对比指纹       │
  │                 │                    │                 │
  │                 │  "指纹匹配,       │                 │
  │                 │   连接安全!"      │                 │
  │                 │ <──────────────────│                 │
  └─────────────────┘                    └─────────────────┘

通俗理解: fingerprint 就像身份证的防伪标识,对方可以通过这个指纹验证你的身份。


a=setup - DTLS 角色
复制代码
a=setup:actpass    (Offer 中)
a=setup:active     (Answer 中)
a=setup:passive    (Answer 中)

通俗解释: 确定 DTLS 连接的"主动方"和"被动方"。

含义 使用场景
actpass 既可以主动连接,也可以等待连接 Offer 中使用
active 主动发起连接 Answer 中使用
passive 等待对方连接 Answer 中使用
复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    DTLS 角色确定                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  Offer (A 发送):                    Answer (B 回复):

  a=setup:actpass                   a=setup:active

  "我可以主动连接,                 "我来主动连接你"
   也可以等你连接"

  结果:B 作为 DTLS 客户端,主动连接 A

通俗理解: 就像两个人握手,总得有一个人先伸出手。setup 决定了谁先伸手。


a=mid - 媒体标识
复制代码
a=mid:0    (音频)
a=mid:1    (视频)

通俗解释: 给每个媒体流一个"编号",用于 BUNDLE 关联。

复制代码
a=group:BUNDLE 0 1    ← 把 mid:0 和 mid:1 打包在一起

m=audio ...
a=mid:0               ← 音频的编号是 0

m=video ...
a=mid:1               ← 视频的编号是 1

通俗理解: 就像给每个包裹贴上标签,方便识别。


a=sendrecv / sendonly / recvonly - 媒体方向
复制代码
a=sendrecv    - 发送和接收
a=sendonly    - 只发送
a=recvonly    - 只接收
a=inactive    - 不活跃

通俗解释: 告诉对方这个媒体流的方向。

方向 通俗解释 使用场景
sendrecv 双向通话 正常的视频通话
sendonly 只发不收 直播场景
recvonly 只收不发 观看直播
inactive 暂停 暂时静音/黑屏

通俗理解: 就像电话的"听筒"和"话筒",sendrecv 表示两个都用,sendonly 表示只用话筒,recvonly 表示只用听筒。


3.3 编解码器相关字段详解

a=rtpmap - RTP 映射
复制代码
a=rtpmap:<payload type> <encoding name>/<clock rate>[/<encoding parameters>]

示例:
a=rtpmap:111 opus/48000/2      # Opus 音频,48kHz,双声道
a=rtpmap:96 VP8/90000          # VP8 视频,90kHz
a=rtpmap:100 H264/90000        # H.264 视频,90kHz

通俗解释: 把 Payload Type(数字编号)映射到具体的编解码器。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Payload Type 映射类比                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  就像菜单上的编号:

  编号    菜品名称
  ────    ────────
  111  →  Opus 音频(48kHz 立体声)
  96   →  VP8 视频
  100  →  H.264 视频

  m=audio ... 111 63 103  ← 这些数字就是"菜单编号"
  m=video ... 96 98 100   ← 对应不同的编解码器

Payload Type 范围:

  • 0-95:静态分配(标准规定)
  • 96-127:动态分配(WebRTC 使用)

a=fmtp - 格式参数
复制代码
a=fmtp:<payload type> <format specific parameters>

示例:
a=fmtp:111 minptime=10;useinbandfec=1
a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f

通俗解释: 编解码器的"详细配置参数"。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    fmtp 参数解释                                             │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

Opus 音频参数 (a=fmtp:111 ...):
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  minptime=10         │ 最小打包时间 10ms                                                   │
│                      │ 通俗理解:每 10ms 打包一次音频数据                                  │
│                      │ 打包时间越长,延迟越高,但效率也越高                                │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  useinbandfec=1      │ 启用带内前向纠错                                                    │
│                      │ 通俗理解:丢包时可以恢复部分数据,不需要重传                        │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

H.264 视频参数 (a=fmtp:100 ...):
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  level-asymmetry-allowed=1 │ 允许编解码使用不同的级别                                       │
│                            │ 通俗理解:发送和接收可以使用不同的编码质量                      │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  packetization-mode=1      │ 非交错模式(单 NALU)                                          │
│                            │ 通俗理解:最常用的打包方式,兼容性最好                          │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  profile-level-id=42e01f   │ 编码配置标识                                                    │
│                            │ 42 = Constrained Baseline Profile                              │
│                            │ 通俗理解:这是硬件加速支持最好的 H.264 配置                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

a=rtcp-fb - RTCP 反馈
复制代码
a=rtcp-fb:<payload type> <feedback type>

示例:
a=rtcp-fb:96 goog-remb      # 接收端估计最大比特率
a=rtcp-fb:96 transport-cc   # 传输拥塞控制
a=rtcp-fb:96 ccm fir        # 关键帧请求
a=rtcp-fb:96 nack           # 丢包重传请求
a=rtcp-fb:96 nack pli       # 图像丢失指示

通俗解释: 告诉对方"如果出现问题,怎么通知我"。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    RTCP 反馈机制类比                                         │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  就像快递追踪系统:

  ┌─────────────────┐                    ┌─────────────────┐
  │   发送方 A       │                    │   接收方 B       │
  │                 │                    │                 │
  │  发送视频数据    │ ──────────────────>│  接收视频数据    │
  │                 │                    │                 │
  │                 │                    │  发现丢包!      │
  │                 │                    │                 │
  │  "请重发第 5 帧"│ <──────────────────│  发送 NACK      │
  │                 │                    │                 │
  │  重发数据        │ ──────────────────>│  收到重发数据    │
  │                 │                    │                 │
  │                 │                    │  画面卡住了!    │
  │                 │                    │                 │
  │  发送关键帧      │ <──────────────────│  发送 PLI 请求  │
  │                 │                    │                 │
  └─────────────────┘                    └─────────────────┘

  常见反馈类型:
  - nack: 丢包重传请求(通俗理解:"我漏收了,请重发")
  - pli: 图像丢失指示,请求关键帧(通俗理解:"画面坏了,发个新关键帧")
  - fir: 完整内部请求,请求关键帧
  - remb: 接收端带宽估计(通俗理解:"我的网络能承受这么多数据")
  - transport-cc: 传输拥塞控制(通俗理解:"网络堵了,慢点发")

3.4 SSRC 和 MSID 相关字段

复制代码
a=ssrc:12345678 cname:user1
a=ssrc:12345678 msid:localStream audioTrack
a=ssrc:12345678 mslabel:localStream
a=ssrc:12345678 label:audioTrack

通俗解释: SSRC 是媒体流的"身份证号",MSID 是媒体流的"名字"。

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SSRC 和 MSID 关系                                         │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  MediaStream (localStream) - 媒体流容器
  │
  ├── MediaStreamTrack (audioTrack) - 音频轨道
  │   │
  │   └── SSRC: 12345678 - 同步源标识符(随机生成的唯一 ID)
  │       │
  │       ├── cname: user1 - 规范名称,用于关联多个 SSRC
  │       └── msid: localStream audioTrack - 媒体流和轨道的关联
  │
  └── MediaStreamTrack (videoTrack) - 视频轨道
      │
      └── SSRC: 87654321 - 另一个唯一 ID
          │
          ├── cname: user1 - 相同的 cname,表示属于同一个用户
          └── msid: localStream videoTrack

  通俗理解:
  - MediaStream = 一个"通话会话"(就像一个文件夹)
  - MediaStreamTrack = 会话中的"音频"或"视频"(就像文件夹里的文件)
  - SSRC = 每个轨道的唯一编号(就像文件的 ID)
  - cname = 标识这些轨道属于同一个人(就像文件的作者)

4. Offer/Answer 模型

4.1 什么是 Offer/Answer

Offer/Answer 是 SDP 交换的标准模式,就像两个人谈判:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Offer/Answer 生活类比                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  场景:两个人商量去哪里吃饭

  A (Offer 发起方)                                B (Answer 回复方)
      │                                                │
      │  "我提议去吃火锅或者烧烤                        │
      │   时间可以是中午或晚上                          │
      │   你觉得怎么样?"                              │
      │ ───────────────────────────────────────────────>│
      │                                                │
      │                               "我觉得火锅不错   │
      │                                时间定在晚上吧"   │
      │ <───────────────────────────────────────────────│
      │                                                │
      │  ════════════════════════════════════════════════
      │  达成一致:火锅,晚上
      │  ════════════════════════════════════════════════

  SDP 中:
  - Offer = A 列出所有支持的编解码器(相当于"菜单")
  - Answer = B 选择一个共同支持的编解码器(相当于"点菜")

4.2 Offer/Answer 详细流程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Offer/Answer 详细流程                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     呼叫方 A (Caller)                                    被呼叫方 B (Callee)
        │                                                       │
        │                                                       │
        │  ┌─────────────────────────────────────────────────┐ │
        │  │ 步骤 1: createOffer()                           │ │
        │  │                                                 │ │
        │  │ 创建 Offer SDP,包含:                          │ │
        │  │ - A 支持的所有编解码器(相当于"菜单")         │ │
        │  │ - A 的 ICE 候选者(相当于"地址")              │ │
        │  │ - A 的 DTLS 指纹(相当于"身份证")             │ │
        │  │ - a=setup:actpass (角色未定)                   │ │
        │  └─────────────────────────────────────────────────┘ │
        │                                                       │
        │  ┌─────────────────────────────────────────────────┐ │
        │  │ 步骤 2: setLocalDescription(offer)              │ │
        │  │                                                 │ │
        │  │ 将 Offer 设置为本地描述                         │ │
        │  │ 开始收集 ICE 候选者                             │ │
        │  └─────────────────────────────────────────────────┘ │
        │                                                       │
        │  发送 Offer ─────────────────────────────────────────>│
        │                                                       │
        │                                    ┌──────────────────┴──────────────────┐
        │                                    │ 步骤 3: setRemoteDescription(offer) │
        │                                    │                                     │
        │                                    │ 将 A 的 Offer 设置为远程描述        │
        │                                    │ 知道了 A 的能力和地址               │
        │                                    └─────────────────────────────────────┘
        │                                    ┌─────────────────────────────────────┐
        │                                    │ 步骤 4: createAnswer()              │
        │                                    │                                     │
        │                                    │ 创建 Answer SDP,包含:             │
        │                                    │ - 选择一个共同支持的编解码器        │
        │                                    │ - B 的 ICE 候选者                   │
        │                                    │ - B 的 DTLS 指纹                    │
        │                                    │ - a=setup:active (确定角色)         │
        │                                    └─────────────────────────────────────┘
        │                                    ┌─────────────────────────────────────┐
        │                                    │ 步骤 5: setLocalDescription(answer) │
        │                                    │                                     │
        │                                    │ 将 Answer 设置为本地描述            │
        │                                    └─────────────────────────────────────┘
        │                                                       │
        │  接收 Answer <─────────────────────────────────────────│
        │                                                       │
        │  ┌─────────────────────────────────────────────────┐ │
        │  │ 步骤 6: setRemoteDescription(answer)            │ │
        │  │                                                 │ │
        │  │ 将 B 的 Answer 设置为远程描述                    │ │
        │  │ 知道了 B 的选择和地址                            │ │
        │  └─────────────────────────────────────────────────┘ │
        │                                                       │
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │  SDP 协商完成!
        │  双方知道了:
        │  - 使用什么编解码器
        │  - 对方的网络地址
        │  - 如何建立安全连接
        │  
        │  开始 ICE 连接检查 → 建立 P2P 连接 → 开始传输媒体
        │  ═══════════════════════════════════════════════════════════════════════════════════
        │                                                       │
        ▼                                                       ▼

4.3 Offer 和 Answer 的关键区别

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Offer 与 Answer 的关键区别                                │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ 编解码器列表                                                                           │
│                                                                                             │
│  Offer:  m=audio ... 111 63 103 104 9 0 8                                                  │
│          列出所有支持的编解码器(相当于"菜单")                                             │
│                                                                                             │
│  Answer: m=audio ... 111                                                                    │
│          只选择一个编解码器(相当于"点菜")                                                 │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  2️⃣ DTLS 角色                                                                              │
│                                                                                             │
│  Offer:  a=setup:actpass                                                                    │
│          "我可以主动连接,也可以等你连接"                                                   │
│                                                                                             │
│  Answer: a=setup:active                                                                     │
│          "我来主动连接你"(角色确定了)                                                     │
│                                                                                             │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                             │
│  3️⃣ 创建时机                                                                               │
│                                                                                             │
│  Offer:  发起呼叫时创建                                                                     │
│          A 想要呼叫 B,所以 A 创建 Offer                                                    │
│                                                                                             │
│  Answer: 收到 Offer 后创建                                                                  │
│          B 收到 A 的 Offer 后,创建 Answer 回复                                             │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4.4 本项目中的 Offer/Answer 实现

kotlin 复制代码
// 创建 Offer(发起呼叫时调用)
fun createOffer() {
    executor.execute {
        // 设置约束条件:要求接收音频和视频
        val constraints = MediaConstraints().apply {
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
            mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
        }
        
        // 创建 Offer
        peerConnection?.createOffer(object : SdpObserver {
            override fun onCreateSuccess(sdp: SessionDescription) {
                // Offer 创建成功,设置本地描述
                peerConnection?.setLocalDescription(object : SdpObserver {
                    override fun onSetSuccess() {
                        // 本地描述设置成功,发送 Offer 给对端
                        onSdpToSend("offer", sdp.description)
                    }
                }, sdp)
            }
        }, constraints)
    }
}

// 处理远程 Offer 并创建 Answer(收到呼叫时调用)
private fun handleRemoteOffer(senderId: String, sdp: String) {
    // 验证 SDP 格式
    if (!sdpManager.validateSdp(sdp)) return
    
    executor.execute {
        // 创建 SessionDescription 对象
        val sessionDescription = sdpManager.createSessionDescription("offer", sdp)
        
        // 设置远程描述
        peerConnection?.setRemoteDescription(object : SdpObserver {
            override fun onSetSuccess() {
                // 远程描述设置成功,创建 Answer
                createAnswer()
            }
        }, sessionDescription)
    }
}

5. SDP 类型判断

5.1 如何判断 SDP 是 Offer 还是 Answer

kotlin 复制代码
// 本项目中的判断方法
fun parseSdpType(sdp: String): String {
    return if (sdp.contains("a=setup:actpass")) "offer" else "answer"
}

原理:

  • Offer 中 a=setup:actpass(角色未定)

  • Answer 中 a=setup:activea=setup:passive(角色已定)

    ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
    │ SDP 类型判断示例 │
    └──────────────────────────────────────────────────────────────────────────────────────────────┘

    Offer SDP 片段:
    ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
    │ ... │
    │ a=setup:actpass ← 包含 actpass,所以是 Offer │
    │ ... │
    └─────────────────────────────────────────────────────────────────────────────────────────────┘

    Answer SDP 片段:
    ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
    │ ... │
    │ a=setup:active ← 是 active 或 passive,所以是 Answer │
    │ ... │
    └─────────────────────────────────────────────────────────────────────────────────────────────┘

5.2 SDP 验证

kotlin 复制代码
// 本项目中的 SDP 验证
fun validateSdp(sdp: String): Boolean {
    // 检查 SDP 是否为空
    if (sdp.isEmpty()) {
        Log.e(TAG, "[sdp] SDP内容为空")
        return false
    }
    
    // 检查是否包含版本行(最基本的 SDP 格式检查)
    if (!sdp.contains("v=")) {
        Log.e(TAG, "[sdp] SDP格式无效,缺少版本行")
        return false
    }
    
    return true
}

6. 编解码器详解

6.1 音频编解码器

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    WebRTC 音频编解码器                                       │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  编解码器    │ PT  │ 采样率    │ 比特率        │ 特点                                      │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  Opus       │ 111 │ 48kHz     │ 6-510 kbps   │ ⭐ 首选!高质量,自适应比特率              │
│  PCMU       │ 0   │ 8kHz      │ 64 kbps      │ G.711 μ-law,北美标准,兼容性好           │
│  PCMA       │ 8   │ 8kHz      │ 64 kbps      │ G.711 A-law,欧洲标准                     │
│  G.722      │ 9   │ 16kHz     │ 64 kbps      │ 宽带音频,音质更好                        │
│  CN         │ 13  │ 8kHz      │ -            │ 舒适噪声,静音时播放背景音                │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
Opus 编解码器详解

Opus 是 WebRTC 的首选音频编解码器,因为它:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Opus 编解码器优势                                         │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  1️⃣ 比特率范围极广:6-510 kbps
     ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
     │  窄带语音 (6-8 kbps)  ←→  宽带语音 (16-32 kbps)  ←→  高品质音乐 (64-510 kbps)     │   │
     │                                                                                     │   │
     │  通俗理解:网络差时降低比特率保证通话,网络好时提高音质                            │   │
     └─────────────────────────────────────────────────────────────────────────────────────┘   │

  2️⃣ 低延迟:5-66.5ms
     ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
     │  实时通话需要低延迟,Opus 可以做到 5ms 的超低延迟                                  │   │
     │  通俗理解:说话后对方几乎立刻就能听到                                              │   │
     └─────────────────────────────────────────────────────────────────────────────────────┘   │

  3️⃣ 支持前向纠错 (FEC)
     ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
     │  在丢包时可以恢复部分数据,不需要重传                                              │   │
     │  a=fmtp:111 useinbandfec=1                                                          │   │
     │  通俗理解:丢了数据包也能恢复,通话质量更稳定                                      │   │
     └─────────────────────────────────────────────────────────────────────────────────────┘   │

  4️⃣ 支持不连续传输 (DTX)
     ┌─────────────────────────────────────────────────────────────────────────────────────┐   │
     │  静音时不发送数据,节省带宽                                                        │   │
     │  通俗理解:你不说话时就不发数据,省流量                                            │   │
     └─────────────────────────────────────────────────────────────────────────────────────┘   │

6.2 视频编解码器

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    WebRTC 视频编解码器                                       │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  编解码器    │ PT  │ 特点                                       │ 推荐场景          │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  VP8        │ 96  │ Google 开源,软件编解码效率高               │ 通用场景          │
│  VP9        │ 98  │ VP8 升级版,压缩率更高                      │ 带宽有限场景      │
│  H.264      │ 100 │ 硬件加速支持好,移动设备友好                │ 移动设备 ⭐       │
│  AV1        │ 35  │ 最新编解码器,压缩率最高                    │ 高清视频          │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
VP8 vs H.264 如何选择?
复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    VP8 vs H.264 选择指南                                     │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

  VP8 优势:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ 完全免费开源,无专利问题                                                            │
  │  ✅ 软件编解码效率高,CPU 占用低                                                        │
  │  ✅ 所有浏览器都支持                                                                    │
  │  ✅ 屏幕共享效果好                                                                      │
  │                                                                                         │
  │  通俗理解:适合电脑端,不依赖硬件加速                                                   │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  H.264 优势:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  ✅ 硬件加速支持广泛(手机、电脑都有专用芯片)                                          │
  │  ✅ 移动设备省电                                                                        │
  │  ✅ 与传统视频系统兼容性好                                                              │
  │  ✅ 编码延迟低                                                                          │
  │                                                                                         │
  │  通俗理解:适合手机端,有硬件加速,省电                                                 │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

  选择建议:
  ┌─────────────────────────────────────────────────────────────────────────────────────────┐
  │  📱 移动设备优先选择 H.264(省电、硬件加速)                                            │
  │  💻 桌面端可以选择 VP8(软件编解码效率高)                                              │
  │  🖥️ 屏幕共享选择 VP8 或 VP9                                                            │
  │  🌐 需要最大兼容性时,同时支持 VP8 和 H.264                                            │
  └─────────────────────────────────────────────────────────────────────────────────────────┘

6.3 编解码器协商过程

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    编解码器协商过程示例                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     呼叫方 A                                                    被呼叫方 B
        │                                                            │
        │  Offer:                                                    │
        │  m=video ... 96 98 100 127                                 │
        │                                                            │
        │  "我支持以下视频编解码器:                                  │
        │   - VP8 (PT=96)                                            │
        │   - VP9 (PT=98)                                            │
        │   - H.264 (PT=100)                                         │
        │   - H.264 High Profile (PT=127)"                           │
        │                                                            │
        │ ─────────────────────────────────────────────────────────>│
        │                                                            │
        │                                     B 支持的编解码器:      │
        │                                     - VP8 ✓               │
        │                                     - H.264 ✓              │
        │                                     - VP9 ✗               │
        │                                                            │
        │                                     选择第一个共同支持的: │
        │                                     VP8 (PT=96)            │
        │                                                            │
        │  Answer:                                                   │
        │  m=video ... 96                                            │
        │                                                            │
        │  "我选择 VP8"                                              │
        │ <─────────────────────────────────────────────────────────│
        │                                                            │
        │  ═══════════════════════════════════════════════════════════
        │  协商结果:双方使用 VP8 编解码器
        │  通俗理解:A 提供菜单,B 点菜,双方达成一致
        │  ═══════════════════════════════════════════════════════════

7. SDP 与 ICE 的关系

7.1 ICE 候选者在 SDP 中的表示

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 候选者格式                                            │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

a=candidate:<foundation> <component-id> <transport> <priority> <connection-address> <port> typ <candidate-type> [raddr <related-address> rport <related-port>]

示例:
a=candidate:1 1 UDP 2122260223 192.168.1.100 5000 typ host
a=candidate:2 1 UDP 1686052607 203.0.113.10 12345 typ srflx raddr 192.168.1.100 rport 5000
a=candidate:3 1 UDP 41885439 198.51.100.10 60000 typ relay raddr 198.51.100.10 rport 60000

通俗解释:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    ICE 候选者通俗解释                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

a=candidate:1 1 UDP 2122260223 192.168.1.100 5000 typ host

分解解释:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  字段              │ 值              │ 通俗解释                                            │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  foundation        │ 1               │ 候选者标识符                                        │
│  component-id      │ 1               │ 1=RTP, 2=RTCP                                      │
│  transport         │ UDP             │ 传输协议                                            │
│  priority          │ 2122260223      │ 优先级(数字越大优先级越高)                        │
│  connection-address│ 192.168.1.100   │ IP 地址                                             │
│  port              │ 5000            │ 端口号                                              │
│  typ               │ host            │ 候选者类型                                          │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

候选者类型:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  类型      │ 通俗解释                        │ 优先级                                   │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  host     │ 本地地址(局域网 IP)            │ 最高 ⭐⭐⭐                              │
│           │ 通俗理解:你家里的地址           │ 优先尝试直连                            │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  srflx    │ 服务器反射地址(公网 IP)        │ 中 ⭐⭐                                  │
│           │ 通俗理解:通过 STUN 获取的公网地址│ host 不通时尝试                         │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  prflx    │ 对等反射地址(动态发现)         │ 中 ⭐⭐                                  │
│           │ 通俗理解:对方告诉你的地址       │ 意外发现的可达地址                      │
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  relay    │ 中继地址(TURN 服务器)          │ 最低 ⭐                                 │
│           │ 通俗理解:通过中转服务器转发     │ 其他方式都不通时使用                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

7.2 Trickle ICE

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    Trickle ICE 机制                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

传统方式(等待所有候选者):
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  A 收集候选者 ──────────────────────────────> 发送完整 SDP(包含所有候选者)               │
│  │                                                                                          │
│  │  等待...                                                                                │
│  │  等待...                                                                                │
│  │  等待...                                                                                │
│  │  收集完成!                                                                             │
│  │                                                                                          │
│  问题:需要等待所有候选者收集完成,连接建立慢                                               │
│  通俗理解:等所有快递都到了才一起发货,太慢了                                               │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

Trickle ICE(逐步发送):
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  A 收集候选者:                                                                             │
│  │                                                                                          │
│  ├── 收集到 host 候选者 ──────────────────> 立即发送                                       │
│  │   通俗理解:本地地址找到了,先发过去                                                     │
│  │                                                                                          │
│  ├── 收集到 srflx 候选者 ─────────────────> 立即发送                                       │
│  │   通俗理解:公网地址找到了,再发过去                                                     │
│  │                                                                                          │
│  └── 收集到 relay 候选者 ─────────────────> 立即发送                                       │
│      通俗理解:中转地址找到了,最后发过去                                                   │
│                                                                                             │
│  优势:可以立即开始连接检查,加快连接建立速度                                               │
│  通俗理解:有一个地址就发一个,不用等                                                       │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

SDP 中标识支持 Trickle:
a=ice-options:trickle

8. SDP 重新协商

8.1 什么时候需要重新协商

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    需要重新协商的场景                                        │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

1️⃣ 添加/移除媒体轨道
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  场景:通话过程中开启/关闭摄像头                                                            │
│                                                                                             │
│  关闭摄像头:                                                                               │
│  a=sendrecv → a=recvonly(只接收视频,不发送)                                             │
│  通俗理解:我看不到你了,但我还能听到你                                                     │
│                                                                                             │
│  开启摄像头:                                                                               │
│  a=recvonly → a=sendrecv(恢复双向视频)                                                   │
│  通俗理解:现在又能视频了                                                                   │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2️⃣ 切换媒体方向
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  场景:从双向通话变为单向直播                                                               │
│                                                                                             │
│  主播端:a=sendrecv → a=sendonly(只发送)                                                 │
│  观众端:a=sendrecv → a=recvonly(只接收)                                                 │
│                                                                                             │
│  通俗理解:主播说话,观众听,观众不能说话                                                   │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

3️⃣ ICE 重启
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  场景:网络切换(WiFi → 4G)                                                                │
│                                                                                             │
│  需要重新收集 ICE 候选者,建立新的网络连接                                                  │
│                                                                                             │
│  通俗理解:换了个网络,地址变了,需要重新告诉对方                                           │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4️⃣ 屏幕共享
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  场景:开始/停止屏幕共享                                                                    │
│                                                                                             │
│  添加新的视频轨道(屏幕共享流)                                                             │
│                                                                                             │
│  通俗理解:在视频通话基础上增加一个"桌面画面"                                               │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

8.2 重新协商流程

重新协商的流程与初始协商相同,都是 Offer/Answer 模式:

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    重新协商流程                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

     发起方 A                                                     接收方 B
        │                                                            │
        │  1. 修改本地状态(如关闭摄像头)                           │
        │                                                            │
        │  2. createOffer()                                          │
        │     生成新的 Offer SDP                                     │
        │     (o= 行的 sess-version 递增)                          │
        │                                                            │
        │  3. setLocalDescription(newOffer)                         │
        │                                                            │
        │  4. 发送新 Offer ─────────────────────────────────────────>│
        │                                                            │
        │                                     5. setRemoteDescription(newOffer)
        │                                                            │
        │                                     6. createAnswer()       │
        │                                                            │
        │                                     7. setLocalDescription(newAnswer)
        │                                                            │
        │  8. 接收新 Answer <─────────────────────────────────────────│
        │                                                            │
        │  9. setRemoteDescription(newAnswer)                       │
        │                                                            │
        │  ═══════════════════════════════════════════════════════════
        │  重新协商完成,新的媒体配置生效
        │  ═══════════════════════════════════════════════════════════

9. SDP 调试技巧

9.1 如何阅读 SDP 日志

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 日志阅读技巧                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

本项目中的 SDP 日志输出:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  [sdp][本地] SDP信息 | 类型: offer | 行数: 85                                              │
│  [sdp][本地] 音频编解码器: a=rtpmap:111 opus/48000/2                                       │
│  [sdp][本地] 视频编解码器: a=rtpmap:96 VP8/90000                                           │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

快速检查要点:
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  1️⃣ 检查编解码器协商结果                                                                   │
│     - Offer 和 Answer 的 m= 行是否匹配?                                                   │
│     - 双方是否选择了相同的编解码器?                                                        │
│     通俗理解:确认双方"说同一种语言"                                                       │
│                                                                                             │
│  2️⃣ 检查 ICE 凭证                                                                         │
│     - ice-ufrag 和 ice-pwd 是否正确?                                                      │
│     - ICE 候选者是否正确交换?                                                             │
│     通俗理解:确认双方能"找到对方"                                                         │
│                                                                                             │
│  3️⃣ 检查 DTLS 设置                                                                        │
│     - fingerprint 是否存在?                                                               │
│     - setup 角色是否正确?                                                                 │
│     通俗理解:确认双方能"安全连接"                                                         │
│                                                                                             │
│  4️⃣ 检查媒体方向                                                                           │
│     - sendrecv/sendonly/recvonly 是否符合预期?                                            │
│     通俗理解:确认双方"能说能听"                                                           │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

9.2 常见问题排查

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 常见问题排查                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

问题 1: 视频无法播放,只有音频
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - 编解码器协商失败(双方没有共同支持的视频编解码器)                                       │
│  - H.264 profile-level-id 不兼容                                                           │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查 Offer 中的 m=video 行                                                             │
│  2. 检查 Answer 中的 m=video 行                                                            │
│  3. 确认双方有共同支持的编解码器                                                            │
│                                                                                             │
│  通俗理解:就像两个人说不同的语言,无法沟通                                                 │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

问题 2: ICE 连接一直处于 CHECKING 状态
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - ICE 候选者没有正确交换                                                                   │
│  - STUN/TURN 服务器配置错误                                                                │
│  - 网络不通                                                                                │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查 SDP 中的 a=candidate 行                                                           │
│  2. 检查 STUN/TURN 服务器是否可达                                                          │
│  3. 检查防火墙设置                                                                         │
│                                                                                             │
│  通俗理解:就像快递找不到收件地址                                                           │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

问题 3: DTLS 连接失败
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - fingerprint 不匹配                                                                       │
│  - setup 角色冲突                                                                          │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查 SDP 中的 a=fingerprint 行                                                         │
│  2. 检查 a=setup 是否正确(Offer: actpass, Answer: active/passive)                        │
│                                                                                             │
│  通俗理解:就像身份证验证失败                                                               │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

问题 4: 对方听不到我的声音
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  可能原因:                                                                                 │
│  - 媒体方向设置错误(sendrecv 被设置为 recvonly)                                          │
│  - 音频轨道未正确添加                                                                       │
│                                                                                             │
│  排查方法:                                                                                 │
│  1. 检查 SDP 中的 a=sendrecv 行                                                            │
│  2. 检查本地音频轨道是否已添加到 PeerConnection                                             │
│                                                                                             │
│  通俗理解:话筒没开,或者对方把耳朵堵上了                                                   │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

10. SDP 最佳实践

10.1 处理建议

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 处理最佳实践                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

1️⃣ 不要手动修改 SDP
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  ❌ 错误做法:手动拼接或修改 SDP 字符串                                                     │
│  ✅ 正确做法:使用 WebRTC API 生成和处理 SDP                                                │
│                                                                                             │
│  原因:SDP 格式复杂,手动修改容易出错                                                       │
│  通俗理解:不要自己造轮子,用现成的工具                                                     │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2️⃣ 正确处理 SDP 交换顺序
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  正确顺序:                                                                                 │
│  1. createOffer/createAnswer                                                               │
│  2. setLocalDescription                                                                    │
│  3. 发送 SDP 给对端                                                                        │
│  4. 对端 setRemoteDescription                                                              │
│                                                                                             │
│  通俗理解:先准备好自己的信息,再发给对方                                                   │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

3️⃣ 处理 ICE 候选者时机
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  ICE 候选者在 setLocalDescription 后开始收集                                               │
│  应该在远程描述设置后才能添加远程 ICE 候选者                                                │
│                                                                                             │
│  通俗理解:等对方准备好了,再告诉他你的地址                                                 │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4️⃣ 实现重新协商
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                             │
│  当需要改变媒体配置时,触发重新协商:                                                       │
│  - 添加/移除媒体轨道                                                                       │
│  - 改变媒体方向                                                                           │
│  - 网络切换时重启 ICE                                                                     │
│                                                                                             │
│  通俗理解:情况变了就重新商量                                                               │
│                                                                                             │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

10.2 性能优化

复制代码
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│                                    SDP 性能优化建议                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────┘

1️⃣ 使用 BUNDLE
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  将音频和视频复用到同一个连接,减少端口占用,简化 NAT 穿透                                 │
│  通俗理解:一个快递包裹装所有东西,省事                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

2️⃣ 使用 Trickle ICE
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  逐步发送 ICE 候选者,加快连接建立速度                                                     │
│  通俗理解:找到一个地址就发一个,不用等                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

3️⃣ 选择合适的编解码器
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - 移动设备优先选择 H.264(硬件加速)                                                       │
│  - 桌面端可以选择 VP8(软件编解码效率高)                                                   │
│  - 音频首选 Opus(高质量、自适应)                                                         │
│  通俗理解:根据设备选择最合适的编码方式                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

4️⃣ 启用 RTCP 反馈
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│  - nack: 丢包重传                                                                         │
│  - pli/fir: 关键帧请求                                                                    │
│  - remb/transport-cc: 拥塞控制                                                            │
│  通俗理解:建立反馈机制,有问题及时沟通                                                    │
└─────────────────────────────────────────────────────────────────────────────────────────────┘

11. 参考资料

11.1 相关 RFC 文档

RFC 编号 标题 说明
RFC 4566 SDP: Session Description Protocol SDP 协议规范
RFC 3264 An Offer/Answer Model with SDP Offer/Answer 模型
RFC 8866 SDP: Session Description Protocol SDP 协议更新版
RFC 5245 Interactive Connectivity Establishment (ICE) ICE 协议
RFC 8839 SDP Offer/Answer Procedures for ICE ICE 的 SDP 处理

11.2 本项目相关文件

文件路径 说明
SdpManager.kt SDP 管理,包含验证、类型判断、缓存处理
WebRTCClient.kt WebRTC 客户端,包含 Offer/Answer 创建和处理
IceCandidateManager.kt ICE 候选者管理
PeerConnectionManager.kt PeerConnection 管理

本系列文章:
【音视频通话系统】之架构详解
【音视频通信系统】之呼叫完整时序图
【音视频通信系统】之STUN服务详解
【音视频通信系统】之TURN 服务详解
【音视频通信系统】WebRTC ICE 候选类型详解:对等反射候选者(Peer Reflexive Candidate)

相关推荐
YYDataV数据可视化13 小时前
【P2P音视频通信系统】之STUN服务详解
webrtc·p2p·stun·音视频通信
YYDataV数据可视化20 小时前
音视频呼叫完整时序图
音视频
mseaspring21 小时前
35.7k Star的开源项目,用Claude Code 调用Remotion 以编程的方式自动生成视频
音视频
BryanGG1 天前
[教程]通用稳定器运镜技巧
音视频·稳定器·运镜
YYDataV数据可视化1 天前
WebRTC ICE 候选类型详解:对等反射候选者(Peer Reflexive Candidate)
webrtc·实时音视频·ai编程
YYDataV数据可视化1 天前
【音视频通话系统】架构详解
音视频·webrtc·实时音视频
linux_cfan1 天前
打造智慧校园视听新基建:高校与在线教育平台 Web 视频播放器选型指南 (2026版)
前端·学习·音视频·教育电商
YZ0992 天前
Sora2 AI视频去水印接口
人工智能·音视频·api·ai编程
硅谷秋水2 天前
mimic-video:机器人控制的可泛化视频-动作模型,超越VLA模型
人工智能·机器学习·计算机视觉·机器人·音视频