Android的WebRTC入门——回环点对点视频通话
点对点视频通话风靡一时,现在每个应用都内置了音频/视频通话。本系列教程旨在探索以简单易懂的方式为初学者介绍此类功能。
本教程系列大致基于Codelabs for WebRTC。
本文主要是基于上述代码实验(Codelabs)的第 2 步 ,我们使用WebRTC中的PeerConnection在同一设备中的两个对等体之间传输音频和视频数据。
这是“ Android 版 WebRTC 入门 ”系列的 第 3 部分 ,如果您是第一次看到此篇文章,请确保在继续阅读这一部分之前,您已经阅读了本系列的前几部分。
第 1 部分: WebRTC 简介
第 2 部分: PeerConnection 简介
第 3 部分: 点对点视频通话 — 回环(本文)
第 4 部分: 使用 socket.io 进行点对点视频通话
让我们想象以下情景:一位勇敢的先生想给一位女士打电话(当然,打电话的目的是求婚!)
他计划使用一款具有强大的视频和音频通话功能的应用程序。因为他想看到他在求婚时对方的反应(哦~)。以下步骤发生在对等连接的两端。
-
最初,我们的应用程序创建了一个对等连接和一个SDP指令。此指令包含调用对等方的数据,并用于识别对等方的编解码器和其他实体。
-
这个指令之后被存储在呼叫方的 "本地描述 "中,然后通过一些信号介质发送到被呼叫方那里(通常,大多数系统使用WebSocket作为信令介质。它可能会根据您的使用方法和要求而有所不同)。
-
一旦我们在被呼叫方的应用程序收到指令,它就知道有一个呼叫需要建立。从而将“指令”存储为“远程描述”,并创建相应的“应答”SDP。
-
这个应答SDP类似于呼叫方的指令 SDP,它将指出该对等体的具体细节。
-
被呼叫方的应用程序将这个 "应答SDP “存储为"本地描述”,然后通过信号通道将其发送给呼叫方。
-
呼叫方收到应答并将它作为 "远程描述 "储存起来。
-
然后呼叫方和被呼叫方通过信号通道传输与他们有关的 Ice Candidates。收到这些candidates后,对等方会将这些candidates添加到它们的 PeerConnection 实例中。
-
一旦Ice Candidates的传输完成,对等方就清楚地知道如何在他们之间传输媒体数据。媒体数据传输通过RTP进行,由WebRTC框架负责。
如果您看过WebRTC codelabs的 第 2 步,您可能会注意到它们在呼叫方和被呼叫方之间创建了一个循环。
单个设备既是本地对等体,又是远程对等体,从而完成指令和应答的部分。虽然它没有任何实际用途,但最好通过这一步来深入了解它的工作原理。
我们一起看看下面的代码,了解循环工作是如何实现的。
public void start() {
//Initialize PeerConnectionFactory globals.
//Params are context, initAudio,initVideo and videoCodecHwAcceleration
PeerConnectionFactory.initializeAndroidGlobals(this, true, true, true);
//Create a new PeerConnectionFactory instance.
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
peerConnectionFactory = new PeerConnectionFactory(options);
//Now create a VideoCapturer instance. Callback methods are there if you want to do something! Duh!
VideoCapturer videoCapturerAndroid = getVideoCapturer(new CustomCameraEventsHandler());
//Create MediaConstraints - Will be useful for specifying video and audio constraints.
audioConstraints = new MediaConstraints();
videoConstraints = new MediaConstraints();
//Create a VideoSource instance
videoSource = peerConnectionFactory.createVideoSource(videoCapturerAndroid, videoConstraints);
localVideoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
//create an AudioSource instance
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
localAudioTrack = peerConnectionFactory.createAudioTrack("101", audioSource);
localVideoView.setVisibility(View.VISIBLE);
//create a videoRenderer based on SurfaceViewRenderer instance
localRenderer = new VideoRenderer(localVideoView);
// And finally, with our VideoRenderer ready, we
// can add our renderer to the VideoTrack.
localVideoTrack.addRenderer(localRenderer);
}
private void call() {
//we already have video and audio tracks. Now create peerconnections
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
//create sdpConstraints
sdpConstraints = new MediaConstraints();
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveAudio", "true"));
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("offerToReceiveVideo", "true"));
//creating localPeer
localPeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("localPeerCreation") {
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
super.onIceCandidate(iceCandidate);
onIceCandidateReceived(localPeer, iceCandidate);
}
});
//creating remotePeer
remotePeer = peerConnectionFactory.createPeerConnection(iceServers, sdpConstraints, new CustomPeerConnectionObserver("remotePeerCreation") {
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
super.onIceCandidate(iceCandidate);
onIceCandidateReceived(remotePeer, iceCandidate);
}
@Override
public void onAddStream(MediaStream mediaStream) {
super.onAddStream(mediaStream);
gotRemoteStream(mediaStream);
}
});
//creating local mediastream
MediaStream stream = peerConnectionFactory.createLocalMediaStream("102");
stream.addTrack(localAudioTrack);
stream.addTrack(localVideoTrack);
localPeer.addStream(stream);
//creating Offer
localPeer.createOffer(new CustomSdpObserver("localCreateOffer"){
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
//we have localOffer. Set it as local desc for localpeer and remote desc for remote peer.
//try to create answer from the remote peer.
super.onCreateSuccess(sessionDescription);
localPeer.setLocalDescription(new CustomSdpObserver("localSetLocalDesc"), sessionDescription);
remotePeer.setRemoteDescription(new CustomSdpObserver("remoteSetRemoteDesc"), sessionDescription);
remotePeer.createAnswer(new CustomSdpObserver("remoteCreateOffer") {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
//remote answer generated. Now set it as local desc for remote peer and remote desc for local peer.
super.onCreateSuccess(sessionDescription);
remotePeer.setLocalDescription(new CustomSdpObserver("remoteSetLocalDesc"), sessionDescription);
localPeer.setRemoteDescription(new CustomSdpObserver("localSetRemoteDesc"), sessionDescription);
}
},new MediaConstraints());
}
},sdpConstraints);
}
private void hangup() {
localPeer.close();
remotePeer.close();
localPeer = null;
remotePeer = null;
}
private void gotRemoteStream(MediaStream stream) {
//we have remote video stream. add to the renderer.
final VideoTrack videoTrack = stream.videoTracks.getFirst();
AudioTrack audioTrack = stream.audioTracks.getFirst();
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
remoteRenderer = new VideoRenderer(remoteVideoView);
remoteVideoView.setVisibility(View.VISIBLE);
videoTrack.addRenderer(remoteRenderer);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public void onIceCandidateReceived(PeerConnection peer, IceCandidate iceCandidate) {
//we have received ice candidate. We can set it to the other peer.
if (peer == localPeer) {
remotePeer.addIceCandidate(iceCandidate);
} else {
localPeer.addIceCandidate(iceCandidate);
}
}
如果你看一下上面的代码,我们能总结出3个方法。
start()
本质上是创建本地音频和视频源,并将它们添加到SurfaceViewRenderer(这是WebRTC提供的,这样我们就可以直接在这个视图上绘制我们的视频帧)。
hangup()
只是一段简单的代码,用于清除所有PeerConnection实例。
call()
的过程中会发生很多有趣的事儿。
-
我们创建
two
对等体连接实例(一个用于本地对等体,另一个用于远程对等体)。一旦这些对等体被创建,我们让本地对等体创建一个Offer
,该指令被设置为其本地描述,也是远程对等体的远程描述。 -
然后我们让远程对等体做出相应的
answer
,该应答被设置为其本地描述和本地对等体的远程描述。
我们还有onIceCandidateReceived()
方法,它的工作是将从一个对等方接收到的Ice candidates设置给另一个对等方。
你可以看看 Git 存储库的的Step-2文件夹,了解Loopback peerconnection的完整工作代码。
现在一切都很顺利。我们的应用程序将能够让您看到自己的脸,就像我们在第一部分做的那样。只是这次我们的应用程序是通过对等连接来显示脸部。
我们了解了基本的工作原理。我们有两个对等体,它们与candidates一起传输指令和应答SDP,现在它们都可以传输数据。但这并不是真实的情况。实际上人们会互相呼叫,而不是自己呼叫自己。将此解决方案扩展到实际用例很容易。我们只需要一个媒介来传输SDP和一些STUN和TURN设置。
这些内容对于单个帖子来说已经足够充实了。 我们很快就会看到(希望如此!)成品,届时你将拥有一个能够工作的点对点视频通话应用程序。
一定要按下绿色的按键来表达支持哦!有关更多 droid-y 帖子,请关注 Adventurous Android!
原文作者:Vivek Chanddru
原文链接:https://vivekc.xyz/peer-to-peer-video-calling-webrtc-for-android-4132fd0ac54