rtsp协议_Chromium(35):rtsp客户端
streamUsingTCP。rtsp协议要传的数据可分为两种,⼀是⽤于管理rtsp会话的消息,像DESCRIBE、SETUP、PLAY,它们⼀定通过TCP,端⼝号像554。第⼆种是媒体数据,像视频、⾳频,这些数据被封装成rtp包,于是称rtp数据。传送rtp数据可⽤UDP也可⽤TCP。⽤UDP时,需新开端⼝,以UDP收发。TCP时,rtp数据将和rtsp会话命令分时复⽤,统⼀通过554端⼝收发。
streamUsingTCP指⽰⽤哪种⽅法,true表⽰使⽤第⼆种。
1. 从⽹络收rtsp,解析出rtp,并从rtp拆包出编码过的视/⾳频帧。所⽤技术:live555。
2. 解码视/⾳频帧。所⽤技术:webrtc中的解码模块。
3. 渲染解码出的数据。所⽤技术:从webrtc解码输出中取VideoFrame,即基于rtc::VideoSinkInterface::OnFrame获得解码帧。
以上是之前⽤live555+webrtc实现的rtsp客户端,考虑到⼀些原因要⽤chromium代替live555。
chromium提供了⾮常好的连接溢出时间机制,能实现灵活的rtsp重连。
app基于Ro,Ro已在⼴泛使⽤chromium,live555⼜⾃带⼀套消息循环和socket库,这重复了。
说是⽤chromium代替live555,具体实现上是⽤chromium的消息循环和socket库,⾄于协议处理逻辑还是⽤live555中代码。
注:为让chromium中的GURL⽀持解析rtsp url,需修改chromium源码。具体是增加变量kRtspScheme,把它加到kStandardURLSchemes。
<chromium>/url/url_constants
const char kRtspScheme[] = "rtsp";
<chromium>/url/url_util
const SchemeWithType kStandardURLSchemes[] = {
......
{kRtspScheme, SCHEME_WITH_PORT}, // Rtsp.
};
注:只要把live555.cpp中的u_chromium值改为fal,就会使⽤live555⾃带的消息循环和socket库。
⼀、线程模型
图1 rtsp的线程模型
系统中⾄少存在1+1+N个线程,第⼀个1是main线程,第⼆个1是socket线程,N表⽰要同时接收N个rtsp设备,每个设备需要⼀个DecodingThread。
main线程。main函数所在的线程,系统创建。⽤于和各设备建⽴rtsp连接。
socket线程。使⽤Chromium中的ba::Thread创建。使⽤TCP传rtsp时,建⽴rtsp连接(DESCRIBE、SETUP、PLAY)和后⾯接收媒体流数据是⽤同⼀个socket,加上Chromium硬性规定,“同⼀个socket上的操作必须放在同⼀个线程,包括创建、连接、读、写、关闭”,于是新开⼀个线程,专门处理和socket相关任务。当要接收N个rtsp设备时,系统也只有⼀个socket线程。
DecodingThread。webrtc创建。通过socket线程收到⼀帧后,解码,解码出的帧通过rtc::VideoSinkInterface::OnFrame传给app。
⼆、建⽴阶段
图1中“live555::start”执⾏建⽴rtsp连接,过程中要发送DESCRIBE、SETUP、PLAY。以下是流程。
1. main线程向socket线程投递Start_chromium。
2. 新建⼀个RunLoop对象。
3. main线程向⾃已投递rtsp_tup_slice。rtsp_tup_slice做成定时器,⽤于检测发了请求后,如果
4.5秒后还没应答,向main线程
投递Quit,让退出步骤4运⾏的RunLoop。
征西将军
4. 运⾏RunLoop::Run。
步骤1的Start_chromium运⾏在socket线程,设置next_state_是解析域名状态,随即调⽤DoLoop。
int RTSPClient::DoLoop(int result) {
DCHECK_NE(next_state_, STATE_NONE);
int rv = result;
do {
State state = next_state_;
next_state_ = STATE_NONE;
switch (state) {
ca STATE_RESOLVE_HOST:
DCHECK_EQ(net::OK, rv);
rv = DoResolveHost();
break;
ca STATE_RESOLVE_HOST_COMPLETE:
rv = DoResolveHostComplete(rv);
break;
ca STATE_TRANSPORT_CONNECT:
驾驶员培训内容DCHECK_EQ(net::OK, rv);
rv = DoTransportConnect();
break;
ca STATE_TRANSPORT_CONNECT_COMPLETE:
rv = DoTransportConnectComplete(rv);
break;
ca STATE_TRANSPORT_WRITE_COMPLETE:
rv = DoTransportWriteComplete(rv);
break;
幼儿手指游戏ca STATE_TRANSPORT_READ_COMPLETE:
rv = DoTransportReadComplete(rv);
break;
default:
NOTREACHED();
rv = net::ERR_FAILED;
break;
}
} while (rv != net::ERR_IO_PENDING && next_state_ != STATE_NONE);
return rv;
}
void RTSPClient::OnIOComplete(int result)
{
DoLoop(result);
}
悸动的意思
以上是基于Chromium的socket库写的标准DoLoop、OnIOComplete(关于这两函数细节参考“Chromium(2/4):消息循环和socket 库”中“⼆、Socket库”)。DoLoop把建⽴rtsp连接分为三个步骤:解析域名、连接和发请求/收应答。发请求/收应答的个数是
1+N+1。N是sdp中的媒体流数⽬,有多少个媒体流就须要处理多少个SETUP。以下摘取当中DoTransportWriteComplete进⾏分析。
int RTSPClient::DoTransportWriteComplete(int result)
{
if (result > 0) {
next_state_ = STATE_TRANSPORT_READ_COMPLETE;
VALIDATE(fResponBufferBytesLeft > 1, null_str);
// why fResponBufferBytesLeft - 1, references to begin of handleResponBytes1:
if (!read_by_rtpinterface_) {
const int max_bytes = SDL_min(envir().read_buf->size(), (int)fResponBufferBytesLeft - 1);
result = ctrl_socket_->Read(envir().(), max_bytes, envir().iocomplete);
} el {
// let RTPInterface read this socket. net::ERR_IO_PENDING make DoLoop exit.
if (!in_rtpinterface_iocomplete_) {
SDL_Log("RTSPClient::DoTransportWriteComplete(result: %i)", result);
envir().iocomplete.Run(SPECIAL_CALL_MAGIC);
}
result = net::ERR_IO_PENDING;
}
if (result > 0) {
result = DoTransportReadComplete(result);
}
}
if (result < 0 && result != net::ERR_IO_PENDING) {
next_state_ = STATE_NONE;
socket_io_fail();
}
return result;
}
参数result>0时,表⽰之前Write成功,于是发起Read。当Read返回值>0,表⽰此次Read是同步读,⽴即调⽤DoTransportReadComplete,让处理读到的应答。否则或是异步(ERR_IO_PENDING),或错误,是错误时调⽤soket_io_fail。是异步时返回ERR_IO_PENDING,回到上层DoLoop,不再满
⾜while继续循环条件,DoLoop以ERR_IO_PENDING退出。后续任务得靠系统触发OnIOComplete才能处理。
苹果香味Read时,为什么要使⽤read_by_rtpinterface_?——使⽤tcp时,rver发出SETUP应答后,就有可能向外发rtp包。此时在554上会出现rtsp消息和rtp包混杂情况,read_by_rtpinterface_=true表⽰client收到过⼀个SETUP应答了,为区分是SETUP/PLAY应答还是rtp数据,后⾯⼀个字节⼀个字节接收(rtp还是按负载长度收),直到收完PALY应答,它确保了不会收⾛⼀个本属于rtp的字节。
三、接收媒体数据阶段
⾸先说下TCP传输时流媒体数据的顶层格式。
整个流被拆分成好多个MTU,每个MTU由两部分组成,4字节前缀和payload。4字节前缀中第⼀个字节是“$”,第⼆个字节channel 号,第三个、第四个字节是后⾯payload字节数。RTP包位在payload中。
MTU长度是服务器⾃个设的⼀个值,像4+1408。⽽⼀视频帧不可能才1000多字节,于是⼀帧会被拆成多个MTU。
会不会发⽣⼀个MTU包含多帧数据?举个例⼦,#8帧只有最后400个字节了,于是放在了接下MTU a
中,此时MTU a还有数百字节空着,会不会放#9帧的前⾯数百字节。——个⼈认为不会出现这种情况。
要了解live555如何接收rtsp数据可参考“live555从RTSP服务器读取数据到使⽤接收到的数据流程分析”。核⼼函数是MultiFramedRTPSource::networkReadHandler1,它既负责从socket接收媒体流,⼜负责处理收到的数据。处理过程包括,1)数据存到BufferedPacket链表,2)收到完整的⼀帧后,存到app要求的fTo,并调⽤app设置的、收到⼀帧后的回调函数afterGettingFrame。
3.1 live555::frame_slice接收⼀帧
让回看图1中“live555::frame_slice”,它向socket线程接收⼀帧数据。以下是流程。
1. DecodingThread向socket线程投递continuePlaying_chromium。
2. DecodingThread线程向⾃已投递quit_runloop,延迟时间5秒。⼀旦5秒后步骤3运⾏的RunLoop还没退出(意味着5秒也没收到⼀怎么治口臭最快最简单
帧),让退出RunLoop。
3. 运⾏RunLoop::Run。导致Run退出的可能原因,1)接收到⼀帧,2)中间发⽣错误,3)5秒超时。
步骤1的continuePlaying_chromium运⾏在socket线程,执⾏两个操作。1)调⽤continuePlaying,它设置相关变量,让live555进⼊NeedDelivery状态。当中有个参数叫fIsCurrentlyAwaitingData,执⾏前必须fal,执⾏后设为true。2)调⽤
RTPInterface::chromium_slice,它会循环调⽤SocketDescriptor::tcpReadhandle1_chromium,直到后者返回fal。
tcpReadhandle1_chromium来⾃live555提供的tcpReadhandle1,只是把读socket部分改为⽤chromium的socket库。从socket收到数据后,原有的live555处理逻辑都不⽤变。
3.2 fIsCurrentlyAwaitingData变量
为什么要额外说这个变量,让看tcpReadHandler1_chromium中代码。
Boolean SocketDescriptor::tcpReadHandler1_chromium(int rv, ...)
{
...
Boolean callAgain = True;
switch (fTCPReadingState) {
...
ca AWAITING_PACKET_DATA: {
callAgain = Fal;
fTCPReadingState = AWAITING_DOLLAR;
// fStreamChannelId存储着此个MTU的channel号,由channel号找到能处理它的RTPInterface。
RTPInterface* rtpInterface = lookupRTPInterface(fStreamChannelId);
if (rtpInterface->fNextTCPReadSize == 0) {
// 已读出该MTU的所有payload,告知caller再次以rv=0调⽤tcpReadHandler1_chromium
callAgain = true;
break;
}
if (rtpInterface->fReadHandlerProc != NULL) {
fTCPReadingState = AWAITING_PACKET_DATA;
rtpInterface->fLastTCPReadResult_ = rv;
rtpInterface->fReadHandlerProc(rtpInterface->fOwner, 0);
if (rtpInterface->fNextTCPReadSize == 0) {
装饰公司简介if (fEnv.tup_finished) {
RTPSource* source = nullptr;
if (rtpInterface->fOwner->isSource()) {
// 此个RTPInterface⽤于RTPSource
source = static_cast<RTPSource*>(rtpInterface->fOwner);
} el {
// 此个RTPInterface既然不⽤于RTPSource,那⼀定⽤于RTCPInstance
VALIDATE(rtpInterface->fOwner->isRTCPInstance(), null_str);
}
if (source == nullptr || source->isCurrentlyAwaitingData()) {
// 如果此个RTPInterface⽤于RTPSource,还须满⾜isCurrentlyAwaitingData=true,caller才主动发read。isCurrentlyAwaitingData=fal意味着是由DummyS callAgain = true;
break;
}
} el {
// during tup, continue finish it.
callAgain = true;
break;
}
}
}
break;
} // ca AWAITING_PACKET_DATA
} // switch (fTCPReadingState)
return callAgain;
}
处理了MTU前缀4字节后,SocketDescriptor进⼊AWAITING_PACKET_DATA状态,rtpInterface->fNextTCPReadSize存储着4字节
中后两字节的值,即payload长度。rtpInterface->fReadHandlerProc会很快调⽤核⼼函数
成人零基础学英语
MultiFramedRTPSource::networkReadHandler1,后者去读socket时,每次最多读fNextTCPReadSize字节。随着
networkReadHandler1不断被执⾏,socket读出数据越多,fNextTCPReadSize会不断减少,减少到0时表⽰已读完这个MTU。⼀旦读
完MTU,后续不会再有异步触发出OnIOComplete,此刻需要主动把callAgain置为true,通知上层的chromium_slice或OnIOComplete
再次调⽤tcpReadHandler1_chromium,去读下⼀个MTU。当isCurrentlyAwaitingData=fal,意味着是由
DummySink::continuePlaying_chromiumy主动发read。
fIsCurrentlyAwaitingData有什么⽤?——多个MTU组成⼀帧,总会遇到读完⼀个MTU时,该帧恰好读完,这变量让知道什么时候已读完
⼀帧。它在continuePlaying时被置为true。networkReadHandler1判断出收完⼀帧后,在调⽤app的afterGettingFrame前会把它置为
fal。