SDP: Session Description Protocol

SDP 本身

介绍

SDP(Session Description Protocol)会话描述协议,提供一个标准的信息描述格式。SDP 是一个单纯的会话描述格式,未与传输协议结合,因此它可以用于不同的传输协议(例如:Session Initiation Protocol(SIP),Real Time Streaming Protocol(RTSP)等)。

专有术语

Conference:多媒体会议(multimedia conference)由两个或多个通信用户以及他们用来通信的软件组成。

Session:多媒体会话(multimedia session)由多媒体发送者、多媒体接收者和从发送者流向接收者的数据流组成。

Session Description:一种定义明确的格式,用于传递足够的信息以加入和参与多媒体会话。

要求和建议

SDP会话描述包括以下:

  • 会话名称和目的
  • 会话处于活动状态的时间(Timing)
  • 会话的媒体
  • 接收媒体所需的信息 (地址,端口,格式等)

由于参与会话所需的资源可能有限,因此可能还需要一些额外的信息:

  • 会话使用的带宽
  • 会议负责人的联系信息

通常,SDP 必须传达足够的信息以使应用程序能够加入会话(加密密钥可能除外),并向可能需要知道的任何非参与者宣布要使用的资源。 (后一个功能主要用于 SDP 与多播会话公告协议一起使用时)

媒体和传输信息

SDP会话描述包括以下媒体信息:

  • 媒体类型(video, audio 等)
  • 传输协议(RTP/UDP/IP, H.320 等)
  • 媒体格式(H.264 video, MPEG video 等)

除了媒体格式和传输协议之外,SDP 还描述地址和端口详细信息。

对于 IP 多播会话,包括:媒体的广播组地址媒体的传输端口,该地址和端口是多播流的目标地址和目标端口,无论是发送、接收还是两者。

对于 单播 IP 会话,包括:媒体的远程地址媒体的远程传输端口,该地址和端口的语义取决于定义的媒体和传输协议。默认情况下,这应该是数据发往的远程地址和远程端口。

Timing 信息

会话在时间上可能是有界的或无界的。无论它们是否有界,它们可能仅在特定时间处于活动状态。
SDP可以传达:

  • 绑定会话的开始和停止时间的列表
  • 对于每一个绑定,可设置重复时间,例如:"every Wednesday at 10am for
    one hour"

此时间信息是全球一致的,与本地时区或夏令时无关

私有会话

可以创建公共会话和私人会话。 SDP 本身并不区分这些;私人会话通常通过在分发期间加密会话描述来传达。如何执行加密的细节取决于用于传送 SDP 的机制。

如果会话宣告是私有的,则可以使用该私有宣告来传达对会议中的每个媒体进行解码所需的加密密钥,包括足够的信息来了解每个媒体使用哪种加密方案。

获取有关会话的更多信息

会话描述应该传达足够的信息来决定是否参与会话。 SDP 可能包含统一资源标识符 (URI) 形式的附加指针,以获取有关会话的更多信息。

分类

当 SAP 或任何其他广告机制正在分发许多会话描述时,可能需要从不感兴趣的会话公告中过滤出感兴趣的会话公告。 SDP 支持能够自动化的会话分类机制(“a=cat:”属性)

国际化

SDP 规范建议在 UTF-8 编码 中使用 ISO 10646 字符集,以允许表示许多不同的语言。

当然,SDP 还允许在需要时使用其他字符集(帮助进行紧凑的表示),例如 ISO 8859-1。

SDP 规范

SDP 会话描述完全是使用 UTF-8 编码的 ISO 10646 字符集的文本。SDP 字段名称和属性名称仅使用 UTF-8 的 US-ASCII 子集,但文本字段和属性值可以使用完整的 ISO 10646 字符集。

SDP 会话描述由媒体类型 “application/sdp” 表示

SDP 会话描述由以下形式的多行文本组成:
<type>=<value>
其中 <type> 必须恰好是一个区分大小写的字符,而 <value> 的格式取决于 <type>。通常,<value> 是由单个空格字符或自由格式字符串分隔的多个字段,并且是区分大小写的。注意“=”符号两侧不得使用空格。

SDP 会话描述由一个 session-level section 组成,session-level section 后跟零个或多个 media-level sections。session-level section 以“v=”行开始,接着到第一个 media-level section 。每个 media-level section 都以“m=”行开始,接着到下一个 media-level section 或整个会话描述的结尾。一般来说,session-level 的值是所有媒体的默认值,除非被 media-level 的值覆盖。

每个描述中的某些行是必需的,某些行是可选的,但所有行都必须完全按照下面给出的顺序出现(固定顺序极大地增强了错误检测并简单化解析器)。其中,可选项描述被标记为 “*”

Session description

         v=  (protocol version)
         o=  (originator and session identifier)
         s=  (session name)
         i=* (session information)
         u=* (URI of description)
         e=* (email address)
         p=* (phone number)
         c=* (connection information -- not required if included in
              all media)
         b=* (zero or more bandwidth information lines)
         One or more time descriptions ("t=" and "r=" lines; see below)        /* Time description */
         z=* (time zone adjustments)
         k=* (encryption key)
         a=* (zero or more session attribute lines)
         Zero or more media descriptions                                       /* Media description */

Time description

         t=  (time the session is active)
         r=* (zero or more repeat times)

Media description, if present

         m=  (media name and transport address)
         i=* (media title)
         c=* (connection information -- optional if included at
              session level)
         b=* (zero or more bandwidth information lines)
         k=* (encryption key)
         a=* (zero or more media attribute lines)

Example

      v=0
      o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
      s=SDP Seminar
      i=A Seminar on the session description protocol
      u=http://www.example.com/seminars/sdp.pdf
      e=j.doe@example.com (Jane Doe)
      c=IN IP4 224.2.17.12/127
      t=2873397496 2873404696
      a=recvonly
      m=audio 49170 RTP/AVP 0
      m=video 51372 RTP/AVP 99
      a=rtpmap:99 h263-1998/90000

v=
Session Description Protocol 的版本。

o=
o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
o=字段给出了会话的发起者(用户名<username>和用户主机的地址<nettype> <addrtype> <unicast-address>)加上会话标识符<sess-id>和版本号<sess-version>

  • <nettype> - IN 表示 Internet 。
  • <addrtype> - IP4IP6

s=
s=<session name>
s=描述会话名称。每个会话描述必须有一个且只有一个s=描述。 如果存在“a=charset”属性,则它指定S=字段中使用的字符集。如果“a=charset”属性不存在,s=必须包含 UTF-8 编码的 ISO 10646 字符。如果会话没有有意义的名称,则应使用值s= (即值为空)。

i=
i=<session description>
i= 提供有关会话的描述信息。每个会话描述最多(可以没有)有一个会话级 i= 字段,每个媒体最多(可以没有)有一个i=字段。如果存在“a=charset”属性,则它指定“i=”字段中使用的字符集。如果“a=charset”属性不存在,“i=”字段必须包含 UTF-8 编码的 ISO 10646 字符。

u=
u=<uri>
URI 是 WWW 客户端使用的统一资源标识符。 URI 应该是指向有关会话的附加信息的指针。该字段是可选的,但如果存在,则必须在第一个媒体字段之前指定。每个会话描述最多允许一个 URI 字段。

e= and p=
e=<email-address>
p=<phone-number>
e=p= 指定会议负责人的联系信息。

c=
c=<nettype> <addrtype> <connection-address>
会话描述必须在每个媒体描述中至少包含一个c=字段,或者在会话级别包含一个c=字段。它可以既包含单个会话级别的c=字段,又包含每个媒体描述的附加c=字段,在这种情况下,每个媒体的值会覆盖相应媒体的会话级别设置。

  • <nettype> 通常为 IN 表示 Internet 。
  • <addrtype> 通常为 IP4IP6
  • connection-address = <base multicast address>[/<ttl>]/<number of addresses>

example :

  • c=IN IP4 224.2.36.42/127

  • c=IN IP4 224.2.1.1/127/3

    • c=IN IP4 224.2.1.1/127
    • c=IN IP4 224.2.1.2/127
    • c=IN IP4 224.2.1.3/127
  • c=IN IP6 FF15::101/3

    • c=IN IP6 FF15::101
    • c=IN IP6 FF15::102
    • c=IN IP6 FF15::103

b=
b=<bwtype>:<bandwidth>

t=
t=<start-time> <stop-time>
如果会话在多个不规则间隔的时间处于活动状态,则可以使用多个 t= 行;每个附加的 t= 行指定会话将处于活动状态的附加时间段。如果会话定期处于活动状态,则应使用 r= 行(见下文)以及 t= 行,在这种情况下,t= 行指定重复的开始和停止时间。

<start-time> 和 <stop-time> 的值是自 1900 年以来以秒为单位的网络时间协议 (NTP) 时间值的十进制表示。要将这些值转换为 UNIX 时间,请减去十进制 2208988800。

如果 <stop-time> 设置为零,则会话不受限制,但直到 <start-time> 之后才会变为活动状态。如果 <start-time> 也为零,则认为会话是永久的。

r=
r=<repeat interval> <active duration> <offsets from start-time>

  • offsets 可指多个 offset ,使用空格隔开。

t=3034423619 3042462419
r=604800 3600 0 90000
为了使描述更简洁,也可以以单位为单位给出天、小时或分钟的时间,例如:

  • d - days (86400 seconds)
  • h - hours (3600 seconds)
  • m - minutes (60 seconds)
  • s - seconds (allowed for completeness)

r=7d 1h 0 25h

z=
z=<adjustment time> <offset> <adjustment time> <offset> ....

若要安排从夏令时到标准时间或从标准时间到夏令时的重复会话(repeated session),有必要指定与基准时间的偏移量。因为不同的时区在一天中的不同时间更改时间,不同的国家/地区在不同的日期进入夏令时或结束夏令时,并且一些国家根本没有夏令时。

夏令时,(Daylight Saving Time:DST),也叫夏时制,又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。各个采纳夏时制的国家具体规定不同。全世界有近110个国家每年要实行夏令时。

1986年4月,中国中央有关部门发出“在全国范围内实行夏时制的通知”,具体做法是:每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即将表针由2时拨至3时,夏令时开始;到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即将表针由2时拨至1时,夏令时结束。从1986年到1991年的六个年度,除1986年因是实行夏时制的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。在夏令时开始和结束前几天,新闻媒体均刊登有关部门的通告。1992年起,夏令时暂停实行。

因此,为了安排同时在冬季和夏季进行的会话,必须能够明确地指定会话被安排在哪个时区。为了简化接收者的这项任务,我们允许发送者指定时区发生调整的 NTP 时间(<adjustment time>)以及调整的偏移量(<offset>)。

z=允许发送者指定这些调整时间的列表以及与基准时间的偏移量。

例如:z=2882844526 -1h 2898848070 0 指定在时间 2882844526 后,计算会话重复时间的时基向后移动 1 小时,并且在时间 2898848070 后,恢复会话的原始时基。调整总是相对于指定的开始时间——它们不是累积的。调整适用于会话描述中的所有 t=r= 行。

k=
k=<method>
k=<method>:<encryption key>
如果通过安全和可信的通道传输SDP,则 SDP 可用于传送加密密钥。密钥字段 ("k=") 提供了一种简单的密钥交换机制,尽管这主要是为了与较旧的实现兼容,并且不推荐使用它。

methods:
k=clear:<encryption key>:加密密钥未经转换包含在此密钥字段中。
k=base64:<encoded encryption key>:加密密钥包含在此密钥字段中,但已进行base64编码[12],因为加密密钥包含SDP中禁止的字符。
k=uri:<URI to obtain key>:uri 包含在此密钥字段中,同过 uri 获取加密密钥。
k=prompt:此SDP描述中不包含密钥,但此密钥字段修饰的会话或媒体流将加密。尝试加入会话时,应提示用户输入密钥,然后使用用户提供的密钥解密媒体流。不建议使用用户指定的密钥,因为此类密钥往往具有弱安全属性。

a=
a=<attribute>
a=<attribute>:<value>
attribute 是扩展 SDP 的主要方法。attribute 可以定义为 session-level 的 attribute,media-level 的 attribute 或两者兼用。
example:
a=recvonly
a=orient:landscape

m=
m=<media> <port> <proto> <fmt> ...

  • <media>:当前有 audio, video, text, application, message, 未来将扩展。
  • <port>:将媒体流送往的传输端口,传输端口的含义取决于相关 c= 字段中指定的正在使用的网络,以及m字段的<proto>中定义的传输协议。媒体应用程序使用的其他端口(例如 RTCP使用的端口)可以通过算法从基本媒体端口派生,也可以在单独的属性中指定(例如,a=RTCP:)。如果使用非连续端口,或者如果它们不遵循偶数RTP端口和奇数RTCP端口的奇偶校验规则,则必须使用a=RTCP:属性。此时必须将RTP发送到<port>中指示的端口,并将rtcp发送到a=rtcp:属性中指明的端口。

对于将分层编码的流发送到单播地址的应用程序,可能需要指定多个传输端口。这是使用类似于c=字段中用于IP多播地址的符号来完成的:
m=<media> <port>/<number of ports> <proto> <fmt> ...
example: m=video 49170/2 RTP/AVP 31

  • <proto>:传输协议描述。

    • udp:表示在 UDP 上运行的未指定的协议(裸UDP)
    • RTP/AVP:表示在 UDP 上运行的 RTP
    • RTP/SAVP:表示在 UDP 上运行的 SRTP
  • <fmt>:媒体格式描述

    • 如果<proto>RTP/AVPRTP/SAVP,则<fmt>指定RTP有效载荷类型(<payload type>)。
    • a=fmtp: 属性可用于指定格式参数
    • 如果 <proto> 子字段为udp,则 <fmt> 必须引用描述 audiovideotextapplicationmessage 的格式。

Attributes

a=cat:<category> 以点分隔提供会话的分层类别,使接收方能够按类别筛选不需要的会话。它是会话级属性,且不依赖于字符集。

a=keywds:<keywords> 与cat属性一样,a=keywds:<keywords> 有助于识别接收方想要的会话。这允许接收方根据描述会话目的的关键字选择感兴趣的会话。它是会话级属性。它是一个依赖于字符集的属性,这意味着如果指定了字符集,则其值应在为会话描述指定的字符集中进行解释,或者在默认情况下在ISO 10646/UTF-8中进行解释。

a=tool:<name and version of tool> 提供用于创建 SDP 的工具名称和版本号。它是会话级属性,不依赖于字符集。

a=ptime:<packet time> 给出了数据包中媒体表示的时间长度(毫秒)。这可能只对音频数据有意义。当然,如果有意义,也可以用于其他媒体类型。它旨在作为音频编码/打包的建议。它是媒体级别的属性,不依赖于字符集。

a=maxptime:<maximum packet time> 给出了每个数据包中可以封装的最大媒体量,以毫秒表示。时间应计算为数据包中媒体所代表的时间总和。对于基于帧的编解码器,时间应该是帧大小的整数倍。此属性可能仅对音频数据有意义。当然,如果有意义,可以与其他媒体类型一起使用。它是媒体级别的属性,不依赖于字符集。请注意,此属性是在RFC 2327之后引入的,未更新的实现将忽略此属性。

a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] 该属性从RTP有效负载类型(<payload type>)(如 m= 行中所用的<fmt>指示的<payload type>)映射到表示 payload<encoding name> 。它还提供有关时钟频率和编码参数的信息。它是一个不依赖于字符集的媒体级别属性。

example:
m=audio 49232 RTP/AVP 98
a=rtpmap:98 L16/16000/2
or
m=audio 49230 RTP/AVP 96 97 98
a=rtpmap:96 L8/8000
a=rtpmap:97 L16/8000
a=rtpmap:98 L16/11025/2

  • RTP profiles 指定使用动态 payload type 必须定义一组有效的 encoding names。在上例中,媒体类型为 audio/l8audio/l16

  • 对于音频流,<encoding parameters>表示音频通道的数量。此参数为可选参数,如果通道数为1,则可以省略,前提是不需要其他参数。

  • 对于视频流,当前未指定编码参数。

  • 注意:RTP 音频格式通常不包括有关每个数据包的样本数的信息。如果需要非默认(如 RTP 音频/视频配置文件中定义的)打包,则使用上面给出的 ptime 属性。

a=recvonly 指定 tools 以仅接收模式启动。它可以是会话级或媒体级属性,并且不依赖于字符集。recvonly 仅对媒体数据生效,而不适用于任何相关的控制协议(例如,基于 RTP 的系统在 recvonly 模式下仍应发送 RTCP 数据包)。(默认为 recvonly 模式)

a=sendrecv 这指定 tools 以发送和接收模式启动。它可以是会话或媒体级属性,并且不依赖于字符集。

a=sendonly 指定 tools 以仅发送模式启动。一个例子可能是:一个单播地址用于流量目的地(向目的发送(sendonly)),另一个不同的单播地址用于流量源(从源接收(recvonly))。在这种情况下,可以使用两种媒体描述,一种仅发送,一种仅接收。它可以是会话级或媒体级属性,但通常仅用作媒体属性。它不依赖于字符集。请注意,sendonly 仅适用于媒体,任何相关的控制协议(例如,RTCP)仍应正常接收和处理。

a=inactive 指定 tools 在非活动模式下启动。这对于用户可以暂停其他用户的交互式会议是必要的。不通过非活动媒体流发送任何媒体。请注意,基于 RTP 的系统应该仍然发送 RTCP,即使启动时处于非活动状态。它可以是会话或媒体级属性,并且不依赖于字符集。

a=orient:<orientation> 通常这仅用于白板或演示工具。它指定屏幕上工作区的方向。它是一个媒体级别的属性。允许的值为portraitlandscapeseascape。它不依赖于字符集。

a=type:<conference type> 这指定了会议的类型。建议值为 broadcastmeetingmoderatedtestH332recvonly 应该是 type:broadcast 会话的默认值,type:meeting 应该暗示 sendrecvtype:moderated 应该表示使用发言控制工具,并且媒体工具启动时静音。指定属性 type:H332 表示该松散耦合会话是 ITU H.332 规范 [26] 中定义的 H.332 会话的一部分,媒体工具应该recvonly启动。建议指定属性 type:test 作为提示,除非另有明确要求,否则接收者可以安全地避免向用户显示此会话描述。a=type:<conference type> 属性是会话级别的属性,它不依赖于字符集。

a=charset:<character set> 指定用于显示会话名称和信息数据的字符集。默认情况下,使用 UTF-8 编码的 ISO-10646 字符集。如果需要更紧凑的表示,则可以使用其他字符集。例如 a=charset:ISO-8859-1 。这是一个会话级属性,不依赖于字符集。

a=sdplang:<language tag> 可以是会话级属性或媒体级属性。作为会话级属性,它指定会话描述的语言。作为媒体级属性,它指定与该媒体相关的任何媒体级 SDP 信息字段的语言。如果会话描述或媒体中的使用多种语言,则可以在会话或媒体级别提供多个 sdplang 属性,在这种情况下,属性的顺序指示会话或媒体中各种语言从最重要的重要性顺序到最不重要。

a=lang:<language tag> 可以是会话级属性或媒体级属性。作为会话级属性,它指定所描述会话的默认语言。作为媒体级属性,它指定该媒体的语言,覆盖任何指定的会话级语言。如果会话描述或媒体使用多种语言,则可以在会话或媒体级别提供多个语言属性,在这种情况下,属性的顺序指示会话或媒体中各种语言的重要性顺序,从最重要到最不重要。

a=framerate:<frame rate> 给出以 frames/sec 为单位的最大视频帧速率。它旨在作为视频数据编码的建议。允许使用符号<integer>.<fraction>的小数值的十进制表示。它是一个媒体级别的属性,只为视频媒体定义,它不依赖于字符集。

a=quality:<quality> 提供整数值的编码质量建议。视频质量属性的目的是指定帧速率和静止图像质量之间的非默认折衷。对于视频,取值范围为 0 到 10,建议含义如下:

  • 10 - 编解码器设计者可以提供的最佳静止图像质量。
  • 5 - 没有质量建议的默认行为。
  • 0 - 编解码器设计者认为仍然可用的最差静止图像质量。

它是一个媒体级别的属性,它不依赖于字符集。

a=fmtp:<format> <format specific parameters> 此属性允许指定特<format specific parameters>以 SDP 不必理解的方式传达。<format> 必须是为媒体指定的格式之一。 <format specific parameters> 未更改地提供给将使用该格式的媒体工具。每种格式最多允许该属性的一个实例。 它是一个媒体级别的属性,它不依赖于字符集。

SDP Offer/Answer

An Offer/Answer Model with the Session Description Protocol (SDP)

SDP 最初被设想为一种描述 Mbone 上承载的多播会话的方式。Session Announcement Protocol (SAP) 被设计为一种多播机制来承载 SDP 消息。尽管 SDP 规范允许单播操作,但它并不完整。多播的所有参与者都使用会话的全局视图;单播会话涉及两个参与者,会话的完整视图需要来自两个参与者的信息,以及他们之间的参数协议。

Mbone ("multicast backbone" 组播主干网的简称)是一种实验性的骨干网和建立在互联网之上的虚拟网络,用于在互联网上承载 IP 组播业务。它开发于1990年代初,需要专门的硬件和软件。由于大多数互联网路由器的运营商由于担心带宽跟踪和计费而禁用了 IP 多播,Mbone 被创建来连接现有互联网基础设施上的多播能力的网络。

例如:

  1. 多播会话需要为特定媒体流传送单个多播地址。但是,对于单播会话,需要两个地址 - 每个参与者一个。
  2. 多播会话需要指示将在会话中使用哪些编解码器。然而,对于单播,编解码器的集合需要通过在每个参与者支持的集合中找到重叠来确定。
  3. ...

因此,尽管 SDP 具有描述单播会话的表达能力,但它缺少实际完成方式的语义和操作细节。目前,互联网上流行基于 SDP 的简单 Offer/Answer 模型来解决这个问题。

在此模型中,会话中的一个 participant 生成 SDP 消息作为 offer 表示 offerer 希望使用的媒体流和编解码器集以及用于接收媒体的 IP 地址和端口。offer 被传达给另一个 participant,称为 answerer 。 answerer 生成一个SDP 消息作为 answer,用于响应 offerer 提供的 offer。answer 为 offer 中的每个流都有一个匹配的 media stream,指示该流是否被接受,以及将使用的编解码器以及 answer 想要用来接收媒体的 IP 地址和端口。

定义

agent:agent 是 offer/answer 交换中涉及的协议实现。offer/answer 交换涉及两个代理。

offer:offerer 发送的 SDP 消息。

offerer:一个 agent,生成 session description 以创建或修改会话。

answer:由 answerer 发送的 SDP 消息,响应来自 offerer 的 offer。

answerer:一个 agent,接收另一个 agent 的 session description (SD) (描述描述期望媒体传播的方方面面),然后用它自己的 session description 进行响应。

media stream :根据 RTSP ,媒体流是单个媒体实例,例如,audio stream 或 video stream 以及 single whiteboard 或 shared application group。在 SDP 中,media stream 由 m= 行及其相关属性来描述

Protocol Operation

offer/answer 交换假设存在更高层协议(例如 SIP),更高层协议能够交换 SDP 以在 agent 之间建立会话。

Protocol operation 开始于一个 agent 向另一个 agent 发送 initial offer。假设高层协议提供了某种上下文的维护,它允许各种 SDP 交换关联在一起,那么如果 offer 超出已经通过更高层协议建立的任何上下文,则 offer 是 initial offer (初始 offer)。

接收 offer 的 agent 可以生成一个 answer,或者它可以拒绝 offer。拒绝 offer 的方法取决于更高层的协议。offer/answer 的交换是原子的,如果答案被拒绝,会话将恢复到提议之前的状态(或者是没有会话)。

在任何时候,任一 agent 都可以生成 update session 的 new offer。但是,如果它收到了尚未 answer 或 reject 的 offer,则它暂时不得生成新的 offer(直到它完成 answer)。并且,如果它已生成的offer尚未收到 answer 或 reject,则它不得生成新的 offer。如果 agent 在发送 offer 之后但在收到 answer 之前收到 offer,则这被视为 "glare" 情况。

"glare" 眩光一词最初用于电路交换电信网络中,用于描述两个交换机都试图同时占用同一干线上的同一可用电路的情况。在这里,这意味着两个 agent 都试图同时发送更新 offer。

高层协议需要提供解决 "glare" 的方法。高层协议需要提供一种在每个方向上对消息进行排序的方法。 比如 SIP 满足这个要求。

Initial Offer

  1. 为 offer/answer 制定的 SDP 可以省略 e=p= 行。
  2. o= 中的 session id 和 version 必须可以用 64位有符号整数表示。version 的初始值必须小于 (2**62)-1, 避免翻转。
  3. 尽管 SDP 规范允许将多个会话描述连接到一个大的 SDP 消息中,但是在 offer/answer 模型中使用的 SDP 消息必须只包含一个会话描述。
    4.SDP 的 s=传达会话的主题,对于多播是合理定义的,但对于单播来说定义不正确。对于单播会话,建议指定为空字符或破折号 (-) 。
  4. SDP 的 t= 传达会话的时间。通常,单播会话的流是通过外部信令手段(例如 SIP)创建和销毁的。在这种情况下,t=0 0
  5. offer 将包含零个或多个 media stream(每个 media stream 由 m= 及其相关属性描述)。零个 media stream 意味着 offerer 希望进行 communicate,但会话的 streams 将在稍后通过 modified offer 添加。流可能是单播和多播的混合;多播显然意味着 c = 行中的多播地址。

每个提供的流的构造取决于流是 Multicast 还是 Unicast 。

Unicast Streams

offer 的 stream 标记

  • 如果 offerer 希望在一个 stream 仅 send media,必须使用 a=sendonly 将 stream 标记为 sendonly 。
  • 如果 offerer 希望在一个 stream 上 only recv media,必须使用 a=recvonly 将 stream 标记为 recvonly 。
  • 如果 offerer 希望 communicate ,但既不 send media 也不 recv media,则必须使用 a=inactive 将 stream 标记为 inactive。
  • 如果 offerer 希望在一个 stream 上 既 send media 又 recv media,可以使用 a=sendrecv 将 stream 标记为 sendrecv,默认是 sendrecv 。

相应地,answer 将对 stream 标记进行 answer 。

media format list

每个 media stream 的 media formats list 传达两条信息:

  • offerer 能够 sending 和/或 receving 的 set of formats(编解码器和与编解码器相关的任何参数,在 RTP 的情况下)
  • 在 RTP 的情况下,用于识别这些 format 的 RTP payload type。

如果列出了多种格式,则意味着 offerer 能够在会话期间使用这些格式中的任何一种。换句话说,answer 可以在会话中间更改格式,使用列出的任何格式,而不发送新的 offer。对于 sendonly 流,offer 应该指明 offerer 愿意为这个流发送的格式。对于 recvonly 流,offer 应该指明 offerer 愿意为这个流接收的格式。对于 sendrecv 流,offer 应该指明 offerer 愿意发送和接收的格式。

相应地,answerer 将对 stream format 进行选择并 answer 。

Multicast Streams

如果会话描述包含一个多播媒体流,它被列为仅接收(发送),这意味着参与者(包括提供者和应答者),只能在该流上接收(发送)。详见 SDP 的初始设计:rfc2327

Answer

anser 提供的会话描述的基于 offer 的会话描述。如果 answer 与 offfer 有任何不同(不同的 IP 地址、端口等),则 answer 中的 o= 行必须不同,因为 answer 是由不同的生成的。在这种情况下,answer o= 行中的版本号与offer o 行中的版本号无关。

对于 offer 中的每个 m= 行,answer 中必须有相应的 m= 行。answer 必须包含与 offer 完全相同数量的 m = 行。这意味着如果 offer 包含零个 m= 行,则 answer 必须包含零个 m= 行。

answer 中的 t= 行必须等于 offer 的 t= 行,否则无法协商会话的时间。

由于任何原因,offer 的 stream 可能会在 answer 中被拒绝。如果 stream 被拒绝,offerer 和 answerer 不得为该 stream 生成媒体(或 RTCP 数据包)。要拒绝 offfer 的stream,answer 中相应 stream 中的 port 必须设置为零

对于单播和多播,为每个提供的流构建 answer 是不同的。

Unicast Streams

如果为 stream 提供 unicast address,则 stream 的 answer 必须包含 unicast address 。

answer 中 stream 的 media type 必须与 offer 的 media type 相匹配。

answer 的流标记

  • 如果 offer 将 stream 标记为 sendonly ,则 stream 在 answer 中必须标记为 recvonly 或inactive。
  • 如果 offer 将 stream 标记为 recvonly ,则 stream 在 answer 中必须标记为 sendonly 或inactive。
  • 如果 offer 将 stream 标记为 sendrecv(或采用默认值:sendrecv),则 stream 在 answer 中可以标记为 sendonly、recvonly、sendrecv 或 inactive。
  • 如果 offer 将 stream 标记为 inactive,则 stream 在 answer 中可以标记为 inactive 。

media format answer

  • 对于在 answer 中标记为 recvonly 的流,answer 的 m=行必须包含至少一种 answerer 愿意的属于 offer 的 media format list 中的媒体格式。stream 可以指示 answer 愿意接收的附加媒体格式,这些格式可以未列在 offer 的 media format list 中。

  • 对于在 answer 中标记为 sendonly 的流,answer 的 m=行必须包含至少一种 answerer 愿意的属于 offer 的 media format list 中的媒体格式。

  • 对于在 answer 中标记为 sendrecv 的流,answer 的 m=行必须包含至少一种 answerer 愿意发送和接收且属于 offer 的 media format list 中的媒体格式。流可以指示 answerer 愿意发送或接收的附加媒体格式,这些格式可以未列在 offer 的 media format list 中。(当然,此时它无法发送它们,因为它没有列在提供)。

  • 对于在 answer 中标记为 inactive 的流,媒体格式列表是根据 offer 构建的。

其他

  • 在 RTP 的情况下,如果在 offer 中使用 payload type 引用了特定的编解码器,则应在 answer 中为该编解码器使用相同的 payload type。即使使用相同的 payload type,answer 也必须包含 rtpmap 属性来定义动态 payload type 的有效 payload type 射,并且应该包含静态 payload type 的映射。 m=行中的媒体格式必须按优先顺序列出,列出的第一个格式是首选。在这种情况下,首选意味着 offer 应该使用 answer 中具有最高偏好的格式。

  • answer 中的连接地址和端口表示 answerer 希望接收媒体的地址(在 RTP 的情况下,RTCP 将在更高的端口上接收,除非另有明确指示)。即使对于 sendonly 流,也必须存在此地址和端口;在 RTP 的情况下,高一端口仍然用于接收 RTCP。

Multicast Streams

与单播不同,单播有两个流视图,多播只有一个流视图。因此,生成对多播提供的 answer 通常涉及修改流的一组有限的方面。

如果一个多播流被接受,answer 中的地址和端口信息必须与提供的 offer。类似地,answer 中的方向性信息(sendonly、recvonly 或 sendrecv)必须等于 offer 的方向性信息。这是因为多播会话中的所有参与者都需要对会话参数有相同的看法,这是 rfc2327 对多播的基本假设。

answer 中的媒体格式集合必须等于或是 offer 中的媒体格式的子集。删除格式是 answer 表示该格式不受支持的一种方式。

如果所有 stream 都没有共同的媒体格式,则拒绝整个提供的会话。

answerer Processing after the answer
一旦 answerer 发送了 answer,它必须准备好接收 answer 标记为 recvonly 或 sendrecv 的媒体,并且它可以立即发送 answer 所标记的 sendonly 或 sendrecv 的媒体。answer 必须准备好接受 answer 中列出的媒体格式的媒体,同时也应该使用 answer 中的 offer 最喜欢的媒体格式发送媒体。

当发送媒体时,它应该使用一个分组间隔,该间隔等于 offer 中的 ptime 属性的值(如果有的话)。它应该使用不高于 offer 中 bandwidth 的带宽发送媒体(如果有的话)。

Offerer Processing of the answer

当 offerer 收到 answer 时,它可以在answer 接受的 stream 上发送媒体(假设它在 answer 中列为 sendrecv 或 recvonly), 它必须使用 answer 中列出的媒体格式发送,并且在发送时应该使用答案中列出的第一个媒体格式。

offerer 应该根据 answer 中的 ptime 和 bandwidth 发送媒体。

offerer 可以立即停止接受 initial offer 中列出但未出现在 answer 中的媒体格式。

修改 session

在 session 期间的任何时候,任何一个 participant 都可以发出新的 offer 来修改 session 的特征。 offer/answer 模型的基本操作是使用上面定义的完全相同的 offer/answer 过程来修改现有会话的参数。

offer 可能与提供给另一方的最后一个 SDP 相同(可能是一个 offer ,也可能是一个 answer ),也可能不同。 我们将提供的最后一个 SDP 称为 "previous SDP"。 如果 offer 相同,则 answer 可能与 answerer 之前的 SDP 相同,也可能不同。 如果提供的 SDP 与之前的 SDP 不同,则会对其构造施加一些限制,如下所述。

新的 offer 几乎可以修改会话的所有方面。 可以添加新的流,可以删除现有的流,并且可以更改现有流的参数。 当发出修改会话的 offer 时,除了o= 中的 version 必须从前一个 SDP 增加一以外,新SDP 的 o= 行的其他信息必须与前一个 SDP 中的相同。 如果 o= 行中的 version 不增加,则 SDP 必须与具有该版本号的 SDP 相同。 answerer 必须准备好接收包含 SDP 且版本未更改的要约; 这实际上是一个空操作。 但是,answerer 必须根据 answer 中定义的程序生成一个有效的答案(可能与来自 answerer 的先前 SDP 相同,也可能不同)。

如果 offer 了一个与之前的 SDP 不同的 SDP,那么新的 SDP 必须为之前的 SDP 中的每个媒体流提供一个匹配的媒体流。 换句话说,如果之前的 SDP 有 N 个 "m=" 行,那么新的 SDP 必须至少有 N 个 "m=" 行。 前一个 SDP 中的第 i 个媒体流(从顶部开始计数)必须匹配新 SDP 中的第 i 个媒体流(从顶部开始计数)。 为了让 answerer 确定新 SDP 中的哪个流对应于前一个 SDP 中的流,这种匹配是必要的。 由于这些要求,流中的“m=”行数永远不会减少,而是保持不变或增加。 从先前的 SDP 中删除的媒体流不得在新的 SDP 中删除; 但是,删除的流的属性(a=行)不需要存在。

Adding a Media Stream
方法一:在现有的媒体描述 m= 最下方,附加新的媒体描述 m=
方法二:重用旧媒体提供的 "slot" 创建(旧媒体在 "previous SDP" 已通过将其端口设置为零来禁用,相当于现在重新启用)。

当 answerer 从 offerer 处接收到比之前的 SDP 具有更多媒体描述的 SDP,或者它在端口先前为零的 "slot" 中接收到具有媒体流的 SDP 时,answerer 便知道正在添加新的媒体流。 这可以通过在 answer 中放置适当结构化的媒体描述来拒绝或接受。

Removing a Media Stream
通过在新的 SDP 将流的端口号设置为零来删除现有的媒体流。 已删除的流描述可以省略之前存在的所有属性(a=行),并且可以只列出一种媒体格式。offer 零端口的流必须在 answer 中用端口零标记。 与 offer 一样,answer 能会省略之前存在的所有属性(a=行),并且可以仅列出 offer 中的一种媒体格式。

媒体流的移除意味着不再为该流发送媒体,并且任何接收到的媒体都将被丢弃。 在 RTP 的情况下,RTCP 传输也会停止,任何接收到的 RTCP 数据包的处理也会停止。 任何与之相关的资源都可以被释放。 例如,用户界面可能会通过关闭 PC 上的相关窗口来指示流已终止。

修改 Media Stream

几乎可以修改媒体流的所有特征。

Modifying Address, Port or Transport

流的端口号可以更改。 为此,offerer 可以修改之前的 SDP 中的描述, m= 行中的端口号与之前 SDP 中的不同。 如果只改变端口号,媒体流描述的其余部分应该保持不变。 offerer 必须准备好在发送 offer 后立即在旧端口和新端口上同时接收媒体。 在收到 answer 并且媒体到达新端口之前,提供者不应停止在旧端口上监听媒体,这样可以避免过渡期间丢失媒体。通常,直到新端口上的媒体到达播放缓冲区的顶部时,才会停止监听旧端口上的媒体。

answer 中对应的 media stream 可能与应答者上一个 SDP 中的流相同,也可能不同。 如果 answerer 接受更新的 media stream,answerer 应该立即开始将该流的流量发送到新端口(send 时),并且 answerer 需要在发送 answer 后立即在旧端口和新端口上接收媒体(recv 时)。 在媒体到达新端口之前,answerer 不得停止监听旧端口上的媒体。

当然,如果提供的流被拒绝,offerer 一旦收到拒绝就可以停止使用新端口接收。

IP address 和 transport 可按照更改端口号的相同过程进行更改。

Changing the Set of Media Formats

会话中使用的媒体格式列表更改。 为此,offer 创建一个新的媒体描述,其中 m= 行中的媒体格式列表与先前 SDP 中的相应媒体流不同。 此列表可能包括新格式,并且可能删除以前 SDP 中存在的格式。 但是,在 RTP 的情况下,从特定动态 payload type 到该 media stream 中特定编解码器的映射在会话期间不得更改。 例如,如果 A 生成一个将 G.711 分配给动态 payload_type = 46 的 offer,则 payload_type = 46 必须从该点开始在会话中该媒体流的任何 offer 或 answer 中引用 G.711。 但是,可以将多个 payload_type 映射到同一个编解码器,以便更新的 offer 也可以将 payload_type = 72 用于 G.711。

当 agent 停止使用媒体格式(通过不在 offer 或 answer 中列出该格式,即使它在以前的 SDP 中),agent 仍需要准备在短时间内接收具有该格式的媒体。

  • agent 如何知道何时可以准备停止使用该格式接收?
  • 如果 agent 需要知道,可以应用三种技术:
    • 首先,代理除了改变格式之外还可以改变端口。当媒体到达新端口时,它知道对等方已停止使用旧格式发送,它可以停止准备接收旧格式。这种方法的好处是独立于媒体格式。但是,端口的更改可能需要更改资源预留或重新设置安全协议的密钥。
    • 第二种方法是在丢弃一个编解码器时为所有编解码器使用一组全新的动态 payload type。当接收到具有新负载类型之一的媒体时,代理知道对等方已停止使用旧格式发送。这种方法不会影响保留或安全上下文,但它是 RTP 特定的并且浪费了非常小的有效 payload type 空间。
    • 第三种方法是使用计时器。当收到来自对端的 SDP 时,设置定时器。当它触发时,agent 可以停止准备以旧格式接收。一分钟的值通常会绰绰有余。
  • 在某些情况下,agent 可能不在乎,因此会不断地准备接收旧格式。在这种情况下不需要做任何事情。

当然,如果所 offer 的流被拒绝,则一旦收到拒绝,就可以停止准备使用任何新格式接收该流。

Changing Media Types

流的媒体类型(音频、视频等)可以更改。 通常,在传输相同的逻辑数据但只是采用不同的媒体格式时,可以更改媒体类型(而不是添加新流)。 这对于在单个流中的语音带传真和传真之间进行切换特别有用,它们都是独立的媒体类型。 为此,offerer 可以修改之前的 SDP 中的描述。

假设流是可接受的,answerer 在收到 offer 后立即开始使用新的媒体类型和格式发送。 offerer 必须准备好同时接收新旧类型的媒体,直到收到 answer 且接收到新类型的媒体到达播放缓冲区的顶部。

Changing Attributes

媒体描述中的任何其他属性都可以在 offer 或 answer 中更新。 通常,一旦收到带有更改的 SDP,agent 必须使用新参数发送媒体(如果流的方向性允许)。

举例

Basic Exchange

initial offer
假设 caller - Alice 在她的提议中包含以下描述。它包括一个双向音频流和两个双向视频流,使用 H.261(有效负载类型 31)和 MPEG(有效负载类型 32)。提供的 SDP 是:

   v=0
   o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
   s=
   c=IN IP4 host.anywhere.com
   t=0 0
   m=audio 49170 RTP/AVP 0
   a=rtpmap:0 PCMU/8000
   m=video 51372 RTP/AVP 31
   a=rtpmap:31 H261/90000
   m=video 53000 RTP/AVP 32
   a=rtpmap:32 MPV/90000

answer
callee - Bob 不想接收或发送第一个视频流,因此他返回以下 SDP 作为 answer:

   v=0
   o=bob 2890844730 2890844730 IN IP4 host.example.com
   s=
   c=IN IP4 host.example.com
   t=0 0
   m=audio 49920 RTP/AVP 0
   a=rtpmap:0 PCMU/8000
   m=video 0 RTP/AVP 31
   m=video 53000 RTP/AVP 32
   a=rtpmap:32 MPV/90000
  • m=video 0 RTP/AVP 31 将 media stream 拒绝。

updated offer
稍后,Bob 决定更改他将接收和发送音频流的端口(从 49920 到 65422),同时添加一个额外的音频流作为仅接收,使用事件的 RTP 有效负载格式 [9] 。Bob 在 offer 中提供以下 SDP:

   v=0
   o=bob 2890844730 2890844731 IN IP4 host.example.com
   s=
   c=IN IP4 host.example.com
   t=0 0
   m=audio 65422 RTP/AVP 0
   a=rtpmap:0 PCMU/8000
   m=video 0 RTP/AVP 31
   m=video 53000 RTP/AVP 32
   a=rtpmap:32 MPV/90000
   m=audio 51434 RTP/AVP 110
   a=rtpmap:110 telephone-events/8000
   a=recvonly

answer
Alice 接受新的对端端口和附加的媒体流,因此生成以下 answer:

   v=0
   o=alice 2890844526 2890844527 IN IP4 host.anywhere.com
   s=
   c=IN IP4 host.anywhere.com
   t=0 0
   m=audio 49170 RTP/AVP 0
   a=rtpmap:0 PCMU/8000
   m=video 0 RTP/AVP 31
   a=rtpmap:31 H261/90000
   m=video 53000 RTP/AVP 32
   a=rtpmap:32 MPV/90000
   m=audio 53122 RTP/AVP 110
   a=rtpmap:110 telephone-events/8000
   a=sendonly

N 个 Codec 的选择

initial offer
Alice 发往 Bob 的 initial offer 指定一个支持 3 种音频 codec 的 audio stream,audio stream 暂时被置为 inactive(a=inactive),因为 media 无法被接收直到一个 codec 被锁定。

   v=0
   o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
   s=
   c=IN IP4 host.anywhere.com
   t=0 0
   m=audio 62986 RTP/AVP 0 4 18
   a=rtpmap:0 PCMU/8000
   a=rtpmap:4 G723/8000
   a=rtpmap:18 G729/8000
   a=inactive

answer
Bob 仅支持 PCMU 和 G.723 。因此,他发送了以下 answer:

   v=0
   o=bob 2890844730 2890844731 IN IP4 host.example.com
   s=
   c=IN IP4 host.example.com
   t=0 0
   m=audio 54344 RTP/AVP 0 4
   a=rtpmap:0 PCMU/8000
   a=rtpmap:4 G723/8000
   a=inactive

updated offer
然后 Alice 可以选择这两种编解码器中的任何一种。因此,她使用 sendrecv 流发送更新的 offer(锁定一种编码器):

   v=0
   o=alice 2890844526 2890844527 IN IP4 host.anywhere.com
   s=
   c=IN IP4 host.anywhere.com
   t=0 0
   m=audio 62986 RTP/AVP 4
   a=rtpmap:4 G723/8000
   a=sendrecv

answer
Bob 接收该编码器,因此 answer :

   v=0
   o=bob 2890844730 2890844732 IN IP4 host.example.com
   s=
   c=IN IP4 host.example.com
   t=0 0
   m=audio 54344 RTP/AVP 4
   a=rtpmap:4 G723/8000
   a=sendrecv

SDP for the WebRTC

WebRTC SDP

[WebRTC] 提出 JavaScript 应用程序来完全指定和控制多媒体会话的信令平面。 JSEP(Javascript Session Exchange Protocol) 提供了创建会话特征和媒体定义信息的机制,以根据 SDP 交换(offers/answers)开展会话。在这种情况下,SDP 有两个目的:1. 提供语法结构。2. 语义上传达参与者的意图和能力( 构造 offers/answers,描述(媒体和非媒体)流)用于成功地协商会话。

[WebRTC] 不捕获标准SDP的所有内容,[WebRTC] 的 SDP 可如下分解:


                                                 +---------------------+
                                                 |        v=           |
                                                 +---------------------+
                 +---------------------+         +---------------------+
         ====    |   Session Metadata  |  =====  |        o=           |
         |       +---------------------+         +----------------------
         |                                       +---------------------+
         |                                       |        t=           |
         |                                       +---------------------+
         |
         |
         |                                       +---------------------+
         |                                       |        c=           |
         |                                       +---------------------+
         |       +---------------------+
         ====    | Network Description |   =====
         |       +---------------------+
         |                                       +---------------------+
         |                                       |    a=candidate      |
         |                                       +---------------------+
         |
         |
         |                                       +---------------------+
         |                                       |        m=           |
         |                                       +---------------------+
         |        +---------------------+        +---------------------+
         ====     | Stream Description  |  ===== |      a=rtpmap       |
         |        +---------------------+        +----------------------
         |                                       +---------------------+
         |                                       |      a=fmtp         |
         |                                       +---------------------+
         |                                       +---------------------+
         |                                       |      a=sendrecv..   |
         |                                       +---------------------+
 +---------------+
 |    SEMANTIC   |
 | COMPONENTS OF |
 |     SDP       |
 +---------------+
         |                                       +---------------------+
         |                                       |      a=crypto       |
         |                                       +---------------------+
         |         +---------------------+       +---------------------+
         ====      |Security Descriptions|  =====|      a=ice-frag     |
         |         +---------------------+       +----------------------
         |                                       +---------------------+
         |                                       |      a=ice-pwd      |
         |                                       +---------------------+
         |                                       +---------------------+
         |                                       |     a=fingerprint   |
         |                                       +---------------------+
         |
         |
         |
         |                                       +---------------------+
         |                                       |      a=rtcp-fb      |
         |                                       +---------------------+
         |         +---------------------+       +---------------------+
         ====      |   Qos,Grouping      |       |                     |
                   |   Descriptions      |  =====|       a=group       |
                   +---------------------+       +----------------------
                                                 +---------------------+
                                                 |       a=rtcpmux     |
                                                 +---------------------+

WebRTC SDP Attribute

a=setup
[RFC4145] - 介绍 a=setup:role

Setup Attribute:
a=setup:role (role = active \ passive \ actpass \ holdcnn)

  • active:endpoint 将发起传出连接。
  • passive:endpoint 将接收传入连接。
  • actpass:endpoint 愿意接受传入连接或发起传出连接。
  • holdconn:endpoint 暂时不希望建立连接。

Offer/Answer Model:

            Offer      Answer
            ________________
            active     passive / holdconn
            passive    active / holdconn
            actpass    active / passive / holdconn
            holdconn   holdconn

DTLS - role:
a=setup

  • active:endpoint 将发起 DTLS Handshake (ClientHello)。
  • passive:endpoint 将接收 DTLS Handshake(ServerHello)。
  • actpass:endpoint 愿意接受 DTLS Handshake 或 DTLS Handshake。
  • holdconn:endpoint 暂时不希望建立连接。

a=fingerprint
[rfc5763] - DTLS certificate fingerprint

offer/answer model :
offer:
offerer 可选择 actpass ,由 answerer 选择 active 或 passive,offerer 在 offer 种携带 offerer 证书指纹 a=fingerprint 用于 DTLS 握手完成证书验证。
a=setup:actpass
a=fingerprint:sha-1 99:41:49:83:4a:97:0e:1f:ef:6d:f7:c9:c7:70:9d:1f:66:79:a8:07
answer:
当 answerer 接受 offer 时,它会向 offerer 提供一个包含 answerer 证书指纹的 answer 。
a=setup:active
a=fingerprint:sha-1 c9:c7:70:9d:1f:66:79:a8:07:99:41:49:83:4a:97:0e:1f:ef:6d:f7
此时 answerer 将作为 active 主动发起 DTLS 握手(可以和 answer 并行发起)。但是在DTLS 握手途中,offerer 可能只是暂时接受 answerer 的证书,因为它可能还没有 answerer 的证书指纹(可能由于并行发起的原因,还没收到 answer)。在 DTLS 握手种需要通过 certificate
fingerprint 验证 certificate 。

a=crypto
[rfc4568] - SDP Security Descriptions for Media Streams
a=crypto:<tag> <crypto-suite> <key-params> [<session-params>]

一个名为crypto的新媒体级 SDP 属性描述了单播媒体线路的加密套件、密钥参数和会话参数。 a=crypto必须仅出现在 SDP 媒体级别(而不是会话级别)。

a=crypto:1 AES_CM_128_HMAC_SHA1_80
inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32
<crypto-suite> 是 AES_CM_128_HMAC_SHA1_80,<key-params> 由以inline: 开头的文本定义,此处省略 session-params。

在 offer/answer model 中,<crypto-suite> 是一个协商参数。

注:可以不使用 a=crypto,因为在 DTLS Handshake 中会协商加密套件、密钥参数和对称密钥。

a=ice-ufrag and a=ice-pwd
rfc5245 - Interactive Connectivity Establishment (ICE)
a=ice-ufrag:username_fragment
a=ice-pwd:password

ice-ufragice-pwd 作为 ICE 消息校验的 username fragment 和 password。

  • ice-ufrag 与 Interactive Connectivity Establishment (ICE) 一起使用,提供用于在 STUN 的端到端连接检查中构造 username 的 fragment。
  • ice-pwd 与 Interactive Connectivity Establishment (ICE) 一起使用,提供用于 STUN 的端到端连接检查的 password 。

example:
a=ice-ufrag:8hhY
a=ice-pwd:asd88fgpdd777uzjYhagZg

ice-pwdice-ufrag 属性可以出现在会话级别或媒体级别。当两者都存在时,媒体级别中的值优先。因此,会话级别的值实际上是适用于所有媒体流的默认值,除非被媒体级别的值覆盖。如果两个媒体流具有相同的 ice-ufrag,它们必须具有相同的 ice-pwd。

ice-ufragice-pwd 属性值必须在会话开始时随机选择。 ice-ufrag 属性值必须包含至少 24 位的随机性,而 ice-pwd 属性值必须包含至少 128 位的随机性。这意味着 ice-ufrag 属性至少有 4 个字符长,ice-pwd 至少有 22 个字符长,因为这些属性的语法允许每个字符有 6 位随机性。当然最多 256 个字符。

a=candidate
rfc5245 - Interactive Connectivity Establishment (ICE)

a=candidate 与 Interactive Connectivity Establishment (ICE) 一起使用,并提供许多通信候选地址。这些地址通过使用 Session Traversal Utilities for NAT (STUN) 的端到端连接检查进行验证。

a=candidate:<foundation> <component-id> <transport> <priority> <connection-address> <port> <cand-type> [rel-addr] [rel-port]

  • <foundation>:由 1 到 32 个 <ice-char> 组成。它是一个标识符,对于具有相同类型、共享相同 base 并且来自相同 STUN 服务器的两个 candidates 是等效的。foundation 用于优化 Frozen 算法中的 ICE 性能。
  • <component-id>:是一个介于 1 和 256 之间的正整数,用于标识作为 candidate 的 media stream 的特定组件。它必须从 1 开始,并且必须为特定 candidate 的每个组件增加 1。对于基于 RTP 的媒体流, RTP 的 candidates 必须 <component-id> 为 1 ,RTCP 的 candidates 必须 <component-id> 为 2。其他类型的媒体流若需要多个 component 的必须制定映 components 到 <component-id> 的映射。有关将 ICE 扩展到新媒体流的更多讨论,请参见:rfc5245
  • <transport>:指示 candidate 的传输协议。rfc5245 仅定义 UDP。然而,未来可以支持更多的传输协议与 ICE 一起使用,例如 TCP rfc6544
  • <priority>:一个 1 - (2 * * 31 - 1) 的正整数,表示 candidate 被 check 的优先级,priority 越大,优先级越高。
  • <connection-address>:candidate 的 IP 地址,允许使用 IPv4 地址、IPv6 地址和 fully qualified domain names (FQDNs) 。解析此字段时,agent 可以通过其值中是否存在冒号来区分 IPv4 地址和 IPv6 地址 - 存在冒号表示 IPv6。虽然应该使用 IP 地址,但可以使用 FQDN 代替 IP 地址。在这种情况下,当接收到在 a=candidate 属性中包含 FQDN 的 offer 或 answer 时,首先使用 AAAA 记录在 DNS 中查找 FQDN(假设 agent 支持 IPv6),如果未找到结果或 agent 仅支持 IPv4,则使用 A 记录在 DNS 中查找 FQDN。如果 DNS 查询返回多个 IP 地址,则选择一个用于接下来的 ICE 处理。
  • <port>:candidate 的 port 。
  • <cand-type>:candidate 的类型。host 表示 host、srflx表示server reflexiveprflx表示peer reflexiverelay表示relayed candidate 。可在未来扩展更多 candidate type。
  • <rel-addr><rel-port>:描述与 candidate 相关的传输地址,用于诊断和其他目的。 <rel-addr><rel-port> 必须存在于 server reflexivepeer reflexiverelayed candidate 类型的 candidate 中。如果 candidate 是 server reflexivepeer reflexive,则 <rel-addr><rel-port> 等于 server reflexivepeer reflexive candidate 的 base(理解:表示自己的真实地址,方便 server reflexivepeer reflexive 进行转发)。如果 candidate 是 relayed candidate,则 <rel-addr> 和 <rel-port> 等于中继服务器分配响应中为客户端提供的 relayed candidate 的映射地址。如果 candidate 是 host 类型,则省略 <rel-addr> 和 <rel-port>。

server reflexive , peer reflexiverelay 在 ICE 中进行讨论。

例如:
a=candidate:0 1 UDP 2122194687 192.168.1.4 54609 typ host
a=candidate:0 2 UDP 2122194687 192.168.1.4 54609 typ host
a=candidate:1 1 UDP 1685987071 24.23.204.141 64678 typ srflx raddr 192.168.1.4 rport 54609
a=candidate:1 2 UDP 1685987071 24.23.204.141 64678 typ srflx raddr 192.168.1.4 rport 54609

a=ice-options
rfc5245 - Interactive Connectivity Establishment (ICE)
ice-options 是会话级属性。它包含一系列标识 agent 支持的选项的标记。
例如:
a=ice-options:trickle

a=mid 和 a=group
rfc5888-The Session Description Protocol (SDP) Grouping Framework 定义了一个框架来对 SDP 中的 m 行进行分组,以用于不同的目的。该框架使用 a=groupa=mid 的 SDP 属性。

a=mid:identification-tag 指定 "Media Stream Identification" 标识 SDP 中的一个 media stream 。

a=group:semantics 为会话级属性,用于将不同的媒体流分组在一起。

  • semantics=LS / FID / semantics-extensionrfc5888 定义了两个标准 semantics: Lip Synchronization (LS) 和 Flow Identification (FID)。LS 语义适用于必须与音频流同步的视频流,当然两个相同类型的流的播放也可以使用 LS 规定同步。FID 语义将组合在一起的几个 m 行形成一个媒体流,处理包含多个“m”行的媒体流的媒体 agent 必须将媒体的副本发送到作为流一部分的每个“m”行,只要编解码器和方向属性存在于特定的“m”行中允许它。(LS 或 FID 后跟空格间隔的 mid)

例如:
Examples of LS

          v=0
          o=Laura 289083124 289083124 IN IP4 one.example.com
          c=IN IP4 192.0.2.1
          t=0 0
          a=group:LS 1 2
          m=audio 30000 RTP/AVP 0
          a=mid:1
          m=video 30002 RTP/AVP 31
          a=mid:2

Examples of FID

            v=0
            o=Laura 289083124 289083124 IN IP4 three.example.com
            c=IN IP4 192.0.2.1
            t=0 0
            a=group:FID 1 2
            m=audio 30000 RTP/AVP 3
            a=rtpmap:3 GSM/8000
            a=mid:1
            m=audio 30002 RTP/AVP 97
            a=rtpmap:97 AMR/8000
            a=fmtp:97 mode-set=0,2,5,7; mode-change-period=2; mode-change-neighbor; maxframes=1
            a=mid:2

a new SDP Grouping Framework extension —— 'BUNDLE'

BUNDLE 扩展可以与 SDP Offer/Answer 机制一起使用来协商一组 m= section,这些 m= section 将成为 BUNDLE group 的一部分。BUNDLE 扩展可以与 SDP Offer/Answer 机制一起使用,以协商使用单个传输(5 元组)来发送和接收由多个 SDP 媒体描述(m= section)描述的媒体。这种传输称为 BUNDLE 传输,媒体称为捆绑媒体。

BUNDLE 扩展使用具有 BUNDLE 语义值 的 SDP group 属性来实现扩展。每个 bundled m= section 被分配一个标识标签(mid),每个标识标签都列在 SDP group:BUNDLE 的标识标签列表中。标识标签列表中列出的标识标签的每个 m= section 都与给定的 BUNDLE group 相关联。

SDP 主体可以包含多个 BUNDLE groups。任何给定的 bundled m= section 在任何给定时间都不得与多个 BUNDLE group 相关联。

offer:

     v=0
     o=alice 2890844526 2890844526 IN IP6 2001:db8::3
     s=
     c=IN IP6 2001:db8::3
     t=0 0
     a=group:BUNDLE foo bar

     m=audio 10000 RTP/AVP 0 8 97
     b=AS:200
     a=mid:foo
     a=rtcp-mux
     a=rtpmap:0 PCMU/8000
     a=rtpmap:8 PCMA/8000
     a=rtpmap:97 iLBC/8000
     a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid

     m=video 10002 RTP/AVP 31 32
     b=AS:1000
     a=mid:bar
     a=rtcp-mux
     a=rtpmap:31 H261/90000
     a=rtpmap:32 MPV/90000
     a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
  • a=group:BUNDLE foo bar 提议 foo 和 bar 进行 bundle 。
    answer:
     v=0
     o=bob 2808844564 2808844564 IN IP6 2001:db8::1
     s=
     c=IN IP6 2001:db8::1
     t=0 0
     a=group:BUNDLE foo bar

     m=audio 20000 RTP/AVP 0
     b=AS:200
     a=mid:foo
     a=rtcp-mux
     a=rtpmap:0 PCMU/8000
     a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid

     m=video 0 RTP/AVP 32
     b=AS:1000
     a=mid:bar
     a=bundle-only
     a=rtpmap:32 MPV/90000
     a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
  • m=video 0 RTP/AVP 32 端口置为 0,a=bundle-only 表示接受 bundle,因此 foo 和 bar 成为 bundle media 并使用 bundle transport (foo的端口被保留,bar的端口被置零,显然是采用 foo 的端口进行传输)进行发送和接受数据

offer/answer 对 bundle 的协商,详见:a new SDP Grouping Framework extension —— 'BUNDLE'

a=ssrc 和 a=ssrc-group
会话描述协议 (SDP) 提供了一种机制来描述多媒体会话中的多媒体会话和各个媒体流的属性,但不提供任何机制来描述多媒体会话中的一个媒体流中的各个媒体源。rfc5576-Source-Specific Media Attributes in the Session Description Protocol (SDP) 定义了一种机制来描述 RTP 媒体源,这些 RTP 媒体源在 SDP 中由它们的同步源 (SSRC) 标识符标识,以将属性与这些源相关联,并表达源之间的关系。

a=ssrc:<ssrc-id> <attribute>
a=ssrc:<ssrc-id> <attribute>:<value>

SDP 媒体属性 ssrc 表示 RTP 会话内的媒体源的属性(称为“源级属性”)。 <ssrc-id> 是正在描述的源的同步源 (SSRC) ID,值为网络字节顺序中的 32 位无符号整数,并以十进制表示。 <attribute><attribute>:<value> 表示给定媒体源的源级属性,源级属性遵循 SDP "a=" 行的语法。因此,源级属性由单个属性名称(标志)或属性名称和值组成,例如 cname:user@example.com

<attribute><attribute>:<value>

  • a=ssrc:<ssrc-id> cname:<cname>
  • a=ssrc:<ssrc-id> previous-ssrc:<ssrc-id> ...
  • a=ssrc:<ssrc> fmtp:<format> <format specific parameters>
  • ...

a=ssrc-group:<semantics> <ssrc-id> ...

SDP 媒体属性 ssrc-group 表示一个 RTP 会话的几个媒体源之间的关系。它类似于 group 会话级属性 [RFC3388],group 表达了 SDP 多媒体会话中媒体流之间的关系(即几个逻辑相关的 RTP 会话之间的关系)。

由于源已经由它们的 SSRC ID 标识,因此不需要类似于 mid 属性的属性,ssrc-group 直接使用 <ssrc-id> 组成。

<semantics> 参数取自 group 属性 [RFC3388] 的规范。为 ssrc-group 属性定义的初始语义值是 FID(流标识)[RFC3388] 和 FEC(前向纠错)[RFC4756]。在每种情况下,ssrc-group 之间的关系与使用 SDP group 属性分组的媒体流中的相应源之间的关系相同。

尽管 ssrc-group 语义值遵循与 group 语义值相同的语法,但它们是独立定义的。所有 ssrc-group 语义值必须使用 [rfc5576] 第 12.3 节中定义的注册表向 IANA 注册。

例如:

   m=video 49174 RTP/AVPF 96 98
   a=rtpmap:96 H.264/90000
   a=rtpmap:98 rtx/90000
   a=fmtp:98 apt=96;rtx-time=3000
   a=ssrc-group:FID 11111 22222
   a=ssrc:11111 cname:user3@example.com
   a=ssrc:22222 cname:user3@example.com
   a=ssrc-group:FID 33333 44444
   a=ssrc:33333 cname:user3@example.com
   a=ssrc:44444 cname:user3@example.com

a=msid
WebRTC MediaStream Identification in the Session Description Protocol

W3C WebRTC API 规范指定 WebRTC 实体之间的通信是通过包含 MediaStreamTracks 的 MediaStreams 完成的。 MediaStreamTrack 通常在 RTP 会话中使用单个 SSRC 承载。

SDP 给出基于 m 行的描述,每个 m 行只描述一个媒体源,如果期望在一个 RTP 会话中携带多个媒体源,则需要使用 BUNDLE I-D.ietf-mmusic-sdp-bundle-negotiation 进行协商;如果不使用 BUNDLE,则每个媒体源都承载在其自己的 RTP 会话中。

SDP group 框架 [RFC5888] 可用于对 m 行进行分组。但是,有时应用程序需要指定一些有关 mgroup 之间关联的应用程序级信息。这在使用 SDP 分组框架时是不可能的。

WebRTC 定义了一个映射,SDP 中的 m 行用于描述每个 MediaStreamTrack,并且使用了 BUNDLE 机制 I-D.ietf-mmusic-sdp-bundle-negotiation 将 MediaStreamTracks 分组到 RTP 会话中。因此,需要为每个 m 行指定一个 MediaStreamTrack 的 ID (MSID)将其关联 MediaStream,这可以通过media-level 的 SDP attribute (a=msid)来完成。

a=msid:<msid-id> [msid-appdata]

  • msid-id 为 MediaStreamTrack 所属的 MediaStream 的 ID。
  • msid-appdata 为 MediaStreamTrack 自身的 ID 。

例如:
msid:examplefoo examplebar

msid 更新:

  • 当描述中出现新的 msid “标识符”值时,接收者可以向其应用程序发出信号,表明已添加了新的 MediaStream。
  • 当描述更新为具有更多具有相同 msid“标识符”值但不同“appdata”值的媒体部分时,接收者可以向其应用程序发出信号,表明新的 MediaStreamTracks 已添加到 MediaStream。
  • 当描述更新为不再列出特定媒体描述的 msid 属性时,接收者可以向其应用程序发出信号,表明相应的 MediaStreamTrack 已结束。

a=rtcp
当RTCP端口不是媒体行中描述的 RTP 端口之后的下一个更高(奇数)端口号时,a=rtcp用于描述用于媒体流的 RTCP 端口。
a=rtcp:port [nettype space addrtype space connection-address]
例如:
m=audio 49170 RTP/AVP 0
a=rtcp:53020

m=audio 49170 RTP/AVP 0
a=rtcp:53020 IN IP4 126.16.64.4

m=audio 49170 RTP/AVP 0
a=rtcp:53020 IN IP6 2001:2345:6789:ABCD:EF01:2345:6789:ABCD

a=rtcpmux
rfc5761
当 SDP 按照 offer/answer model 协商 RTP session 时,a=rtcp-mux表示希望将 RTP 和 RTCP 复用到一个端口。initial offer 必须在 media-level 包含a=rtcp-mux,以请求在单个端口上多路复用 RTP 和 RTCP。

a=rtcp-fb
rfc4585

SDP 属性a=rtcp-fb用于指定 RTCP 反馈的能力。 a=rtcp-fb属性必须仅用作 SDP 媒体属性,不得在会话级别提供。 a=rtcp-fb属性必须仅用于指定了 AVPF 的媒体会话(例如:m=audio 49170 RTP/AVPF 98)。

a=rtcp-fb:<rtcp-fb-pt> <rtcp-fb-val>

  • <rtcp-fb-pt>
    • * 表示应用于所有 formats
    • fmt 表示应用于指定 format (在 RTP 中为 payload type)
  • <rtcp-fb-val>
    • nack rtcp-fb-nack-paramnack表示 feedback type,[rfc4585]中定义了以下三个参数,用于与 nack 和媒体类型 video 一起使用:pli表示使用 Picture Loss Indication 反馈,sli表示使用 Slice Loss Indication 反馈,rpsi表示使用 Reference Picture Selection Indication 反馈。
    • ack rtcp-fb-ack-param
    • rtcp-fb-id rtcp-fb-param
    • trr-int 1*DIGIT
         m=audio 49170 RTP/AVPF 98
         a=rtpmap:98 H263-1998/90000
         a=rtcp-fb:98 nack pli

Offer-Answer
当与 offer/answer model 结合使用时,offerer 可以向其 peer 呈现一组这些 AVPF 属性。answerer 必须删除所有它不理解的属性以及它一般不支持或不希望在此特定媒体会话中使用的属性。answerer 不得将反馈参数添加到媒体描述中,也不得更改此类参数的值(answerer只有删除这些属性的权力)。answer 对媒体会话具有约束力,offerer 和 answerer 都必须使用以这种方式协商的 feedback 机制。offerer 和 answerer 都可以独立决定仅发送协商 feedback 机制子集的 RTCP feedback 消息,但它们需要能够接收且正确处理协商 feeback 机制集合中的所有 RTCP feedback 消息。

a=extmap
rfc8285-A General Mechanism for RTP Header Extensions
当 SDP 用于 RTP 会话时,SDP 的 extmap 属性的存在可以诊断是否使用某种类型的RTP header extension。
a=extmap:<value>["/"<direction>] <URI> <extensionattributes>

  • <value> :是此扩展的本地标识符 (ID),是有效范围内的整数(0 保留用于两种形式的填充,15 保留在 one-byte header 的 形式)。解释:RTP 数据包中的每个扩展元素都有一个本地标识符 (ID) 和一个长度。流中存在的本地标识符必须经过协商或带外定义。本地标识符没有静态分配。每个不同的扩展必须有一个唯一的 ID。 ID 值 0 保留用于填充,不得用作本地标识符。RTP 报头扩展由一系列扩展元素组成,并带有可能的填充(使用 0 值作为 padding,因此 0 不能用作本地标识符)。
One-Byte Header
       0
       0 1 2 3 4 5 6 7
      +-+-+-+-+-+-+-+-+
      |  ID   |  len  |
      +-+-+-+-+-+-+-+-+
4 位 ID 是此元素的本地标识符,范围为 1-14(含)
本地标识符值 15 为将来的扩展保留,不得用作标识符。

Two-Byte Header
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |       ID      |     length    |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • <direction> :可选项,sendonly, recvonlysendrecvinactive 之一,默认为 recvonly,表示 RTP header extension 存在的方向。
  • <URI>:一个 URI 。每个 RTP header extension 都由一个 URI 命名。对于 RFC 中定义的扩展,使用的 URI 应该是一个以 urn:ietf:params:rtp-hdrext: 开头的 URN,后跟一个注册的描述性名称。命名 URI 形式到规范引用的映射由 IANA 管理,根据 [RFC8126] 中定义的“专家审查”的要求,插入此注册表,注册要求在[ rfc8285.html#section-10]。

avt-example-metadata 作为 header extension 且由 RFC 定义的名称示例:

  • urn:ietf:params:rtp-hdrext:avt-example-metadata

非来自 IETF(RFC) 定义的名称示例:

  • http://example.com/082005/ext.htm#example-metadata

例如:
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://example.com/082005/ext.htm#ttime
a=extmap:3/sendrecv http://example.com/082005/ext.htm#xmeta short

Examples

Basic Examples

Audio Only Session

Audio/Video Session

Audio, Video Session with BUNDLE Support Unknown

Audio, Video Session with BUNDLE Unsupported

.....

MultiResolution, RTX, FEC Examples

Sendonly Simulcast Session with 2 cameras and 2 encodings per camera

Successful SVC Video Session

.....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容