65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Rtsp 和 Rtp 进行托管媒体聚合

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (68投票s)

2012年12月11日

CPOL

60分钟阅读

viewsIcon

1311428

引导开发人员实现符合标准的 RtspClient 和 RtspServer。

本文写于近两年前,其内容可能无法反映目前代码的最新状态。

请访问 https://net7mma.codeplex.com/ 获取最新信息和下载。

前言

在我开始正文之前,我想说,.NET 中对音频数据的支持似乎相当全面,如果开发人员想要执行与音频相关的各种任务,如解码、编码和转码,肯定有足够的信息和包含完整实现的库来完成这些任务。当你开始处理视频——这才是真正的问题领域——包括编码和解码视频时,事情就变得完全令人困惑了。这有多种原因,其中最大的误解是托管代码对于处理视频数据来说不够快。

事实是,每种编解码器都大相径庭,编解码器中使用的压缩/解压缩通常受到某种专利的限制。不幸的是,这意味着有人必须为该编解码器中使用的代码支付版税。另一个问题是,视频解码通常是标准化的,但编码则不是。这意味着只要最终结果符合流解码规范,您就可以自由地以任何方式进行编码。这为开发人员提供了很大的自由,但也为他们提供了多种编码数据的方式。然而,有些方法比其他方法更有效,结果在所需时间和产生的结果方面都是可变的。

有一些库可以提供帮助,例如 VLC 或 FFMPEG,它们都使用 LibAvCodec。然而,该库是用“C++”编写的,并且需要考虑该库的许可——更不用说它会为您的代码引入外部依赖项和平台调用。

简而言之,如果您需要一个快速类比,您可以将视频解码和编码比作压缩和解压缩文件……当您解码时,您解压缩数据;当您编码时,您压缩数据。压缩和解压缩,就像 Zip 压缩不同于 Rar 压缩一样,MPEG4 压缩也不同于 H264 压缩。

对于音频编码/解码,例如“mp3”与“wav”格式,情况也是如此。两者之间的主要区别主要与音频和视频有关。音频数据在需要解析的字节数方面远不如视频数据复杂,这带来了另一个问题:音频数据中微小的“故障”或错误在解码时会导致更大的问题——而视频数据中的“故障”或错误在播放时不会导致任何实质性的伪影。

如果您对波形音频格式及其操作感兴趣,请查看 CodeProject 上的这篇文章,其中包含许多很棒的示例。

如果您想了解音频和视频数据在高层次上如何相似而又不同,请查看此演示文稿

通常,解码视频以在计算机屏幕上显示的最大因素是从 YUV 到 RGB 的色彩空间转换,这必须对生成视频中的每个像素执行,这意味着在四分之一公共隔行格式或更简单的 QCIF 分辨率(即 176x220)下,此计算和转换在解码时需要执行约 25,000 次。(每个像素一次,127 * 220 = 29840)如果您使用一些技巧,可能会少一点。

与一系列图像或视频相比,单个图像要简单得多,因为您手边有 BMP、GIF、JPEG、PNG 等工具来为您完成工作。每种格式都有自己的优缺点列表,每种格式都有其位置和用途。

JPEG 图像格式不再受此类知识产权的困扰。此外,它不再是传输甚至存储图片的最佳方式。尽管如此,由于其在过去 20 多年中的广泛采用,Jpeg 文件在许多资源解释如何处理 Jpeg 文件及其包含的数据方面相当易于访问。

MPEG(以及 JPEG2000 和许多其他候选格式)在传输数据可以小得多,因此使用更少带宽的意义上,取代了 JPEG。这增加了压缩和最终查看图像所需的复杂性。不幸的是,如果您想将压缩数据完全转换为 RGB 等格式,这会导致更高的 CPU 利用率。最新版本也受到专利的限制。

通常进行 RGB 转换是因为在大多数机顶盒或电视上,这是计算机屏幕上显示图像所使用的格式。情况并非如此,因此转换成 RGB 的信息要少得多,并且只是以该平台的本机格式(例如 YUV)呈现,这就是这些设备的处理器可能比现代甚至老式计算机的处理器弱得多的原因。

一些图形处理单元(GPU)甚至一些现代中央处理单元(CPU)可以通过使用称为 SIMD 或并行执行的高级数学和处理器内部函数来加速处理,其他的可以直接处理 YUV 编码数据或将多个核心投入到解码过程中,这些核心已经针对此类数据和在某些情况下需要此类高级例程的特定编解码器进行了高度优化,并使用专用硬件,这有点像您自己计算机中的网络堆栈利用网卡处理器来执行校验和验证和其他操作(如果操作系统允许),从而获得更好的性能。

本文和库与编码、解码或转码没有太多关系,它也不以音频或视频为中心,所以让我们来确切了解本文的内容……

引言

该库提供了逐包 RTP 聚合,与 RTP 上使用的底层视频或音频格式无关。这意味着它不依赖或公开底层媒体的详细信息,除了播放媒体所需的对话中获得的信息之外,它还使得派生给定类以支持应用程序特定功能(如 QOS 和节流)或其他视频点播服务器所需的功能变得容易。

它可用于通过 TCP/UDP 将单个(低带宽)媒体聚合到数百个用户,通过包含的 `RtspServer`。(Rtcp 数据包不聚合,将独立计算并适当发送到服务器中的每个 Rtsp/Rtp 会话。)当我使用“聚合”一词时,我指的是重复(而不是转发)到另一个客户端,并进行必要的修改以将数据中继到除其目标端点之外的另一个端点。

它利用 RFC2326 和 RFC3550 兼容的过程来提供此功能以及许多其他功能。

您也可以使用它来从机顶盒等设备进行广播,而无需将其暴露在互联网上,方法是通过局域网连接到它,然后建立一个 `SourceMedia`(此时您还可以添加密码),然后按需将流广播给任何连接的用户。

这意味着您可以拥有许多不同类型的源流,例如 JPEG、MPEG4、H263、H264 等,并且您的客户端都将接收与源流传输相同的视频。

这也意味着您可以将流行的工具与此库中包含的 `RtspServer` 配合使用,例如 FFMPEGVLCQuicktime、Live555、Darwin Streaming Media Server 等。

这使开发人员可以使用这些工具在包含的 `RtspServer` 上回放/转码流或将其保存到文件,甚至提取帧,这样您就不必占用实际源流/设备的带宽或 CPU。

您还可以添加第三层——例如:分离*此*进程,并在不同的服务器上完成工作,而无需担心库之间的互操作性。只需使用此库创建 `RtspServer` 并获取流。此外,从另一个进程/服务器使用 AForge 或其他包装库与 `RtspServer` 通信。然后 RtspServer 与设备通信。运行您的处理,然后为流中所需的每个图像创建 RFC2435 帧。完成此步骤后,使用 RtspServer 通过 Rtp 发送出去。

这个库还为您提供了额外的扩展性和自定义性,因为您的转码和传输是在两个独立的服务器上完成的。如果这听起来不太明白,希望这张图能帮助您理解。

除了提供一个 `RtspServer`,它还提供了一个 `RtspClient` 和 `RtpClient`,允许开发人员连接到任何 `RtspServer` 或端点可用的,为开发人员提供了一种简单的方式来使用流。会话描述可用,为开发人员提供了一种简单的方式来使用流。

简要示例

在上面的图中,您会注意到电话会议有几个手机参与者(一些本地通话者)参与。这很好,通常只需要现有硬件即可发送和接收通话。但是,如果您添加一个有趣的转折,即远程用户也需要能够查看通话,那么由于远程用户所需的带宽和处理要求,问题的复杂性会突然增加 10 倍。

您会立即想到(或者应该想到),“这个小设备怎么能处理比参与此会话的人更多的人呢?”答案是:设备的处理器每个会话只能处理有限数量的观众,因此,只有有限的带宽可用于发送给参与者。更不用说任何最终用户了……

您可以用任何其他设备(例如网络摄像头)替换上述类比中的通话介质,拓扑仍然适用。

您可能会想,“我为什么需要这样做?我的相机已经可以支持 X 个用户……”我回答说,如果您的当前需求今天得到了满足,而明天突然呈指数级增长,那么您的用户需求就会改变,而且有更多的缓冲比没有要好——尤其是在向最终用户提供媒体时。

这就是 `RtspServer` 的用武之地,它提供了一个免费、快速且符合标准的实现,允许您选择性地向公众重复呼叫,此时您可以添加密码。如果需要,它也允许外部用户参与会话。

负载均衡设备会重定向网络负载,而此软件使服务器能够充当媒体消费的集中源,然后具有在其他地方重新生成媒体的能力,从而减轻了终端设备的负载,并允许将处理聚合到任意层数。

如果您说:“我永远不需要做这样的事情,我只会使用 VLC 或这个软件或那个软件”,那么本文不适合您。

您可能还会认为您可以编写我所说的可以编写的客户端/服务器,然后突然意识到实现它所需的标准比您最初看到的要多得多……本文可能会帮助您。

您可能也来到这里,因为您在使用其他库时遇到了某种障碍,并希望找到更灵活的替代方案来取代您当前的实现。如果是这种情况,那么本文也将帮助您!

让我们了解一下问题领域的背景……

问题领域

我需要提供一种方式,让多个用户能够查看来自低带宽链路(如调制解调器或手机)的 RTSP 流。

源本身已经支持 Rtsp 协议,但是当多个用户连接以查看资源时,带宽不足以支持他们;每个连接到摄像机的用户,带宽都会随之减半,因此,即使调整了质量设置,也最多只能支持 2 或 3 个用户,这不足以满足需求。

唯一的解决方案是使用媒体服务器聚合源媒体。这是因为媒体服务器将拥有更好的处理器和更多的可用带宽,这将使源流只需由一个连接(媒体服务器)使用,当客户端想要使用该流时,他们将连接到媒体服务器而不是连接到源媒体。

我研究了一段时间,发现存在其他现有解决方案,例如 DarwinStreamingServer、Live555、VideoLan 或 FFMPEG。然而,它们都用 C++ 编写,对于此类项目来说相当笨重。此外,它们需要托管代码的外部依赖项,这是我真正不想要的,更不用说在这种情况下可能涉及的许可问题。

然后我萌生了一个疯狂的想法,构建我自己的 `RtspServer`,它将接收源 Rtsp 流并通过 `RtspServer` 将流数据传递给 `RtspClient`。毕竟,我熟悉套接字通信,而且我已经构建了一个 HttpServer(以及许多其他服务器),所以我知道我有经验来做这件事。我只需要真正开始。

在阅读任何内容之前,我四处搜索,看看是否有任何可用的现有库,我发现了一些 Rtsp 和 Rtp 的部分实现,但在 C# 中找不到可以直接拿来使用的……幸运的是,以下库中有一些有用的方法和概念:

这些实现的问题在于,它们要么不是跨平台的(无法在 Linux 上运行),要么是为特定目的而设计,并使用专有视频编解码器,这使得它们与大多数基于标准的播放器不兼容,并且它们仅用于与特定系统(例如 VoIP 系统)进行接口。

手头有任务,并且对自己的能力充满信心,我决定继续剖析标准……我预先知道视频解码和编码会是最困难的,仅仅是因为缺乏领域经验,但我也意识到一个好的传输堆栈应该与这些差异无关,因此我的职责是构建一个在所有情况下都可重用的堆栈……一个传输堆栈来统治所有!*(使用 100% 托管代码,特别是 `C#`)*

我阅读了 RFC2326,它描述了 实时流协议Rtsp。这一切都从这里开始,Rtsp 协议的目的是获取有关底层媒体的详细信息,例如格式以及如何将其发送回客户端。它还使客户端能够控制流的状态,例如是否正在播放或录制。

结果发现 Rtsp 请求在设计上与 Http 相似,但并不直接兼容 Http。(除非通过 Http 适当地隧道化)

以这个“OPTIONS”Rtsp 请求为例

C->S:  OPTIONS rtsp://example.com/media.mp4 RTSP/1.0
       CSeq: 1
       Require: implicit-play
       Proxy-Require: gzipped-messages

S->C:  RTSP/1.0 200 OK
       CSeq: 1  
       Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE

一些状态码以及数据的格式可能看起来很熟悉。您可以看到这个协议与使用 Http 相比并没有太大的不同/难度。

如果你能实现一个 Http 服务器,那么你就能实现一个 Rtsp 服务器,主要区别在于所有 Rtsp 请求通常都需要某种“状态”,而有些 Http 请求则不需要。

事实上,如果你想支持所有 Rtsp 变体,你必须支持 Http,因为 Rtsp 请求可以通过 Rtsp 隧道传输。包含的 `RtspServer` 支持 Rtsp over Http,但如果你想了解更多关于如何隧道传输的信息,你可以查看 Apple 的开发者页面。

现在,回到“状态”,当我说状态时,我指的是像 Http 中的会话变量一样,即使连接关闭后也会持续存在。在 Rtsp 中,一个例子是 SessionId,它是在“SETUP”Rtsp 请求期间从服务器分配给客户端的。

C->S: SETUP rtsp://example.com/media.mp4/streamid=0 RTSP/1.0
      CSeq: 3
      Transport: RTP/AVP;unicast;client_port=8000-8001

S->C: RTSP/1.0 200 OK
      CSeq: 3
      Transport: RTP/AVP;unicast;client_port=8000-8001;server_port=9000-9001
      Session: 12345678 

另一个主要区别是请求可以通过 Udp 或 Tcp 发送,这要求服务器有 2 个或更多监听套接字。Rtsp over Tcp 的默认端口是 555,Rtsp over Udp 的默认端口是 554。

Tcp 的 Uri 方案是 'rtsp://',Udp 的是 'rtspu://'

每个方案的语义相同,唯一的区别是 IP 头下方的传输协议是 TCP 还是 UDP。

在处理 Rtsp 时,我发现我需要另一个协议,RFC2326 也引用了 会话描述协议或 Sdp 的 RFC。

Sdp 是这个项目中最小的部分之一,但理解它对于使符合规范的服务器正常运行同样重要。它仅在从服务器到客户端的 Rtsp 通信的“DESCRIBE”请求中使用,但在提供解码流中数据所需的信息方面至关重要。

C->S: DESCRIBE rtsp://example.com/media.mp4 RTSP/1.0
      CSeq: 2

S->C: RTSP/1.0 200 OK
      CSeq: 2
      Content-Base: rtsp://example.com/media.mp4
      Content-Type: application/sdp
      Content-Length: 460

      m=video 0 RTP/AVP 96
      a=control:streamid=0
      a=range:npt=0-7.741000
      a=length:npt=7.741000
      a=rtpmap:96 MP4V-ES/5544
      a=mimetype:string;"video/MP4V-ES"
      a=AvgBitRate:integer;304018
      a=StreamName:string;"hinted video track"
      m=audio 0 RTP/AVP 97
      a=control:streamid=1
      a=range:npt=0-7.712000
      a=length:npt=7.712000
      a=rtpmap:97 mpeg4-generic/32000/2
      a=mimetype:string;"audio/mpeg4-generic"
      a=AvgBitRate:integer;65790  
      a=StreamName:string;"hinted audio track"

RFC4566 – 会话描述协议,它除了 Rtsp 之外还在许多其他地方使用,负责描述媒体。它通常提供有关可用流的信息以及开始解码它们所需的信息。

在我看来,这个协议有点奇怪,因为我相信 XML 对于它的功能和验证方式会起到更好的作用,但这并不相关,并且媒体流需要这种格式,所以必须按照标准实现。(XML 在当时可能不够成熟,并且输出中需要更多的字符,因此需要更多的带宽,这可能不理想)。另一种方法是将这些数据封装在 `SourceDescription` `RtcpPackets` 中,但无论出于何种原因,它被选择为现在的样子,并且必须实现。

Sdp 被设计成一种基于“行”的协议,其目的也是基本上可读且易于解析底层令牌,这些令牌提供的信息通常由“行”中的第一个字符决定。某些“行”项目可以携带与文档中其他项目不同的特定编码,但随附的解析器能够很好地处理此问题。

无论您如何与 RtspServer 建立 Rtsp 连接(例如 TCP、HTTP 或 UDP),您所连接的 RtspServer 都必须使用另一种协议发送媒体数据……

实时传输协议或 RTP 亦称 / RFC3550

同样,这不是一个复杂的协议,它定义了数据包和帧结构以及用于传输它们和计算传输损耗的各种算法,以及要使用的端口。它还概述了接收方如何播放流数据。

RTP 数据包的主要格式如下(感谢维基百科)

RTP 包头
比特偏移 0-1 2 3 4-7 8 9-15 16-31
0 版本 P X CC M PT 序列号
32 时间戳
64 SSRC 标识符
96 CSRC 标识符
...
96+32×CC 配置文件特定扩展头 ID 扩展头长度
128+32×CC 扩展头
...

RTP 头部的最小大小为 12 字节。头部之后可能存在可选的头部扩展。接着是 RTP 有效载荷,其格式由特定的应用类别决定。头部中的字段如下:

  • 版本:(2位)指示协议版本。当前版本为2。
  • P (填充): (1 bit) 用于指示 RTP 包末尾是否有额外的填充字节。填充可能用于填充特定大小的块,例如加密算法所需。填充的最后一个字节包含添加的填充字节数(包括自身)。
  • X (扩展): (1 位) 指示在标准头部和有效载荷数据之间是否存在*扩展头部*。这与应用程序或配置文件相关。
  • CC (CSRC 计数): (4 位) 包含跟随固定头部的 CSRC 标识符(下文定义)的数量。
  • M (标记): (1 位) 在应用程序级别使用,由配置文件定义。如果设置,表示当前数据对应用程序具有特殊意义。
  • PT (负载类型): (7 位) 指示负载的格式并确定应用程序对其的解释。这由 RTP 配置文件指定。例如,参见《RTP 音视频会议配置文件,带最小控制》(RFC 3551)。
  • 序列号: (16 位) 序列号每发送一个 RTP 数据包就增加一,接收方使用它来检测丢包并恢复数据包序列。RTP 不指定丢包时的任何操作;它留给应用程序采取适当的行动。例如,视频应用程序可能会播放最后一个已知帧来代替丢失的帧。 根据RFC 3550,序列号的初始值应该是随机的,以使已知明文攻击加密更困难。RTP 不保证传输,但序列号的存在使得检测丢失的数据包成为可能。
  • 时间戳: (32 位) 用于使接收器能够在适当的时间间隔播放接收到的样本。当存在多个媒体流时,每个流中的时间戳是独立的,不能依赖它们进行媒体同步。计时粒度是应用程序特定的。
  • SSRC: (32 位) 同步源标识符唯一标识流的源。同一 RTP 会话中的同步源将是唯一的。 *这个非常重要*
  • CSRC: 贡献源 ID 枚举了由多个源生成的流的贡献源。
  • 扩展头部: (可选) 第一个 32 位字包含一个配置文件特定标识符 (16 位) 和一个长度指示符 (16 位),该指示符表示扩展的长度 (EHL=扩展头部长度) 以 32 位为单位,不包括扩展头部的 32 位。

RTP 头部之后是可变数量的字节,直到最大数据包大小 1500 字节,在某些网络中,数据包大小可以超过 1500 字节——IP 和 TCP/UDP 头部,除非网络有足够的支撑。

这些字节构成了 `RtpPacket` 的有效载荷,也称为系数

标准的重要部分显然是数据包结构和帧概念,但是还有像 JitterBuffer 和 Lip-synch 这样的术语,我将简要解释。

抖动缓冲器(JitterBuffer)和唇同步(Lip-synch)只是花哨的说法,用于确保您的 `RtpFrames` 中没有间隙,通过确保所包含的 `RtpPackets`(在 `RtpFrame` 中)序列号递增(不跳过),并以适当的延迟播放。在 Translator、Mixer 或 Proxy 实现中,这些概念的用处不大。

这并不是要贬低其他人在这方面所做的工作,然而,它们的值在编码或解码数据以供使用方面具有重要意义,但在传输领域则不那么相关。例如,在编码或解码时,时间戳用于确保音频和视频数据包之间的播放距离合理,从而实现唇形同步,或者换句话说,音频与视频匹配,漂移或延迟很小。

我不会深入探讨所有这些内部细节,我只是想从高层次解释它们的工作原理,如果您有兴趣,可以阅读 RFC。

在继续阅读时,需要记住的关键点是 `RtpPacket`s 有一个名为“Ssrc”的字段。该字段的完整类型名称是 SynchronizationSourceIdentifier,它标识了流以及流的发送方/接收方。这是一个重要的区别,您将在下面阅读到。

在掌握了协议的理解并能够自行创建流之后,我开始像其他逆向工程师一样,开始分析一些现有播放器与现有服务器配合使用的 WireShark 抓包数据。

我的目标是比较我客户端的流量与其他播放器的流量,以确定每个连接到服务器的客户端具体发生了什么变化。

经过对转储文件的一些分析,我发现我的客户端流量几乎完全相同,并且流数据没有变化,只有 `RtspServer` 的 'SETUP' 请求中的单个字段 'Ssrc' 发生了变化,随后所有 `RtcpPackets` `RtpPackets` 中的 'Ssrc' 字段也发生了变化。

我意识到,由于流数据(RTP 有效载荷)以及其内部的任何字节都没有改变……只有 'Ssrc' 字段(它代表了流和数据包的接收方)改变了,这将是一项简单的任务;我只需要修改该字段,以便在从服务器发送回客户端时,通过使用不同的 'Ssrc' 来有效地重新定位数据包。

我也可以使用旧的 'ssrc',将其添加到传出数据包的 'ContributingSources' 中,但我将此作为一项可选任务留给那些出于特定目的创建混音器的人,因为它会使您发送的每个 `RtpPacket` 的数据包大小增加几个字节。

我最初打算对 `RtcpPackets` 也做同样的事情,但是根据标准计算其报告的开销很小,而且在我的实现中可以引用和利用其他项目中现有的代码,所以我为我的 `RtpClient` 添加了根据标准生成和响应自己的 `RtcpPackets` 的能力。

结果高效且令人满意,并且与 VLC、Quicktime、FFMPEG 以及任何其他符合标准的播放器兼容。代码的工作原理如下……

任何从所包含的 `RtspServer` 消费流的客户端都会获得视频/音频流的精确副本,格式完全相同,每秒帧数也完全相同。

如果您的源流丢失了一个数据包,那么所有消费该流的客户端也可能会丢失一个数据包。

如果一个会话丢失了一个数据包,它不会影响服务器上的其他会话,最后,无论有多少流连接到所包含的服务器,源流的带宽消耗都如同只有一个流在消费它,其余的带宽由 `RtspServer` 和连接到它的客户端使用。

每个 `RtpClient` 实例都有一个线程来处理所有发生的音频和视频传输,因此缓冲区的生命周期在音频和视频流之间共享,数据包实例的生命周期很短,通常在缓冲区中完成,但是如果数据包大于缓冲区中分配的大小,它将根据头部字段中所需的信息准确完成。

现在我已接近尾声,如果不是因为 RTP over Tcp 与 RTP over Udp 略有不同,并且为了使事情复杂化,我们必须同时处理 Rtsp(全部在 TCP 的同一个套接字上),我早就应该完成了。经过一番研究,我偶然发现了以下 RFC,因为它由于某种原因没有被 RFC 2326 直接引用。

当视频通过互联网传输给家庭用户或工作场所用户时,通常会因为网络地址转换而出现问题。一个常见的例子是防火墙或路由器阻止某些 UDP 端口上的传入流量,导致播放器无法接收视频。解决此问题的唯一方法是让播放器通过TCP 打洞隧道连接。这允许防火墙允许客户端和服务器之间的流量通过指定端口。

在这种情况下,Rtp 和 Rtcp 流量不能使用不同的端口,这使得情况更加复杂,因为我们已经在使用 Rtsp 在 TCP 套接字上进行通信。

引入 RFC4571 / 独立 TCP

RFC4571 – “封装实时传输协议(RTP)和 RTP 控制协议(RTCP)数据包”
在面向连接的传输上”或者更简洁地说是“*交织*”

本 RFC 解释了在使用面向 TCP 的套接字时,如何使用 RTP / RTCP 通信发送和接收数据。它规定在 RTP 或 RTCP 数据包之前应该有一个 2 字节的长度字段。

Figure 1 defines the framing method.

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------------------------------------------------------+
|             LENGTH            |  RTP or RTCP packet ...       |  
+---------------------------------------------------------------+

结合 RFC2326 的第 10.12 节,该节通过添加一个帧字符和通道字符来分隔长度字段之前的 Rtsp 和 Rtp/Rtcp 流量,从而增强了此机制。

以下面的例子为例

C->S: PLAY rtsp://foo.com/bar.file RTSP/1.0     
   CSeq: 3
   Session: 12345678

S->C: RTSP/1.0 200 OK
   CSeq: 3
   Session: 12345678
   Date: 05 Jun 1997 18:59:15 GMT
   RTP-Info: url=rtsp://foo.com/bar.file;
     seq=232433;rtptime=972948234

S->C: $\000{2 byte length}{"length" bytes data, w/RTcP header}
S->C: $\000{2 byte length}{"length" bytes data, w/RTP header}
C->S: GET PARAMETER rtsp://foo.com/bar.file RTSP/1.0
   CSeq: 5
   Session: 12345678

S->C: RTSP/1.0 200 OK
   CSeq: 5
   Session: 12345678 

S->C: $\000{2 byte length}{"length" bytes data, w/RTP header}
S->C: $\000{2 byte length}{"length" bytes data, w/RTP header}

正如您所看到的,给出的示例 => *'$\000{2 字节长度}{“长度”字节数据,带 RTP 头}'* 在 RFC 中并没有真正地用图表表示出来,所以在这里我将为您做一些解释,并给您一个真实的例子。

在 RTP 数据与 Rtsp 通信在同一个套接字上“交错”发送和接收的情况下,我们需要一种方法来区分连续分配的内存(缓冲区)中 RTP 数据和 Rtsp 数据的开始和结束。

这就是 RFC2336 添加魔法字符“*$\000*”作为帧控制字符以指示套接字上有 RTP 数据的原因。

当遇到“*$\000*”时,通道和长度将随实际的 RTP 数据包一起出现。

$ - 是控制字符。
\0 - 是通道标识符
00 - 是 {数据} 的长度(本例中为 0)

所以如果我们有一个真正的数据包,它的帧头可能看起来像这样

0x36,0x01,{2 字节长度}

其中

(0x36) - 是控制字符。($)
0x01 - 是通道标识符

长度将以网络字节顺序跟随。

如果不存在美元符号“$”字符,则可以通过检查 RTP 和 RTCP 数据包的“PayloadType”字段来确定数据是 Rtp 还是 Rtcp,该字段通常是距离“$”字符应在位置的第五个字节。

(1 个控制字符 + 1 个通道 + 2 个长度)。

在 `RtpPackets` 中,这将是 PayloadType,而在 `RtcpPackets` 中,这将是 `RtcpPacketType`。我原本可以只检查公共版本字节,但这无法将数据包识别为 Rtp 或 Rtcp,因此使用了 PayloadType/PacketType(它们在两个数据包中具有相同的偏移量)。

如果通道字符不存在或通道字符与发送方/接收方未知的通道对应,也可以执行这些检查。

如果 PayloadType 未被识别为 Rtp PayloadType,且不在 `RtcpPackType` 的范围内,则该数据包要么是 Rtsp,要么是其他层的数据,并被包含直到数据中下一次出现控制字符。

 

一旦数据包类型被确定为与底层通道兼容,该数据包就可以由其处理程序进行处理。

开发人员在接收大包时应小心,但他们也应确保自己没有被攻击者注入值,攻击者试图通过让系统解码大量无意义的数据来发起 DOS 攻击,这些数据随后可能以各种方式进一步危害系统。

此库通过一次只接收缓冲区的一半来弥补这一点,然后才尝试解析缓冲区中的数据包。

`RtpClient` 和 `RtspClient` 轻松为您处理这些问题,并且 `RtspClient` 甚至在底层 `RtpClient` 使用套接字处理多个通道时,也能发送和接收许多 `RtspMessages`,这简而言之意味着 `RtpClient` 和 `RtspClient` 都完全支持交织。

所有 *IPacket* 实现现在都共享一个 *Prepare* 方法,该方法将数据包投影到 *Enumerable<byte>* 中,然后可以将其转换为 *Array* 或进一步操作。

全新改进

代码的第一个版本是快速开发的,它遵循 KISS 架构,功能非常强大。一些最大的更改是在运行时进行的,此前确定大多数用户将处理具有音频和视频两个轨道的会话。

在下一个版本中,通过提高性能和添加新功能来完善和抛光库,从而改进了功能。

`TransportContext` 类...它不是您日常使用的 System.Net.TransportContext。

据我所知,它确实是独一无二的,我是第一个设计出这种概念的人。

它之所以运行良好,有两个原因是大多数其他库无法适应的,因为它们以与我不同的方式看待抽象。

我认为它更好、更灵活,但其他人可能不这样认为,我将在下面解释这个概念。

我最初有一个名为 Interleave 的类,用于跟踪状态,但在发送方也可以是接收方(反之亦然)的场景中(想想组播或会议系统),我认为这对于要完成的工作类型更有效,并且在重复使用时对用户/开发人员来说更熟悉。

正式名称可能是“SubSession”,但 `TransportContext` 的目标是成为 SubSession 及更多,例如,如果您想同时实现 Tcp 和 Udp 的 Rtp,据我所知,这个库是唯一允许您这样做的库,如果还有其他库,请评论,我将修改我的声明。(商业或其他)

它维护套接字、传输内存、请求计数器和状态信息,使 RTP 的使用变得轻而易举。

这是 `TransportContext` 的类图

 

请注意抽象设计立场,其中属性名称是第三人称,Sender、Receiver、SinceLastReport。这些属性使 TransportContext 有用和功能。

它是一个活生生的野兽,必要时(以及如果需要或强制)通过使用 RtpClient 从发送者切换到接收者。

阻塞套接字……

通常,高性能代码在非阻塞套接字上运行。这使您能够以高于底层网络速率的速度发送和接收,从而将瓶颈放在硬件和网络设备上。

Windows 环回适配器通常足以处理正常的流量负载,但是当您故意以高速率发送时,尤其是高于网络接口额定速率的速度时,有时会对底层系统产生负面影响。

环回适配器的问题在于,没有“网线”,也没有网络接口处理器,只有一个虚拟的,它驻留在永恒的“新技术内核”(或其残余)中。

这导致分层服务提供程序出现一些问题,该服务提供程序负责在流量到达您交互的对象(例如套接字)之前对其进行验证。

在 Unix 上,这问题较少,我尚未测试 Windows 8,但根据 MSDN 页面,那里可能会有一些变化;然而,我想说的是,如果您在本地系统上测试 Udp 时遇到奇怪问题,同时也在本地系统上进行测试……请在评论或声明库存在问题之前,验证您是否遇到某种软件或硬件问题,或者预读优化。

TCP 交错支持终于完成,这意味着 QuickTime 和其他媒体播放器也将开箱即用。

还有一些事情要做,但是基础工作(例如数据包和报告类已经存在)已经完成,例如:

Rtcp XR 框架 - RFC2032, RFC3611, RFC5450 RFC5760

Rtcp 反馈框架 - RFC4585

Rtsp / Http 隧道支持。

更新

所有数据包类现在都是 *IDisposable*,这确保了 *RtpClient* / *RtspClient* 和 *RtspServer* 的内存使用量始终保持在某个假定大小内。您可以通过 *BaseDisposable* 类提供的每个实例上的 *ShouldDispose* 属性选择数据包是否被处置。

*IPacket* 现在整合了 Rtp 和 Rtcp 数据包实例,因此您可以更轻松地处理它们。(*RtspMessage* 也是 *IPacket*)

*RtpClient* 类已经重新设计,更易于使用和理解。

*RtspServer* 和 *RtspClient* 类现在支持 Rtsp 2.0 草案消息。

新的 *RtcpReport* 类,更易于使用和理解。

现在通过“rtpdump”格式支持录制/存档,存档流可以在 *RtspServer* 上播放或下载到其他地方使用。

完全托管的转码设施已经启动,允许用户输出 H.264 和 MPEG1/2,很快还将支持 MPEG4 及其他格式。

*Common.TaggedException<T>* 和 *ITaggedException* 允许处理异常并提供关于抛出异常的元数据,该类也是集成的绝佳帮手,因为您可以捕获 *Common.TaggedException* 或 *Common.TaggedException<MyType>* 以及普通的 *Exception* 类型。还有与异常相关的实用方法,称为 *Raise* 和 *TryRaise*。

public static class ExceptionExtensions
    {       

         //https://net7mma.codeplex.com/SourceControl/latest#Common/ExceptionExtensions.cs
    }

代码工作原理

在这里,我将以传统(或非传统)方式描述我的模型如何工作,我写得比较高层,所以您应该能够无需图表即可理解。如果我引用了某个特定主题,我将链接到它。如果出现好问题,我可能会考虑向文章的这一部分添加内容。

您可能知道或不知道,.Net 是一种垃圾收集语言,本文中将称为 GC 语言。.Net 也使用时间片或时间共享范式。有关 .Net 的更多信息,请参阅维基百科上的这篇文章

我附带了一些图表,然而您可能会同意它们是杂乱无章的,在我看来就像一个星系。尽管如此,通过适当的描述,您在阅读本文时应该能够彻底理解整个过程。您还可以查看 MSDN 视频,了解如何理解代码地图中的复杂代码图表。

现在我们将稍微深入基础知识,并迅速全面地了解环境和过程,因为我尽职尽责地向最终读者(您)提供对 Rtsp 和 Rtp 以及如何编写利用其概念的程序的完整而简洁的理解。

总而言之,

所有应用程序都有一个名为“Main”的静态入口点,从这一点开始,您的代码负责对可能是一个完整领域产生的影响,如果使用不当,尤其是在 GC 或时间缩放环境中。

我亲身体会到时间是有限的,我努力充分利用我的时间,把每一点可能的事情都挤进每一刻,有时这让我享受自己想做或应该做的事情的时间更少。

对于在处理器上执行的代码来说也是如此,从高层次来看;电力以光速传播。我们使用的设备中组件的尺寸(更重要的是它们的质量)决定了它们相对于光速的响应能力。(广义相对论是这样说的)。

东西越小,电就越成为它本身的一部分,一直到 弦理论。(至少我是这么希望的)

这意味着当您编写代码时,例如

int X = 0; x+= -x * x ^ x + x

您正在通过处理器中的组件(通常是晶体管)移动电流,这些单独的评估(-x * x)、异或、(x + x)被称为指令,就像您有一定数量的步骤来完成某事一样,计算机必须将代码分解为指令。

这些指令通常通过读取二进制值(1 和 0)来利用,其中组件中的正电荷通常表示 1,缺乏电荷表示 0。

当共享时间(以及在其他情况下,例如异步内核过程调用或系统事件)时,您可能正在给出/执行指令的过程中,突然被打断,然后发现实际经过的时间与您实际预期经过的时间之间存在一定量的时间差,这与系统中电流的流动以及在不受磁性和其他电力以及它们可能产生的热量等干扰的情况下记录变化的能力有关。

这叫做时间共享;无论我们是否意识到,我们都在进行时间共享。当我试图和我的狗“Bandit”玩耍时,我正准备和他玩耍,突然有了一个绝妙的主意,于是我试图放弃,把时间分配给思考我的主意和我最初打算做的事情,那就是和我的狗玩耍。

这就是时间共享的概念,在和我狗玩耍时,我会有与玩耍时间交替出现的想法,这些想法会在玩耍的间隙发生(有时还会占据主导)。

当您使用鼠标时也会发生同样的事情,因为设备物理移动并电学上注册中断,然后由硬件观察并传达给底层软件,在那里显示适配器和其他元素会更新。

总之,关于时间共享和所有那些有趣的东西就到此为止,让我们把这些放在一边,专注于您真正来这里的原因:RTP 协议的文档和理解,以及对代码最基本层面的理解。

`RtpClient` 类允许您创建和接收 `RtpPacket`;它包含一个或多个 `TransportContext`,但在某些高级情况下,它们也可能包含 0 个 `TransportContext`,但仍通过尚未在发布的代码中显示的某种机制进行发送和接收。这主要用于测试和开发组播,但是如果需要,也可以在不进行任何更改的情况下使用单播,并且默认情况下就是这样,因为这在大多数组播情况下仍然是最常见的和适用的。

从中要汲取的重要一点是,您会注意到 `RtpClient` 上的 `SendData` 和 `ReceiveData` 方法将 `Socket` 作为参数,以及 `TransportContext`,但这些方法被标记为内部是有原因的,稍后我将实现一个 `RtpClient.Multicast` 构造函数,它将允许此功能。

简而言之,组播 RTP 与单播 RTP 略有不同,仅仅是因为单个会话/会议中可以参与者的数量。整个差异围绕着 Rtcp 的使用,Rtcp 可用于指示会话中接收者的丢失;当在组播上使用 Rtcp 时,单个发送者的报告可能会发送给整个组,并且需要包含相关方的 *ReportBlocks*;这就是可以使用 *Rtp.Conference* 来减少单个 RtpClient 在发送时必须做的工作量以及通过组播等方式减少带宽的地方。

当您发送或接收 RTP 时,您正在参与一个会话,此会话通常通过其他方式进行描述,然后提供给 `RtpClient`。提供描述的方式以会话描述协议的形式出现。

在大多数情况下,您总是会调用 new *RtpClient(...) *来加入一个现有会话,并且大多数会话包含一个或多个“轨道”或媒体描述,这些描述定义了底层媒体是音频、视频、文本还是其他类型的二进制数据,有关 SessionDescription 的更多信息可以在下面或此链接找到。

Rtcp 是一个与 Rtp 密切相关的机制,它旨在仅占用应用程序使用带宽的一部分,它报告了通常必须以相同数据通道或其他带外方式传达的额外发送和接收指标,这会增加复杂性,因此它们大多数时候发生在单独的套接字上,除非您正在进行双工通信,这意味着 Rtp 和 Rtcp 在同一个端口上。

大多数人会争辩说,你可以用一个套接字完成这项工作,并且只需要一个主 Rtcp 和 Rtp 套接字,并且使用 SendTo 和 ReceiveFrom 就可以了,这基本上是正确的,但是一旦你开始利用非常高的数据速率,你会发现通常每个套接字一个线程的场景会带来更好的性能。

此实现为每个 `TransportContext` 使用两个非阻塞套接字,除非您使用 Tcp,在这种情况下您将使用一个阻塞套接字。它有一个在 Connect 方法中创建并生成的一个线程,该线程可以通过调用 Disconnect 方法终止。该线程被划分为发送和接收两部分,使用非常基本的方法读取 `DateTime.UtcNow`,这没有执行时区计算的开销,您实际上是使用一个结构而不是一个长整型来描述嵌入在 `TimeSpan` 和 `DateTime` 中的 TickCount,这些类只是这些值的抽象。

每个 `TransportContext` 都有一个本地缓冲区,其大小为 2 * RtpPacket.MaxPacketSize + 4,默认为 1500。

RFC2326 + RFC4571 字节($、id、{len0、len1})4 字节,Rtp 为 1500,其余(1472)用于 Rtcp。以及开销。

总大小为 3004。(加上一些用于数组信息,例如其版本)。

当调用 `Connect` 方法时,`Disconnect` 不会在 `InactivityTimeout` 过期且本地核心能够通过本地比较处理确定此情况的指令之前被调用。

当我和其他计算机科学家写“本地”时,我们通常指的是处理器执行代码的缓存中的本地,因为所有本地操作都在这里发生,然后从缓存中复制出来。这就是“线程”的本质,它们在执行时共享时间。

当您看到我很少使用锁,并且也停止使用 `Interlocked` 方法(如 Add 和 Increment 或 Read,尽管我已将它们注释掉以进行比较)时,这尤其有趣,因为在时间片场景中,在本地进行函数调用的开销至关重要,在执行锁定时尤其如此。如果另一个线程尝试本地“锁定”访问通过其他地方的“锁”语句“持有”的资源,那么这将导致死锁,其中没有线程可以执行,时间被浪费而无所事事,并导致争用,直到其中一个线程释放锁。

简而言之,除非我故意锁定某些内容以在操作至关重要时制造争用,否则您根本不会发现任何锁的使用。在那里,我使用了两种机制:

[System.Runtime.CompilerServices.MethodImplAttribute(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]

结合 `lock`,以确保关键部分的操作在微观尺度上不会出现太多错误。

简而言之,电力与不确定性结合本身就是一个奇迹,我们今天拥有的计算机在 20 年前可能被认为是外星技术……此外,在不深入探讨计算机科学的总体细节的情况下,我相信真正优秀的代码不需要锁和适当的同步,然而,尤其是在存在时间共享的 GC 语言中,这一点尤为重要。

为了提供一些我个人偏见的见解,我倾向于在大多数其他情况下写入集合时使用它们,但在读取时则不使用。我很少使用 `Mutex` 或 `WaitHandle`,除非情况需要,我不会到处寻找可以说“哦,这需要争用”的地方,但如果我发现并能确认争用是关键问题,我将进行同步。我最后将析构函数描述为一个典型的地方,您可以在大多数情况下锁定字段而不用担心争用,然而,在 GC 语言中,终结器是开销,并导致 GC 运行变慢,因为它必须执行终结器。请参阅 WaitForPendingFinalizers

通常,当您有非常相似的代码时,您会通过函数调用/函数指针提供该代码,以便您的代码更易于维护。在阅读这些函数时,您可能会说,此处和彼处的一些行本可以重构并通过静态实用方法调用,我对此表示同意,并鼓励大家在项目页面的问题跟踪器上创建并提交补丁。

现在有了更多的实用程序和扩展类,但在那个领域仍有工作要做。

我尚未进行抽象的一个主要原因是,当前实现对于一般使用来说性能良好,另一个原因是,需要从当前实现中挤出更多性能的人面临挑战。

该代码适用于生产环境,只要底层网络链路能够支持流量,它就可以处理任意数量的流。就其当前状态下的性能而言,使用自定义构建的服务器,我观察到以下结果:

性能测试表明,5000 个用户在最高 CPU 利用率为 42% 时,内存占用为 210 MB。

- 这些数字是在进程内获得的,包括快速连接的客户端,这些客户端可能会在超时之前断开连接并恢复其会话 -

(平均 CPU 利用率 20%,内存 70MB,平均 750 个活跃连接)

- 这些数据来自一个独立的专用进程,该进程不断从服务器获取媒体,同时我能够用 VLC 等工具观看流 -(但与断开连接测试同时进行)

新的服务器硬件配置如下:64GB Corei7 Extreme,双 GTX670 (4GB) 和 SSD。

高级功能,例如带宽限制,尚未实现,但计划添加这些功能等。

一个明显或不那么明显的地方是发送和接收的并行执行。我不会给出所有细节,但我会说应该使用模型函数而不是内联声明,模型函数应该迭代传出的数据包并以二进制形式存储在另一个连续的集合中,同时从它们所在的位置移除它们,然后另一个函数将迭代连续的分配并对其进行操作以进行发送,并执行任何必要的垃圾回收,并在可能时等待工作线程上的终结器。

通过 RtpClientTest 示例或其他测试来演练这些方法的一个好处是,RtpClient 实现的所有核心都在那里。如果突然你搞砸了什么,不再发送或接收,或者使用率很高,那么罪魁祸首可能就在那里。

`RtpClient` 上的工作线程将执行 SendReceive 循环,直到调用 `RtpClient` 上的 `Disconnect`。

RtspServer 以类似的方式运行,它维护一个本地 ClientSession 对象集合,其中包含正在进行的 Rtsp/Rtp 会话及其底层子会话的状态信息。

RtspServer 实验性地支持 Udp 和 Http,在库的最终版本发布给公众之前应该会稳定下来。

使用代码

向客户端传输媒体可能是一个复杂而昂贵的过程。该项目的目标是让开发人员能够以不到 10 行代码的方式,利用符合标准的协议实现,向客户端传输媒体。

//Create the server optionally specifying the port to listen on
Rtsp.RtspServer server = new Rtsp.RtspServer(/*554*/);

//Create a stream which will be exposed under the name Uri rtsp:///live/

//From the RtspSource rtsp://1.2.3.4/mpeg4/media.amp
Media.Rtsp.Server.MediaTypes.RtspSource source = new Media.Rtsp.Server.MediaTypes.RtspSource("YouTubeRtspSource", 
          "rtsp://v4.cache5.c.youtube.com/CjYLENy73wIaLQlg0fcbksoOZBMYDSANFEIJbXYtZ29vZ2xlSARSBXdhdGNoYNWajp7Cv7WoUQw=/0/0/0/video.3gp");
            
//If the stream had a username and password
//source.Client.Credential = new System.Net.NetworkCredential("user", "password");
            
//If you wanted to password protect the stream when clients connnect with a player
//source.RtspCredential = new System.Net.NetworkCredential("username", "password");

//Add the stream to the server
server.TryAddMedia(source);

//Start the server and underlying streams
server.Start();

//The server is now running,  you can access the stream  with VLC, QuickTime, etc  

开发人员可以在托管代码中创建新的 `RtpPacket`,也可以从 `Byte[]` 中解析它们。他们可以通过调用 `RtpPacket` 的 `Prepare` 方法获取 `RtpPacket` 的二进制表示。他们还可以通过调用重载的准备方法,如下所示。

//Create a RtpPacket in managed code
Rtp.RtpPacket packet = new Rtp.RtpPacket();
//packet.Created is set to DateTime.UtcNow automatically in constructor
packet.SequenceNumber = 1;
packet.SynchronizationSourceIdentifier = 0x0707070;
packet.TimeStamp = Utility.DateTimeToNtp32(DateTime.Now);
//packet.Channel = 0; //Old Code

byte[] someRtpData = packet.Prepare().ToArrya()// Could be a byte[] from a socket or anywhere else

//From a byte[]
packet = new Rtp.RtpPacket(someRtpData, 0);

//Packet as byte[]
byte[] output = packet.Prepare().ToArray();

//Same packet with a different Ssrc
output = packet.Prepare(packet.PayloadType, 0x123456).ToArray();   

RtpPacketsRtcpPackets 都有一个 DateTime Created 属性,允许开发人员跟踪数据包的创建时间。

该库还为创建和(重)写入符合会话描述协议的二进制数据提供了相同的功能。

//Create a SDP in managed code
Sdp.SessionDescription sdp = new Sdp.SessionDescription(0);
sdp.SessionName = "name";

//Add a new MediaDescription with the payload type 26
sdp.Add(new Sdp.SessionDescription.MediaDescription()
{
    MediaFormat = 26
});            

//Output it
string output = sdp.ToString();

//Or parse it from a string
sdp = new Sdp.SessionDescription(sdp.ToString());  

以及创建 `RtspRequest` 或 `RtspResponse` 的相同功能。

//Make a new RtspRequest in managed code
Rtsp.RtspMessage request = new Rtsp.RtspMessage(RtspMessageType.Reqeust);

//Assign some properties
request.CSeq = 1;
request.Method = Rtsp.RtspMethod.PLAY;

//Get the output to send  
byte[] output = request.ToBytes();

//Pase a RtspRequest from bytes
request = new Rtsp.RtspMessage(output);

//Create a new RtspResponse
Rtsp.RtspMessage response = new Rtsp.RtspResponse(RtspMessageType.Response);

//Parse one from bytes
response = new Rtsp.RtspResponse(output = response.Prepare());

其中包含了一个 `RtspClient` 和 `RtpClient`。`RtspClient` 在“SETUP”请求期间自动设置 `RtpClient`,并根据需要从 Udp 切换到 Tcp 或反向切换。

//Create a client
Rtsp.RtspClient client = new Rtsp.RtspClient("rtsp://someUri/live/name");

///The client has a Client Property which is used to access the RtpClient

///Attach events at the packet level
client.Client.RtcpPacketReceieved += 
  new Rtp.RtpClient.RtcpPacketHandler(Client_RtcpPacketReceieved);
client.Client.RtpPacketReceieved += 
  new Rtp.RtpClient.RtpPacketHandler(Client_RtpPacketReceieved);

//Attach events at the frame level
client.Client.RtpFrameChanged += 
  new Rtp.RtpClient.RtpFrameHandler(Client_RtpFrameChanged);

//Performs the Options, Describe, Setup and Play Request
client.StartListening(); 

 //Do something else 
///while (true) { }

//Send the Teardown and Goodbye
client.StopListening(); 

有一个 `InterleavedData` 事件,它向开发人员提供 TCP 套接字中遇到的数据。您不必使用此事件,但如果出于某种原因您想检查交错切片中的数据,它就在那里。通常,该事件提供由服务器推送的 *RtspMessages*,或者当使用 TCP 套接字时,Rtsp 响应有时会在传递给 *RtspClient* 之前通过相同的事件在此处找到。

`RtpClient` 和 `RtspClient` 已经为您处理了此事件,并在确定数据包有效且通道能够接收消息后,为切片数据触发适当的事件,例如 `OnRtpPacket` 或 `OnRtcpPacket`。

一个 `RtspClient` 可以在交错会话期间随意发送和接收,并且 `RtspRequests` 将按预期在交错数据之间处理。


以下是 `RtpClient` 和 `RtspClient` 的类图。

实现细节

媒体服务器需要围绕其源和接收器构建一个构造,以便在会话中正确传输它们。根据 RFC 中的术语,这通常在大多数实现中被称为“Sink”和“Source”。

在所包含的 `RtspServer` 中,目前只支持 `Rtp / Rtsp Sources`,这意味着您首先需要一个已经发送 RTSP / RTP 数据源。支持 Rtmp 和其他协议的工作正在进行中,这将允许您通过 Rtsp 和 Rtp 提供这些流。

如果您想从文件流式传输,您可以创建一个新的 `SourceStream` 类型,例如 MediaFileStream,并从 `SourceStream` 继承。

定义了 *IMediaSink*、*IMediaSource* 和 *IMediaStream* 接口,它们允许派生同时作为源和汇的实现。基本接口是 IMediaStream,它提供了 Guid -> Id 和 SessionDescription 属性,这些属性标识了流。

如果您想创建一个类,该类缓存直播流并允许从当前遇到的任何点播放,那么您将从 `RtspSourceStream` 派生并添加逻辑来存储每个帧,然后不是像直播流那样附加到事件,而是从缓存中跳过和获取帧并将它们发送给客户端,从而允许客户端从源媒体中的任何点播放他们想要的任何持续时间。(*这种功能可能最终会包含在库中,但在撰写本文时,我只是没有时间完成它*)。

`SourceStream`,分配流的 ID 并允许您设置流名称。它是 `RtspServer` 所有源的基础。它还为您提供了一种通过别名属性(包含与流相关联的所有名称)而不是单个名称来命名流的方式。例如,当您更改名称时,旧名称将成为别名。您也可以随时添加别名。

`ChildStream` 继承自 `SourceStream`,允许创建具有与源相同属性的子流。如果您想降低帧速率或进行其他操作,您可以在子流中添加逻辑。

我设计了 SourceStream 类和 `ClientSession` 用于这些目的,其中 SourceStream 通过事件同时封装了“Source”和“Sink”,每个 `ClientSession` 只通过底层 `RtpClient` 的事件获取“Source”流的副本,该 `RtpClient` 属于所述 `SourceStream`。

`ClientSession` 只是一个 `IMediaSink`,因为它只从其他地方发送数据。

目前,**RtspServer** 将 **IMediaStream** 公开为 **Streams** 属性返回的类型,总的来说,这意味着您可以检查特定实例是否为 **IMediaSink** 来确定特定类型的流,此外还有一个 SessionDescription 属性。

目标是最终也允许其他传输封装,并可能实现它们,例如 RTMP,但是由于它是一种主要与 Flash(我讨厌)一起使用的封闭技术,这使得我不太愿意投入时间和精力在该领域,添加支持的基础已经提供,当时间允许时会提供更多,以便该库也可以支持 Flash / Rtmp 到 Rtsp/Rtp 网关功能!

`ClientSession` 的 `OnSourceRtpPacket` 和 `OnSourceRtcpPacket` 方法负责将数据包添加到列表中,这些数据包将在 `ClientSession` 使用的底层 `RtpClient` 的 SendReceive 阶段发送出去。

简而言之,当 `RtpPacket` 到达 SourceStream 时,会触发一个事件,然后由客户端的 ClientSession 通过 ClientSession 的 `OnSourceRtpPacketReceived` 和 `OnSourceRtcpPacketReceived` 方法进行处理,您可以在下面找到这些方法。

/// <summary>        
/// Called for each RtpPacket received in the source RtpClient
/// </summary>
/// <param name="client" />The RtpClient from which the packet arrived
/// <param name="packet" />The packet which arrived
internal void OnSourceRtpPacketRecieved(RtpClient client, RtpPacket packet)
{
    RtpClient.TransportContext trasnportContext = m_RtpClient.GetContextForPacket(packet);

    if (trasnportContext != null)
    {
        if (packet.Timestamp >= trasnportContext.RtpTimestamp)
        {
            //Send on its own thread
            try { m_RtpClient.EnqueRtpPacket(packet); }
            catch { }
        }
    }    
}

/// <summary>
/// Called for each RtcpPacket recevied in the source RtpClient
/// </summary>
/// <param name="stream" />The listener from which the packet arrived
/// <param name="packet" />The packet which arrived
internal void OnSourceRtcpPacketRecieved(RtpClient stream, RtcpPacket packet)
{
    try
    {
        //E.g. when Stream Location changes on the fly etc.
        if (packet.PacketType == RtcpPacket.RtcpPacketType.Goodbye)
        {
            RtpClient.TransportContext trasnportContext = m_RtpClient.GetContextForPacket(packet);

            //Prep the client for a data loss
            if (trasnportContext != null)
            {
                m_RtpClient.SendGoodbye(trasnportContext);
            }

        }
        else if (packet.PacketType == RtcpPacket.RtcpPacketType.SendersReport)
        {
            //The source stream recieved a senders report                
            //Update the RtpTimestamp and NtpTimestamp for our clients also
            SendersReport sr = new SendersReport(packet);

            RtpClient.TransportContext trasnportContext = m_RtpClient.GetContextForPacket(packet);

            if (trasnportContext == null) return;
            else if (sr.NtpTimestamp > trasnportContext.NtpTimestamp)
            {
                trasnportContext.NtpTimestamp = sr.NtpTimestamp;
                trasnportContext.RtpTimestamp = sr.RtpTimestamp;
            }
        }
    }
    catch { }
}

连接到 `RtspServer` 的每个客户端或播放器都由一个 `RtspSession` 表示。

ClientSessions 在符合标准的 `RtspClient` 连接到 RtspServer 时由 `RtspServer` 自动创建。

        /// <summary>
        /// Handles the accept of rtsp client sockets into the server
        /// </summary>
        /// <param name="ar">IAsyncResult with a Socket object in the AsyncState property</param>
        internal void ProcessAccept(IAsyncResult ar)
        {
            if (ar == null) goto End;
            
            //The ClientSession created
            ClientSession created = null;
            
            try
            {
                //The Socket needed to create a ClientSession
                Socket clientSocket = null;

                //See if there is a socket in the state object
                Socket server = (Socket)ar.AsyncState;

                //If there is no socket then an accept has cannot be performed
                if (server == null) goto End;

                //If this is the inital receive for a Udp or the server given is UDP
                if (server.ProtocolType == ProtocolType.Udp)
                {
                    //Should always be 0 for our server any servers passed in
                    int acceptBytes = server.EndReceive(ar);

                    //Start receiving again if this was our server
                    if (m_UdpServerSocket.Handle == server.Handle)
                        m_UdpServerSocket.BeginReceive(Utility.Empty, 0, 0, SocketFlags.Partial, ProcessAccept, m_UdpServerSocket);

                    //The client socket is the server socket under Udp
                    clientSocket = server;
                }
                else if(server.ProtocolType == ProtocolType.Tcp) //Tcp
                {
                    //The clientSocket is obtained from the EndAccept call
                    clientSocket = server.EndAccept(ar);
                }
                else
                {
                    throw new Exception("This server can only accept connections from Tcp or Udp sockets");
                }

                //Make a temporary client (Could move semantics about begin recieve to ClientSession)
                created = CreateOrObtainSession(clientSocket);
            }
            catch(Exception ex)//Using begin methods you want to hide this exception to ensure that the worker thread does not exit because of an exception at this level
            {
                //If there is a logger log the exception
                if (Logger != null)
                    Logger.LogException(ex);

                ////If a session was created dispose of it
                //if (created != null)
                //    created.Disconnect();
            }

        End:
            allDone.Set();

            created = null;

            //Thread exit 0
            return;
        }

        internal ClientSession CreateOrObtainSession(Socket rtspSocket)
        {

            //Handle the transient sockets which may come from clients which close theirs previoulsy, this should only occur for the first request or a request after play.
            //In UDP all connections are such

            //Iterate clients looking for the socket handle
            foreach (ClientSession cs in Clients)
            {
                //If there is already a socket then use that one
                if (cs.RemoteEndPoint == rtspSocket.RemoteEndPoint)
                {
                    return cs;
                }
            }

            //Create a new session with the new socket, there may be an existing session from another port or address at this point....
            //So long as that session does not attempt to access resources in another session given by 'sessionId' of that session then everything should be okay.
            ClientSession session = new ClientSession(this, rtspSocket);

            //Add the session
            AddSession(session);

            //Return a new client session
            return session;
        }

 

RtpSources 暴露了允许处理或转发 RtpPacket 和 RtcpPacket 到另一个 RtpClient 或 RtspSession 的方法。您会注意到 RtspSession 上的方法与 RtpClient 触发的事件处理程序具有相同的签名。这样可以非常容易地随时添加和删除事件。

(您可以在下面的示例中看到一个例子,我处理了“PLAY”请求和响应。)

它们还公开了一个名为 `OnFrameDecoded` 的解码图像事件,该事件将 `RtpPacket` 或 `RtpFrame` 转换为 `System.Drawing.Image`。

由于某些解码通常对处理器来说非常密集,因此逻辑通过事件公开,事件模型允许用户在我们执行所需任务后适当处理事件。

当 `OnRtpFrameCompleted` 被调用时,`RtspSession` 的 `RtpClient` 通常会调用此事件。此方法可以阻塞任意长时间,因为底层 `RtpClient` 将继续触发事件。

internal void OnFrameDecoded(System.Drawing.Image decoded) { if (FrameDecoded != null) FrameDecoded(this, decoded); }
internal virtual void DecodeFrame(Rtp.RtpClient sender, Rtp.RtpFrame frame)
{
    if (RtspClient.Client == null || RtspClient.Client != sender) return;
    try
    {
        if (!frame.Complete) return;
        
        //Get the MediaDescription (by ssrc so dynamic payload types don't conflict
        Rtp.RtpClient.TransportContext tc = 
          this.RtspClient.Client.GetContextBySourceId(frame.SynchronizationSourceIdentifier);
        
        if (tc == null) return;

        Media.Sdp.MediaDescription mediaDescription = tc.MediaDescription;
        
        if (mediaDescription.MediaType == Sdp.MediaType.audio)
        {
            //Could have generic byte[] handlers OnAudioData OnVideoData OnEtc
            //throw new NotImplementedException();
        }
        else if (mediaDescription.MediaType == Sdp.MediaType.video)
        {
            if (mediaDescription.MediaFormat == 26)
            {
               OnFrameDecoded(m_lastFrame = (new Rtp.JpegFrame(frame)).ToImage());
            }
            else if (mediaDescription.MediaFormat >= 96 && mediaDescription.MediaFormat < 128)
            {
                //Dynamic..
                //throw new NotImplementedException();
            }
            else
            {
                //0 - 95 || >= 128
                //throw new NotImplementedException();
            }
        }
    }
    catch
    {
        return;
    }
} 

目前,服务器只能解码或编码 RFC2435 Jpeg,并且该过程对 CPU 消耗不大,事实上,服务器可以以超过 100 FPS 的速度发送数据。

其他类型的编码和解码支持正在进行中,但使用 FFMPEG 或 LibAvCodec 等其他库,您可以轻松支持您需要的任何类型的编码或解码。只需要根据您正在使用的配置文件进行打包即可。

此领域的更新包括对 16 位精度和 DataRestartInterval 标记的支持。

每个 `RtspServer` 实例本身都使用异步套接字进行多线程处理。这意味着每个客户端请求都将在线程池中的独立线程上处理。

当“PLAY”请求进来时,我只需将源 `RtspStream` 的事件连接到客户端的 `RtspSession`,结果是客户端获得源音频/视频流逐包的副本。

ClientSession 的 `RtpClient` 负责在工作线程中执行的 `SendReceive` 阶段,从其队列中取出 `RtpPacket` 时,以正确的“Ssrc”将其发送出去。
 

        /// <summary>
        /// Entry point of the m_WorkerThread. Handles sending out RtpPackets and RtcpPackets in buffer and handling any incoming RtcpPackets.
        /// Sends a Goodbye and exits if no packets are sent of recieved in a certain amount of time
        /// </summary>
        void SendReceieve()
        {
            Begin:
            try
            {

                DateTime lastOperation = DateTime.UtcNow;

                //Until aborted
                while (!m_StopRequested)
                {
                    #region Recieve Incoming Data

                    //Enumerate each context and receive data, if received update the lastActivity
                    //ParallelEnumerable.ForAll(TransportContexts.ToArray().AsParallel(), (tc) =>
                    //{
                    //    ProcessReceive(tc, ref lastOperation);                       
                    //});

                    foreach (TransportContext context in TransportContexts.ToArray())
                    {
                        ProcessReceive(context, ref lastOperation);
                    }

                    if (m_OutgoingRtcpPackets.Count + m_OutgoingRtpPackets.Count == 0)
                    {
                        //Should also check for bit rate before sleeping
                        System.Threading.Thread.Sleep(TransportContexts.Count);
                        continue;
                    }

                    #endregion

                    #region Handle Outgoing RtcpPackets

                    if (m_OutgoingRtcpPackets.Count > 0)
                    {
                        int remove = m_OutgoingRtcpPackets.Count;

                        var rtcpPackets = m_OutgoingRtcpPackets.GetRange(0, remove);

                        if (SendRtcpPackets(rtcpPackets) > 0) lastOperation = DateTime.UtcNow;

                        m_OutgoingRtcpPackets.RemoveRange(0, remove);

                        rtcpPackets = null;
                    }

                    #endregion

                    #region Handle Outgoing RtpPackets

                    if (m_OutgoingRtpPackets.Count > 0)
                    {
                        //Could check for timestamp more recent then packet at 0  on transporContext and discard...
                        //Send only A few at a time to share with rtcp
                        int sent = 0;

                        foreach (RtpPacket packet in m_OutgoingRtpPackets.ToArray())
                        {
                            if (packet == null || packet.Disposed) ++sent;
                            //If the entire packet was sent
                            else if (SendRtpPacket(packet) >= packet.Length)
                            {
                                ++sent;
                                lastOperation = DateTime.UtcNow;
                            }
                        }

                        m_OutgoingRtpPackets.RemoveRange(0, sent);                        
                    }

                    #endregion
                }
            }
            catch { if (!m_StopRequested)  goto Begin; }        
        }

以下是一个示例,展示了当服务器收到客户端的“PLAY”命令时,如何通过 `RtspServer` 使用 `RtspSession` 来聚合数据包,利用 `RtspSourceStream` 的 `RtpClient` 上的事件。

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="session"></param>
        internal void ProcessRtspPlay(RtspMessage request, ClientSession session)
        {
            RtpSource found = FindStreamByLocation(request.Location) as RtpSource;

            if (found == null)
            {
                ProcessLocationNotFoundRtspRequest(session);
                return;
            }

            if (!AuthenticateRequest(request, found))
            {
                ProcessAuthorizationRequired(found, session);
                return;
            }                
            else if (!found.Ready)
            {
                //Stream is not yet ready
                ProcessInvalidRtspRequest(session, RtspStatusCode.PreconditionFailed);
                return;
            }

            RtspMessage resp = session.ProcessPlay(request, found);

            //Send the response to the client
            ProcessSendRtspResponse(resp, session);

            session.m_RtpClient.m_WorkerThread.Priority = ThreadPriority.AboveNormal;

            session.ProcessPacketBuffer(found);
        }

对于“PAUSE”或“TEARDOWN”请求,我只需从源 `RtspStream` 的 `RtpClient` 以及随后的 `RtspSession` 中删除这些事件即可。

        /// <summary>
        /// 
        /// </summary>
        /// <param name="request"></param>
        /// <param name="session"></param>
        internal void ProcessRtspPause(RtspMessage request, ClientSession session)
        {

            RtpSource found = FindStreamByLocation(request.Location) as RtpSource;

            if (found == null)
            {
                ProcessLocationNotFoundRtspRequest(session);
                return;
            }

            if (!AuthenticateRequest(request, found))
            {
                ProcessAuthorizationRequired(found, session);
                return;
            }

            //Might need to add some headers
            ProcessSendRtspResponse(session.ProcessPause(request, found), session);
        }

服务器正在运行,同时通过 UDP 重新提供两个 Rtsp TCP 流的截图。

服务器正在运行,同时重新提供超过 5 个流(TCP 和 UDP),并用 VLC 同时观看它们的截图。

 

兴趣点/注意事项

我最初在不到 30 天内构建了整个代码库!这并不意味着结果不专业或存在问题,这只是表明如果您努力尝试,就能做到!

又花了大约 30 天,在搬到我的第一个家和处理所有这些问题之间,才使其达到现在的水平。

我现在已经在这个库上工作了大约 2 年,它得到了极大的改进。

随着新功能的添加、完成、修复或贡献,我将相应地更新本文!

您找不到任何 *CompondPacket* 类,大多数其他实现用于在单个缓冲区中包含多个 `RtcpPackets` 时进行传输。我不确定其他实现为什么采用这种概念。我通过 `RtcpPacket.GetPackets` 方法处理单个缓冲区中的多个 `RtcpPackets`,该方法返回在该缓冲区中找到的 `RtcpPackets` 数组。

内存使用率低,只使用所需资源,不会不必要地保留任何东西,包括已完成的数据包/帧等。当从包含的 `RtspServer` 向我自己提供两个流时,我发现在测试期间,聚合 10 个源流的内存使用量低于 20-40 MB。

此项目没有外部依赖,并且您可以单独使用所需的部分,而无需其他部分。例如,可以使用 RtpClient 而无需 RtspClient,或者使用 RtspClient 而无需 RtspServer。它们都独立运行良好,并按照 RFC 的要求执行,使其成为在任何支持 .Net 的操作系统中用于任何类型系统的完整实现。这也是为什么内部或受保护访问权限的公开方式是这样的原因。我尝试仅在可能的情况下公开公共属性。

SessionDescription.MediaDescription 这样的对象具有用于添加和删除行的内部方法,但是在实时连接上使用它们时应小心,因为 SessionDescription 的 Version 属性必须在会话期间发生的所有更改时都随之更改。当您通过添加或删除行来更改 MediaDescription 时,无法通知父级 SessionDescription,除非您拥有支持其更改所需的所有管道,因此通过允许某人调用 MediaDescription 行上的 Add 或 Remove,然后在此操作期间根据需要手动更改 Version 来解决此问题。当您从 SessionDescription 中添加或删除 SessionDescriptionLine 时,这会自动为您完成,因为除非您通过可选参数指示不这样做,否则 Version 在同一范围内可访问。

该代码还包括一个新的跨平台实现,可以在微秒 (μs) 级别延迟时间,称为 μTimer

#region Cross Platform μTimer

/// <summary>
/// A Cross platform implementation which can delay time on the microsecond(μs) scale.
/// It operates at a frequencies which are faster then most Platform
///      Invoke results can provide due to the use of Kernel Calls under the hood.
/// Requires Libc.so@usleep on Mono and QueryPerformanceCounter on Windows for uSleep static
/// </summary>
/// <notes>A Tcp Socket will be created on port 7777 by default to help
///   keep track of time. No connections will be recieved from this socket.</notes>
public sealed class μTimer
{
    #region Not Applicable for the MicroFramework
#if(!MF)

    #region Uncesessary Interop (Left for Comparison)
#if MONO
    using System.Runtime.InteropServices;
    [System.Runtime.InteropServices.DllImport("libc.so")] //.a , Not Portable
    static extern int usleep (uint amount);

    ///<notes>The type useconds_t is an unsigned integer type capable of holding
    /// integers in the range [0,1000000]. Programs will be more portable
    /// if they never mention this type explicitly. </notes>
    void uSleep(int waitTime) { usleep(waitTime); }
#else
    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
    static extern bool QueryPerformanceCounter(out long lpPerformanceCount);

    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
    static extern bool QueryPerformanceFrequency(out long lpFrequency);

    /// <summary>
    /// Performs a sleep using a plaform dependent but proven method
    /// </summary>
    /// <param name="amount">The amount of time to sleep in microseconds(μs)</param>
    public static void uSleep(TimeSpan amount) { μTimer.uSleep(((int)(amount.TotalMilliseconds * 1000))); }

    /// <summary>
    /// Performs uSleep by convention of waiting on performance couters
    /// </summary>
    /// <param name="waitTime">The amount of time to wait</param>
    public static void uSleep(int waitTime)
    {
        long time1 = 0, time2 = 0, freq = 0;

        QueryPerformanceCounter(out time1);
        QueryPerformanceFrequency(out freq);

        do
        {
            QueryPerformanceCounter(out time2);
        } while ((time2 - time1) < waitTime);
    }
#endif
    #endregion
#endif
    #endregion

    #region Statics

    //Who but me
    const ushort Port = 7777;

    //Since System.Timespan.TickerPerMicrosecond is constantly 10,000
    public const long TicksPerMicrosecond = 10;

    /// <summary>
    /// A divider used to scale time for waiting
    /// </summary>
    public const long Divider = 1000;

    /// <summary>
    /// The socket we use to keep track of time
    /// </summary>
    static Socket m_Socket = 
      new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    /// <summary>
    /// The memory we give to the socket for events which should not occur
    /// </summary>
    static SocketAsyncEventArgs m_SocketMemory = new SocketAsyncEventArgs();

    /// <summary>
    /// Handles the creation of resources used to provide the μSleep method.
    /// </summary>
    static μTimer()
    {
        try
        {
            //Listen on the Loopback adapter on the specified port
            m_Socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, Port));

            //Only for 1 client
            m_Socket.Listen(1);

            //Assign an event now because in Begin process we will not call it if the even will not raise
            m_SocketMemory.Completed += BeginProcess;

            //If the SocketAsyncEventArgs will not raise it's own event we will call it now
            if (!m_Socket.AcceptAsync(m_SocketMemory))
            {
                BeginProcess(typeof(μTimer), m_SocketMemory);
            }
        }
        catch
        {
            throw;
        }
    }

    /// <summary>
    /// Handles processing on the master time socket.
    /// This should never occcur.
    /// </summary>
    /// <param name="sender">The sender of the event</param>
    /// <param name="e">The SocketAsyncEventArgs from the event</param>
    static void BeginProcess(object sender, SocketAsyncEventArgs e)
    {
        if (e.LastOperation == SocketAsyncOperation.Connect)
        {
            //Dispose the SOB who interrupted us
            Socket dontCare = e.AcceptSocket;
            dontCare.Dispose();

            //Call accept again
            if (!m_Socket.AcceptAsync(e))
            {
                //We are being DOS Attacked..
                throw new System.InvalidProgramException(
                   "A Connection to the system was made by a unauthorized means.");
            }
        }
    }

    /// <summary>
    /// Performs a sleep using a method engineered by Julius Friedman (juliusfriedman@gmail.com)
    /// </summary>
    /// <param name="amount">The amount of time to Sleep</param>
    public static void μSleep(TimeSpan amount)
    {
        //Sample the system clock
        DateTime now = DateTime.UtcNow, then = DateTime.UtcNow;
        TimeSpan waited = now - then;
        //If cpu time is not fast enough to accomadate then you are in bigger trouble then you know
        if (waited > amount) return;
        //A normal sleep with an amount less that 1 but greater than 0 Millisecond will not switch
        else System.Threading.Thread.Sleep(amount - waited);
        waited = now - then;//Waste cycles and calculate time waited in ticks again
        if (waited > amount) return;
        else unchecked
        {
            //Scale time, basis of theory is we shouldn't be able to read from a socket in Accept mode 
            //and it should take more time than a 1000th of the time we need
            if (m_Socket.Poll(((int)((amount.Ticks - waited.Ticks / 
                      TicksPerMicrosecond) / Divider)), SelectMode.SelectRead))
            {
                //We didn't sleep
                //Sample the system clock
                then = DateTime.UtcNow;
                //Calculate waited
                //Subtract time already waited from amount
                amount -= waited;
                //Waited set to now - then to determine wait
                waited = now - then;
                //return or utilize rest of slice sleeping
                if (waited > amount) return;
                else System.Threading.Thread.Sleep(amount - waited);
            }
        }
    }

    /// <summary>
    /// Performs a sleep using a method engineered by Julius Friedman (juliusfriedman@gmail.com)
    /// </summary>
    /// <param name="amount">The amount of time to Sleep in microseconds(μs) </param>
    public static void μSleep(int amount) { μTimer.μSleep(TimeSpan.FromMilliseconds(amount * 1000)); }

    #endregion
}

#endregion 

它在 Mono 和 Windows 上都可以工作,并且胜过我迄今为止见过的任何其他可靠地实现这种时间延迟的方法,包括 StopWatch,并且不使用任何不安全或平台调用代码!

它通过底层使用套接字工作。有关更多信息,请参阅 Stack Overflow 上题为“使用 .Net 获取微秒精度而无需平台调用”的帖子。

RtspServer 与任何 RtspClient 实现兼容,而不仅仅是我的,并且已经用 VLC(2.0.5 Twoflower x64)Quicktime (7.7.3 - 1680.64)FFMPEG (20130103 x64) 进行了测试,让您和您的客户可以使用他们已有的工具!

编译后的代码大小约为 100k,可在 32 位和 64 位环境中使用。它不应该泄漏内存,并且应该高效,这意味着您永远不会丢失来自源流的源数据包,并且您不应该有数据包积压。

代码的主页位于 CodePlex - http://net7mma.codeplex.com

如果您发现任何问题或只想提出一个想法,请使用 CodePlex 网站。

该库还实现了通过 RTP 编码和解码 JPEG,也称为 RFC2435

我测试了支持的标准所允许的最大流,性能良好。

更多的编码/解码原本是该库的一部分,但我没有时间实现成功所需的所有代码和容器。

如果您有兴趣解码接收到的 Jpeg 或编码 Jpeg 以通过 RTP 发送,请查看 RFC2435Stream.cs 源代码!

最后

我坚信 C# 足够强大,关于此类工作需要“汇编”的说法是错误的,应视为谣言。C# 代码无论如何都会被“即时”编译成汇编,所以关键是让您的代码高效,并进行基准测试和进一步的代码分析以获得所需的结果。

希望不久的将来,我将通过计划在该库中提供的 H264 和 MPEG 编码和解码实用程序亲身证明这一点。

目前,最新源代码针对 .Net 4.5.2,以尽可能利用 SIMD 和其他内在函数,因此最终开发人员在使用 .Net Framework 中的视频时可能需要考虑的一切都可以在该库中找到。

参考文献

历史

  • 12/11/12 - 初始版本。
  • 12/11/12 - 添加了包含更多示例和图表的实现细节。
  • 12/11/12 - 添加了更多示例,详细说明了数据包格式并展示了示例请求以及更多实现细节。
  • 12/14/12 - 更新了 CodePlex 上的源解决方案。更正了文章中指向 JpegFrame.cs 源的链接。
  • 1/7/13 - 添加了新信息。更新了 CodePlex 上的源解决方案。
  • 1/9/2013 - 修正了拼写错误。更新了 CodePlex 上的源解决方案。
  • 3/27/2013 - 更新了 RC2 的文章
  • 3/31/2013 - 更新了 RC2 的文章 - 完美无瑕
  • 6/22/2014 - 更新了稳定无瑕版本的文章和对 Bandit 的纪念。
  • 2/7/2014 - 更新了稳定容器版本的文章和对 Bandit 的纪念。

谢谢

显然要感谢那些我借鉴方法并用作参考的现有项目。

还要感谢 ISO 和 IETF 为我们提供了这些出色的标准可供实现!

我还要感谢数字 7、字母“M”和绿色

特别感谢我的狗狗“Bandit”,我为了更频繁地进行这个项目而忽略了给他应有的玩耍时间,他给了我最好的想法,我永远感谢他的陪伴,即使他晚上不让我上我的床!

*Bandit 的更新*

Bandit,我最喜欢的狗狗,于 2014 年 2 月 7 日不幸去世。他刚满 2 岁。

在过去的冬天,我注意到当他在外面长时间时,他会被围栏周围深深吸引。我以为他只是想挖个洞之类的,但随着时间的推移,我注意到他正在接触某些东西,因为任何人在围栏附近,即使是稍微靠近,他都会非常激动,并且再次进入家时会表现得非常奇怪;例如,他走路非常摇晃,非常不服从,但不是我熟悉的方式……我通过尽可能不让他没有我在场的情况下长时间待在外面来纠正这一点,结果我没有像我希望的那样多地从事项目。

终于有一天他被臭鼬袭击了,我不认为臭鼬有狂犬病,但它的确抓伤了他的眼睛,把他弄得臭烘烘的。他的眼睛肿了很长时间,这让他想报复,从那时起,在他不再闻起来像臭鼬之后,你会发现他变得有点野了...

他又好了几天,直到不知为何,他开始在围栏边发狂,当我试图叫他进来时,他会呆呆地望着远方,或者看起来不想听……我只是把它当作他玩得太开心而无法立即回应,但我注意到他进屋后的行为变得非常不像他,变得好斗和易怒……

我无法弄清他为什么会这样,于是决定带他去看兽医,兽医除了可能改变饮食会引起这种情况外,找不到任何原因...

在密切监控他的时候,我发现我的一位好邻居竟然有胆子放下掺有毒药或者对他不健康的食物……我只注意到是因为有一天我出去调查为什么狗叫了几声然后又安静下来,我注意到了一些扔给他的“食物”仍然在地上。

当我走近他时,我能看出他病了,他呕吐了,然后又试图吃呕吐物,又病了……我叫他过来,他摇摇晃晃的,像喝醉了一样,几乎站不稳。我迎面走到一半,把他抱起来,让他脱离了那个境况,然后把他放在沙发上和我一起休息。

几个小时后(我一直没离开他身边),他醒来,被一些噪音吓到了,当我去看是什么噪音时,他咬了我的手臂……

伤势相当严重,但我知道他方向感混乱,我只是抖了抖,让他自己呆着,我清理了伤口,然后忍着疼痛睡了一觉。

第二天早上,Bandit 死了,我的手臂严重感染,我不得不处理那个烂摊子,然后去了医院治疗我的伤口,医院说我的伤口里外都有微量的老鼠药,要么是我被狗咬后擦到了毒药。他们通常甚至不会注意到,但其中一个新棉签在接触某些毒药时会变色,以确保人们不会意外受到污染……这对技术来说是好事,对我的邻居来说是坏事,对我自己和 Bandit 来说更糟!

结果,ASPC 没有对狗进行狂犬病或毒物检测,所以我永远无法追究邻居的责任……但最终,除了我自己,没有人可以追究,我试图消除我的痛苦,但我知道,如果我当时多给他一些关注和时间,事情就会有所不同。

Bandit 是我最好的朋友,他在每一个可能的关头都保护我并挑战我。他将那些不适合我的人推开了我的生活,并帮助我找到了我将永远珍惜的人和事物。

我仍然保留着他的玩具,每晚都把他的项链放在床边睡觉,一想到他我有时还会哭,我有责任完成这个项目并尽我所能维护它,因为我觉得如果我当时多关注他一些,我就能救他的命!

我知道有一天我会再见到他,当我见到他时,我就知道时间到了...

他的精神将永远活在项目代码中,我将为示例添加更多他的照片。他将被永远怀念,不知为何我明白,即使他不在我身边,他仍在平静地守护着我。他将永远是我的“Woo Woo”和“Boo”,永不被遗忘! 

*永远* 

使用 RTSP 和 RTP 的托管媒体聚合 - CodeProject - 代码之家
© . All rights reserved.