Project Icon

SharpRTSP

C#开发的RTSP和RTP数据流处理库

SharpRTSP是一个C#开发的库,用于构建RTSP客户端、服务器和处理RTP数据流。支持H264、H265、G711等音视频格式,提供UDP、TCP和多播传输。包含RTSP客户端、摄像机服务器等示例,实现了多种RTSP/RTP相关功能。该库主要处理传输层,不涉及音视频解码,用户需要使用其他工具(如FFMPEG)来解码生成的原始数据。

Sharp RTSP

Nuget

这是一个用于构建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);
}
项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号