本篇文章简要介绍RTSP协议的相关知识。
在RTSP协议文档中有这样的描述:实时流协议(RTSP)建立并控制一个或几个时间同步的连续流媒体。尽管连续媒体流与控制流有可能交叉,但RTSP 本身通常并不发送连续媒体流。换言之,RTSP 充当多媒体服务器的网络远程控制。
正因为如此,我们经常可以看到,RTSP协议的内容当中,在请求串中均带有会话状态,如SETUP、PLAY、PAUSE等,这就是RTSP的状态。虽然RTSP 中很多方法与状态无关,但下列方法在定义服务器流资源的分配与应用上起着重要的作用: SETUP:让服务器给流分配资源,启动 RTSP 会话。 PLAY 与RECORD:启动 SETUP 分配流的数据传输。 PAUSE:临时停止流,而不释放服务器资源。 TEARDOWN:释放流的资源,RTSP 会话停止。
标识状态的 RTSP 方法使用会话(session)标题域识别RTSP 会话,为回应SETUP请求,服务器生成会话标识。
一个RTSP协议的内容当中拥有非常多的参数,这些参数包括但不限于会话状态、RTSP协议版本、请求URL、会话标识、正常播放时间、回应的状态代码等等。
关于RTSP协议的详细介绍可以查阅RFC2326文档,在此,笔者提供了中文版和英文版的下载地址,如下:
RTSP_RFC2326(CN):http://download.csdn.net/source/2592342 RTSP_RFC2326(EN):http://download.csdn.net/source/2592347
在SharpStreaming项目中,将会使用到的RTSP会话状态包括:OPTIONS、DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN、GET_PARAMETER、SET_PARAMETER。
将会使用到的状态码(Status-Code)包括但不限于:"200"(OK)、"400" (Bad Request)、"401" (Unauthorized)、"404" (Not Found)、"405" (Method Not Allowed)、"461" (Unsupported transport)。
本篇文章简要介绍服务器部分与RTSP协议实现相关的业务代码实现。
在介绍服务器有关RTSP业务代码实现之前,我们首先要明确服务器与客户端建立RTSP通信的基本过程,如下所述:
·C --> S:Send OPTIONS Cmd,S --> C:Handle OPTIONS Cmd and Send Response; ·C --> S:Send DESCRIBE Cmd,S --> C:Handle DESCRIBE Cmd and Send Response; ·C --> S:Send SETUP Cmd,S --> C:Handle SETUP Cmd and Send Response; ·C --> S:Send PLAY Cmd,S --> C:Handle PLAY Cmd and Send Response; ·C --> S:Send PAUSE Cmd,S --> C:Handle PAUSE Cmd and Send Response; ·C --> S:Send TEARDOWN Cmd,S --> C:Handle TEARDOWN Cmd and Send Response。
正如前一篇文章中所提到的那样,在代码上实现RTSP协议的简单应用还是相对简单的。在参考了live555开源项目中关于RTSP协议的实现代码之后, 针对本项目,笔者将只对其中的几个重要会话状态OPTIONS、DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN作初步的代码实 现,同时在本项目中将不考虑用户验证的问题。 以下是关于上述会话状态和若干状态码的主要方法:
+ expand source view plain copy to clipboard print ? /// <summary> /// Handles the incoming request. /// </summary> /// <param name="recvBuffer">The receive buffer.</param> /// <param name="recvBufferSize">Size of the receive buffer.</param> private void HandleIncomingRequest( byte [] recvBuffer, int recvBufferSize) { string cmdName; string rtspUrl; string urlPreSuffix; string urlSuffix; string cseq; string request = Utils.BytesToString(recvBuffer, recvBufferSize); // Parses the request string into command name and 'CSeq', then handle the command. bool isSucceeded = RtspCommon.ParseRequestString( request, out cmdName, out rtspUrl, out urlPreSuffix, out urlSuffix, out cseq); if (isSucceeded) { switch (cmdName) { case Constants.RTSP_CMD_OPTIONS: this .HandleOptionsCmd(cseq); break ; case Constants.RTSP_CMD_DESCRIBE: this .HandleDescribeCmd(cseq, rtspUrl, urlSuffix, request); break ; case Constants.RTSP_CMD_SETUP: this .HandleSetupCmd(cseq, urlPreSuffix, urlSuffix, request); break ; case Constants.RTSP_CMD_PLAY: this .HandlePlayCmd(cseq, request); break ; case Constants.RTSP_CMD_PAUSE: this .HandlePauseCmd(cseq); break ; case Constants.RTSP_CMD_TEARDOWN: this .HandleTearDownCmd(cseq); break ; default : this .HandleNotSupportCmd(cseq); break ; } } else { // Parses request string failed! this .HandleBadCmd(); } // After we handle the client request, we must send the response to the client. // ***** //Converts string to bytes. byte [] sendBuffer = Utils.StringToBytes( this .response); // Sends the rtsp response to the client. this .clientSocket.SendDatagram(sendBuffer); } /// <summary> /// Handles the incoming request. /// </summary> /// <param name="recvBuffer">The receive buffer.</param> /// <param name="recvBufferSize">Size of the receive buffer.</param> private void HandleIncomingRequest(byte[] recvBuffer, int recvBufferSize) { string cmdName; string rtspUrl; string urlPreSuffix; string urlSuffix; string cseq; string request = Utils.BytesToString(recvBuffer, recvBufferSize); // Parses the request string into command name and 'CSeq', then handle the command. bool isSucceeded = RtspCommon.ParseRequestString( request, out cmdName, out rtspUrl, out urlPreSuffix, out urlSuffix, out cseq); if (isSucceeded) { switch (cmdName) { case Constants.RTSP_CMD_OPTIONS: this.HandleOptionsCmd(cseq); break; case Constants.RTSP_CMD_DESCRIBE: this.HandleDescribeCmd(cseq, rtspUrl, urlSuffix, request); break; case Constants.RTSP_CMD_SETUP: this.HandleSetupCmd(cseq, urlPreSuffix, urlSuffix, request); break; case Constants.RTSP_CMD_PLAY: this.HandlePlayCmd(cseq, request); break; case Constants.RTSP_CMD_PAUSE: this.HandlePauseCmd(cseq); break; case Constants.RTSP_CMD_TEARDOWN: this.HandleTearDownCmd(cseq); break; default: this.HandleNotSupportCmd(cseq); break; } } else { // Parses request string failed! this.HandleBadCmd(); } // After we handle the client request, we must send the response to the client. // ***** //Converts string to bytes. byte[] sendBuffer = Utils.StringToBytes(this.response); // Sends the rtsp response to the client. this.clientSocket.SendDatagram(sendBuffer); } 该方法用来处理接收到的客户端请求,首先解析了请求字符串,输出会话命令、请求的URL、流名称、序号等内容(out string cmdName, out string rtspUrl, out string urlPreSuffix, out string urlSuffix, out string cseq);然后根据会话命令通过switch...case语句来分类处理;最后将生成的响应字符串发送给指定的客户端。
以下是方法HandleOptionsCmd、HandleBadCmd、HandleNotSupportCmd、HandleNotFoundCmd、HandleUnsupportedTransportCmd的代码实现部分:
+ expand source view plain copy to clipboard print ? /// <summary> /// Handles the options CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleOptionsCmd( string cseq) { this .response = string .Format( "RTSP/1.0 200 OK/r/nCSeq: {0}/r/nDate: {1}/r/nPublic: {2}/r/n/r/n" , cseq, DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); } /// <summary> /// Handles the bad CMD. /// </summary> private void HandleBadCmd() { this .response = string .Format( "RTSP/1.0 400 Bad Request/r/nDate: {0}/r/nAllow: {1}/r/n/r/n" , DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); this .BeginTeardown(); } /// <summary> /// Handles the not support CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleNotSupportCmd( string cseq) { this .response = string .Format( "RTSP/1.0 405 Method Not Allowed/r/nCSeq: {0}/r/nDate: {1}/r/nAllow: {2}/r/n/r/n" , cseq, DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); this .BeginTeardown(); } /// <summary> /// Handles the not found CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleNotFoundCmd( string cseq) { this .response = string .Format( "RTSP/1.0 404 Stream Not Found/r/nCSeq: {0}/r/nDate: {1}/r/n" , cseq, DateTime.Now.ToLocalTime()); this .BeginTeardown(); } /// <summary> /// Handles the unsupported transport CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleUnsupportedTransportCmd( string cseq) { this .response = string .Format( "RTSP/1.0 461 Unsupported Transport/r/nCSeq: {0}/r/nDate: {1}/r/n" , cseq, DateTime.Now.ToLocalTime()); this .BeginTeardown(); } /// <summary> /// Handles the options CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleOptionsCmd(string cseq) { this.response = string.Format("RTSP/1.0 200 OK/r/nCSeq: {0}/r/nDate: {1}/r/nPublic: {2}/r/n/r/n", cseq, DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); } /// <summary> /// Handles the bad CMD. /// </summary> private void HandleBadCmd() { this.response = string.Format("RTSP/1.0 400 Bad Request/r/nDate: {0}/r/nAllow: {1}/r/n/r/n", DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); this.BeginTeardown(); } /// <summary> /// Handles the not support CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleNotSupportCmd(string cseq) { this.response = string.Format("RTSP/1.0 405 Method Not Allowed/r/nCSeq: {0}/r/nDate: {1}/r/nAllow: {2}/r/n/r/n", cseq, DateTime.Now.ToLocalTime(), Constants.RTSP_ALLOW_COMMAND); this.BeginTeardown(); } /// <summary> /// Handles the not found CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleNotFoundCmd(string cseq) { this.response = string.Format("RTSP/1.0 404 Stream Not Found/r/nCSeq: {0}/r/nDate: {1}/r/n", cseq, DateTime.Now.ToLocalTime()); this.BeginTeardown(); } /// <summary> /// Handles the unsupported transport CMD. /// </summary> /// <param name="cseq">The cseq.</param> private void HandleUnsupportedTransportCmd(string cseq) { this.response = string.Format("RTSP/1.0 461 Unsupported Transport/r/nCSeq: {0}/r/nDate: {1}/r/n", cseq, DateTime.Now.ToLocalTime()); this.BeginTeardown(); }值得注意的是,这里并没有列出方法HandleDescribeCmd、HandleSetupCmd、HandlePlayCmd、 HandlePauseCmd和HandleTearDownCmd的代码实现,主要原因是其中牵扯到媒体源的选择(当然,笔者已初步决定了使用TS流格 式的文件作为媒体源)及RTP/RTCP等的建立过程,而笔者对其中的一些过程尚存在一些不明晰之处,因而思路仍在不断调整之中。
不过,对于处理DESCRIBE指令,其首先要根据流名称查找对应的ServerMediaSession,然后生成SDP描述信息,接着将SDP描述信息等组装成响应字符串并最后发回给客户端。
而对于处理SETUP指令,首先是要确认ServerMediaSession的存在,然后解析传输头信息 (ParseTransportHeader),然后要拿到相关的Parameters,最后组装成响应信息并发回给客户端。关于这一块目前的主要问题是 在对GetStreamParameters的处理尚有一些不明确之处。
对于处理PLAY、PAUSE、TEARDOWN,当前的思路主要是通过ServerMediaSubsession调用相应的StartStream、PauseStream和DeleteStream(是否需要这么做,还有待进一步考虑)方法。
另外,为支持TCP和UDP传输,特别编写了一个ClientSocketBase作为ClientSocketTcp和ClientSocketUdp的基类,该类包含一些事件和虚方法,代码如下所示:
+ expand source view plain copy to clipboard print ? /// <summary> /// A base class for client socket. /// </summary> public class ClientSocketBase { #region Public Class Events public event EventHandler ClientTeardown; public event EventHandler<TSocketEventArgs> DatagramReceived; public event EventHandler<TExceptionEventArgs> ExceptionOccurred; #endregion #region Public Virtual Methods /// <summary> /// Sends the datagram with the asynchronous mode. /// </summary> /// <param name="sendBuffer">The send buffer.</param> public virtual void SendDatagram( byte [] sendBuffer) { // No implementation. } /// <summary> /// Receives the datagram with the asynchronous mode. /// </summary> public virtual void ReceiveDatagram() { // No implementation. } /// <summary> /// Closes the client socket. /// </summary> public virtual void CloseClientSocket() { // No implementation. } #endregion #region Protected Virtual Methods /// <summary> /// Ends to send the datagram. /// </summary> /// <param name="iar">The iar.</param> protected virtual void EndSendDatagram(IAsyncResult iar) { // No implementation. } /// <summary> /// Ends to receive the datagram. /// </summary> /// <param name="iar">The iar.</param> protected virtual void EndReceiveDatagram(IAsyncResult iar) { // No implementation. } /// <summary> /// Called when [client tear down]. /// </summary> protected virtual void OnClientTeardown() { EventHandler handler = this .ClientTeardown; if (handler != null ) { handler(this , EventArgs.Empty); } } /// <summary> /// Called when [datagram received]. /// </summary> /// <param name="recvBuffer">The received buffer.</param> /// <param name="recvBufferSize">Size of the received buffer.</param> protected virtual void OnDatagramReceived( byte [] recvBuffer, int recvBufferSize) { EventHandler<TSocketEventArgs> handler = this .DatagramReceived; if (handler != null ) { TSocketEventArgs e = new TSocketEventArgs(recvBuffer, recvBufferSize); handler(this , e); } } /// <summary> /// Called when [exception occurred]. /// </summary> /// <param name="ex">The ex.</param> protected virtual void OnExceptionOccurred(Exception ex) { EventHandler<TExceptionEventArgs> handler = this .ExceptionOccurred; if (handler != null ) { TExceptionEventArgs e = new TExceptionEventArgs(ex); handler(this , e); } } #endregion }/// <summary> /// A base class for client socket. /// </summary> public class ClientSocketBase { #region Public Class Events public event EventHandler ClientTeardown; public event EventHandler<TSocketEventArgs> DatagramReceived; public event EventHandler<TExceptionEventArgs> ExceptionOccurred; #endregion #region Public Virtual Methods /// <summary> /// Sends the datagram with the asynchronous mode. /// </summary> /// <param name="sendBuffer">The send buffer.</param> public virtual void SendDatagram(byte[] sendBuffer) { // No implementation. } /// <summary> /// Receives the datagram with the asynchronous mode. /// </summary> public virtual void ReceiveDatagram() { // No implementation. } /// <summary> /// Closes the client socket. /// </summary> public virtual void CloseClientSocket() { // No implementation. } #endregion #region Protected Virtual Methods /// <summary> /// Ends to send the datagram. /// </summary> /// <param name="iar">The iar.</param> protected virtual void EndSendDatagram(IAsyncResult iar) { // No implementation. } /// <summary> /// Ends to receive the datagram. /// </summary> /// <param name="iar">The iar.</param> protected virtual void EndReceiveDatagram(IAsyncResult iar) { // No implementation. } /// <summary> /// Called when [client tear down]. /// </summary> protected virtual void OnClientTeardown() { EventHandler handler = this.ClientTeardown; if (handler != null) { handler(this, EventArgs.Empty); } } /// <summary> /// Called when [datagram received]. /// </summary> /// <param name="recvBuffer">The received buffer.</param> /// <param name="recvBufferSize">Size of the received buffer.</param> protected virtual void OnDatagramReceived(byte[] recvBuffer, int recvBufferSize) { EventHandler<TSocketEventArgs> handler = this.DatagramReceived; if (handler != null) { TSocketEventArgs e = new TSocketEventArgs(recvBuffer, recvBufferSize); handler(this, e); } } /// <summary> /// Called when [exception occurred]. /// </summary> /// <param name="ex">The ex.</param> protected virtual void OnExceptionOccurred(Exception ex) { EventHandler<TExceptionEventArgs> handler = this.ExceptionOccurred; if (handler != null) { TExceptionEventArgs e = new TExceptionEventArgs(ex); handler(this, e); } } #endregion } 当前关于TCP和UDP的数据接收和发送,均采用异步模式,但是其中又有一些例外,这将在下一篇文章中讲述,敬请关注。
本篇文章简要介绍客户端有关RTSP的业务代码实现。
客户端有关RTSP的业务逻辑代码均在RtspClient类中实现,在该类中除了提供连接/断开服务器的公有方法之外,还提供了打开流、播放流、暂停 流、停止流的公有方法。其中打开流描述了客户端从发出OPTIONS指令到开始传输流的基本步骤,其代码示例如下:
+ expand source view plain copy to clipboard print ? /// <summary> /// Opens the stream. /// </summary> /// <param name="url">The URL.</param> /// <returns>Succeeded or failed.</returns> public bool OpenStream( string url) { if (! this .isConnected) { return false ; } // Sets the request url: this .requestUrl = url; // Sends "OPTIONS" command and then gets the response: bool result = this .SendOptionsCmd(); if (!result) { this .CloseStream(); return false ; } // Sends "DESCRIBE" command and then gets the SDP description: string sdpDescription = this .SendDescribeCmd(); if ( string .IsNullOrEmpty(sdpDescription)) { this .CloseStream(); return false ; } // Creates a media session object from the SDP description which // we have just received from the server: this .mediaSession = new MediaSession(sdpDescription); // Then, resolves the SDP description and initializes all basic // information: result = this .mediaSession.ResolveSdpDescription(); if (!result) { this .CloseStream(); return false ; } // And then, creates the output file to write the data: result = this .CreateOutFile(); if (!result) { this .CloseStream(); return false ; } // After that, sends the "SETUP" command and setups the stream: result = this .SendSetupCmd(); if (!result) { this .CloseStream(); return false ; } // Finally, sends the "PLAY" command and starts playing stream: result = this .PlayStream(); if (!result) { this .CloseStream(); return false ; } this .OnStreamOpened(); return true ; } /// <summary> /// Opens the stream. /// </summary> /// <param name="url">The URL.</param> /// <returns>Succeeded or failed.</returns> public bool OpenStream(string url) { if (!this.isConnected) { return false; } // Sets the request url: this.requestUrl = url; // Sends "OPTIONS" command and then gets the response: bool result = this.SendOptionsCmd(); if (!result) { this.CloseStream(); return false; } // Sends "DESCRIBE" command and then gets the SDP description: string sdpDescription = this.SendDescribeCmd(); if (string.IsNullOrEmpty(sdpDescription)) { this.CloseStream(); return false; } // Creates a media session object from the SDP description which // we have just received from the server: this.mediaSession = new MediaSession(sdpDescription); // Then, resolves the SDP description and initializes all basic // information: result = this.mediaSession.ResolveSdpDescription(); if (!result) { this.CloseStream(); return false; } // And then, creates the output file to write the data: result = this.CreateOutFile(); if (!result) { this.CloseStream(); return false; } // After that, sends the "SETUP" command and setups the stream: result = this.SendSetupCmd(); if (!result) { this.CloseStream(); return false; } // Finally, sends the "PLAY" command and starts playing stream: result = this.PlayStream(); if (!result) { this.CloseStream(); return false; } this.OnStreamOpened(); return true; }
以下是与每个请求指令相关的代码示例(注意这仅是初步版本,后续可能会进一步修改完善):
(1)OPTIONS
+ expand source view plain copy to clipboard print ? /// <summary> /// Sends the options CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private bool SendOptionsCmd() { if (! this .isConnected) { return false ; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_OPTIONS); // command name of 'OPTIONS' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return false ; } bool isOk = GetRtspResponse(); if (!isOk) { return false ; } return true ; } /// <summary> /// Sends the options CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private bool SendOptionsCmd() { if (!this.isConnected) { return false; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_OPTIONS); // command name of 'OPTIONS' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return false; } bool isOk = GetRtspResponse(); if (!isOk) { return false; } return true; }(2)DESCRIBE
+ expand source view plain copy to clipboard print ? /// <summary> /// Sends the describe CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private string SendDescribeCmd() { if (! this .isConnected) { return string .Empty; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_DESCRIBE); // command name of 'DESCRIBE' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return string .Empty; } bool isOk = GetRtspResponse(); if (!isOk) { return string .Empty; } return string .Empty; } /// <summary> /// Sends the describe CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private string SendDescribeCmd() { if (!this.isConnected) { return string.Empty; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_DESCRIBE); // command name of 'DESCRIBE' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return string.Empty; } bool isOk = GetRtspResponse(); if (!isOk) { return string.Empty; } return string.Empty; }(3)SETUP
+ expand source view plain copy to clipboard print ? /// <summary> /// Sends the setup CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private bool SendSetupCmd() { if (! this .isConnected) { return false ; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_SETUP); // command name of 'SETUP' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n" , this .sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return false ; } bool isOk = GetRtspResponse(); if (!isOk) { return false ; } return true ; } /// <summary> /// Sends the setup CMD. /// </summary> /// <returns>Succeeded or failed.</returns> private bool SendSetupCmd() { if (!this.isConnected) { return false; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_SETUP); // command name of 'SETUP' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n", this.sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { return false; } bool isOk = GetRtspResponse(); if (!isOk) { return false; } return true; }(4)PLAY
+ expand source view plain copy to clipboard print ? /// <summary> /// Plays the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool PlayStream() { if (! this .isConnected) { return false ; } if ( this .Duration < 0) { this .Duration = 0; } else if ( this .Duration == 0 || this .Duration > this .mediaSession.PlayEndTime) { this .Duration = this .mediaSession.PlayEndTime - this .SeekTime; } double startTime = this .SeekTime; double endTime = this .SeekTime + this .Duration; string range; if (startTime < 0) { range = string .Empty; } else if (endTime < 0) { range = string .Format( "Range: npt={0}-" , startTime); } else { range = string .Format( "Range: npt={0}-{1}" , startTime, endTime); } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_PLAY); // command name of 'PLAY' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n" , this .sessionId); // session id sb.AppendFormat("{0}/r/n" , range); // range, 'Range: npt=' sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this .CloseStream(); return false ; } bool isOk = GetRtspResponse(); if (!isOk) { this .CloseStream(); return false ; } this .OnStreamPlaying(); return true ; } /// <summary> /// Plays the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool PlayStream() { if (!this.isConnected) { return false; } if (this.Duration < 0) { this.Duration = 0; } else if (this.Duration == 0 || this.Duration > this.mediaSession.PlayEndTime) { this.Duration = this.mediaSession.PlayEndTime - this.SeekTime; } double startTime = this.SeekTime; double endTime = this.SeekTime + this.Duration; string range; if (startTime < 0) { range = string.Empty; } else if (endTime < 0) { range = string.Format("Range: npt={0}-", startTime); } else { range = string.Format("Range: npt={0}-{1}", startTime, endTime); } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_PLAY); // command name of 'PLAY' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n", this.sessionId); // session id sb.AppendFormat("{0}/r/n", range); // range, 'Range: npt=' sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this.CloseStream(); return false; } bool isOk = GetRtspResponse(); if (!isOk) { this.CloseStream(); return false; } this.OnStreamPlaying(); return true; }(5)PAUSE
+ expand source view plain copy to clipboard print ? /// <summary> /// Pauses the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool PauseStream() { if (! this .isConnected) { return false ; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_PAUSE); // command name of 'PAUSE' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n" , this .sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this .CloseStream(); return false ; } bool isOk = GetRtspResponse(); if (!isOk) { this .CloseStream(); return false ; } this .OnStreamPausing(); return true ; } /// <summary> /// Pauses the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool PauseStream() { if (!this.isConnected) { return false; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_PAUSE); // command name of 'PAUSE' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n", this.sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this.CloseStream(); return false; } bool isOk = GetRtspResponse(); if (!isOk) { this.CloseStream(); return false; } this.OnStreamPausing(); return true; }(6)TEARDOWN
+ expand source view plain copy to clipboard print ? /// <summary> /// Tear downs the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool TeardownStream() { if (! this .isConnected) { return false ; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} " , Constants.RTSP_CMD_TEARDOWN); // command name of 'TEARDOWN' sb.AppendFormat("{0} RTSP/1.0/r/n" , this .requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n" , ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n" , this .sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n" , Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this .CloseStream(); return false ; } bool isOk = GetRtspResponse(); if (!isOk) { this .CloseStream(); return false ; } this .OnStreamStopped(); return true ; } /// <summary> /// Tear downs the stream. /// </summary> /// <returns>Succeeded or failed.</returns> public bool TeardownStream() { if (!this.isConnected) { return false; } StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} ", Constants.RTSP_CMD_TEARDOWN); // command name of 'TEARDOWN' sb.AppendFormat("{0} RTSP/1.0/r/n", this.requestUrl); // request url sb.AppendFormat("CSeq: {0}/r/n", ++rtspSeqNum); // sequence number sb.AppendFormat("Session: {0}/r/n", this.sessionId); // session id sb.AppendFormat("User-Agent: {0}/r/n/r/n", Constants.USER_AGENT_HEADER); // user agent header bool isSucceeded = SendRtspRequest(sb.ToString()); if (!isSucceeded) { this.CloseStream(); return false; } bool isOk = GetRtspResponse(); if (!isOk) { this.CloseStream(); return false; } this.OnStreamStopped(); return true; }客户端每次发出请求指令时,通常需要立即得到服务器的响应信息。所以针对这样的情形,客户端发送指令和接收响应信息采用了同步方式进行通信。发送请求通过 SendRtspRequest方法完成,接收响应通过GetRtspResponse方法完成,这两个方法的代码示例如下:
+ expand source view plain copy to clipboard print ? /// <summary> /// Sends the RTSP request. /// </summary> /// <param name="request">The request.</param> /// <returns>Success or failed.</returns> private bool SendRtspRequest( string request) { if ( this .socket == null ) { return false ; } try { byte [] sendBuffer = Utils.StringToBytes(request); int sendBytesCount = this .socket.Send(sendBuffer, sendBuffer.Length, SocketFlags.None); if (sendBytesCount < 1) { return false ; } return true ; } catch (System.Exception e) { this .OnExceptionOccurred(e); return false ; } } /// <summary> /// Sends the RTSP request. /// </summary> /// <param name="request">The request.</param> /// <returns>Success or failed.</returns> private bool SendRtspRequest(string request) { if (this.socket == null) { return false; } try { byte[] sendBuffer = Utils.StringToBytes(request); int sendBytesCount = this.socket.Send(sendBuffer, sendBuffer.Length, SocketFlags.None); if (sendBytesCount < 1) { return false; } return true; } catch (System.Exception e) { this.OnExceptionOccurred(e); return false; } }
+ expand source view plain copy to clipboard print ? /// <summary> /// Gets the RTSP response. /// </summary> /// <returns>Success or failed.</returns> private bool GetRtspResponse() { bool isOk = false ; int revBytesCount = 0; byte [] revBuffer = new byte [1024 * 4]; // 4 KB buffer response = string .Empty; // Set the timeout for synchronous receive methods to // 5 seconds (5000 milliseconds.) socket.ReceiveTimeout = 5000; while ( true ) { try { revBytesCount = socket.Receive(revBuffer, revBuffer.Length, SocketFlags.None); if (revBytesCount >= 1) { // Here, we have received the data from the server successfully, so we break the loop. break ; } } catch (SocketException se) { // Receive data exception, may be it has come to the ReceiveTimeout or other exception. this .OnExceptionOccurred(se); break ; } } // Just looking for the RTSP status code: if (revBytesCount >= 1) { response = Utils.BytesToString(revBuffer, revBytesCount); if (response.StartsWith(Constants.RTSP_HEADER_VERSION)) { string [] dstArray = response.Split( ' ' ); // Separate by a blank if (dstArray.Length > 1) { string code = dstArray[1]; if (code.Equals(Constants.RTSP_STATUS_CODE_OK)) // RTSP/1.0 200 OK ... { isOk = true ; } } } } return isOk; } /// <summary> /// Gets the RTSP response. /// </summary> /// <returns>Success or failed.</returns> private bool GetRtspResponse() { bool isOk = false; int revBytesCount = 0; byte[] revBuffer = new byte[1024 * 4]; // 4 KB buffer response = string.Empty; // Set the timeout for synchronous receive methods to // 5 seconds (5000 milliseconds.) socket.ReceiveTimeout = 5000; while (true) { try { revBytesCount = socket.Receive(revBuffer, revBuffer.Length, SocketFlags.None); if (revBytesCount >= 1) { // Here, we have received the data from the server successfully, so we break the loop. break; } } catch (SocketException se) { // Receive data exception, may be it has come to the ReceiveTimeout or other exception. this.OnExceptionOccurred(se); break; } } // Just looking for the RTSP status code: if (revBytesCount >= 1) { response = Utils.BytesToString(revBuffer, revBytesCount); if (response.StartsWith(Constants.RTSP_HEADER_VERSION)) { string[] dstArray = response.Split(' '); // Separate by a blank if (dstArray.Length > 1) { string code = dstArray[1]; if (code.Equals(Constants.RTSP_STATUS_CODE_OK)) // RTSP/1.0 200 OK ... { isOk = true; } } } } return isOk; }
上述的GetRtspResponse方法的代码示例中,设置了同步接收的超时时长,并通过while循环不停地尝试接收,直到接收到数据或者发生了异常 (如超时等)。当接收到响应数据后,首先进行解析,然后判断该响应串中是否包含了响应成功状态码(200)。之后就是返回继续执行其他工作。
关于客户端在收到服务器对DESCRIBE请求的响应后,解析SDP描述信息的过程,这里不作介绍。客户端的与RTSP业务逻辑相关的工作主要由 RtspClient类来完成,而与RTP/RTCP、文件处理等相关的初始工作则有MediaSession类来完成。