用NetCore手撸RTSP交互协议


注意点
1. DESCRIBE 第1次发送时返回401错误,返回信息带 realm,nonce,参数使用MD5校验后重新发送;
2. SETUP 的url信息,由DESCRIBE返回的head中的sdp字符串中解析,readonly,x-dimensions(视频分辨率),control(视频url),rtpmap(编解码信息)
3.PLAY的Session信息,由SETUP返回的head中的Session
4.解析完后,tcpclient接收rtp数据包,可用ffmepg进行解码

核心代码RTSPClient.cs

using System.Net.Sockets;
using System.Text;
namespace RtspClientCore
{
class RtspClient
{
string rtspUrl = "rtsp://192.168.0.2:554/h264/ch1/main/av_stream";
string username = "admin";
string password = "thzn123456";
TcpClient tcpClient;
NetworkStream tcpStream;
private int cseq;
public Uri rtspUri { get; set; }
public int NewCSeq
{
get
{
return ++cseq;
}
}
string Authorization = "";
string Session = "";
string UserAgent = "C# RTSP Client";
public RtspClient(string rtspUrl, string username, string password)
{
this.rtspUrl = rtspUrl;
this.username = username;
this.password = password;
this.rtspUri = new Uri(rtspUrl);
this.tcpClient = new TcpClient(rtspUri.Host, rtspUri.Port);
this.tcpStream = tcpClient.GetStream();
}
RTSPResponse ExcuteRequest(string method, string url, string request, bool skip = false)
{
string temp = request + "\r\n";
temp = temp.Replace("@CSeq", NewCSeq.ToString());
Logger.Info(temp);
// SendRequest
byte[] requestBytes = Encoding.ASCII.GetBytes(temp);
tcpStream.Write(requestBytes, 0, requestBytes.Length);
// ReadResponse
StreamReader reader = new StreamReader(tcpStream);
RTSPResponse r1 = new RTSPResponse(reader, skip);
switch (r1.StatusCode)
{
case "401":
{
//RTSP / 1.0 401 Unauthorized
//CSeq: 1
//WWW - Authenticate: Digest realm = "IP Camera(G7574)", nonce = "d355a9a6a081d0d5ce50d0dd90a14148", stale = "FALSE"
//Date: Thu, Mar 16 2023 10:34:24 GMT
string realm = string.Empty;
string nonce = string.Empty;
string authType = string.Empty;
var auth = r1.Headers.Where(x => x.Key == "WWW-Authenticate").FirstOrDefault();
if (auth.Value.Contains("Digest"))
{
// 摘要认证
authType = "Digest";
RtspUtil.GetDigestParams(auth.Value, ref realm, ref nonce);
}
else if (auth.Value.Contains("Basic"))
{
// 基本认证
authType = "Basic";// Authorization: Basic YWRtaW46YWRtaW4=\r\n\r\n
}
else
{
throw new Exception("Server auth mode not support:" + auth);
}
Authorization = RtspUtil.GetAuthorization(authType, url, username, password, realm, nonce, method);
request += "Authorization:" + Authorization + "\r\n";
r1 = ExcuteRequest(method, url, request, false);
}
break;
}
return r1;
}
public RTSPResponse DESCRIBE()
{
string mothed = "DESCRIBE";
StringBuilder request = new StringBuilder();
request.Append(mothed + " " + rtspUrl + " RTSP/1.0\r\n");
request.Append("CSeq: @CSeq\r\n");
request.Append("User-Agent: " + UserAgent + "\r\n");
request.Append("Accept: application/sdp\r\n");
return ExcuteRequest(mothed, rtspUrl, request.ToString());
}
public RTSPResponse SETUP(string url)
{
string mothed = "SETUP";
StringBuilder request = new StringBuilder();
request.Append(mothed + " " + url + " RTSP/1.0\r\n");
request.Append("CSeq: @CSeq\r\n");
request.Append("Authorization: " + Authorization + "\r\n");
request.Append("User-Agent: " + UserAgent + "\r\n");
request.Append("Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n");
return ExcuteRequest(mothed, rtspUrl, request.ToString());
}
public RTSPResponse PLAY(string url)
{
string mothed = "PLAY";
StringBuilder request = new StringBuilder();
request.Append(mothed + " " + url + " RTSP/1.0\r\n");
request.Append("CSeq: @CSeq\r\n");
request.Append("Authorization: " + Authorization + "\r\n");
request.Append("User-Agent: " + UserAgent + "\r\n");
request.Append("Session: " + this.Session + "\r\n");
request.Append("Range: npt=0.000-\r\n");
return ExcuteRequest(mothed, rtspUrl, request.ToString(), true);
}
public RTSPResponse TEARDOWN(string url, string session)
{
string mothed = "TEARDOWN";
StringBuilder request = new StringBuilder();
request.Append(mothed + " " + url + " RTSP/1.0\r\n");
request.Append("CSeq: @CSeq\r\n");
request.Append("Authorization: " + Authorization + "\r\n");
request.Append("User-Agent: " + UserAgent + "\r\n");
request.Append("Session: " + this.Session + "\r\n");
return ExcuteRequest(mothed, rtspUrl, request.ToString(), true);
}
public void Start()
{
RTSPResponse r1 = DESCRIBE();
if ("200" == r1.StatusCode)
{
SDP sdp_data = new SDP(r1.Response);
bool find = false; bool video1 = false; bool video2 = false; var dimensions = string.Empty; var control = string.Empty; var rtpmap = string.Empty;
for (int x = 0; x < sdp_data.MediaDescribes.Count; x++)
{
var items = sdp_data.MediaDescribes[x].a;
foreach (var item in items)
{
string mediainfo = item.ToLower();
if (mediainfo.Contains("recvonly"))
{
video1 = true;
}
if (mediainfo.Contains("x-dimensions"))
{
video2 = true;
dimensions = item;
}
if (mediainfo.Contains("control"))
{
control = item.Replace("control:", "");
}
if (mediainfo.Contains("rtpmap"))
{
rtpmap = item.Replace("rtpmap:", "");
}
if (video1 && video2 & !string.IsNullOrEmpty(control) & !string.IsNullOrEmpty(rtpmap))
{
find = true;
break;
}
}
if (find)
{
break;
}
}
RTSPResponse r2 = SETUP(control);
if ("200" == r2.StatusCode)
{
string sessionVal = r2.Headers.Where(x => x.Key == "Session").FirstOrDefault().Value;
if (!string.IsNullOrEmpty(sessionVal))
{
string[] sessionParms = sessionVal.Split(';');
if (sessionParms.Length > 1)
{
this.Session = sessionParms[0];
}
else
{
this.Session = sessionVal;
}
RTSPResponse r3 = PLAY(control);
if ("200" == r3.StatusCode)
{
// 接收rtp数据
byte[] buffer = new byte[1024];
while (true)
{
int bytesRead = tcpStream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
break;
}
// 处理接收到的RTP数据
//Console.WriteLine($"Received {bytesRead} bytes of RTP data.");
}
}
}
}
}
}
}
}