Sharp RTSP
这是一个用于构建RTSP客户端、RTSP服务器和处理RTP数据流的C#库。该库包含多个示例。
- RTSP客户端示例 - 将连接到RTSP服务器并接收H264、H265/HEVC、G711、AAC和AMR格式的视频和音频。支持UDP、TCP和多播。接收到的数据将写入文件。
- RTSP摄像机服务器示例 - 一个YUV图像生成器和一个非常简单的H264编码器生成H264 NAL单元,然后通过RTSP服务器传送给客户端。
- RTP接收器 - 将接收RTP和RTCP数据包并将它们传递给传输处理程序。
- RTSP服务器 - 将接受RTSP连接并与客户端通信。
- RTP发送器 - 将RTP数据包发送给客户端。
- 传输处理程序 - 提供了H264、H265/HEVC、G711和AMR的传输处理程序。
:warning: : 此库不处理视频或音频的解码(例如将H264转换为位图)。SharpRTSP仅限于传输层,生成您需要输入到视频解码器或音频解码器的原始数据。许多人使用FFMPEG或使用硬件加速的操作系统API来进行解码。
RTSP客户端示例演练
这是RTSP客户端示例的旧版本演练,突出展示了使用该库的主要方式。
-
步骤1 - 打开与RTSP服务器的TCP套接字连接
// 连接到RTSP服务器 tcp_socket = new Rtsp.RtspTcpTransport(host,port); if (tcp_socket.Connected == false) { Console.WriteLine("错误 - 未连接"); return; }
这为"TCP"模式的RTSP/RTP会话打开了一个连接,其中RTP数据包在RTSP套接字中设置。
-
步骤2 - 创建RTSP监听器并将其附加到RTSP TCP套接字
// 将RTSP监听器连接到TCP套接字以发送消息并侦听回复 rtsp_client = new Rtsp.RtspListener(tcp_socket); rtsp_client.MessageReceived += Rtsp_client_MessageReceived; rtsp_client.DataReceived += Rtsp_client_DataReceived; rtsp_client.Start(); // 开始从服务器读取消息
RTSP监听器类允许您向RTSP服务器发送消息(见下文)。 RTSP监听器类有一个工作线程,用于监听来自RTSP服务器的回复。 当收到回复时,会触发MessageReceived事件。 当收到RTP数据包时,会触发DataReceived事件。
-
步骤3 - 向RTSP服务器发送消息
以下示例展示如何发送消息。
使用以下代码发送OPTIONS:
Rtsp.Messages.RtspRequest options_message = new Rtsp. Messages.RtspRequestOptions(); options_message.RtspUri = new Uri(url); rtsp_client.SendMessage(options_message);
使用以下代码发送DESCRIBE:
// 发送Describe Rtsp.Messages.RtspRequest describe_message = new Rtsp.Messages.RtspRequestDescribe(); describe_message.RtspUri = new Uri(url); rtsp_client.SendMessage(describe_message); // 回复将包含SDP数据
使用以下代码发送SETUP:
// 'control'的值来自解析所需视频或音频子流的SDP Rtsp.Messages.RtspRequest setup_message = new Rtsp.Messages.RtspRequestSetup(); setup_message.RtspUri = new Uri(url + "/" + control); setup_message.AddHeader("Transport: RTP/AVP/TCP;interleaved=0"); rtsp_client.SendMessage(setup_message); // 回复将包含会话信息
使用以下代码发送PLAY:
// 'session'的值来自SETUP命令的回复 Rtsp.Messages.RtspRequest play_message = new Rtsp.Messages.RtspRequestPlay(); play_message.RtspUri = new Uri(url); play_message.Session = session; rtsp_client.SendMessage(play_message);
-
步骤4 - 当MessageReceived事件触发时处理回复
此示例假设主程序发送一个OPTIONS命令。 它寻找服务器对OPTIONS的回复,然后发送DESCRIBE。 它寻找服务器对DESCRIBE的回复,然后发送SETUP(用于视频流)。 它寻找服务器对SETUP的回复,然后发送PLAY。 一旦发送了PLAY,将以RTP数据包的形式接收视频。
private void Rtsp_client_MessageReceived(object sender, Rtsp.RtspChunkEventArgs e) { Rtsp.Messages.RtspResponse message = e.Message as Rtsp.Messages.RtspResponse; Console.WriteLine("收到 " + message.OriginalRequest.ToString()); if (message.OriginalRequest != null && message.OriginalRequest is Rtsp.Messages.RtspRequestOptions) { // 发送DESCRIBE Rtsp.Messages.RtspRequest describe_message = new Rtsp.Messages.RtspRequestDescribe(); describe_message.RtspUri = new Uri(url); rtsp_client.SendMessage(describe_message); } if (message.OriginalRequest != null && message.OriginalRequest is Rtsp.Messages.RtspRequestDescribe) { // 收到DESCRIBE的回复 // 检查SDP Console.Write(System.Text.Encoding.UTF8.GetString(message.Data)); Rtsp.Sdp.SdpFile sdp_data; using (StreamReader sdp_stream = new StreamReader(new MemoryStream(message.Data))) { sdp_data = Rtsp.Sdp.SdpFile.Read(sdp_stream); } // 处理SDP中的每个"媒体"属性。 // 如果属性是视频,则发送SETUP for (int x = 0; x < sdp_data.Medias.Count; x++) { if (sdp_data.Medias[x].GetMediaType() == Rtsp.Sdp.Media.MediaType.video) { // 搜索属性中的control、fmtp和rtpmap String control = ""; // "轨道"或"流ID" String fmtp = ""; // 保存SPS和PPS String rtpmap = ""; // 保存负载格式,96通常用于H264 foreach (Rtsp.Sdp.Attribut attrib in sdp_data.Medias[x].Attributs) { if (attrib.Key.Equals("control")) control = attrib.Value; if (attrib.Key.Equals("fmtp")) fmtp = attrib.Value; if (attrib.Key.Equals("rtpmap")) rtpmap = attrib.Value; } // 获取视频流的负载格式编号 String[] split_rtpmap = rtpmap.Split(' '); video_payload = 0; bool result = Int32.TryParse(split_rtpmap[0], out video_payload); // 为视频流发送SETUP // 使用交错模式(RTP帧通过RTSP套接字传输) Rtsp.Messages.RtspRequest setup_message = new Rtsp.Messages.RtspRequestSetup(); setup_message.RtspUri = new Uri(url + "/" + control); setup_message.AddHeader("Transport: RTP/AVP/TCP;interleaved=0"); rtsp_client.SendMessage(setup_message); } } } if (message.OriginalRequest != null && message.OriginalRequest is Rtsp.Messages.RtspRequestSetup) { // 收到SETUP的回复 Console.WriteLine("收到Setup的回复。会话为 " + message.Session);
String session = message.Session; // 与Play、Pause、Teardown一起使用的会话值
// 发送PLAY Rtsp.Messages.RtspRequest play_message = new Rtsp.Messages.RtspRequestPlay(); play_message.RtspUri = new Uri(url); play_message.Session = session; rtsp_client.SendMessage(play_message); }
if (message.OriginalRequest != null && message.OriginalRequest is Rtsp.Messages.RtspRequestPlay) { // 收到PLAY的回复 Console.WriteLine("收到Play回复 " + message.Command); } }
* 第5步 - 处理RTP视频
此代码处理每个传入的RTP数据包,将属于同一视频帧的RTP数据包组合在一起(使用标记位)。
一旦接收到完整帧,就可以将其传递给解包器以获取压缩的视频数据
```C#
List<byte[]> temporary_rtp_payloads = new List<byte[]>();
private void Rtsp_client_DataReceived(object sender, Rtsp.RtspChunkEventArgs e)
{
// RTP数据包头
// 0 - 版本、P、X、CC、M、PT和序列号
//32 - 时间戳
//64 - SSRC
//96 - CSRC(可选)
//nn - 扩展ID和长度
//nn - 扩展头
int rtp_version = (e.Message.Data[0] >> 6);
int rtp_padding = (e.Message.Data[0] >> 5) & 0x01;
int rtp_extension = (e.Message.Data[0] >> 4) & 0x01;
int rtp_csrc_count = (e.Message.Data[0] >> 0) & 0x0F;
int rtp_marker = (e.Message.Data[1] >> 7) & 0x01;
int rtp_payload_type = (e.Message.Data[1] >> 0) & 0x7F;
uint rtp_sequence_number = ((uint)e.Message.Data[2] << 8) + (uint)(e.Message.Data[3]);
uint rtp_timestamp = ((uint)e.Message.Data[4] <<24) + (uint)(e.Message.Data[5] << 16) + (uint)(e.Message.Data[6] << 8) + (uint)(e.Message.Data[7]);
uint rtp_ssrc = ((uint)e.Message.Data[8] << 24) + (uint)(e.Message.Data[9] << 16) + (uint)(e.Message.Data[10] << 8) + (uint)(e.Message.Data[11]);
int rtp_payload_start = 4 // V,P,M,SEQ
+ 4 // 时间戳
+ 4 // ssrc
+ (4 * rtp_csrc_count); // 零个或多个csrc
uint rtp_extension_id = 0;
uint rtp_extension_size = 0;
if (rtp_extension == 1)
{
rtp_extension_id = ((uint)e.Message.Data[rtp_payload_start + 0] << 8) + (uint)(e.Message.Data[rtp_payload_start + 1] << 0);
rtp_extension_size = ((uint)e.Message.Data[rtp_payload_start + 2] << 8) + (uint)(e.Message.Data[rtp_payload_start + 3] << 0);
rtp_payload_start += 4 + (int)rtp_extension_size; // 扩展头和扩展负载
}
Console.WriteLine("RTP数据"
+ " V=" + rtp_version
+ " P=" + rtp_padding
+ " X=" + rtp_extension
+ " CC=" + rtp_csrc_count
+ " M=" + rtp_marker
+ " PT=" + rtp_payload_type
+ " Seq=" + rtp_sequence_number
+ " Time=" + rtp_timestamp
+ " SSRC=" + rtp_ssrc
+ " Size=" + e.Message.Data.Length);
if (rtp_payload_type != video_payload)
{
Console.WriteLine("忽略此RTP负载");
return; // 忽略此数据
}
// 如果rtp_marker为'1',则这是此数据包的最后一次传输。
// 如果rtp_marker为'0',我们需要累积具有相同时间戳的数据
// 待办 - 检查时间戳是否匹配
// 添加到临时RTP列表
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // 移除RTP头的负载
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // 复制负载
temporary_rtp_payloads.Add(rtp_payload);
if (rtp_marker == 1)
{
// 处理RTP帧
Process_RTP_Frame(temporary_rtp_payloads);
temporary_rtp_payloads.Clear();
}
}
- 第6步 - 处理RTP帧
RTP帧由1个或多个RTP数据包组成 H264视频被打包成一个或多个RTP数据包,此示例提取普通打包和 分片单元类型A打包(常见的两种) 此示例将视频写入.264文件,可以用FFPLAY播放
FileStream fs = null;
byte[] nal_header = new byte[]{ 0x00, 0x00, 0x00, 0x01 };
int norm, fu_a, fu_b, stap_a, stap_b, mtap16, mtap24 = 0; // 统计计数器
public void Process_RTP_Frame(List<byte[]>rtp_payloads)
{
Console.WriteLine("RTP数据由 " + rtp_payloads.Count + " 个rtp数据包组成");
if (fs == null)
{
// 创建文件
String filename = "rtsp_capture_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".h264";
fs = new FileStream(filename, FileMode.Create);
// 待办。从SDP属性(fmtp属性)获取SPS和PPS并写入文件
// 针对仅在带外输出SPS和PPS的IP摄像机
}
for (int payload_index = 0; payload_index < rtp_payloads.Count; payload_index++) {
// 检查第一个rtp_payload和第一个字节(NAL头)
int nal_header_f_bit = (rtp_payloads[payload_index][0] >> 7) & 0x01;
int nal_header_nri = (rtp_payloads[payload_index][0] >> 5) & 0x03;
int nal_header_type = (rtp_payloads[payload_index][0] >> 0) & 0x1F;
// 如果 NAL 头类型在 1 到 23 的范围内,这是一个普通的 NAL(未分片)
// 因此将 NAL 写入文件
if (nal_header_type >= 1 && nal_header_type <= 23)
{
Console.WriteLine("普通 NAL");
norm++;
fs.Write(nal_header, 0, nal_header.Length);
fs.Write(rtp_payloads[payload_index], 0, rtp_payloads[payload_index].Length);
}
else if (nal_header_type == 24)
{
// 有 4 种聚合包类型(一个 RTP 包中包含多个 NAL)
Console.WriteLine("不支持聚合 STAP-A");
stap_a++;
}
else if (nal_header_type == 25)
{
// 有 4 种聚合包类型(一个 RTP 包中包含多个 NAL)
Console.WriteLine("不支持聚合 STAP-B");
stap_b++;
}
else if (nal_header_type == 26)
{
// 有 4 种聚合包类型(一个 RTP 包中包含多个 NAL)
Console.WriteLine("不支持聚合 MTAP16");
mtap16++;
}
else if (nal_header_type == 27)
{
// 有 4 种聚合包类型(一个 RTP 包中包含多个 NAL)
Console.WriteLine("不支持聚合 MTAP24");
mtap24++;
}
else if (nal_header_type == 28)
{
Console.WriteLine("分片包类型 FU-A");
fu_a++;
// 解析分片单元头
int fu_header_s = (rtp_payloads[payload_index][1] >> 7) & 0x01; // 开始标记
int fu_header_e = (rtp_payloads[payload_index][1] >> 6) & 0x01; // 结束标记
int fu_header_r = (rtp_payloads[payload_index][1] >> 5) & 0x01; // 保留位,应为 0
int fu_header_type = (rtp_payloads[payload_index][1] >> 0) & 0x1F; // 原始 NAL 单元头
Console.WriteLine("分片 FU-A s=" + fu_header_s + "e=" + fu_header_e);
// 开始标志设置
if (fu_header_s == 1)
{
// 写入 00 00 00 01 头
fs.Write(nal_header, 0, nal_header.Length); // 0x00 0x00 0x00 0x01
// 修改 RTP 包开头的 NAL 头
// 保留 F 和 NRI 标志,但用 fu_header_type 替换类型字段
byte reconstructed_nal_type = (byte)((nal_header_nri << 5) + fu_header_type);
fs.WriteByte(reconstructed_nal_type); // NAL 单元类型
fs.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length - 2); // 从 NAL 单元类型和 FU 头字节之后开始
}
if (fu_header_s == 0)
{
// 将此负载附加到输出 NAL 流
// 数据从 NAL 单元类型字节和 FU 头字节之后开始
fs.Write(rtp_payloads[payload_index], 2, rtp_payloads[payload_index].Length-2); // 从 NAL 单元类型和 FU 头字节之后开始
}
}
else if (nal_header_type == 29)
{
Console.WriteLine("不支持分片包 FU-B");
fu_b++;
}
else
{
Console.WriteLine("未知 NAL 头 " + nal_header_type);
}
}
// 确保视频写入磁盘
fs.Flush(true);
// 打印总计
Console.WriteLine("普通=" + norm + " ST-A=" + stap_a + " ST-B=" + stap_b + " M16=" + mtap16 + " M24=" + mtap24 + " FU-A=" + fu_a + " FU-B=" + fu_b);
}