【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:active 或 a=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)

相关推荐
微帧Visionular36 分钟前
AI视频单帧清晰,播放时却闪烁,原因是什么?
人工智能·深度学习·音视频
辰辰观影2 小时前
Windows网盘视频播放器推荐:无广告4K HDR怎么选
音视频
2601_966827454 小时前
电视端云盘在线播放怎么选?支持4K HDR的应用推荐
音视频
开开心心就好5 小时前
音频降噪剪辑工具,音量裁剪一键处理
运维·服务器·前端·spring·ocr·音视频·1024程序员节
AI视频工具指南9 小时前
AI视频的声音和画面怎么配合?先分清谁在传递信息
人工智能·aigc·音视频
辰辰观影11 小时前
无广告播放器推荐来啦
音视频
TrueDei11 小时前
GStreamer 使用 valve 分别控制音视频流的暂停与恢复
音视频
一条破秋裤12 小时前
基于 Hi3516CV610 的立定跳远视频测量系统调研与实施方案
arm开发·音视频
2601_9668274512 小时前
视频里的特效字幕怎么翻译?外挂字幕与AI字幕一键处理教程
音视频
八月的雨季 最後的冰吻13 小时前
音视频面试--延迟优化
面试·职场和发展·音视频