RTSP Spec中文版(12-16)

RTSP Spec中文版(1-11)
RTSP Spec中文版(12-16)
RTSP Spec中文版(附录)

12 头域定义(Header Field Definitions)

HTTP/1.1或其他未标准化头域未在这里列出,接收者应当忽略暂未公认定义的头域。

下表中列出了RTSP中使用到的头域,类型"g"表示通用头,类型"R"表示请求头,类型"r"表示回复头,类型"e"表示实体头。标记为"req."的头说明是必需的(required),标记为"opt."表示可选的(optional)。"entity"表示所有方法应当含有消息主体。

Header type support methods
Accept R opt. entity
Accept-Encoding R opt. entity
Accept-Language R opt. all
Allow r opt. all
Authorization R opt. all
Bandwidth R opt. all
Blocksize R opt. all but OPTIONS, TEARDOWN
Cache-Control g opt. SETUP
Conference R opt. SETUP
Connection g req. all
Content-Base e opt. entity
Content-Encoding e req. SET_PARAMETER
Content-Encoding e req. DESCRIBE, ANNOUNCE
Content-Language e req. DESCRIBE, ANNOUNCE
Content-Length e req. SET_PARAMETER, ANNOUNCE
Content-Length e req. entity
Content-Location 3 opt. entity
Content-Type e req. SET_PARAMETER, ANNOUNCE
Content-Type r req. entity
CSeq g req. all
Date g opt. all
Expires e opt. DESCRIBE, ANNOUNCE
From R opt. all
If-Modified-Since R opt. DESCRIBE, SETUP
Last-Modified e opt. entitiy
Proxy-Authenticate
Proxy-Require R req. all
Public r opt. all
Range R opt. PLAY,PAUSE,RECORD
Range r opt. PLAY,PAUSE,RECORD
Referer R opt. all
Require R req. all
Retry-After r opt. all
Rtp-Info r req. PLAY
Scale Rr opt. PLAY,RECORD
Session Rr req. all but SETUP,OPTIONS
Server r opt. all
Speed Rr opt. PLAY
Transport Rr req. SETUP
Unsupported r req. all
User-Agent R opt. all
Via g opt. all
WWW-Authenticate r opt. all

12.1 Accept

Accept请求头域可被用于指定回复中可被接受的呈现描述内容类型。

类型描述中的“level”参数应该在MIME类型注册中定义,而不是这里。

示例:
    Accept: application/rtsl,application/sdp;level=2

12.2 Accept-Encoding

见[H14.3]

12.3 Accept-Language

见[H14.4]注意语言作用于呈现描述和原因词组,而不是媒体内容

12.4 Allow

Allow回复头域中列出了请求URI中指定资源支持的所有方法,其目的是告知接收者资源相关的有效方法。Allow头y域必须出现在405(方法不支持)回复中。

示例:
    Allow:SETUP,PLAY,RECORD,SET_PARAMETER

12.5 Authorization

见[H14.8]

12.6 Bandwidht

带宽请求头域描述了预计给客户端的带宽,为正整数,单位为位每秒(bps)。RTSP会话过程中带宽可能发生变化。

Bandwidth = "Bandwidth" ":" 1*DIGIT
示例:
    Bandwidth: 400

12.7 Blocksize

该请求头域从客户端发送至媒体服务器,询问服务器特定媒体分组大小。该分组大小不包括底层头如IP、UDP或RTP。服务器可自由选择比请求块更小的size,服务器可能会截断分组以贴近媒体指定的最小块大小,或者r如有必要,直接使用媒体指定的大小。块大小必须为正的十进制数,以字节为单位。只有在值出现语法错误时,才可以返回(416)错误。

12.8 Cache-Control

缓存控制通用头域用于指定请求/回复链中所有缓存机制必须遵守的策略。
缓存策略必须通过代理或网关程序传输,而不用管是否对程序本身是否有意义,因为该策略很有可能对请求/回复链中其他接收者有所作用。同时,对特定魂村指定缓存策略是不可能的。

缓存控制只能在SETUP请求和回复中指定,要注意的是,和HTTP一样,缓存并不作用于回复,而是作用于SETUP请求中标识的流。除了DESCRIBE回复外,其他RTSP回复均不能进行缓存。

Cache-Control          = "Cache-Control" ":" 1#cache-directive
cache-directive         = cache-request-directive
                        | cache-response-directive
cache-request-directive = "no-cache"
                        | "max-stale"
                        | "min-fresh"
                        | "only-if-cached"
                        | cache-extension
cache-response-directive = "public"
                        | "private"
                        | "no-cache"
                        | "no-transform"
                        | "must-revalidate"
                        | "proxy-revalidate"
                        | "max-age" "=" delta-seconds
                        | cache-extension
cache-extension         = token ["=" (token | quoted-string)]
  • no-cache:
    表示媒体流不应该被缓存,这使得即使服务器已经配置缓存的情况下仍然可以及时回复客户端。
  • public:
    表示媒体流可使用任意缓存
  • private:
    表示媒体流只针对单个用户,并且不能使用共享缓存进行缓存,只能使用私有缓存进行缓存
  • no-transform:
    使用间接缓存(代理)时,能够转换具体流的媒体类型是比较有用的。比如,一个代理可以转换视频格式以节省缓存空间以在慢链接上降低流量。当然也可能会发生一些严重操作问题,比如,当这些转换作用到到针对特定场景流上时。举个例子,医疗影像类应用、科学数据分析以及其他端对端授权都严格依赖与原始实体主体中位完全一致。此外,如果回复中包含了不转换的策略,那么间接缓存或代理绝不能修改流中编码。与HTTP不同的是,RTSP不提供部分转换,如转换为另一语言。
  • only-if-cached:
    有些情况下,如特别差的网络连接时,客户端可能只希望缓存返回已经预存的媒体流,而不要从原始服务器上获取。为了达到这个目标,客户端必须在请求中包含only-if-cached指令。当收到这个指令时,缓存可选择要么发送已缓存的媒体流,或者直接回复“504(Gateway Timeout)”状态。如果一组缓存可以被统一组织、高效利用,这样的请求则可使用一组缓存。
  • max-stale:
    表示客户端可以接受延时的媒体流,如果max-stale有给定值(单位为秒),那么表示客户端希望收到延时范围在该值内的媒体数据;如果没有给定值,则可以接受任何延时范围媒体流。
  • min-fresh:
    表示客户端希望接收至少指定秒(当前时间加上min-fresh)内数据,即客户端希望至少提前指定时间就可以拥有后续新鲜数据
  • must-revalidate:
    当cache收到来自SETUP回复中的must-revalidate字段时,缓存不应该在后续请求中沿用原入口。也就是说,缓存必须每次都进行端对端验证。

12.9 会议(Conference)

会议请求头域在RTSP流和已建立会议之间建立了一条逻辑连接,对于同一RTSP会话,其会议id必须保持一致。

Conference = "Conference" ":" conference-d
e.g.  Conference:199702170042.SAA08642@obiwan.arl.wustl.edu%20Starr

当无法找到会议id时可回复“452(未找到会议)”。

12.10 连接(Connection)

见[H14.10]

12.11 内容基础(Content-Base)

见[H14.11]

12.12 内容编码(Content-Encoding)

见[H14.12]

12.13 内容语言(Content-Language)

见[H14.13]

12.14 内容长度(Content-Length)

该域包含了本方法中内容的长度(从紧跟最后一个头的两个CRLF开始计算)。与HTTP不同的是,该域必须存在于所有携带除头信息内容以外的消息中,如果该域不存在,默认值将赋为0。解释部分可参见[H14.14]

12.15 内容位置(Content-Location)

见[H14.15]

12.16 内容类型(Content-Type)

见[H14.18],注意RTSP内容类型可能会被呈现描述和参数值类型所限制

12.17 序号CSeq

CSeq域标识RTSP请求-回复对序号,该域所有请求和回复中必须有。对于每一个包含特定序号的RTSP请求而言,一定有一个拥有相同序号的对应回复。重传的请求必须使用和原始序号一致的值。

12.18 Date

见[H14.19]

12.19 过期(Expires)

Expires实体头域给出了一个日期和时间,超过该时间的媒体流或描述将被视为过期的。最终解释权归方法所有。

过期了的缓存入口可能不会被返回,除非它是首次与原始服务器验证。进一步讨论可阅读13小节。

出现Expires域时并不代表原始资源会在该时间点前、该时间点上、以及时间点后修改或停止存在。

Expires使用HTTP-date中定义的绝对日期和时间,它必须遵循RFC1123-date格式:

Expires="Expires" ":" HTTP-date
e.g. Expires: Thu, 01 Dec 1994 16:00:00 GMT

RTSP/1.0客户端和缓存必须视其他日期格式,特别是值“0”为已经过期。

如需标记一个回复为已过期(“already expired”),一个原始服务器必须使用与Date头中相同的过期值。如需标记一个回复为永不过期,原始服务器应使用回复发送时间加上一年作为过期时间。

The presence of an Expires header field with a date value of some time in the future on a media stream that otherwise would by default be non-cachable indicates that the media stream is cacheable, unless indicated otherwise by a Cache-Control header field.

12.20 From

见[H14.22]

12.21 Host

该HTTP请求头域对于RTSP并非必需,可在发送时直接忽略。

12.22 If-Match

见[H14.25]
该域对于确保呈现描述的完整性特别有用,如通过RTSP以外方式获取呈现描述,或服务器实现中用于确保DESCRIBE消息和SETUP消息间描述的完整性。

标识符并不透明,因此不会指向任何特定会话描述语言。

12.23 If-Modified-Since

If-modified-Since请求头域配合DESCRIBE和SETUP方法使用,使得它们成为附带条件的。如果请求的变量从该域指定的时间开始一直没有被修改,则方法为DESCRIBE时,描述永远不会从服务器返回,而当方法为SETUP时,流不会被建立。相反,会返回一条无任何消息主体"304(未修改)"的回复。

If-Modified-Since = "If-Modified-Since" ":" HTTP-date
e.g. If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT

12.24 Last-Modified

Last-Modified实体头域标明服务器中承诺描述呈现或流最后修改的时间和日期。见[H14.29]。对于DESCRIBE和ANNOUNCE而言,该头域表示描述的最后修改时间,对于SETUP而言则针对媒体流。

12.25 Location

见[H14.30]

12.26 代理授权(Proxy-Authenticate)

见[H14.33]

12.27 代理请求(Proxy-Require)

代理请求头用于指明代理必须支持的proxy-sensitive特性,代理对于任何代理不支持的请求头必须给予否定确认应答。服务器应将该域和请求域视为相同。

本消息的更多细节详见12.32小节。

12.28 Public

见[H14.35]

12.29 Range

请求和回复头域中指定一个时间范围,其单位有几种。本篇规格中定义smpte、npt和clock范围单元。在RTSP中,byte ranges[H14.36.1]没有意义且不能被使用。头中可能还包含一个UTC格式的时间参数,标明该操作何时生效。支持Range头的服务器必须也支持NPT范围格式,应该支持SMPTE范围格式。Range回复头表示实际播放和录制的时间。如果Range头是以不能理解的格式给出的,那么接收者应回复"501 未实现"错误。

Range是半闭合区间,包含低点,而不包含高点。换言之,a-b表示从a点开始,在b点前结束。只偶有音视频的起始点才是有关联的,比如每隔40ms产生一个视频帧,10.0-10.1的范围说明一个视频帧从10.0开始,最后一个视频帧时间为10.08而不是10.1。

Range             = "Range" ":" 1\#range-specifier
                  [":" "time" "=" utc-time ]
ranges-specifier  = npt-range | utc-range | smpte-range
e.g. 
    Range: clock=19960213T143205Z-;time=19970123T143720Z

Range概念类似于HTTP/1.1中byte-range头的概念,它允许客户端从媒体对象中选择片段,然后正常播放出来。播放的起始点可以试试未来的任一位置,尽管有些服务器会拒绝长时间保持资源。

12.30 Referer

见[H14.37] URL指向呈现描述,通常可通过HTTP获取

12.31 Retry-After

见[H14.38]

12.32 Require

Require头被客户端用于查询服务器是否支持某选项,对于不支持的选项,服务器必须明确给出否定应答。
这是为了确保客户端-服务器交互中当选项彼此都支持时可以没有延时,而只有选项不能识别是才会有延缓。对于良好匹配的客户端-服务器对而言,交互必须是快速的,而减少往返时间通常需要沟通机制。此外,它还能消除服务器不理解客户端所请求特性时可能的状态歧义。

Require = "Require" ":" 1#option-tag
e.g.
    C->S:   SETUP rtsp://server.com/foo/bar/baz.rm RTSP/1.0
            CSeq: 302
            Require: funky-feature
            Funky-Parameter: funkystuff
    
    S->C:   RTSP/1.0 551 Option not supported
            CSeq: 302
            Unsupported: funky-feature
        
    C->S:   SETUP rtsp://server.com/foo/bar/baz.rm RTSP/1.0
            CSeq: 303
    
    S->C:   RTSP/1.0 200 OK
            CSeq: 303

本例中,"funky-feature"是标明Funky-Parameter域是必需的特性标签,“funky-feature”和Funky-Parameter间的关系并不通过RTSP交换进行沟通,因为这种关系本来就是“funky-feature”的恒定属性,因此无需沟通。

代理及其他中介设备应当忽略该域内不能理解的特性,如某一特定扩展需要中介设备支持,那么该扩展应在Proxy-Require域内标记。

12.33 RTP-Info

该域用于在PLAY回复中设置RTP相关的参数。

  • url:
    指明之后RTP参数所针对的流URL
  • seq:
    指明流中的第一个分组序号,这使得客户端在Seek时可以更便捷地处理分组,因为客户端可使用该值区分seek操作前后不同的分组
  • rtptime
    指明基于Range回复头中的RTP时间戳,客户端使用该值来完成RTP时间至NPT的映射。(Note: For aggregate control, a particular stream may not actually generate a packet for the Range time value returned or implied. Thus, there is no guarantee that the packet with the sequence number indicated by seq actually has the timestamp indicated by rtptime)

RTP时间戳至NTP时间戳的映射表可以通过RTCP获得,但该信息对于映射并没有多少助力。更进一步地说,为了确保该信息在某些时间(如紧跟启动后或一次seek后)可用,映射表应至于RTSP控制通道中。

In order to compensate for drift for long, uninterrupted presentations, RTSP clients should additionally map NPT ot NTP, using initial RTCP sender reports to do the mapping, and later reports to check drift against the mapping.

语法:
    RTP-Info    = "RTP-Info" ":" 1#stream-url 1*parameter
    stream-url   = "url" "=" url
    parameter    = ";" "seq" "=" 1*DIGIT
                   | ";" "rtptime" "=" 1*DIGIT
e.g.
    RTP-Info: url=rtsp://foo.com/bar.avi/streamid=0;seq=45102,
              url=rtsp://foo.com/bar.avi/streamid=1;seq=30211

12.34 Scale

Scale表示当前速率和正常观看速率之间的比例。为1时表示以正常观看速度播放或录制。例如,scale为2时表示两倍的观看速度(快进),为0.5时表示正常观看速度的一半(慢放)。换言之,scale为2时将时钟频率倍乘2,每秒内将会传送出2秒的数据。而Scale为负数时表示操作方向相反。

虽然Scale改变了播放的速度,但数据传输的速度无法改变,因此scale实现方式依赖具体服务器和媒体类型,对于视频,服务器可能通过只传送关键帧甚至预选了的关键帧来达到目的,而对于音频,服务器可能会在保持音调的基础上按时间缩放音频甚至别无选择地传送音频分片。

服务器应当尽可能贴近观看速度,但必须严格遵循支持Scale的范围值。回复中必须包含服务器所选择的的确切Scale值。

如请求中包含Range参数,新的Scale参数必须立即生效。

Scale = "Scale" ":" ["-"] 1*DIGIT ["." *DIGIT]
e.g.   Scale: -3.5

12.35 Speed

Speed在请求头中用于设置客户端向服务器传送数据的速率,具体实现取决于服务器的能力和态度。该能力对于服务器而言是可选的,默认为流的比特率。

参数值使用十进制小数比表示,如2.0表示数据以两倍速率传输。0值时无效的。如请求中包含Range参数,新的速度值将在对应时刻生效。

Speed = "Speed" ":" 1*DIGIT ["." *DIGIT]
e.g.   Speed: 2.5

使用该域会影响数据传输的带宽,它是特定环境下以更高或更低预览呈现的一种调整方式。实现者应记住会话的带宽可能会预先商定(通过RTSP以外的方式),因此重新协商是可能有必要的。当数据通过UDP传送时,推荐使用类似RTCP方式进行分组丢失率的追踪。

12.36 Server

见[H14.39]

12.37 Session

Session在请求回复头中标识RTSP会话中的媒体流,该流在服务器SETUP回复中启动,在TEARDOWN呈现URL时结尾。会话标识符由媒体服务器选择(见3.4小节)。一旦客户端接收到会话标识符,it must return it for any request related to that session. 如有其它方式进行标识会话,如动态生成的URL,那么服务器也没必要一定设置会话标识符。

Session = "Session" ":" session-id [";" "timeout" "=" delta-seconds]

timeout参数仅允许在回复头中使用,服务器使用它来告知客户端其多久不活动(发送RTSP命令)后会话将会被关闭。timeout以秒为单位,默认值为60s(1分钟)。

注意一个会话标识符可跨传输会话或连接使用,操作多个RTSP URL的控制消息可能通过同一个RTSP会话进行发送。因此,客户端使用同一会话控制同一呈现中来自同一服务器的多个流也是有可能的。此外,同一客户端中对于相同URL的不同的用户会话必须使用不同的会话标识符。

会话标识符用于区分来自同一客户端对于相同URL的不同传输请求。

如会话标识符无效,则返回“454(未找到会话)”错误。

12.38 Timestamp

Timestamp描述了客户端何时发送请求给服务器。该值仅对客户端有意义,可能使用任意时间格式。服务器必须返回完全相同的时间,并且如有可能,附上接收到处理完成进行回复所经过的整体时间。Timestamp用于客户端计算到服务器的往返时间,以便调整重传延时最大值。

Timestamp   = "Timestamp" ":" *(DIGIT) ["." *(DIGIT)] [ delay ]
delay       = *(DIGIT) ["." *(DIGIT)]

12.39 Transport

Transport头标明要使用何种传输协议,并为流配置其参数如目标地址、压缩、多播TTL(time-to-live)以及目标端口。它设置那些呈现描述未能确定的值。

Transport之间以逗号分隔,依次列出。每个transport可添加参数,以分号隔开。

Transport还可能用于修改具体transport参数,服务器可以拒绝修改已存在流上的参数。

服务器可在回复中的Transport头里标明实际使用的参数值。

Transport请求头域中可能包含一系列客户端支持的传输选项,这种情况下,服务器必须返回实际选择的某一选项。

transport/profile/lower-tranpsort

"lower-tranport"的默认参数值与profile密切相关。对于RTP/AVP,默认为UDP。

通用参数
  • unicast | multicast
    互斥选项,要么单播要么多播,默认值为多播。同时支持多播和单播的客户端必须在参数中分别列出完整传输规格(transport-spec)以说明能力范围

  • destination
    流发送的目标地址,客户端可在destination参数中指定多播地址。为了避免在不经意间成为远程控制dos(denial-of-service)攻击的肇事者,服务器应当对客户端进行认证,并且在允许客户端将媒体流直接给予未经过服务器选择的地址。这在RTSP命令通过UDP发送时尤为重要,但实现也不能依赖TCP作为客户端识别的有效方式。一个服务器不应该允许客户端传送媒体流至一个与命令来源不同过得地址。

  • source
    如果流的源地址与可传输的RTSP端点地址不同(服务器在播放或客户端在录制中),source字段将被指定。
    该信息还可从SDP中获取。由于这更多地是像一个传输特性而不是媒体初始化,因此该信息的权威source应是SETUP回复中出现过的。

  • layers
    该流可使用的多播层数量,所有层将发送给以目标地址开头的连续地址

  • mode
    mode参数用于标示当前会话支持的所有方法,有效值为PLAY和RECORD。如不提供,则默认为PLAY

  • append
    如果mode参数包含了RECORD,紧跟的参数标示媒体数据应当追加到已存在的资源上而不是覆盖。如请求中追加的参数服务器并不支持,它必须拒绝该请求而不是使用URI覆盖资源标识符。如mode未包含RECORD,则追加参数互备直接忽略。

  • interleaved
    交错参数表示在控制流使用的任何协议中交错传输媒体流和控制流,交错方法使用10.12小节中的机制。interleaved提供了语句中将要使用到的channel序号。该参数可能被指派一个范围,如interleaved=4-5 以便媒体流选择传输时做决定。

    这使得RTP/RTCP可使用与UDP一致的方法处理,如RTP一个channel,其他channel则给RTCP使用。

  • 多播相关(Multicast specific)

    • ttl
      多播time-to-live
  • RTP Specific

    • port
      port参数为多播会话提供RTP/RTCP端口对,以范围形式给出,如 port=3456-3457

    • client_port
      该参数为被选择用于接收媒体数据和控制信息的客户端提供了单播RTP/RTCP端口对,以范围形式给出,如 client_port=3456-3457

    • server_port
      参数为被选择用于接收媒体数据和控制信息的服务器提供了单播RTP/RTCP端口对,以范围形式给出,如 server_port=3456-3457

    • ssrc
      ssrc参数标明了RTP SSRC值可能会被媒体服务器使用(请求中应当使用,而回复中一定会使用到),该参数仅对单播传输有效。它标示与媒体流相联系的同步源。

Transport             =    "Transport" ":"
                           1\#transport-spec
transport-spec        =    transport-protocol/profile[/lower-transport]
                          *parameter
transport-protocol    =    "RTP"
profile               =    "AVP"
lower-transport       =    "TCP" | "UDP"
parameter             =    ( "unicast" | "multicast" )
                    |    ";" "destination" [ "=" address ]
                    |    ";" "interleaved" "=" channel [ "-" channel ]
                    |    ";" "append"
                    |    ";" "ttl" "=" ttl
                    |    ";" "layers" "=" 1*DIGIT
                    |    ";" "port" "=" port [ "-" port ]
                    |    ";" "client_port" "=" port [ "-" port ]
                    |    ";" "server_port" "=" port [ "-" port ]
                    |    ";" "ssrc" "=" ssrc
                    |    ";" "mode" = <"> 1\#mode <">
ttl                    =    1*3(DIGIT)
port                =    1*5(DIGIT)
ssrc                =    8*8(HEX)
channel             =    1*3(DIGIT)
address             =    host
mode                =    <"> *Method <"> | Method

示例:
    Transport: RTP/AVP;multicast;ttl=127;mode="PLAY"
               RTP/AVP;unicast;client_port=3456-3457;mode="PLAY"

    传输头用于描述单一RTP流(RTSP也可用于像一个整体一样控制多个流),将其作为RTSP的一部分而不是依赖一堆会话描述格式可以极大简化防火墙的设计。  

12.40 不支持(Unsupported)

Unsupported回复头中列出了服务器不支持的特性,在前述通过Proxy-Require域(12.32小节)指定特性情况中,如果在客户端讷河服务器间有同路,那么proxy必须在回复中插入“551 选项不支持”错误消息。

使用示例可见12.32小节。

12.41 User-Agent

见[H14.42]

12.42 Vary

见[H14.43]

12.43 Via

见[H14.44].

12.44 WWW-Authentica

见[H14.46].

13 缓存(Caching)
在HTTP中,请求-回复对会被缓存。在这一点上,RTSP明显不同。回复是不进行缓存的,除非是通过DESCRIBE或ANNOUNCE方法返回呈现描述(因为DESCRIBE和GET_PARAMETER并不返回任何数据,因此缓存回复对请求没有影响)。而且,对于连续媒体数据、会话描述,特别是RTSP中通过带外传输,都建议被缓存。

在接收到SETUP或PLAY请求后,代理检查其是否有一个连续媒体内容及其描述的最新副本。它可以通过SETUP或DESCRIBE请求来确定副本是否最新。如果不是最新,则将SETUP传输参数修改合适并将请求递送给原始服务器。后续控制命令如PLAY或PAUSE则继续传送未经代理修改过的版本。代理在传输连续媒体数据给客户端的同时,如有可能应制作副本方便后续使用。允许魂村的确切操作是通过12.8小节中cache-response指令完成。如果缓存为请求者提供流,那么它必须回复任何DESCRIBE请求,因为流描述底层细节可能被原始服务器进行修改。

注意RTSP缓存与HTTP缓存不同,是“cut-through”类型。与从原始服务器获取整个资源不同,缓存只拷贝经过其流向客户端的流数据。也就是说,它并不会引入额外延时。

对于客户端而言,RTSP代理缓存就像一个正常媒体服务器,对于服务器而言,又像一个客户端。正如HTTP缓存需为其缓存的对象存储内容类型、内容语言等等,媒体缓存也需要存储呈现描述。典型的有,缓存消除了描述呈现中的所有传输应用(如,多播信息),由于这些独立的数据从缓存传送到客户端。编码信息仍保持原状,如果缓存要能够转换缓存的媒体数据,它必须创建一个包含所有能提供编码可能的新的呈现描述。

14 示例(Examples)

下列示例参考了未标准化的流描述格式,如RTSL,并不能作为这些格式使用参考。

14.1 媒体点播(单播)(Media on Demand (Unicast))

客户端C从服务器A(audio.example.com)和服务器V(video.example.com)请求一个影片。媒体描述存储在web服务器W上。

媒体描述中包括呈现及其所有流信息、支持的编码器、动态RTP负载类型、协议栈以及如语言、版权等内容信息。甚至有可能涉及影片的timeline。

本例中,客户端只希望接收影片的最后一部分。

C->W:   GET /twister.sdp HTTP/1.1
        Host: www.example.com
        Accept: application/sdp

W->C:   HTTP/1.0 200 OK
        Content-Type: application/sdp
        v=0
        o=- 2890844526 2890842807 IN IP4 192.16.24.202
        s=RTSP Session
        m=audio 0 RTP/AVP 0
        a=control:rtsp://audio.example.com/twister/audio.en
        m=video 0 RTP/AVP 31
        a=control:rtsp://video.example.com/twister/video
        
C->A:   SETUP rtsp://audio.example.com/twister/audio.en RTSP/1.0
        CSeq: 1
        Transport: RTP/AVP/UDP;unicast;client_port=3056-3057
        
A->C:   RTSP/1.0 200 OK
        CSeq: 1
        Session: 12345678
        Tranport: RTP/AVP/UDP;unicast;client_port=3056-3057;
                  server_port=5000-5001

C->V:   SETUP rtsp://video.example.com/twister/vidoe RTSP/1.0
        CSeq: 1
        Transport: RTP/AVP/UDP;unicast;client_port=3058-3059
        
V->C:   RTSP/1.0 200 OK
        CSeq: 1
        Session: 23456789
        Transport: RTP/AVP/UDP;unicast;client_port=3058-3059;
                   server_port=5002-5003
                   
C->V:   PLAY rtsp://video.example.com/twister/video RTSP/1.0
        CSeq: 2
        Session: 23456789
        Range: smpte=0:10:00-

V->C:   RTSP/1.0 200 OK
        CSeq: 2
        Session: 23456789
        Range: smpte=0:10:00-0:20:00
        RTP-Info: url=rtsp://video.example.com/twister/video;
                  seq=12312232;rtptime=78712811

C->A:   PLAY rtsp://audio.example.com/twister/audio.en RTSP/1.0
        CSeq: 2
        Session: 12345678
        Range: smpte=0:10:00-
        
A->C:   RTSP/1.0 200 OK
        CSeq: 2
        Session: 12345678
        Range: smpte=0:10:00-0:20:00
        RTP-Info: url=rtsp://audio.example.com/twister/audio.en;
                  seq=876655;rtptime=1032181

C->A:   TEARDOWN rtsp://audio.example.com/twister/audio.en RTSP/1.0
        CSeq: 3
        Session: 12345678
        
A->C:   RTSP/1.0 200 OK
        CSeq: 3
        
C->V:   TEARDOWN rtsp://video.example.com/twister/video RTSP/1.0
        CSeq: 3
        Session: 23456789

V->C:   RTSP/1.0 200 OK
        CSeq: 3

即使音视频轨可能来自不同的服务器,甚至有不同的起始时间,客户端都能够通过标准RTP方法将两者同步,特别是RTCP发送报告总的时间刻度。

14.2 容器文件串流(Streaming of a Container file)

本例中,容器文件表示一个存储实体中含有同一终端呈现相关的多个连续媒体类型,实际上,容器文件表示了一个RTSP呈现,它的每个组件都是RTSP流。容器文件是存储这类呈现的通用方法。由于所有组件都以独立流形式传输,因此需要在服务器端为所有流维护一个通用上下文(Context)。

这使得服务器保持一个简单存储句柄变得简单,它还允许平等对待所有流以防服务器对流进行优先级排序。

有可能呈现的作者不希望客户端选择性获取流,以便保护媒体呈现的艺术效果。同样地,对于这样紧密结合的呈现,通过一条简单的作用于聚合URL的控制消息来控制所有流也是比较合适的。

下面给出了一个使用RTSP会话来控制多个流的例子,它还演示了聚合URL的使用。

客户端C从媒体服务器M上获取一个呈现,影片存放在一个容器文件中。客户端已获得一个指向容器文件的RTSP URL。

C->M:   DESCRIBE rtsp://foo/twister RTSP/1.0
        CSeq: 1
        
M->C:   RTSP/1.0 200 OK
        CSeq: 1
        Content-Type: application/sdp
        Content-Length: 164
        v=0
        o=- 28908442586 2890842808 IN IP4 172.16.2.93
        s=RTSP Session
        i=An Example of RTSP Session Usage
        a=control:rtsp://foo/twister
        t=0 0
        m=audio 0 RTP/AVP 0
        a=control:rtsp://foo/twister/audio
        m=video 0 RTP/AVP 26
        a=control:rtsp://foo/twister/video
        
C->M:   SETUP rtsp://foo/twister/audio RTSP/1.0
        CSeq: 2
        Transport: RTP/AVP;unicast;client_port=8000-8001
        
M->C:   RTSP/1.0 200 OK
        CSeq: 2
        Transport: RTP/AVP;unicast;client_port=8000-8001;
                   server_port=9000-9001
        Session: 12345678

C->M:   SETUP rtsp://foo/twister/video RTSP/1.0
        CSeq: 3
        Transport: RTP/AVP;unicast;client_port=8002-8003
        Session: 12345678

M->C:   RTSP/1.0 200 OK
        CSeq: 3
        Transport: RTP/AVP;unicast;client_port=8002-8003;
                   server_port=9004-9005
        Session: 12345678

C->M:   PLAY rtsp://foo/twister RTSP/1.0
        CSeq: 4
        Range: npt=0-
        Session: 12345678
        
M->C:   RTSP/1.0 200 OK
        CSeq: 4
        Session: 12345678
        RTP-Info: url=rtsp://foo/twister/video;seq=9810092;rtptime=3450012

C->M:   PAUSE rtsp://foo/twister/video RTSP/1.0
        CSeq: 5
        Session: 12345678

M->C:   RTSP/1.0 460 Only aggregate operation allowed
        CSeq: 5

C->M:   PAUSE rtsp://foo/twister RTSP/1.0
        CSeq: 6
        Session: 12345678

M->C:   RTSP/1.0 200 OK
        CSeq: 6
        Session: 12345678
        
C->M:   SETUP rtsp://foo/twister RTSP/1.0
        CSeq: 7
        Transport: RTP/AVP;unicast;client_port=10000

M->C:   RTSP/1.0 459 Aggregate operation not allowed
        CSwq: 7

第一次错误是因为客户端尝试只暂停呈现中的某一个流(这里是视频),而服务器并不支持对呈现做该操作。
第二次错误中则是因为URL不能用在SETUP方法中,必须对其中流分别进行参数SETUP。

这使得传输头变得简单而且更便于防火墙解析传输信息。

14.3 单一流容器文件(Single Stream Container Files)

部分RTSP服务器可能将所有文件都视为容器文件,而其他服务器可能并没有这个概念。因此,客户端应当使用会话描述中规则去请求URL,而不是假设会一直使用同一个URL。下面是一个期望multi-stream服务器提供single-stream服务的例子:

        Accept: application/x-rtsp-mh, application/sdp
        CSeq: 1
        
S->C:   RTSP/1.0
        CSeq: 1
        Content-base: rtsp://foo.com/test.wav/
        Content-type: applciation/sdp
        Content-length: 48
        
        v=0
        o=- 872653257 872653257 IN IP4 172.16.2.187
        s=mu-law wave file
        i=audio test
        t=0 0 
        m=audio 0 RTP/AVP 0
        a=control:streamid=0
        
C->S:   SETUP rtsp://foo.com/test.wav/streamid=0 RTSP/1.0
        Transport: RTP/AVP/UDP;unicast;client_port=6970-6971;mode=play
        CSeq: 2

S->C:   RTSP/1.0 200 OK
        Transport: RTP/AVP/UDP;unicast;client_port=6970-6971;
                   server_port=6970-6971;mode=play
        CSeq: 2
        Session: 2034820394

C->S:   PLAY rtsp://foo.com/test.wav RTSP/1.0
        CSeq: 3
        Session: 2034820394

S->C:   RTSP/1.0 200 OK
        CSeq: 3
        Session: 2034820394
        RTP-Info: url=rtsp://foo.com/test.wav/streamid=0;
                  seq=98188;rtptime=3781123

注意SETUP命令中的URL有所不同,紧跟着的PLAY命令仍然使用聚合URL。这使得对多个流进行聚合控制的概念更加完整,而当流数目只有一个时,会略显得没有那么直观。

在这个特定场景下,服务器最好能兼容(it is recommended that servers be forgiving of implementations)如下操作:

C->S:   PLAY rtsp://foo.com/test.wav/streamid=0 RTSP/1.0
        CSeq: 3

实在不行,服务器至少应回复:

S->C:   RTSP/1.0 460 Only aggregate operation allowed
        CSeq: 3

如有可能,服务器也可以支持如下操作:

C->S:   SETUP rtsp://foo.com/test.wav RTSP/1.0
        Transport: rtp/avp/udp;client_port=6970-6971;mode=play
        CSeq: 2

因为文件中并没有其他流,所以并不会引起歧义。

14.4 使用多播的直播呈现(Live Media Presentation Using Multicast)

媒体服务器M选择多播地址和端口。假设web服务器只包含指向完整描述的指针,而媒体服务器M维护着完整描述信息。

C->W:   GET /concert.sdp HTTP/1.1
        Host: www.example.com

W->C:   HTTP/1.1 200 OK
        Content-Type: application/x-rtsl
        
        <session>
            <track src="rtsp://live.example.com/concert/audio">
        </session>

C->M:   DESCRIBE rtsp://live.example.com/concert/audio RTSP/1.0
        CSeq: 1
        
M->C:   RTSP/1.0 200 OK
        CSeq: 1
        Content-Type: application/sdp
        Content-Length: 44
        
        v=0
        o=- 2890844526 2890842807 IN IP4 192.168.24.202
        s=RTSP Session
        m=audio 3456 RTP/AVP 0
        a=control:rtsp://live.example.com/concert/audio
        c=IN IP4 224.2.0.1/16
    
C->M:   SETUP rtsp://live.example.com/concert/audio RTSP/1.0
        CSeq: 2
        Transport: RTP/AVP;multicast

M->C:   RTSP/1.0 200 OK
        CSeq: 2
        Transport: RTP/AVP;multicast;destination=224.2.0.1;
                   port=3456-3457;ttl=16
        Session: 0456804596
        
M->C:   RTSP/1.0 200 OK
        CSeq: 2
        Transport: RTP/AVP;multicast;destination=224.2.0.1;
                   port=3456-3457;ttl=16
        Session: 0456804596

C->M:   PLAY rtsp://live.example.com/concert/audio RTSP/1.0
        CSeq: 3
        Session: 0456804596
        
M->C:   RTSP/1.0 200 OK
        CSeq: 3
        Session: 0456804596

14.5 在已存在会话中播放媒体(Playing media into an existing session)

一个会议参与者C希望媒体服务器M在现有会议中播放一个demo,C告知了媒体服务器M会议的网络地址和加密后的key,所以服务器没必要进行地址选择。下例中忽略了简单的ACK回复。

C->M:   DESCRUBE rtsp://server.example.com/demo/548/sound RTSP/1.0
        CSeq: 1
        Accept: application/sdp

M->C:   RTSP/1.0 200 1 OK
        Content-type: application/sdp
        Content-Length: 44
        
        v=0
        0=- 2890844526 2890842807 IN IP4 192.16.24.202
        s=RTSP Session
        i=See above
        t=0 0
        m=audio 0 RTP/AVP 0

C->M:   SETUP rtsp://server.example.com/demo/548/sound RTSP/1.0
        CSeq: 2
        Transport: RTP/AVP;multicast;destination=225.219.201.15;
                   port=7000-7001;ttl=127
        Conference: 199702170042.SAA08642@obivan.arl.wustl.edu%20Starr
        
M->C:   RTSP/1.0 200 OK
        CSeq: 2
        Transport: RTP/AVP;multicast;destination=225.219.201.15;
                    port=7000-7001;ttl=127
        Session: 91389234234
        Conference: 199702170042.SAA08642@obivan.arl.wustl.edu%29Starr
        
C->M:   PLAY rtsp://server.example.com/demo/548/sound RTSP/1.0
        CSeq: 3
        Session: 91389234234
        
M->C:   RTSP/1.0 200 OK
        CSeq: 3

14.6 录制(Recording)

会议参与者客户端C请求服务器M去录制会议音视频部分,客户端使用ANNOUNCE方法来想服务器提供录制会话的元信息。

C->M:   ANNOUNCE rtsp://server.example.com/meeting RTSP/1.0
        CSeq: 90
        Content-Type: application/sdp
        Content-Length: 121
        
        v=0
        o=cameral 3080117314 3080118787 IN IP4 195.27.192.36
        s=IETF Meeting, Munich - 1
        i=The thirty-ninth IETF meeting will be held in Munich, Germany
        u=http://www.ietf.org/meetings/Munich.html
        e=IETF Channel 1 <ietf39-mbone@uni-koeln.de>
        p=IETF Channel 1 +49-172-2312 451
        c=IN IP4 224.0.1.11/127
        t=3080271600 3080703600
        a=tool:sdr v2.4a6
        a=type:test
        m=audio 21010 RTP/AVP 5
        c=IN IP4 224.0.1.11/127
        a=ptime:40
        m=video 61010 RTP/AVP 31
        c=IN IP4 224.0.1.12/127

M->C:   RTSP/1.0 200 OK
        CSeq: 90

C-M:    SETUP rtsp://server.example.com/meeting/audiotrack RTSP/1.0
        CSeq: 91
        Transport: RTP/AVP;multicast;destination=224.0.1.11;
                   port=21010-21011;mode=record;ttl=127

M->C:   RTSP/1.0 200 OK
        CSeq: 91
        Session: 50887676
        Transport: RTP/AVP;multicast;destination=224.0.1.11;
                   port=21010-21011;mode=record;ttl=127

C->M:   SETUP rtsp://server.example.com/meeting/videotrack RTSP/1.0
        CSeq: 92
        Session: 50887676
        Transport: RTP/AVP;multicast;destination=224.0.1.12;
                   port=61010-61011;mode=record;ttl=127
                   
M-C:   RTSP/1.0 200 OK
        CSeq: 92
        Transport: RTP/AVP;multicast;destination=224.0.1.12;
                   port=61010-61011;mode=record;ttl=127
                   
C->M:   RECORD rtsp://server.example.com/meeting RTSP/1.0
        CSeq: 93
        Session: 50887676
        Range: clock=19961110T1925-19961110T2015

M-C:   RTSP/1.0 200 OK
        CSeq: 93

15 语法(Syntax)

RTSP语法使用RFC 2068中的巴科斯范式(BNF,argumented Backus-Naur form)进行描述。

15.1 基础语法(Base Syntax)

OCTET           = <any 8-bit sequence of data>
CHAR             = <any US-ASCII character (octets 0-127)>
UPALPHA       = <any US-ASCII uppercase letter "A".."Z">
LOALPHA       = <any US_ASCII lowercase letter "a".."z">
ALPHA           = UPALPHA | LOALPHA
DIGIT           = <any US-ASCII digit "0".."9">
CTL           = <any US-ASCII control character (octets 0-31) and DEL(127)>
CR             = <US-ASCII CR, carriage return(13)>
LF             = <US-ASCII LF, linefeed(10)>
SP             = <US-ASCII SP, space(32)>
HT             = <US-ASCII HT, horizontal-tab(9)>
<">           = <US-ASCII double-quote mark(34)>
CRLF             = CR LF
LWS           = [CRLF] 1*( SP | HT )
TEXT             = <any OCTET except CTLs>
tspecials      = "(" | ")" | "<" | ">" | "@"
                 | "," | ";" | ":" | "\" | <">
                 | "/" | "[" | "]" | "?" | "="
                 | "{" | "}" | SP | HT
token           = 1*<any CHAR except CTLs or tspecials>
quoted-string   = (<"> * (qdtext) <">)
qdtext         = <any TEXT except <">>
quoted-pari   = "\" CHAR
message-header  = field-name ":" [field-value] CRLF
field-name    = token
field-value  = *( field-content | LWS )
field-content   = <the OCTETs making up the field-value and 
                    consisiting of either *TEXT or 
                    combinations of token, tspecials, 
                    and quoted-string>
safe            = "\$" | "-" | "_" | "." | "+"
extra          = "!" | "*" | "$'$" | "(" | ")" | ","
hex          = DIGIT | "A" | "B" | "C" | "D" | "E" | "F"
                        | "a" | "b" | "c" | "d" | "e" | "f"
escape        = "\%" hex hex
reserved       = ";" | "/" | "?" | ":" | "@" | "&" | "="
unreserved      = alpha | digit | safe | extra
xchar       = unreserved | reserved | escape

16 安全须知(Security Considerations)

由于RTSP服务器和HTTP服务器语法上类似,因此主要安全须知已在[H15]中列出。其他需要注意的如下: 、

  • 认证机制(Authentication Mechanisms)
    RTSP和HTTP共享通用认证方案,因此也必须遵循相同的认证描述方式。客户端认证可参考[H15.1],对于多种认证机制则可参考[H15.2]

  • 服务器日志信息的滥用(Abuse of Server Log Information)
    RTSP和HTTP服务器应该也是使用相同的日志机制,因此也都需要保护日志内容,及对服务器用户的隐私进行保护。可参考[H15.3]中HTTP服务器对一些日志的建议

  • 敏感信息传输(Transfer of Sensitive Information)
    没有理由可以相信通过RTSP传输信息会比直接通过HTTP传输更不敏感。事实上,所有关于数据隐私和用户隐私的防御措施都应用到RTSP客户端、服务器和代理的实现上,进一步细节可参考[H15.4]

  • 基于文件和路径名称的攻击(Attacks Based On File and Path Names)
    尽管RTSP URL是不透明的操作句柄,使用时没必要有文件系统的语义掺入,但预期很多实现还是会将请求URL部分转换为文件系统调用。这种情况下,文件系统应当遵循[H15.5]中提到的主要预防措施,如路径中检查".."关键字等。

  • 个人信息(Personal Information)
    RTSP客户端中通常将HTTP客户端中视为隐私的信息(如用户名、位置等)进行保护,更多建议请阅读[H15.6]

  • 与Accept头有关隐私问题(Privacy Issues Connected to Accept Headers)
    由于RTSP和HTTP中可能存在相同"Accept"头,其他注意事项已在[H15.7]中列出,使用时根据具体情况进行调整

  • DNS欺骗(DNS Spoofing)
    相对HTTP会话,有可能给予更长连接时间给RTSP会话,RTSP客户端DNS客户端也暂未流行。此外,[H15.8]中提供了基于DNS-to-IP映射实现相应的建议

  • 位置头和欺骗(Location Headers and Spoofing)
    如一个简单服务器支持多个互不信赖的组织,那么服务器必须校验控制生成的回复中Location和Content-Location头,以确保他们不会尝试废弃没有权限的资源([H15.9])。

在现有HTTP规格基础上,未来HTTP可能会提供对安全问题提供额外的保障。

下列内容时RTSP实现时应额外考虑的部分:

  • 集中DOS攻击(Concentrated denial-of-service attack)
    RTSP提供了远程控制DOS攻击的机会,这些攻击者可能通过在SETUP请求中指定一至多个IP地址初始化工作流。由于这种情况下攻击者的IP地址是可见的,在防御更多攻击和查明攻击者身份后,攻击并不总能有效。RTSP服务器应当只允许认证过身份的客户端在RTSP初始化时指定目标地址,认证方法可以是通过RTSP授权机制(preferably digest authentication or stronger)建立用户数据库,或者其他安全措施。

  • 会话劫持(Session jijacking)
    由于传输层和RTSP会话层没有关联,有可能存在恶意客户端使用随机会话标识符进行请求从而影响其他未知客户端。服务器应当使用一个大、随机而且无序的会话标识符来尽可能减小此种攻击的可能性。

  • 认证(Authentication)
    服务器应当实现basic和digest认证,当环境需要给控制消息更严格的安全机制时,RTSP控制流可能需要被加密。

  • 流问题(Stream issues)
    RTSP只提供流控制部分,流传输的问题并不包含在本文内。RTSP实现很大程度上需要依赖其他协议如RTP,IP多播,RSVP和IGMP,所以要在这些协议及其他可使用规格中提请解决安全考虑。

  • 持续的可疑行为(Persistently suspicious behavior)
    RTSP服务器应当对判定存在安全风险的行为回应“403(Forbidden)”错误,RTSP服务器应当能够察觉是否有客户端在搜寻服务器漏洞和入口的尝试,如发现则应判定为违反本地安全策略,并果断断开并忽略后续任何请求。

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

推荐阅读更多精彩内容