根据log 分析 RTSPServer::RTSPClientConnection::handleRequestBytes 中对DESCRIBE 请求的处理
1. 解析 请求
和OPTIONS 请求一样
fprintf(stderr, "RTSPClientConnection[%p]::handleRequestBytes() %s %d new bytes:%s\n",
this, numBytesRemaining > 0 ? "processing" : "read", newBytesRead, ptr);
log打印接收客户端消息如下
RTSPClientConnection[0xef5bfc40]::handleRequestBytes() read 127 new bytes:
DESCRIBE rtsp://192.168.0.10:8554/H264Video.mkv RTSP/1.0
CSeq: 2
Accept: application/sdp
Date: Sat, 01 Jan 2000 04:49:58 GMT
并且通过 parseRTSPRequestString解析数据获得消息如下
Boolean parseSucceeded = parseRTSPRequestString((char*)fRequestBuffer,
fLastCRLF+2 - fRequestBuffer,
cmdName, sizeof cmdName,
urlPreSuffix, sizeof urlPreSuffix,
urlSuffix, sizeof urlSuffix,
cseq, sizeof cseq,
sessionIdStr, sizeof sessionIdStr,
contentLength);
parseRTSPRequestString() succeeded, returning cmdName "DESCRIBE", urlPreSuffix "", urlSuffix "H264Video.mkv", CSeq "2", Content-Length 0, with 0 bytes following the message.
成功解析了数据,就要处理数据,最后返回response 给客户端
2. 处理解析数据 获取session:
Boolean const requestIncludedSessionId = sessionIdStr[0] != '\0';
if (requestIncludedSessionId) {
clientSession
= (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr));
if (clientSession != NULL) clientSession->noteLiveness();
}
requestIncludedSessionId 的是通过判断 请求消息是否含有 session: 来判断,当前 DESCRIBE 请求消息中,在log中并没有,所以,这里直接忽略,不创建clientSession
3. 处理 DESCRIBE 请求消息
根据上面解析的结果
returning cmdName "DESCRIBE", urlPreSuffix "", urlSuffix "H264Video.mkv", CSeq "2", Content-Length 0, with 0 bytes following the message
urlPreSuffix = “”
urlSuffix = “H264Video.mkv”
handleCmd_DESCRIBE(urlPreSuffix, urlSuffix, (char const*)fRequestBuffer);
在处理DESCRIBE 请求的时候,可以看到最后返回客户端的数据格式如下:
snprintf((char*)fResponseBuffer, sizeof fResponseBuffer,
"RTSP/1.0 200 OK\r\nCSeq: %s\r\n"
"%s"
"Content-Base: %s/\r\n"
"Content-Type: application/sdp\r\n"
"Content-Length: %d\r\n\r\n"
"%s",
fCurrentCSeq,
dateHeader(),
rtspURL,
sdpDescriptionSize,
sdpDescription);
这里主要分析 rtspURL,sdpDescriptionSize, sdpDescription如何获取的
urlTotalSuffix[0] = '\0';
if (urlPreSuffix[0] != '\0') {
strcat(urlTotalSuffix, urlPreSuffix);
strcat(urlTotalSuffix, "/");
}
strcat(urlTotalSuffix, urlSuffix);
// We should really check that the request contains an "Accept:" #####
// for "application/sdp", because that's what we're sending back #####
// Begin by looking up the "ServerMediaSession" object for the specified "urlTotalSuffix":
session = fOurServer.lookupServerMediaSession(urlTotalSuffix);
// Then, assemble a SDP description for this session:
sdpDescription = session->generateSDPDescription();
unsigned sdpDescriptionSize = strlen(sdpDescription);
// Also, generate our RTSP URL, for the "Content-Base:" header
// (which is necessary to ensure that the correct URL gets used in subsequent "SETUP" requests).
rtspURL = fOurRTSPServer.rtspURL(session, fClientInputSocket);
上面就是获取 rtspURL,sdpDescriptionSize, sdpDescription整个过程,首先确定urlTotalSuffix ,根据判断最后执行 strcat(urlTotalSuffix, urlSuffix); 拼接字符串 urlTotalSuffix = “H264Video.mkv “
然后根据 urlTotalSuffix 在ServerMediaSession table 表中查找ServerMediaSession 具体逻辑如下
ServerMediaSession* DynamicRTSPServer
::lookupServerMediaSession(char const* streamName, Boolean isFirstLookupInSession) {
// First, check whether the specified "streamName" exists as a local file:
FILE* fid = fopen(streamName, "rb");
Boolean fileExists = fid != NULL;
// Next, check whether we already have a "ServerMediaSession" for this file:
ServerMediaSession* sms = RTSPServer::lookupServerMediaSession(streamName);
Boolean smsExists = sms != NULL;
// Handle the four possibilities for "fileExists" and "smsExists":
if (!fileExists) {
if (smsExists) {
// "sms" was created for a file that no longer exists. Remove it:
removeServerMediaSession(sms);
sms = NULL;
}
return NULL;
} else {
if (smsExists && isFirstLookupInSession) {
// Remove the existing "ServerMediaSession" and create a new one, in case the underlying
// file has changed in some way:
removeServerMediaSession(sms);
sms = NULL;
}
if (sms == NULL) {
sms = createNewSMS(envir(), streamName, fid);
addServerMediaSession(sms);
}
fclose(fid);
return sms;
}
}
上面代码比较重要,是判断那一种传输流媒体方式,是通过摄像头,还是本地文件流,就在这里区分
由于我进行的是本地测试
FILE* fid = fopen(streamName, "rb");
然后在 已有session表中查找
// Next, check whether we already have a "ServerMediaSession" for this file:
ServerMediaSession* sms = RTSPServer::lookupServerMediaSession(streamName);
很明显第一次创建 会话肯定没有,所以新建streamName 的对应的session
sms = createNewSMS(envir(), streamName, fid);
addServerMediaSession(sms);
创建新的 session 然后通过addServerMediaSession 添加到 session table表中
具体流程图
createNewSMS 函数很重要 根据streamName 来判断创建什么类型 session
static ServerMediaSession* createNewSMS(UsageEnvironment& env,
char const* fileName, FILE* /*fid*/) {
// Use the file name extension to determine the type of "ServerMediaSession":
char const* extension = strrchr(fileName, '.');
if (extension == NULL) return NULL;
ServerMediaSession* sms = NULL;
Boolean const reuseSource = False;
if (strcmp(extension, ".aac") == 0) {...
if (strcmp(extension, ".amr") == 0) {...
if (strcmp(extension, ".ac3") == 0) {...
if (strcmp(extension, ".m4e") == 0) {...
if (strcmp(extension, ".264") == 0) {...
// Assumed to be a H.264 Video Elementary Stream file:
NEW_SMS("H.264 Video");
OutPacketBuffer::maxSize = 100000; // allow for some possibly large H.264 frames
sms->addSubsession(H264VideoFileServerMediaSubsession::createNew(env, fileName, reuseSource));
} else if (strcmp(extension, ".265") == 0) {...
// Assumed to be a H.265 Video Elementary Stream file:
NEW_SMS("H.265 Video");
OutPacketBuffer::maxSize = 100000; // allow for some possibly large H.265 frames
sms->addSubsession(H265VideoFileServerMediaSubsession::createNew(env, fileName, reuseSource));
} else if (strcmp(extension, ".mp3") == 0) {...
if (strcmp(extension, ".mpg") == 0) {...
if (strcmp(extension, ".vob") == 0) {...
if (strcmp(extension, ".ts") == 0) {...
if (strcmp(extension, ".wav") == 0) {...
if (strcmp(extension, ".dv") == 0) {...
if (strcmp(extension, ".mkv") == 0 || strcmp(extension, ".webm") == 0) {
// Assumed to be a Matroska file (note that WebM ('.webm') files are also Matroska files)
OutPacketBuffer::maxSize = 100000; // allow for some possibly large VP8 or VP9 frames
NEW_SMS("Matroska video+audio+(optional)subtitles");
// Create a Matroska file server demultiplexor for the specified file.
// (We enter the event loop to wait for this to complete.)
MatroskaDemuxCreationState creationState;
creationState.watchVariable = 0;
MatroskaFileServerDemux::createNew(env, fileName, onMatroskaDemuxCreation, &creationState);
env.taskScheduler().doEventLoop(&creationState.watchVariable);
ServerMediaSubsession* smss;
while ((smss = creationState.demux->newServerMediaSubsession()) != NULL) {
sms->addSubsession(smss);
}
} else
if (strcmp(extension, ".ogg") == 0 || strcmp(extension, ".ogv") == 0 || strcmp(extension, ".opus") == 0) {}
return sms;
}
因为我用的是本地mkv 视频 ,创建sms
#define NEW_SMS(description) do {\
char const* descStr = description\
", streamed by the LIVE555 Media Server";\
sms = ServerMediaSession::createNew(env, fileName, fileName, descStr);\
} while(0)
创建好 sms 创建专门针对 MKV 格式的MatroskaFileServerDemux 多路复用对象 Matroska是一种新的多媒体封装格式,支持MKV
MatroskaFileServerDemux::createNew(env, fileName, onMatroskaDemuxCreation, &creationState);
ServerMediaSubsession* smss;
while ((smss = creationState.demux->newServerMediaSubsession()) != NULL) {
sms->addSubsession(smss);
}
上面代码的详细流程画出来了,有点复杂,需要慢慢看,主要的目的,创建ServerMediaSession 和一个subServerMediaSession,保存sdp 需要的数据,过程中读取了文件,多媒体文件的文件信息
这样就获得了sms 对象
先看DESCRIBE RESPONSE 返回消息log
sending response: RTSP/1.0 200 OK
CSeq: 2
Date: Sat, Jan 01 2000 00:05:00 GMT
Content-Base: rtsp://192.168.100.10:554/H264Video.mkv
Content-Type: application/sdp
Content-Length: 491
v=0
o=- 946685097694861 1 IN IP4 192.168.100.10
s=Session streamed by "RTSPServer"
i=H264Video.mkv
t=0 0
a=tool:LIVE555 Streaming Media v2018.02.28
a=type:broadcast
a=control:*
a=range:npt=0-
a=x-qt-text-nam:Session streamed by "RTSPServer"
a=x-qt-text-inf:H264Video.mkv
m=video 0 RTP/AVP 96
c=IN IP4 0.0.0.0
b=AS:1
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42E01F;sprop-parameter-sets=Z0LgH0VoBQBboQAAgAAAHgAAhAAAAAFoSOPI,aEjjyA==
a=control:track1
上面的代码获取了 在handleCmd_DESCRIBE 中的session 对象,然后通过generateSDPDescription 获取sdp信息
sdpDescription = session->generateSDPDescription();
sdp格式
char const* const sdpPrefixFmt =
"v=0\r\n"
"o=- %ld%06ld %d IN IP4 %s\r\n"
"s=%s\r\n"
"i=%s\r\n"
"t=0 0\r\n"
"a=tool:%s%s\r\n"
"a=type:broadcast\r\n"
"a=control:*\r\n"
"%s"
"%s"
"a=x-qt-text-nam:%s\r\n"
"a=x-qt-text-inf:%s\r\n"
"%s";
通过 snprintf 格式化 reponse 消息
// Generate the SDP prefix (session-level lines):
snprintf(sdp, sdpLength, sdpPrefixFmt,
fCreationTime.tv_sec, fCreationTime.tv_usec, // o= <session id>
1, // o= <version> // (needs to change if params are modified)
ipAddressStr.val(), // o= <address>
fDescriptionSDPString, // s= <description>
fInfoSDPString, // i= <info>
libNameStr, libVersionStr, // a=tool:
sourceFilterLine, // a=source-filter: incl (if a SSM session)
rangeLine, // a=range: line
fDescriptionSDPString, // a=x-qt-text-nam: line
fInfoSDPString, // a=x-qt-text-inf: line
fMiscSDPLines); // miscellaneous session SDP lines (if any)
其他的具体参数,在代码中细看,这里列出来
通过session获取rtspURL
rtspURL = fOurRTSPServer.rtspURL(session, fClientInputSocket);
处理方式 fClientInputSocket 获取ip地址和端口 session获取 传给客户端的文件名称(带地址)
socket 获取ip 和端口的过程如下:
char* RTSPServer::rtspURLPrefix(int clientSocket) const {
struct sockaddr_in ourAddress;
SOCKLEN_T namelen = sizeof ourAddress;
getsockname(clientSocket, (struct sockaddr*)&ourAddress, &namelen);
char urlBuffer[100]; // more than big enough for "rtsp://<ip-address>:<port>/"
portNumBits portNumHostOrder = ntohs(fServerPort.num());
if (portNumHostOrder == 554 /* the default port number */) {
sprintf(urlBuffer, "rtsp://%s/", AddressString(ourAddress).val());
} else {
sprintf(urlBuffer, "rtsp://%s:%hu/",
AddressString(ourAddress).val(), portNumHostOrder);
}
return strDup(urlBuffer);
}
以上基本上处理完毕
最后 send 发送给客户端
send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0);
说明: 代码逻辑结构比较复杂,所以流程图需要亲自跟一下,不然都不知道在说什么,省略了很多细节,细节梳理起来没没了
总结:
在 处理description的时候, rtsp服务器 工作
1. 创建connection 的 media session ,包含发送数据流的名称,或者说是源,是根据文件的格式创建不同的session
2. 获取 服务器ip地址 端口 流,过程中,读取文件的基本信息
主要是session 中数据转化为 sdp ,比较复杂,估计也需要好好学习一番,每一行 都是有用的,我遇到了再去分析