这是一个用于构建RTSP客户端、RTSP服务器和处理RTP数据流的C#库。该库包含多个示例。
:warning: : 此库不处理视频或音频的解码(例如将H264转换为位图)。SharpRTSP仅限于传输层,生成您需要输入到视频解码器或音频解码器的原始数据。许多人使用FFMPEG或使用硬件加速的操作系统API来进行解码。
这是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();
}
}
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); }
AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知 和代码自动完成功能,是提升开发效率的理想工具。
AI小说写作助手,一站式润色、改写、扩写
蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。 提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。
全能AI智能助手,随时解答生活与工作 的多样问题
问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。
实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超 精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。
一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支 持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
AI助力,做PPT更简单!
咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。
选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。
专业的AI公文写作平台,公文写作神器
AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文 文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号