请选择 进入手机版 | 继续访问电脑版

基于live555的rtsp播放器:数据接收(拉流)

[复制链接]
小浣熊 发表于 2021-1-3 12:04:48 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
live555的使用都是从研究源码中的testRTSPClient例子开始的,这个例子包罗了RTSP消息交互和数据吸收。
一.RTSP消息交互

一次根本的RTSP操纵过程如下:C表现RTSP客户端,S表现RTSP服务端
1.第一步:查询服务器端可用方法
C->S:OPTIONrequest          //询问S有哪些方法可用
S->C:OPTIONresponse       //S回应信息的public头字段中包罗提供的所有可用方法
2.第二步:得到媒体形貌信息
C->S:DESCRIBE request    //要求得到S提供的媒体形貌信息
S->C:DESCRIBE response  //回应媒体形貌信息,一般是sdp信息
3.第三步:创建RTSP会话
C->S:SETUPrequest            //通过Transport头字段列出可继承的传输选项,请求S创建会话
S->C:SETUPresponse          //创建会话,通过Transport头字段返回选择的详细转输选项,并返回创建的Session ID;
4.第四步:请求开始传送数据
C->S:PLAY request               //C请求S开始发送数据
S->C:PLAYresponse              //S回应该请求的信息
5.第五步:数据传送播放中
S->C:发送流媒体数据             //通过RTP协议传送数据
6.  第六步:关闭会话,退出
C->S:TEARDOWN request    //C请求关闭会话
S->C:TEARDOWN response //S回应该请求
上述的过程只是尺度的、友好的rtsp流程,但实际的需求中并不一定按此过程。
此中第三和第四步是必须的!第一步,只要服务器客户端约定好,有哪些方法可用,则option请求可以不要。第二步,如果我们有其他途径得到媒体初始化形貌信息(好比http请求等等),则我们也不需要通过rtsp中的describe请求来完成。

关于RTSP消息格式和SDP协议格式详见:https://blog.csdn.net/caoshangpa/article/details/53191630
testRTSPClient例子的不敷之处在于所有变量、函数及其回调都写在了一个文件中,不方便管理。而且收到数据后,只是简朴的打印了数据流的范例、字节数和时间戳,并没有举行下一步的处理处罚。
关于testRTSPClient中变量、函数及其回调的拆分,github上有个demo可以参考:https://github.com/drriguz/Kamera
这个demo中实现了h264视频流的解码和显示,例子中最可取的做法是将数据吸收放到了一个线程中,也就是将env->taskScheduler().doEventLoop(&this->eventLoopWatchVariable);这一句直接放到了线程中。线程的竣事通过eventLoopWatchVariable变量来控制。这样的话一个线程就是一个session,拉取多路流只需要创建多个session即可。
二.数据吸收

继承父类MediaSink即可实现数据的吸收。在子类的构造函数中,可以先根据MediaSubsession参数获取一些有用的信息。
1.视频信息(h264和h265(HEVC))
  1.     if(strcmp(m_subsession.mediumName(), "video") == 0)    {        if(strcmp(m_subsession.codecName(), "H264") == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_H264;            unsigned int extraSize = 0;            uint8_t *extra = h264_parse_config(m_subsession.fmtp_spropparametersets(),extraSize);            if(extra&&extraSize>8)            {                format->extraSize = extraSize;                format->extra = new uint8_t[extraSize];                memcpy(format->extra, extra, extraSize);                delete [] extra;            }            delete format;        }        else if(strcmp(m_subsession.codecName(), "H265") == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_H265;            unsigned int extraSizeVPS = 0, extraSizeSPS = 0, extraSizePPS = 0, extraSizeTotal = 0;            uint8_t *extraVPS = h264_parse_config(m_subsession.fmtp_spropvps(),extraSizeVPS);            uint8_t *extraSPS = h264_parse_config(m_subsession.fmtp_spropsps(),extraSizeSPS);            uint8_t *extraPPS = h264_parse_config(m_subsession.fmtp_sproppps(),extraSizePPS);            extraSizeTotal = extraSizeVPS + extraSizeSPS + extraSizePPS;            if(extraVPS&&extraSPS&&extraPPS&&extraSizeTotal>12)            {                format->extraSize = extraSizeTotal;                format->extra = new uint8_t[extraSizeTotal];                memcpy(format->extra, extraVPS, extraSizeVPS);                memcpy(format->extra+extraSizeVPS, extraSPS, extraSizeSPS);                memcpy(format->extra+extraSizeVPS+extraSizeSPS, extraPPS, extraSizePPS);                delete [] extraVPS;                delete [] extraSPS;                delete [] extraPPS;            }            delete format;        }    }
复制代码
对于h264格式,fmtp_spropparametersets包罗了从SDP中获取的SPS和PPS信息的base64编码,SPS和PPS中间用逗号隔开。分析后的数据extra在初始化视频解码器的时候需要用到。
一个范例的h264视频流的SDP信息如下图:

红圈处sprop-parameter-sets等号背面就是fmtp_spropparametersets函数获取到的值。
需要特别注意一点的是SPS和PPS信息并不是在SDP中强制提供的,即sprop-parameter-sets等号背面可以是空的。因此为了保险起见,在视频流中总首次吸收到SPS和PPS信息时,需要再解码之前,再次传给解码器。
对于h265格式,VPS和SPS和PPS是分三次获取的,分析后要组装到一起,同理,组装后的数据extra在初始化视频解码器的时候需要用到。h265也要注意上面提到的问题。
2.音频信息(MPEG4-GENERIC(AAC)、PCMA(G711a)、PCMU(G711u)和G726)
之所以要处理处罚这么多音频编码范例,是因为这些范例险些所有的安防摄像机都支持。
  1.     if(strcmp(m_subsession.mediumName(), "audio") == 0)    {        if(strcmp(m_subsession.codecName(), "MPEG4-GENERIC") == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_AAC;            unsigned int extraSize = 0;            uint8_t *extra = parseGeneralConfigStr(m_subsession.fmtp_config(),extraSize);            if(extra)            {                format->extraSize = extraSize;                format->extra = new uint8_t[extraSize];                memcpy(format->extra, extra, extraSize);                delete [] extra;            }            delete format;        }        else if(strcmp(m_subsession.codecName(), "PCMA") == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_PCM_ALAW;            format->samplerate = m_subsession.rtpTimestampFrequency();            format->channels = m_subsession.numChannels();            format->bitspersample = 8;            delete format;        }        else if(strcmp(m_subsession.codecName(), "PCMU") == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_PCM_MULAW;            format->samplerate = m_subsession.rtpTimestampFrequency();            format->channels = m_subsession.numChannels();            format->bitspersample = 8;            delete format;        }        else if(strncmp(m_subsession.codecName(), "G726", 4) == 0)        {            Format *format = new Format();            format->codecID = AV_CODEC_ID_ADPCM_G726;            format->samplerate = 8000;            format->channels = 1;            if(strcmp(m_subsession.codecName()+5, "40") == 0)            {                format->bitrate = 40000;            }            else if(strcmp(m_subsession.codecName()+5, "32") == 0)            {                format->bitrate = 32000;            }            else if(strcmp(m_subsession.codecName()+5, "24") == 0)            {                format->bitrate = 24000;            }            else if(strcmp(m_subsession.codecName()+5, "16") == 0)            {                format->bitrate = 16000;            }            delete format;        }    }
复制代码
fmtp_config包罗了采样率和通道等信息,由下图红圈处的rtpmap参数获取。这个信息SDP是必须提供的,因为在吸收数据时无法再获取到这些信息了,它们将用于初始化音频解码器。

3.流数据处理处罚
通过循环调用FrameSouce类的getNextFrame(m_receiveBuffer, RECEIVE_BUFFER_SIZE,afterGettingFrame, this, onSourceClosure, this);函数来获取数据,参数m_receiveBuffer是吸收buffer,afterGettingFrame是回调函数。
在回调函数中afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned /*durationInMicroseconds*/)可以对吸收到的音视频流数据举行处理处罚。
此时获取到的是完整的帧数据,参数frameSize是数据巨细,参数presentationTime的范例timeval是live555内置的时间布局,转换成pts的方法如下:
  1. int64_t pts = (int64_t)presentationTime.tv_sec * INT64_C(1000000) + (int64_t)presentationTime.tv_usec;
复制代码
这个时间是绝对时间,单位是微秒。
在处理处罚数据之前,先看看live555官方的FAQ:http://www.live555.com/liveMedia/faq.html#testRTSPClient-how-to-decode-data
Question:I have successfully used the "testRTSPClient" demo application to receive a RTSP/RTP stream. Using this application code as a model, how can I decode the received video (and/or audio) data?
The "testRTSPClient" demo application receives each (video and/or audio) frame into a memory buffer, but does not do anything with the frame data. You can, however, use this code as a model for a 'media player' application that decodes and renders these frames. Note, in particular, the "DummySink" class that the "testRTSPClient" demo application uses - and the (non-static) "DummySink::afterGettingFrame()" function. When this function is called, a complete 'frame' (for H.264 or H.265, this will be a "NAL unit") will have already been delivered into "fReceiveBuffer". Note that our "DummySink" implementation doesn't actually do anything with this data; that's why it's called a 'dummy' sink.
If you want to decode (or otherwise process) these frames, you would replace "DummySink" with your own "MediaSink" subclass. Its "afterGettingFrame()" function would pass the data (at "fReceiveBuffer", of length "frameSize") to a decoder. (A decoder would also use the "presentationTime" timestamp to properly time the rendering of each frame, and to synchronize audio and video.)
If you are receiving H.264 video data, there is one more thing that you have to do before you start feeding frames to your decoder. H.264 streams have out-of-band configuration information ("SPS" and "PPS" NAL units) that you may need to feed to the decoder to initialize it. To get this information, call "MediaSubsession::fmtp_spropparametersets()" (on the video 'subsession' object). This will give you a (ASCII) character string. You can then pass this to "parseSPropParameterSets()" (defined in the file "include/H264VideoRTPSource.hh"), to generate binary NAL units for your decoder.
(If you are receiving H.265 video, then you do the same thing, except that you have three separate configuration strings, that you get by calling "MediaSubsession::fmtp_spropvps()", "MediaSubsession::fmtp_spropsps()", and "MediaSubsession::fmtp_sproppps()". For each of these three strings, in turn, pass them to "parseSPropParameterSets()", then feed the resulting binary NAL unit to your decoder.)
大抵意思对h264或h265, 吸收到的buffer是一个 "NAL unit",需要注意的是这个NAL unit不带四字节起始码0x00000001,但是ffmpeg解码的时候需要在吸收buffer前添加起始码,否则ffmpeg就解码错误no frame。
  1. Block *block=new Block();block->esType=ES_VIDEO;block->codecType=CODEC_H264;block->frameType=frameType;block->pts=pts;block->dts=pts;uint8_t *receiveBufferAV = new uint8_t[frameSize + 4];receiveBufferAV[0] = 0;receiveBufferAV[1] = 0;receiveBufferAV[2] = 0;receiveBufferAV[3] = 1;memcpy(receiveBufferAV + 4, m_receiveBuffer, frameSize);block->buffer=receiveBufferAV;block->bufferSize=frameSize + 4;
复制代码
每个NAL unit的第一个字节是NAL头,通过NAL头可以分析出NAL unit的范例,对于h264:uint8_t nalType=(m_receiveBuffer[0] & 0x1f)。nalType=5表现当前NAL unit是IDR图像,nalType=1表现当前NAL unit是非IDR图像,nalType=7或8表现当前NAL unit是SPS大概PPS。
关于h264的格式分析详见:https://blog.csdn.net/caoshangpa/article/details/53019793?utm_source=blogxgwz3
SPS信息和PPS信息可用于初始化视频解码器,如前文所述,如果从SDP中未获取到,从这里也可以获取。
SPS中包罗了视频的宽高和帧率等信息,如何解码SPS获取分辨率和帧率可参考:https://blog.csdn.net/caoshangpa/article/details/53083410?utm_source=blogxgwz6
需要注意的是帧率并不是SPS中必须包罗的,因为有些流的帧率是可变的,解出来帧率值大概为0。
这里IDR图像一定是I帧,但是I帧不一定是IDR图像,关于I帧、P帧和B帧将在下篇文章中总结一下。
为了防止解码第一帧时出现马赛克,吸收时需要判断吸收到帧是否是IDR图像(注意不是判断I帧),如果是IDR图像,则开始解码。我的做法是SPS和PPS都吸收到且传给相识码器,再判断当前帧是否是IDR图像,如果是则开始解码当前帧和之后收到的IDR图像和非IDR图像,也就是说SPS和PPS只用处理处罚一次。大概如下:
0x00, 0x00, 0x00, 0x01, pps, 0x00, 0x00, 0x00, 0x01, sps, 0x00, 0x00, 0x00, 0x01, IDR frame...........
对于h265:uint8_t nalType=((m_receiveBuffer[0] & 0x7e)>>1),详细范例判断如下。
  1. uint8_t nalType=((m_receiveBuffer[0] & 0x7e)>>1);if(nalType == 32)//VPS{          }else if(nalType == 33)//SPS{             }else if(nalType == 34)//PPS{                }if(nalType==19 || nalType==20)//key frame{     m_hasKeyFrame=true;}if(((nalType>=1 && nalType=16 && nalTypecodecType=CODEC_AAC;            }            else if(strcmp(m_subsession.codecName(), "PCMA") == 0)            {                block->codecType=CODEC_G711A;            }            else if(strcmp(m_subsession.codecName(), "PCMU") == 0)            {                block->codecType=CODEC_G711U;            }            else if(strncmp(m_subsession.codecName(), "G726" , 4) == 0)            {                block->codecType=CODEC_G726;            }            block->esType=ES_AUDIO;            block->pts=pts;            block->dts=pts;            uint8_t *receiveBufferAV = new uint8_t[frameSize];            memcpy(receiveBufferAV, m_receiveBuffer, frameSize);            block->buffer=receiveBufferAV;            block->bufferSize=frameSize;        }    }
复制代码
原创不易,转载请标明出处:https://blog.csdn.net/caoshangpa/article/details/112063679

来源:https://blog.csdn.net/caoshangpa/article/details/112063679
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )