SDL中文论坛
标题:
让webrtc解码rtsp流
[打印本页]
作者:
ancientcc
时间:
2018-10-15 20:36
标题:
让webrtc解码rtsp流
本帖最后由 ancientcc 于 2020-10-12 14:16 编辑
webrtc如何解码rtsp,有种方案是在服务器把rtsp转成webrtc要求的格式,然后通过网络传给客户端,这里描述的方案是不通过服务器,客户端按收rtsp流,然后用通过webrtc解码,并渲染。
流程涉及到几个重要步骤及相关技术。
从网上收rtsp,并拆包成rtp。所用技术:live555。
解码rtsp收到的视频帧。所用技术:webrtc中的解码模块。
渲染解码出的数据。所用技术:从webrtc解码输出中取VideoFrame,即基于rtc::VideoSinkInterface::OnFrame获得解码帧。
当中较难的是第二步,要写出这一步的逻辑首先要清楚webrtc的解码流程。
一、webrtc的解码流程
WebRtcVideoChannel::WebRtcVideoChannel中生成std::vector<VideoCodecSettings> recv_codecs;
枚举recv_codecs,每个单元生成std::unique_ptr<webrtc::VideoDecoder>。
new_decoder = decoder_factory_->CreateVideoDecoder(webrtc::SdpVideoFormat(recv_codec.codec.name, recv_codec.codec.params));
这个时候会生成H264DecoderImpl。生成的webrtc::VideoDecoder放在config_.decoders中。
VideoReceiveStream::Start()时,枚举config_.decoders中的每个decoder,把decoder注册到video_receiver_的dec_external_map_和dec_map_
注册到dec_external_map_:video_receiver_.RegisterExternalDecoder(decoder.decoder, decoder.payload_type);
注册到dec_map_:video_receiver_.RegisterReceiveCodec(&codec, num_cpu_cores_, false)。当中codec是以decoder为参数,调用下面的语句生成。VideoCodec codec = CreateDecoderVideoCodec(decoder);
VideoReceiver:
ecode在解码一帧前会调用_codecDataBase.GetDecoder去获取解码器,_codecDataBase类型是VCMCodecDataBase。于是可知,GetDecode即VCMCodecDataBase::GetDecoder。VCMCodecDataBase内部,用于存储h264Decoder的是一个类型是 std::unique_ptr<VCMGenericDecoder>变量ptr_decoder_,VCMGenericDecoder不和其它类有血缘关系,是单独存在。它和H264DecoderImpl发生关系是它内部有个类型是std::unique_ptr<VideoDecoder>的变量decoder_。以下是它的构造函数。
VCMGenericDecoder::VCMGenericDecoder(VideoDecoder* decoder, bool isExternal);
复制代码
那ptr_decoder_是何时构建的?第一次调用GetDecoder时ptr_decoder_总是null,它使用第3步得到的dec_external_map_,如果dec_external_map_不支持解码该格式则使用dec_map_。对h264,dec_external_map_总是支持的,于是构建VCMGenericDecoder使用dec_external_map_。
_codecDataBase.GetDecoder在构建出ptr_decoder_后就会调用H264DecoderImpl::InitDecode。综上所述,H264DecoderImpl的InitDecode、Decode都是在DecodingThread被调用。
二、解码流程中涉及到的几种类型
2.1 VideoDecoder
以下是H264DecoderImpl的血缘关系,下面在是上面的派生类。
VideoDecoder
H264Decoder
H264DecoderImpl
复制代码
2.2 codecType和plType
class VideoCodec {
VideoCodecType codecType;
unsigned char plType;
};
class VCMCodecDataBase {
private:
VideoCodec receive_codec_;
std::unique_ptr<VCMGenericDecoder> ptr_decoder_;
DecoderMap dec_map_;
ExternalDecoderMap dec_external_map_;
};
复制代码
VideoCodec中有两种type,codecType是从0开始的enum值,像kVideoCodecVP8、kVideoCodecH264。plType表示的是网上收到的数据类型,像100表示h264。VCMCodecDataBase要解码第一帧时,receive_codec_中的plType值是初始值,即0,发比较plType和网络数据的payload_type(100),发现不一样,于是就去找到解出该payload_type的解码器,并构建出ptr_decoder_。
给出payload_type,VCMCodecDataBase是如何创建ptr_decoder_?dec_map_、dec_external_map_的first就是payload_type。根据second创建出ptr_decoder_。创建ptr_decoder_,调用InitDecode后,会把dec_map_对应的VideoCodec赋值给VCMCodecDataBase的receive_codec_,于是解码下一帧时,receive_codec_中的plType等于网络数据的payload_type(100),可直接使用ptr_decoder_了。
可以这么认为,plType表示的是收到的数据的编码类型,有着标准定义。VideoCodecType是webrtc模块的自个定义。或许吧,将来会有另一外一套plType定义,但到了webrtc内部,它都统一到VideoCodecType。
三、生成同一编码格式的多个实例
webrtc内置了这么条规则:一个plType只能对应一个解码器实例。参考构建decoder实例过程,像dec_map_、dec_external_map_。这会给须要解码多路同一格式视频的app造成问题。
int first_h264_payload_type_ = 200
void trtspcapture::start_rtsp()
{
recv_codecs_ = cricket::AssignPayloadTypesAndDefaultCodecs(encoder_factory_->GetSupportedFormats());
cricket::VideoCodec h264_codec;
for (std::vector<cricket::VideoCodec>::iterator it = recv_codecs_.begin(); it != recv_codecs_.end(); ++ it) {
cricket::VideoCodec& codec = *it;
if (codec.name == cricket::kH264CodecName) {
codec.id = first_h264_payload_type_;
h264_codec = codec;
break;
}
}
h264_codec.id = first_h264_payload_type_ + 1;
h264_codec.SetParam("rtsp", "ch1");
recv_codecs_.push_back(h264_codec);
......
}
复制代码
以上是“间接”让一个plType(h264=100)对应两个解码器实例。基本思路是1)找出想要多路的那个cricket::VideoCodec,克隆一个,并修改它的plType。2)为方便记住新plType,以及原有的那plType,顺便修改原来plType,让它们是连续值。3)修改新增cricket::VideoCodec的Param,这么做的原因是要让由它生成的SdpVideoFormat和原来不一样。
webrtc::SdpVideoFormat video_format(codec.name, codec.params)
复制代码
四、plType在remote、local中的匹配过程
(, 下载次数: 1995)
上传
点击文件名下载附件
4.1 remote流
如何计算收到的payloadType?这个payloadType没有出现在编码器发送的码流,而在执行video_receiver_.Decode之前,会把收到的此帧(data, len)封装成一个RtspFrameObject对象(VCMEncodedFrame是它父类),在封装时“主观”给的一个值,对应_payloadType字段。目前这个值强制是200、201(at_=1)、依此类推。
4.2 local
比较是双方的,以上是remote“发来”的payloadType,而且是个“主观”值,本地如何能确保这个“主观值”有对应的解码器。这个看dec_map_。local在枚举出recv_codecs(std::vector<cricket::VideoCodec>)后,发现该VideoCodec的name是H264,则强制把这个VideoCodec的playloadType改为200。至此,以200在dec_map_搜出的正是第一个h264解码器。
这里有个问题,当系统枚举出多个name是H264的VideoCodec,200对应的是一个第一个h264。因为没有remote流与之匹配的payloadType,第二个或后面的其实不会被使用。实测下来,多个h264时,它们只是profile-level-id不同,举个例子,第一个是42001f,一个是42e01f,那这个值如何改变VideoDecoder行为?——至少在windows,好像没影响。而在iOS、Android,都是只枚举出一个h264。
如果要做完善,local搜到出多个h264时,需跟据remote流中的profile-level-id去搜recv_codecs,找到“最能解码”它的profile-level-id的VideoCodec。
作者:
ancientcc
时间:
2018-10-21 11:43
标题:
MediaCodec解码h264流
本帖最后由 ancientcc 于 2018-10-21 11:48 编辑
1、让webrtc能搜出这个MediaCodec解码器
MediaCodecVideoDecoder.java的findDecoder负责搜索系统内MediaCodec支持的解码器。对匹配条件,除了MIME,还有解码器名称,具体是名称必须满足要求的前缀。对MIME="video/avc",supportedH264HwCodecPrefixes列出了内置支持的前缀。
String[] supportedH264HwCodecPrefixes = {"OMX.qcom.", "OMX.Intel.", "OMX.Exynos."};
复制代码
一些android开发板解码器的名称前缀可能不在这里头,会使得webrtc枚举不到这解码器。举个例子,firefly的AIO-rk3288就不在这里,它的名称前缀是“OMX.rk.”,就须要修改supportedH264HwCodecPrefixes。
String[] supportedH264HwCodecPrefixes = {"OMX.qcom.", "OMX.Intel.", "OMX.Exynos.", "OMX.rk."};
复制代码
即使app只希望解码,但最初找cricket::VideoCodec时是用编码器找的。MediaCodecVideoEncoder.java的findHwEncoder执行搜索编码器,它在匹配上也有名称前缀问题,也须按findHwEncoder要求的去改。。
2、增大kMaxPendingFramesH264值
kMaxPendingFramesH264用于指示允许“悬”着未解码帧的最大帧数,一旦超过这帧数,androidmediadecoder.cc会复位MediaCodec模块。要理解这个,先说下MediaCodec解码机制。
webrtc调用MediaCodec解码使用同步方式,在DecodingThread内,解码具体一帧没为MediaCodec开新的线程,那它怎么“估算”调用queueInputBuffer、dequeueOutputBuffer之间间隔?把编码数据放入inputbuffer后,调用queueInputBuffer告知MediaCodec,这inputbuffer可用于解码了。MediaCodec解这一帧须要时间,webrtc不可能严格预知这时间。——它采用的方法是调用dequeueOutputBuffer时设置“最优”溢出时间,即参数timeoutUs。运气好时,解码花费时间恰好“等于”timeoutUs,不花费额外开销。运气不好时,timeoutUs小于解码须要时间,没解出该帧,dequeueOutputBuffer返回INFO_TRY_AGAIN_LATER。那是不是app永远拿不到这帧了,不是的。下一次调用dequeueOutputBuffer时,它首先会取到该帧!也就是说,此次queueInputBuffer了B帧,但有可能dequeueOutputBuffer到上次的A帧。
webtc这逻辑会不会有bug?一次queueInputBuffer对应一个dequeueOutputBuffer,即一次queueInputBuffer只会得到一解码帧,如果outputBuffers因意外累积了多帧……AIO-rk3288上,outputBuffers长度是6。
回到kMaxPendingFramesH264。webrtc统计queueInputBuffer次数和dequeueOutputBuffer成功解出帧的次数,它们之间差值就是“悬”着未解码帧的帧数,而这数目一旦超过门限,就认为得复位MediaCodec了。kMaxPendingFramesH264存储着这个门限值,官方设的是4,即“悬”着未解码帧的帧数超过4帧,就要复位MediaCodec。为什么要增大kMaxPendingFramesH264?实际使用时遇到了些码流,它初始时有一段不能解,像2秒,但后面能正常解码。为支持这种码流,就要适当增大kMaxPendingFramesH264。我改到200,对fps=30来说,已容许6秒没解出有效帧。
3、VCMDecodedFrameCallback:
ecoded
解码器成功解码一帧后,它会调用VCMDecodedFrameCallback:
ecoded,参数decodedImage就是解出的图像。为什么要改这函数?——它要基于多个时间戳进行计算,一旦算出认为这帧“来得不是时候”,就会丢掉该帧,导致app层的OnFrame收不到这帧。实际使用时,一些rtsp设备可能码流不平稳,提供时间戳严重错乱,修改VCMDecodedFrameCallback:
ecoded,使得让会算出“来得不是时候”帧也上传到OnFrame。以下是一种修改法,一进入就调用FrameToRender。
void VCMDecodedFrameCallback::Decoded(VideoFrame& decodedImage, ...)
{
_receiveCallback->FrameToRender(decodedImage, qp, webrtc::VideoContentType::UNSPECIFIED);
return;
}
复制代码
作者:
ancientcc
时间:
2019-4-13 09:51
标题:
MediaCodec编码h264流
1、设置编码码率
<webrtc>/media/engine/webrtcvideoengine.cc
std::vector<webrtc::VideoStream> EncoderStreamFactory::CreateEncoderStreams(
int width,
int height,
const webrtc::VideoEncoderConfig& encoder_config)
{
...
int max_bitrate_bps =
(encoder_config.max_bitrate_bps > 0)
? encoder_config.max_bitrate_bps
: GetMaxDefaultVideoBitrateKbps(width, height) * 1000;
...
}
复制代码
如果encoder_config.max_bitrate_bps <= 0,会用webrtc内置函数GetMaxDefaultVideoBitrateKbps计算码率。对于想自定义场合,方法是让encoder_config.max_bitrate_bps > 0,单位bps,像4000000表示4Mbps。encoder_config哪来的?app调用webrtc::VideoStreamEncoder::ConfigureEncoder(..)传入的第一个参数。
欢迎光临 SDL中文论坛 (http://www.libsdl.cn/bbs/)
Powered by Discuz! X3.3