本篇文章简要介绍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作初步的代码实 现,同时在本项目中将不考虑用户验证的问题。 以下是关于上述会话状态和若干状态码的主要方法:
- /// <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的代码实现部分:
- /// <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的基类,该类包含一些事件和虚方法,代码如下所示:
- /// <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指令到开始传输流的基本步骤,其代码示例如下:
- /// <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
- /// <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
- /// <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
- /// <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
- /// <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
- /// <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
- /// <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方法完成,这两个方法的代码示例如下:
- /// <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>
- /// 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类来完成。