注:本文内容基于WebRTC M76分支,部分代码细节可能和后续WebRTC代码有差异。但笔者查看了4324(M88),基本上差异不大,同样适用,但再往后的版本就无法保证了。
首先说一句,simulcast的支持,不仅仅是客户端修改就可以了,服务器端也需要修改。Licode、mediasoup、janus这些比较流行的WebRTC Server方案都是支持simulcast的。这里只描述了客户端的修改,不涉及服务器端。
如果你对simulcast所涉及的两种sdp格式不太熟悉,可以先看看这篇文章:Simulcast and Janus: what’s new? (and where’s my SSRC?) ,这里面非常详细地给出了simulcast的两种sdp显示形态。
下面是在Chrome中使用js推送2个分辨率的sdp示例:
本地SDP:
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116
此处省略大幅无关内容
a=rid:r0 send
a=rid:r1 send
a=simulcast:send r0;r1
远端SDP:
m=video 7 RTP/SAVPF 125 107
此处省略大幅无关内容
a=rid:r0 recv
a=rid:r1 recv
a=simulcast:recv r0;r1
在Web里通过js实现simulcast是非常简单方便的,Chrome自身已经支持了。但是对于Android native SDK来说,就不是那么容易了。甚至我发现Android native SDK中是没有simulcast encoder adapter的调用的。如上图中看到的SimulcastEncoderAdapter,对应C++代码是:media\engine\simulcast_encoder_adapter.cc,而在webrtc的sdk目录下搜索 SimulcastEncoderAdapter 是找不到任何调用的。所以,让Android native SDK支持simulcast,主要是把WebRTC代码中的 SimulcastEncoderAdapter 这个类利用起来。
simulcast sdp的产生
根据上面提到的文章中描述的那样,在Android中也可以使用类似的方法来产生。
如果你使用的是unified plan格式的sdp,产生simulcast的关键代码如下:
List<RtpParameters.Encoding> encodings = new ArrayList<RtpParameters.Encoding>();
encodings.add(new RtpParameters.Encoding("main_stream", true, 1.0));
// 2.0 表示第2个分辨率取主分辨率的一半,如果是4.0那就是1/4
encodings.add(new RtpParameters.Encoding("second_stream", true, 2.0));
RtpTransceiver.RtpTransceiverInit init = new RtpTransceiver.RtpTransceiverInit(
RtpTransceiver.RtpTransceiverDirection.SEND_RECV, Collections.emptyList(), encodings);
// pc是PeerConnection
RtpTransceiver transceiver = pc.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, init);
在webrtc M76中,RtpParameters.Encoding是私有的,我们需要把它变成public,代码位于:\sdk\android\api\org\webrtc\RtpParameters.java。不过如果你使用的是比较新的webrtc版本,这个不需要做。因为在 2019.12.3这天,名为Amit Hilbuch的作者已经把它改成了public了(SHA-1: e725fdbcc11abb82ad64b7e90d6aa5d576e18e32)
如果你使用的是plan-b格式的sdp,关键代码如下:
MediaConstraints sdpConstraints = new MediaConstraints();
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNumSimulcastLayers", Integer.toString(2)));
pc.createOffer(SdpObserver, sdpConstraints);
不过比较推荐使用unified plan,这个与web端保持了一致的方式,方便对比调试,也是更靠近webrtc新标准的方式。
引入SimulcastEncoderAdapter
实现simulcast比较关键的是把 webrtc的SimulcastEncoderAdapter这个类拿过来用一下。让我们先来简单回顾一下Android上视频编码器的创建时序。
假设我们调用的是 WebRTC Android native SDK 的默认编码器工厂 DefaultVideoEncoderFactory 来创建编码器,根据 sdk\android\api\org\webrtc\DefaultVideoEncoderFactory.java 的实现,可能通过 HardwareVideoEncoderFactory 创建硬编码器,也可能通过 SoftwareVideoEncoderFactory 创建软编码器,也可能是软硬结合的一个wrapper:VideoEncoderFallback。然后在构造PeerConnectionFactory.builder对象的时候,将编码器工厂传过去(详情见PeerConnectionFactory.java的实现)即可。之后会通过JNI层层调用到native层,顺序是这样:
- JNI_PeerConnectionFactory_CreatePeerConnectionFactory (peer_connection_factory.cc)
- CreateVideoEncoderFactory (video.cc)
- VideoEncoderFactoryWrapper ctor (video_encoder_factory_wrapper.cc)
其中,VideoEncoderFactoryWrapper::CreateVideoEncoder()的实现是这样的:
std::unique_ptr<VideoEncoder> VideoEncoderFactoryWrapper::CreateVideoEncoder(
const SdpVideoFormat& format) {
JNIEnv* jni = AttachCurrentThreadIfNeeded();
ScopedJavaLocalRef<jobject> j_codec_info =
SdpVideoFormatToVideoCodecInfo(jni, format);
ScopedJavaLocalRef<jobject> encoder = Java_VideoEncoderFactory_createEncoder(
jni, encoder_factory_, j_codec_info);
if (!encoder.obj())
return nullptr;
return JavaToNativeVideoEncoder(jni, encoder);
}
它会调用相应的VideoEncoderFatory的createEncoder来完成编码器的创建。例如,调用 HardwareVideoEncoderFactory.createEncoder() 来完成硬编码器的创建。调用SoftwareVideoEncoderFactory.createEncoder()来完成libvp8/vp9(默认只支持VPX,你可以手动添加H264软编码器)编码器的创建。
因此,我们只要在这里进行“偷梁换柱”,把Java_VideoEncoderFactory_createEncoder换掉,直接创建一个 SimulcastEncoderAdapter(它也继承于VideoEncoder)对象返回即可。而 SimulcastEncoderAdapter 就是我们刚才在Chrome浏览器看到的那个Simulcast的实现,里面的大部分代码基本上都是拿来即用的。
下面是经过改造以后,从VideoStreamEncoder (video\video_stream_encoder.cc) 中发起的,创建、设置、初始化编码器的调用时序图:
从上图中,可以基本上看到SimulcastEncoderAdapter作为一个编码器“中介”所起到的作用。
以上就是针对Android native SDK中增加Simulcast推流2个或者更多分辨率的实现思路。