1.参考文献资料
- WebRtc所需服务器搭建流程
- WebRtc所需服务器搭建流程备用参考地址 2
- 开启WebRTC的一些"试用特性" (FieldTrials)
- WebRtc官网
- turn服务器Git仓库
- 官方WebRtc安卓端参考Demo
- 在WebRTC中如何控制传输速率呢?
- 关于H.264的码率,720P、1080P输出比特率设置
- webRtc打洞:p2p链接建立过程
- webrtc M66 华为手机h264硬编解码不支持问题
2.技术实现方案详情
WebRtc介绍
- WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
- WebRtc起初是由Global IP公司创造出来的,后来被Google收购并将这门技术改名为WebRtc。
- WebRTC是一项实时通信领域革命性的技术,在实时音视频通信领域已经产生了深远的影响。
- WebRtc主要被用于音视频即时会议通话。它不仅仅只支持Web端,它还支持Android、Ios、Pc等平台。现有的音视频通话、音视频多人会议都离不开WebRtc的使用。
- WebRtc SDK内集成了音频的回音消除、噪音一致、自动增益控制、高通滤波器等功能。
在webrtc出现之前,音视频通话是通过服务器来进行转发的。
存在以下问题:
1、延时太高,因为多了一层的转发操作
2、服务器承载有限
WebRtc的出现则很有效的解决了这些问题的。
WebRtc多方通信架构:
WebRTC应用------无论是Web端还是Native端------通常都会有3种运行模式:P2P(Peer to Peer)模式,SFU(Selective Forwarding Unit)模式和MCU(Multi-point Control Unit)模式。
- Mesh 方案,即多个终端之间两两进行连接,形成一个网状结构。比如 A、B、C 三个终端进行多对多通信,每个客户端都和当前网络里的其他两个客户端均建立了独立的链接。
- MCU(Multipoint Conferencing Unit)方案,该方案由一个服务器和多个终端组成一个星形结构。各终端将自己要共享的音视频流发送给服务器,服务器端会将在同一个房间中的所有终端的音视频流进行混合,最终生成一个混合后的音视频流再发给各个终端,这样各终端就可以看到 / 听到其他终端的音视频了。实际上服务器端就是一个音视频混合器,这种方案服务器的压力会非常大。
- SFU(Selective Forwarding Unit)方案,该方案也是由一个服务器和多个终端组成,但与 MCU 不同的是,SFU 不对音视频进行混流,收到某个终端共享的音视频流后,就直接将该音视频流转发给房间内的其他终端。它实际上就是一个音视频路由转发器。
MCU和SFU都是经过一个中转服务器来转发数据:
在WebRtc中,两两客户端之间会有一个peer链接,里面可以推送MediaStream流,可以往MediaStream中添加视频轨、音频轨。如两个客户端之间各自在本地配置了一条视频轨和一条音频轨。那么每个客户端都会有四个通道。分别是:视频流下发、视频流上行、音频流上行、音频流下发。
WebRtc基本组成
Turn服务器
turn服务器的作用是用于打洞,也叫网络net地址穿透。可以直接使用官方提供的服务器包部署使用即可。不需要自己手写代码。
大致规则如下:需要建立链接的两个客户端,他们都必须能连接上我们的turn服务器(因为这两个客户端可以访问我们的turn服务器,那么就说明他们两个客户端是一定能够通讯的),turn服务器通过比对两个客户端的网络地址,给需要建立链接的两个客户端计算出最短的网络链接路径(即绕过尽可能少的路由器)。
网络穿透的大致原理则是:两个客户端通过服务器交换了对方的路由地址后,各自主动给对方的路由发送一条信息,这样可以使客户端当前的路由记录下目标的路由地址到路由表中,记录以后,对方的客户端给当前客户端发送信息时,信息就不会被客户端的父路由器给拒收掉。
房间服务器
这部分需要自己手写代码,部署一个房间服务器,所有客户端都与该服务器建立socket链接。根据客户端与服务器之间定义的协议来为房间内的客户端进行一个sdp的交换工作
集成了WebRtc SDK的客户端
- 客户端负责集成WebRtc SDK,使用SDK的Api给WebRtc配置我们的Turn服务器。
与房间服务器建立长链接,借助房间服务器来给其他加入通话房间的设备交换SDP、ICE后,即可完成p2p链接的建立
PS:房间服务器和turn服务只负责引导客户端建立p2p链接,链接建议完成后,只要两个设备还处于一个局域网中,即使断开于turn服务、房间服务器的链接,通信也依然能够建立。
WebRtc Android端接入流程
1、引入WebRtc依赖库(也可以自己去拿WebRtc源码来进行编译AAR包,编译极慢,要好几天)
arduino
/** 引入webRtc **/
implementation 'org.webrtc:google-webrtc:1.0.+'
/** 引入webSocket,用于链接房间服务器 **/
api 'org.java-websocket:Java-WebSocket:1.3.9'
2、创建WebSocketManager,负责建立与房间服务器的链接(这部分的实现是与后端一起定义的,实现会不太一样,不贴代码了),借助房间服务器来交换SDP、ICE。
3、创建PeerConnectionManager类,此类负责的则是WebRtc相关的通讯协议建立,这部分详细讲述:
3.1、在此类初始化时,进行turn服务器地址的配置。
ini
//定义一个存储IceServer的集合
private ArrayList<PeerConnection.IceServer> mICEServers;
//我们的turn服务器的地址 turn:IP?transport=udp
String turnServerIp="turn:"+Config.ServerIP+"?transport=udp";
PeerConnection.IceServer iceServer = PeerConnection.IceServer
.builder(turnServerIp)
.setUsername("admin")
.setPassword("123456")
.createIceServer();
mICEServers.add(iceServer);
//在Peer链接创建时会将上面存储的turn服务器地址添加进WebRtc的配置信息中
private PeerConnection createPeerConnection(){
if (mPeerConnectionFactory == null) {
mPeerConnectionFactory=createConnectionFactory();
}
//创建PeerConnection mICEServers 服务器可以有多个,如果第一个失败则会找下一个
PeerConnection.RTCConfiguration rtcConfiguration =
new PeerConnection.RTCConfiguration(mICEServers);
return mPeerConnectionFactory.createPeerConnection(rtcConfiguration,this);
}
3.2、创建PeerConnectionFactory(通常只需要一个PeerConnectionFactory,借助PeerConnectionFactory来创建每个要链接的终端的链接Peer)
scss
private PeerConnectionFactory createConnectionFactory() {
/**
* 对PeerConnectionFactory 进行全局初始化
*/
PeerConnectionFactory.initialize(PeerConnectionFactory
.InitializationOptions
.builder(mActivity.getApplicationContext())
.setFieldTrials("WebRTC-H264Simulcast/Enabled/")
.createInitializationOptions());
//编码 分为 音频编码 和 视频编码
//解码 分为 音频解码 和 视频解码
VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), true, true);
VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(options)
// .setAudioDeviceModule(JavaAudioDeviceModule.builder(mActivity).createAudioDeviceModule())
.setVideoDecoderFactory(videoDecoderFactory)
.setVideoEncoderFactory(videoEncoderFactory)
.createPeerConnectionFactory();
return peerConnectionFactory;
}
- PeerConnectionFactory.initialize()用于WebRtc的全局初始化
- .setFieldTrials("WebRTC-H264Simulcast/Enabled/") 为配置WebRtc的实验特性,当前配置的"WebRTC-H264Simulcast/Enabled/"意为启用 H264Simulcast 特性,该特性效果为,在推流给多个设备时,编码端会编码多个不同码率、质量的视频码流,根据对方设备的性能、网络来发送对应质量的画面。即:编码端编码多种不同画面质量的h264视频流,WebRtc会根据接收端的性能、网络状况来发送对应画面质量的码流。以此来提高视频实时性。减少设备性能差距导致的视频帧解析耗时不一致问题。
- WebRtc的mRootEglBase创建:mRootEglBase=EglBase.create();
- new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), true, true);传入的参数依顺序是:WebRtc的全局上下文(与我们android的上下文不是同一个)、是否启用VP8编码支持、是否启用H264编码支持。
3.3、创建本地的流
mMediaStream是我们本地生成的总流,里面有多个轨道,如音频轨道、视频轨道。
通过调用addTrack将我们的音视频轨道添加进去总流中
csharp
private void createLoaclStream() {
//添加一个总流
mMediaStream=mPeerConnectionFactory.createLocalMediaStream("ARDAMS");//需要与后面的ARDAMSa0前缀一致
// MediaConstraints audioConstraints = createAudioConstraints();
// AudioSource audioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
// //音频是数据源.创建一个音频轨道
// /**
// * id规则 ARDAMS + a/v + 轨道号 a:audio音频 v:video视频
// */
// AudioTrack audioTrack = mPeerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
// mMediaStream.addTrack(audioTrack);
// //音频轨道创建成功 添加音频轨道的数据源
if (videoEnable){
//录屏的
//摄像头分前置/后置;camera1/camera2
//videoCapturer 数据源
createVideoCapturer("screencast");
}
}
3.4、创建音频轨道,并加入总流:
java
// googEchoCancellation 回音消除
public static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
// googNoiseSuppression 噪声抑制
public static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";
// googAutoGainControl 自动增益控制
public static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl";
// googHighpassFilter 高通滤波器
public static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
//音频轨道的功能开关
public MediaConstraints createAudioConstraints(){
//类似于一个hashmap
MediaConstraints audioConstraints = new MediaConstraints();
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_ECHO_CANCELLATION_CONSTRAINT,"true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_NOISE_SUPPRESSION_CONSTRAINT,"true"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT,"false"));
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(Config.AUDIO_HIGH_PASS_FILTER_CONSTRAINT,"true"));
return audioConstraints;
}
MediaConstraints存储的是WebRtc中定义的一些功能、规则
通过mPeerConnectionFactory.createAudioSource来创建AudioSource
再通过创建的mPeerConnectionFactory,来创建我们的音频轨
之后将音频轨加入到我们本地的总流中
ini
MediaConstraints audioConstraints = createAudioConstraints();
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
//音频是数据源.创建一个音频轨道
/**
* id规则 ARDAMS + a/v + 轨道号 a:audio音频 v:video视频
*/
AudioTrack audioTrack = mPeerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
mMediaStream.addTrack(audioTrack);
3.5创建视频轨道,并添加进总流:
WebRtc自带了摄像头、屏幕共享等视频源作为码流(以下都是):
a.流程基本是一样,使用官方的VideoCapturer的子类,创建对象得到VideoCapture。
b.借助mPeerConnectionFactory.createVideoSource 创建VideoSource
c.将VideoCapture绑定到VideoSource中
d.调用videoCapturer.startCapture进行视频流的开启录制
e.调用mPeerConnectionFactory.createVideoTrack,将生成的VideoSource作为入参,得到VideoTrack视频轨道
f.将得到的视频轨道Videotrack添加进自己本地的总流即可
java
VideoCapturer videoCapturer = null;
mActivity.startScreenRecord(new ScreenBroadcastListener() {
@Override
public void getScreenIntent(Intent intent) {
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
ScreenCapturerAndroid videoCapturer = new ScreenCapturerAndroid(intent, new MediaProjection.Callback() {
@Override
public void onStop() {
super.onStop();
}
});
// videoCapturer.getMediaProjection()
//videoSource 实例化
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
//将videoCapturer绑定到videoSource中
SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(Thread.currentThread().getName(), mRootEglBase.getEglBaseContext());
videoCapturer.initialize(surfaceTextureHelper,mActivity,videoSource.getCapturerObserver());
//设置预览
videoCapturer.startCapture(1280, 720,0);//参数按顺序: 宽度、高度、关键帧间隔(多久一个I帧)
//视频轨道关联
VideoTrack videoTrack = mPeerConnectionFactory.createVideoTrack("ARDAMSv0",videoSource);
mMediaStream.addTrack(videoTrack);
// if (mActivity != null) {
// //播放自己的流
// mActivity.onSetLocalStream(mMediaStream,myId);
// }
}
}
});
ini
if (Camera2Enumerator.isSupported(mActivity)){
Camera2Enumerator camera2Enumerator = new Camera2Enumerator(mActivity);
videoCapturer=createCameraCapture(camera2Enumerator);
}else{
Camera1Enumerator camera1Enumerator = new Camera1Enumerator(true);
videoCapturer = createCameraCapture(camera1Enumerator);
}
/**
* 获取前置摄像头 如果没有则使用后置摄像头
* @param cameraEnumerator
* @return null:没有找到摄像头
*/
private VideoCapturer createCameraCapture(CameraEnumerator cameraEnumerator) {
String[] deviceNames = cameraEnumerator.getDeviceNames();
for (String deviceName : deviceNames) {
if (cameraEnumerator.isFrontFacing(deviceName)){
//前置摄像头
VideoCapturer videoCapturer=cameraEnumerator.createCapturer(deviceName, this);
if (videoCapturer!=null){
return videoCapturer;
}
}
}
for (String deviceName : deviceNames) {
if (!cameraEnumerator.isFrontFacing(deviceName)) {
VideoCapturer videoCapturer=cameraEnumerator.createCapturer(deviceName,this);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}
3.6、创建Peer对象,用来存储当前客户端所链接的客户端的PC对象及链接处理。
Peer实例对象能存在多个,一个Peer实例对象就代表与一个设备的链接。
在创建Peer实例对象时,在其构造方法中会去创建一个PeerConnection链接。在创建时将我们前面的turn服务器地址给配置了进去。
此时这个链接只是被创建了,还需要进行本地sdp、远端sdp的配置与ICE信令的交换后才能真正的与目标设备链接成功。
Peer对象的创建是通过房间服务器,在进入房间时、以及新设备加入房间后会得到新的设备ID列表,并根据ID数量创建对应数量的peer空链接。
typescript
private class Peer implements SdpObserver, PeerConnection.Observer{
private PeerConnection pc;
private String targetSocketId;
public Peer(String targetSocketId) {
this.targetSocketId = targetSocketId;
pc = createPeerConnection();
}
private PeerConnection createPeerConnection(){
if (mPeerConnectionFactory == null) {
mPeerConnectionFactory=createConnectionFactory();
}
//创建PeerConnection
PeerConnection.RTCConfiguration rtcConfiguration = new PeerConnection.RTCConfiguration(mICEServers);//mICEServers 服务器可以有多个,如果第一个失败则会找下一个
return mPeerConnectionFactory.createPeerConnection(rtcConfiguration,this);
}
}
3.7、将我们本地的视频流绑定到我们创建的PeerConnection对象中。
下方代码mConnectionPeerDic参数负责存储我们创建的所有PeerConnection对象,通过遍历mConnectionPeerDic来给每一个链接添加我们的本地总流进去,这样在建立连接后,远端的客户端就能够接收到本地总流的内容。
scss
private void addLocalStreamsPushTwoOtherDevice() {
for (Map.Entry<String, Peer> entry : mConnectionPeerDic.entrySet()) {
if (mMediaStream == null && mIsSpeaker) {
createLoaclStream();
}
entry.getValue().pc.addStream(mMediaStream);
}
}
3.8、因为推流涉及到多个流程。所以分为主叫端和被叫端来讲。房间服务器的通信部分则尽可能的省略代码。
3.8.1、主叫端创建主叫邀请
csharp
/**
* 发送hello
*/
private void createOffers() {
for (Map.Entry<String, Peer> entry : mConnectionPeerDic.entrySet()) {
//此时是主叫
role=Role.Caller;
Peer peer = entry.getValue();
//创建要发送给给其他客户端的打招呼消息,用于让当前路由存储对方设备的地址到路由表中
peer.pc.createOffer(peer,offerOrAnswerConstraint());//获取本地到其他客户端路由的sdp 获取成功以后,会走 onCreateSuccess
}
}
private MediaConstraints offerOrAnswerConstraint() {
MediaConstraints mediaConstraints = new MediaConstraints();
ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", String.valueOf(videoEnable)));
mediaConstraints.mandatory.addAll(keyValuePairs);
return mediaConstraints;
}
3.8.2、主叫创建完成邀请信息后,会走onCreateSuccess回调,我们可以在该回调中,拿到自己的sdp信息。
调用pc.setLocalDescription设置本主叫端的sdp信息
java
/**
*
* @param origSdp 包含路由的地址,音频视频所走的端口
*/
@Override
public void onCreateSuccess(SessionDescription origSdp) {
Log.e(TAG, "onCreateSuccess: SDP回写成功");
//需要设置好双方的sdp,即才建立联系,当前只设置了本地,还需要设置远端的setRemoteDescription
String newSdp = preferCodec(origSdp.description,VIDEO_CODEC_H264, false);
//设置本地的sdp
pc.setLocalDescription(Peer.this,new SessionDescription(origSdp.type,newSdp)); //设置成功则走 onSetSuccess 设置失败则走 onSetFailure
}
3.8.3、主叫端设置setLocalDescription( )成功后,会走onSetSuccess()回调 此时主讲端再通过房间服务器,将自身的sdp发送给被叫客户端
3.8.4、被叫端收到房间服务器转发来的主叫端的sdp信息。
此时将主叫端的sdp设置进我们的setRemoteDescription()远端sdp中。
因为被叫端在初始化Peer对象时就已经设置了自己的sdp。
所以才设置完成远端的sdp后,即完成了SDP的交换。
typescript
private void handleOffer(Map map) {
Log.e(TAG, "handleOffer: ");
Map data = (Map) map.get("data");
Map sdpDic;
if (data != null) {
sdpDic = (Map) data.get("sdp");
String socketId = (String) data.get("socketId");
String sdp = (String) sdpDic.get("sdp");
mPeerConnectionManager.onReceiveOffer(socketId, sdp);
}
}
public void onReceiveOffer(String socketId, String sdpStr) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
// 角色 1 主叫 被叫 2
role = Role.Receiver;
Peer mPeer = mConnectionPeerDic.get(socketId);
SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, sdpStr);
if (mPeer != null) {
mPeer.pc.setRemoteDescription(mPeer, sdp);//此时会走当前客户端的setSuccess,同样主叫端的setSuccess也会被触发。
}
}
});
}
3.8.5、被叫端通知WebRtc请求对方的路由器,并设置ICE
csharp
if (signalingState==PeerConnection.SignalingState.HAVE_REMOTE_OFFER){
//因为初始化时就设置本地的sdp,所以走到这个判断代码块时,本地的sdp肯定设置好了。
//设置了远端的sdp
//通知webRtc请求对方的路由器:设置ICE
pc.createAnswer(Peer.this, offerOrAnswerConstraint());
}
private MediaConstraints offerOrAnswerConstraint() {
MediaConstraints mediaConstraints = new MediaConstraints();
ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false"));
keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", String.valueOf(videoEnable)));
mediaConstraints.mandatory.addAll(keyValuePairs);
return mediaConstraints;
}
3.8.6、此时被叫端打洞完成,准备将被叫端的sdp发送给主叫端。
**在setSenderSetting()中进行当前通道发送端口的信息配置,用于配置编码参数,如码率控制、帧数控制等参数。(尽量主动配置码率和帧率,不然会出现投屏帧率很低问题)
**打洞完成后,通过房间服务器将被叫端的sdp发送给主叫端。
ini
if (signalingState==PeerConnection.SignalingState.STABLE){
//ice打洞完成,交换完了
//开始链接
//设置发送的配置信息
setSenderSetting();
if (role == Role.Receiver) {
Log.e(TAG, "onSetSuccess: 打洞完成,把自己的sdp发送给主叫");
mWebSocketManager.sendAnswer(targetSocketId, pc.getLocalDescription().description);
}
}
/**
* 设置发送的配置信息
*/
private void setSenderSetting() {
Log.e(TAG, "Peer: setSenderSetting getSenders:"+ JSON.toJSON(pc.getSenders()));
for (RtpSender sender : pc.getSenders()) {
if (sender.track().kind().equals("video")) {
RtpParameters parameters = sender.getParameters();
parameters.degradationPreference=MAINTAIN_FRAMERATE;//保证帧率平稳优先
if (parameters.encodings.size()>0) {
RtpParameters.Encoding encoding = parameters.encodings.get(0);
encoding.maxFramerate = 30;//最高帧数
encoding.maxBitrateBps = 3000 * BPS_IN_KBPS;//最大码率 720p建议2000-3000kbs之间
encoding.minBitrateBps = 2000 * BPS_IN_KBPS;//最低码率
sender.setParameters(parameters);
}
}
}
}
3.8.7、主叫端收到被叫发来的sdp信息后,配置远端sdp地址,重新走上一个步骤8.6(不过这次是作为主叫走的,所以不需要再通过房间服务器发送自己的sdp信息了)
3.8.8、双方交换完sdp后,打洞服务器开始执行,他会走onIceCandidate()回调,此时主叫和被叫端都会得到当前端生成的ICE,再通过房间服务器将ICE发送给对方。
typescript
/**
* 指挥客户端A 进行请求对方的服务器
* @param iceCandidate 本机----我到我的路由器所要经过的地址
*/
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
Log.e(TAG, "onIceCandidate: "+iceCandidate);
mWebSocketManager.sendIceCandidate(targetSocketId,iceCandidate);
}
在通过房间服务器转发后收到对方的ICE,给当前链接的PeerConnection,调用addIceCandidate函数,添加对方客户端的ICE。此时完成ICE交换。
typescript
/**
* 尝试在路由中添加这个记录
* @param socketId
* @param iceCandidate
*/
public void onRemoteIceCandidate(String socketId, IceCandidate iceCandidate) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
Peer peer = mConnectionPeerDic.get(socketId);
if (peer != null) {
// 调用addIceCandidate 当前客户端会在内部请求路由器,使路由器的路由表中存储对方客户端的路由信息
peer.pc.addIceCandidate(iceCandidate);
}
}
});
}
3.9、获取到远端的流。
在通过上述步骤,完成sdp、ice的交换后,主叫与被叫端的链接才算是真正建立,建立完成后会将本地mediaStream的本地总流的数据推送给对方。
ice交换完成后会走这个回调onAddStream()
typescript
/**
* ice交换完成后会回调这个函数
* @param mediaStream 远端的流
*/
@Override
public void onAddStream(MediaStream mediaStream) {
Log.e(TAG, "onAddStream: ");
mActivity.onAddRoomStream(mediaStream,this.targetSocketId);
}
3.10、渲染WebRtc接收到的流MediaStream
此时将mediaStram的流给到WebRtc自带的一个Surface组件SurfaceViewRenderer中进行视频的渲染展示
ini
<org.webrtc.SurfaceViewRenderer
android:id="@+id/surface_view_renderer"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
scss
// //使用SurfaceViewRenderer创建SurfaceView
// SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this);
//初始化SurfaceViewRenderer
mBinding.surfaceViewRenderer.init(mRootEglBase.getEglBaseContext(),null);
//设置缩放模式
mBinding.surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
//设置视频流是否应该水平镜像
mBinding.surfaceViewRenderer.setMirror(false);
//是否打开硬件进行拉伸
// mBinding.surfaceViewRenderer.setEnableHardwareScaler(false);
// mBinding.surfaceViewRenderer.setFpsReduction(30);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mBinding.surfaceViewRenderer.setLayoutParams(layoutParams);
这样一个WebRtc的链接流程就基本描述完成了。可以发现,集成WebRtc的SDK后,是可以不去接触码流,不去接触MediaCodec,编解码、音频增益降噪等都交给了WebRtc来进行处理。
WebRtc很好的解决音视频通讯的即时性问题。
Demo(Android) :gitee.com/linxunyou/W...
(Demo中使用的服务器地址是在公网搭建的云服务器,服务器有效期一个月,现已过期。 服务器部署资料: WebRtc所需服务器搭建流程)
相关资料:见第一部分参考文献