SDL中文论坛

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 3346|回复: 2
打印 上一主题 下一主题

[webrtc] 让webrtc解码rtsp流

[复制链接]

187

主题

346

帖子

2450

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2450
跳转到指定楼层
楼主
发表于 2018-10-15 20:36:22 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 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_。以下是它的构造函数。
    1. 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的血缘关系,下面在是上面的派生类。
  1. VideoDecoder
  2.   H264Decoder
  3.     H264DecoderImpl
复制代码

2.2 codecType和plType
  1. class VideoCodec {
  2.         VideoCodecType codecType;
  3.         unsigned char plType;
  4. };
  5. class VCMCodecDataBase {
  6. private:
  7.         VideoCodec receive_codec_;
  8.         std::unique_ptr<VCMGenericDecoder> ptr_decoder_;
  9.         DecoderMap dec_map_;
  10.         ExternalDecoderMap dec_external_map_;
  11. };
复制代码

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造成问题。
  1. int first_h264_payload_type_ = 200
  2. void trtspcapture::start_rtsp()
  3. {
  4.         recv_codecs_ = cricket::AssignPayloadTypesAndDefaultCodecs(encoder_factory_->GetSupportedFormats());

  5.         cricket::VideoCodec h264_codec;
  6.         for (std::vector<cricket::VideoCodec>::iterator it = recv_codecs_.begin(); it != recv_codecs_.end(); ++ it) {
  7.                 cricket::VideoCodec& codec = *it;
  8.                 if (codec.name == cricket::kH264CodecName) {
  9.                         codec.id = first_h264_payload_type_;
  10.                         h264_codec = codec;
  11.                         break;
  12.                 }
  13.         }
  14.         h264_codec.id = first_h264_payload_type_ + 1;
  15.         h264_codec.SetParam("rtsp", "ch1");
  16.         recv_codecs_.push_back(h264_codec);
  17.         ......
  18. }
复制代码
以上是“间接”让一个plType(h264=100)对应两个解码器实例。基本思路是1)找出想要多路的那个cricket::VideoCodec,克隆一个,并修改它的plType。2)为方便记住新plType,以及原有的那plType,顺便修改原来plType,让它们是连续值。3)修改新增cricket::VideoCodec的Param,这么做的原因是要让由它生成的SdpVideoFormat和原来不一样。
  1. webrtc::SdpVideoFormat video_format(codec.name, codec.params)
复制代码


四、plType在remote、local中的匹配过程

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。
回复

使用道具 举报

187

主题

346

帖子

2450

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2450
沙发
 楼主| 发表于 2018-10-21 11:43:21 | 只看该作者

MediaCodec解码h264流

本帖最后由 ancientcc 于 2018-10-21 11:48 编辑

1、让webrtc能搜出这个MediaCodec解码器
MediaCodecVideoDecoder.java的findDecoder负责搜索系统内MediaCodec支持的解码器。对匹配条件,除了MIME,还有解码器名称,具体是名称必须满足要求的前缀。对MIME="video/avc",supportedH264HwCodecPrefixes列出了内置支持的前缀。
  1. String[] supportedH264HwCodecPrefixes = {"OMX.qcom.", "OMX.Intel.", "OMX.Exynos."};
复制代码

一些android开发板解码器的名称前缀可能不在这里头,会使得webrtc枚举不到这解码器。举个例子,firefly的AIO-rk3288就不在这里,它的名称前缀是“OMX.rk.”,就须要修改supportedH264HwCodecPrefixes。
  1. 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。
  1. void VCMDecodedFrameCallback::Decoded(VideoFrame& decodedImage, ...)
  2. {
  3.         _receiveCallback->FrameToRender(decodedImage, qp, webrtc::VideoContentType::UNSPECIFIED);
  4.         return;
  5. }
复制代码

回复 支持 反对

使用道具 举报

187

主题

346

帖子

2450

积分

版主

Rank: 7Rank: 7Rank: 7

积分
2450
板凳
 楼主| 发表于 2019-4-13 09:51:08 | 只看该作者

MediaCodec编码h264流

1、设置编码码率
  1. <webrtc>/media/engine/webrtcvideoengine.cc
  2. std::vector<webrtc::VideoStream> EncoderStreamFactory::CreateEncoderStreams(
  3.     int width,
  4.     int height,
  5.     const webrtc::VideoEncoderConfig& encoder_config)
  6. {
  7.   ...
  8.   int max_bitrate_bps =
  9.       (encoder_config.max_bitrate_bps > 0)
  10.           ? encoder_config.max_bitrate_bps
  11.           : GetMaxDefaultVideoBitrateKbps(width, height) * 1000;
  12.   ...
  13. }
复制代码
如果encoder_config.max_bitrate_bps <= 0,会用webrtc内置函数GetMaxDefaultVideoBitrateKbps计算码率。对于想自定义场合,方法是让encoder_config.max_bitrate_bps > 0,单位bps,像4000000表示4Mbps。encoder_config哪来的?app调用webrtc::VideoStreamEncoder::ConfigureEncoder(..)传入的第一个参数。
回复 支持 反对

使用道具 举报

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

本版积分规则

Archiver|手机版|小黑屋|丽谷软件|libsdl.cn

GMT+8, 2024-5-20 01:52 , Processed in 0.070156 second(s), 22 queries .

Powered by Discuz! X3.3

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表