基于WebRtc实现安卓视频一对一聊天

WebRtc是谷歌2010年收购GlobalIPSolutions公司而获得的一项实时语音对话或视频对话的技术。之后谷歌将其开源,有很好的跨平台性。官方网址:https://webrtc.org/

最近由于公司项目需求,刚刚接触webrtc,由于国内这方面的资料少之又少,学习起来也有点困难。这一个月来对webrtc也稍微有点了解吧,特此写个博客纪念下,结合自己写的小Demo给刚入坑的新人一点建议。

基本流程

基于WebRtc实现安卓视频一对一聊天

使用webrtc###
1. Maven

<dependency>
<groupId>org.webrtc</groupId>
<artifactId>google-webrtc</artifactId>
<version>1.0.20723</version>
<type>pom</type>
</dependency>

注意:安卓6.0以上请自行处理CAMERA、RECORD_AUDIO、WRITE_EXTERNAL_STORAGE等危险权限。

介绍Webrtc一些关键类###
1. PeerConnectionFactory
webrtc核心类,用于创建其他关键类,稍后在做介绍。在使用PeerConnectionFactory之前,请先初始化,类似这样。

PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(getApplicationContext())
.setEnableVideoHwAcceleration(true)
.createInitializationOptions());

PeerConnectionFactory.InitializationOptions作为PeerConnectionFactory初始化传入参数,该类采用构造模式,可以对初始化参数进行一些配置。初始化之后就可以创建PeerConnectionFactory实例了。

PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
mPeerConnectionFactory = new PeerConnectionFactory(options);


2. VideoCapturer
视频捕捉器的一个顶级接口,它的的子接口为CameraVideoCapturer,封装了安卓相机的使用方法,使用它们可以轻松的获取设备相机数据,切换摄像头,获取摄像头数量等。该对象的创建如下。

private CameraVideoCapturer createVideoCapture(Context context) {
CameraEnumerator enumerator;
if (Camera2Enumerator.isSupported(context)) {
enumerator = new Camera2Enumerator(context);
} else {
enumerator = new Camera1Enumerator(true);

final String[] deviceNames = enumerator.getDeviceNames();

for (String deviceName : deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);

if (videoCapturer != null) {
return videoCapturer;
}
}
}

for (String deviceName : deviceNames) {
if (!enumerator.isFrontFacing(deviceName)) {
CameraVideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null) {
return videoCapturer;
}
}
}
return null;
}

3. VideoSource/VideoTrack
VideoSource为视频源,通过核心类PeerConnectionFactory创建,VideoTrack是对VideoSource的包装,可以方便的将视频源在本地进行播放,添加到MediaStream中进行网络传输。

CameraVideoCapturer mVideoCapturer = createVideoCapture(this);
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(mVideoCapturer);
VideoTrack mVideoTrack = mPeerConnectionFactory.createVideoTrack("videtrack", videoSource);


4. AudioSource/AudioTrack
AudioSource/AudioTrack和上面的VideoSource/VideoTrack类似,从名字上面就知道是对音频的获取和处理了,AudioSource的创建很简单,直接用PeerConnectionFactory创建就可以了。

AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
AudioTrack mAudioTrack = mPeerConnectionFactory.createAudioTrack("audiotrack", audioSource);


AudioSource 创建的时候需要传入MediaConstraints这个对象的实例,其用于对媒体的一些约束限制,创建的时候可以直接使用默认的。如果你想自己定义,就需要自己填入相应的键值对了。

MediaConstraints audioConstraints = new MediaConstraints();
//回声消除
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
//自动增益 
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
//高音过滤 
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
//噪音处理
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));


5. MediaStream
音视频的媒体流,通过PeerConnectionFactory创建,用于PeerConnection通过网络传输发送给另一方。在媒体流传输之前,需要将前面获取的VideoTrack和AudioTrack添加进去。

MediaStream mMediaStream = mPeerConnectionFactory.createLocalMediaStream("localstream");
mMediaStream.addTrack(mVideoTrack);
mMediaStream.addTrack(mAudioTrack);



6. PeerConnection
用于p2p网络传输,双方信令的交换。webrtc是基于p2p的,因此在双方通信之前需要服务器帮助传递信令,并且需要添加STUN和TURN服务器网络穿透。在双方通道打开之后就可以将媒体流发送给另一方了。下面是PeerConnection的创建,并将媒体流添加到其中用于网络传输。

PeerConnection peerConnection = mPeerConnectionFactory.createPeerConnection(iceServers, pcConstraints, this);
peerConnection.addStream(mMediaStream);



参数说明如下:
iceServers连接到外网和网络穿用到的,添加STUN和TURN服务器可以帮助你连接。
constraints是一个MediaConstrains的实例。应该包含offerToRecieveAudio和offerToRecieveVideo。
observer是一个PeerConnectionObserver的实例,对PeerConnection的一些连接状态的监听。

信令交换###
在实现媒体流的网络传输之前,需要交换双方信令,将连接通道打开,下面介绍一下webrtc的信令交换机制。

A向B发起建立连接的请求,通过PeerConnection的createOffer()方法创建一个offer信令,创建成功后调用SdpObserver监听中的onCreateSuccess()回调函数,调用PeerConnection的setLocalDescription方法设置自己的offer信令,同时将offer信令通过服务器转发给B。

peerConnection.createOffer(sdpObserver, sdpMediaConstraints);

//SdpObserver
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
peerConnection.setLocalDescription(this, sessionDescription);
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("type", sessionDescription.type.canonicalForm());
jsonObject.put("description", sessionDescription.description);
} catch (JSONException e) {
e.printStackTrace();
}
mSocket.emit("SdpInfo", jsonObject.toString());
}


B收到A的offer信令后,创建一个SessionDescription(SDP描述符包含媒体信息,如分辨率、编解码能力等)对象将A的offer信令解析出来,并调用PeerConnection的setRemoteDescription方法设置A的SDP描述符,然后B通过PeerConnection的createAnswer()方法创建一个answer信令,创建成功后调用SdpObserver监听中的onCreateSuccess()回调函数,调用PeerConnection的setLocalDescription方法设置自己的answer信令,同时将answer信令通过服务器转发给A。

@Override
public void call(Object... args) {
if (mPeer == null) {
mPeer = new Peer();
}
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
SessionDescription description = new SessionDescription
(SessionDescription.Type.fromCanonicalForm(jsonObject.getString("type")),
jsonObject.getString("description"));
mPeer.peerConnection.setRemoteDescription(mPeer, description);
if (!isOffer) {
mPeer.peerConnection.createAnswer(mPeer, sdpConstraints);
}
} catch (JSONException e) {
e.printStackTrace();
}
}



A收到B的answer信令后,解析B的answer信令再调用PeerConnection的setRemoteDescription方法设置B的SDP描述符。这样双方的信令交换就算完成了。
在非局域网下,信令的交换还需要借助于STUN和TURN服务器网络穿透,创建PeerConnection的时候需要传入iceServers这个参数,这里面存放的就是穿透地址变换的服务器地址了,类似的,Ice穿透也需要信令的交换,过程大致如下。

当A和B创建好配置了iceServers的PeerConnection实例后,当网络候可用时,回调PeerConnection.Observer的onIceCandidate()函数,在回调函数里面将IceCandidate对象发送给对方。
在收到对方的IceCandidate信令后,解析出来并用PeerConnection的addIceCandidate()方法设置对方的信令。

@Override
public void onIceCandidate(IceCandidate iceCandidate) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("label", iceCandidate.sdpMLineIndex);
jsonObject.put("id", iceCandidate.sdpMid);
jsonObject.put("candidate", iceCandidate.sdp);
mSocket.emit("IceInfo", jsonObject.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}

@Override
public void call(Object... args) {
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
IceCandidate candidate = null;
candidate = new IceCandidate(
jsonObject.getString("id"),
jsonObject.getInt("label"),
jsonObject.getString("candidate")
);
mPeer.peerConnection.addIceCandidate(candidate);
} catch (JSONException e) {
e.printStackTrace();
}
}

现在,双方的连接通道就完全打开了,PeerConnection.Observer就会调用onAddStream()响应函数,里面包含对方的媒体流Mediastream,将媒体流播放就可以了。

@Override
public void onAddStream(MediaStream mediaStream) {
remoteVideoTrack = mediaStream.videoTracks.get(0);
remoteVideoTrack.addRenderer(new VideoRenderer(remoteView));
}


播放媒体流###
媒体流的播放需要用到webrtc封装的控件SurfaceViewRenderer,它继承于安卓的SurfaceView。
在播放之前,需要对该控件初始化和配置一些属性。

localView = findViewById(R.id.localVideoView);
//创建EglBase对象
mEglBase = EglBase.create();
//初始化localView
localView.init(mEglBase.getEglBaseContext(), null);
localView.setKeepScreenOn(true);
localView.setMirror(true);
localView.setZOrderMediaOverlay(true);
localView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
localView.setEnableHardwareScaler(false);



注意:SurfaceViewRenderer的初始化需要在主线程

初始化完成后,将localView包装成VideoRenderer对象并添加到VideoTrack中就可以进行播放了。

mVideoTrack.addRenderer(new VideoRenderer(localView));

Demo###
demo里面包含了用IoSocket简单写的java服务器(webrtc文件夹),里面的地址改成自己电脑的本机ip4地址即可测试。

demo地址:demo传送门

附上demo运行效果图

原文链接:https://blog.csdn.net/qq_35054800/article/details/78647545