02 本地回环
本文是基于 Android WebRTC完整入门教程 这篇文章的实践过程记录,自己新增的内容主要体现在代码的注释中
这部分还可以参考大神PIASY的WebRTC native源码解析系列文章:https://blog.piasy.com/2017/08/30/WebRTC-P2P-part1/index.html
介绍WebRTC中最核心的概念PeerConnection , 给同一手机中的前后摄像头建立虚拟的连接, 相互传输画面
PeerConnection
PeerConnection也就是Peer-to-Peer connection(P2P), 就是两个"人"的连接. 双方分别创建PeerConnection对象, 然后向对方发送自己的网络状况ICE和多媒体编码格式SDP(因为这时候连接还没建立, 所以发送内容是通过服务器完成的). 当双方网络和编码格式协商好后, 连接就建立好了, 这时从PeerConnection中能获取到对方的MediaStream数据流, 也就能播放对方的音视频了
ICE
Interactive Connectivity Establishment, 交互式连接建立. 其实是一个整合STUN和TURN的框架, 给它提供STUN和TURN服务器地址, 它会自动选择优先级高的进行NAT穿透
SDP
Session Description Protocol: 会话描述协议. 发送方的叫Offer, 接受方的叫Answer, 除了名字外没有区别. 就是一些文本描述本地的音视频编码和网络地址等
主要流程
A(local)和B(remote)代表两个人, 初始化PeerConnectionFactory并分别创建PeerConnection , 并向PeerConnection 添加本地媒体流
- A创建Offer
- A保存Offer(set local description)
- A发送Offer给B
- B保存Offer(set remote description)
- B创建Answer
- B保存Answer(set local description)
- B发送Answer给A
- A保存Answer(set remote description)
- A发送ICE Candidates给B
- B发送ICE Candidates给A
- A, B收到对方的媒体流并播放
流程图如下
准备步骤
主要是初始化PeerConnectionFactory和使用相机
public class MainActivity extends AppCompatActivity {
PeerConnectionFactory peerConnectionFactory;
PeerConnection peerConnectionLocal;
PeerConnection peerConnectionRemote;
SurfaceViewRenderer localView;
SurfaceViewRenderer remoteView;
MediaStream mediaStreamLocal;
MediaStream mediaStreamRemote;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
verifyStoragePermissions(this);
EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext();
PeerConnectionFactory.initialize(PeerConnectionFactory.
InitializationOptions.
builder(this).
createInitializationOptions();
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory(eglBaseContext,
true,
true);
DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(eglBaseContext);
peerConnectionFactory = PeerConnectionFactory.
builder().
setOptions(options).
setVideoEncoderFactory(defaultVideoEncoderFactory).
setVideoDecoderFactory(defaultVideoDecoderFactory).
createPeerConnectionFactory();
SurfaceTextureHelper localSurfaceTextureHelper = SurfaceTextureHelper.create("localCaptureThread", eglBaseContext);
VideoCapturer localVideoCapturer = createCameraCapturer(true);
VideoSource localVideoSource = peerConnectionFactory.createVideoSource(localVideoCapturer.isScreencast());
localVideoCapturer.initialize(localSurfaceTextureHelper, getApplicationContext(), localVideoSource.getCapturerObserver());
localVideoCapturer.startCapture(480, 640, 30);
localView = findViewById(R.id.localView);
localView.setMirror(true);
localView.init(eglBaseContext, null);
VideoTrack localVideoTrack = peerConnectionFactory.createVideoTrack("100", localVideoSource);
SurfaceTextureHelper remoteSurfaceTextureHelper = SurfaceTextureHelper.create("remoteCaptureThread", eglBaseContext);
VideoCapturer remoteVideoCapturer = createCameraCapturer(false);
VideoSource remoteVideoSource = peerConnectionFactory.createVideoSource(remoteVideoCapturer.isScreencast());
remoteVideoCapturer.initialize(remoteSurfaceTextureHelper, getApplicationContext(), remoteVideoSource.getCapturerObserver());
remoteVideoCapturer.startCapture(480, 640, 30);
remoteView = findViewById(R.id.remoteView);
remoteView.setMirror(true);
remoteView.init(eglBaseContext, null);
VideoTrack remoteVideoTrack = peerConnectionFactory.createVideoTrack("100", remoteVideoSource);
mediaStreamLocal = peerConnectionFactory.createLocalMediaStream("mediaStreamLocal");
mediaStreamLocal.addTrack(localVideoTrack);
mediaStreamRemote = peerConnectionFactory.createLocalMediaStream("mediaStreamRemote");
mediaStreamRemote.addTrack(remoteVideoTrack);
call(mediaStreamLocal, mediaStreamRemote);
}
...
}
使用相机
对createCameraCapturer()方法略作修改, 传入boolean参数就能分别获取前后摄像头, MainActivity.java中
private VideoCapturer createCameraCapturer(boolean isFront){
Camera1Enumerator enumerator = new Camera1Enumerator(false);
final String[] deviceNames = enumerator.getDeviceNames();
for (String deviceName : deviceNames){
if (isFront ? enumerator.isFrontFacing(deviceName) : enumerator.isBackFacing(deviceName)){
VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
if (videoCapturer != null){
return videoCapturer;
}
}
}
return null;
}
拨打
建立连接的两人肯定有一个是拨打方, 另一个是接受方. 拨打方创建Offer发给接受方, 接收方收到后回复Answer。
private void call(MediaStream mediaStreamLocal, MediaStream mediaStreamRemote){
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
peerConnectionLocal = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("localConnection"){
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
super.onIceCandidate(iceCandidate);
peerConnectionRemote.addIceCandidate(iceCandidate);
}
@Override
public void onAddStream(MediaStream mediaStream) {
super.onAddStream(mediaStream);
VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
runOnUiThread(()->{
remoteVideoTrack.addSink(localView);
});
}
});
peerConnectionRemote = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("remoteConnection"){
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
super.onIceCandidate(iceCandidate);
peerConnectionLocal.addIceCandidate(iceCandidate);
}
@Override
public void onAddStream(MediaStream mediaStream) {
super.onAddStream(mediaStream);
VideoTrack localVideoTrack = mediaStream.videoTracks.get(0);
runOnUiThread(()->{
localVideoTrack.addSink(remoteView);
});
}
});
peerConnectionLocal.addStream(mediaStreamLocal);
peerConnectionLocal.createOffer(new SdpAdapter("local offer sdp"){
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
super.onCreateSuccess(sessionDescription);
peerConnectionLocal.setLocalDescription(new SdpAdapter("local set local"), sessionDescription);
peerConnectionRemote.addStream(mediaStreamRemote);
peerConnectionRemote.setRemoteDescription(new SdpAdapter("remote set remote"), sessionDescription);
peerConnectionRemote.createAnswer(new SdpAdapter("remote answer sdp"){
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
super.onCreateSuccess(sessionDescription);
peerConnectionRemote.setLocalDescription(new SdpAdapter("remote set local"), sessionDescription);
peerConnectionLocal.setRemoteDescription(new SdpAdapter("local set remote"), sessionDescription);
}
}, new MediaConstraints());
}
}, new MediaConstraints());
}
添加PeerConnectionAdapter类作为PeerConnection的观察者
public class PeerConnectionAdapter implements PeerConnection.Observer {
private String tag;
public PeerConnectionAdapter(String tag){
this.tag = "bo" + tag;
}
public void log(String str){
Log.d(this.tag, str);
}
@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
log("onSignalingChange" + signalingState);
}
@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
log("onIceConnectionChange" + iceConnectionState);
}
@Override
public void onIceConnectionReceivingChange(boolean b) {
log("onIceConnectionReceivingChange" + b);
}
@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
log("onIceGatheringChange" + iceGatheringState);
}
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
log("onIceCandidate" + iceCandidate);
}
@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
log("onIceCandidatesRemoved" + iceCandidates);
}
@Override
public void onAddStream(MediaStream mediaStream) {
log("onAddStream" + mediaStream);
}
@Override
public void onRemoveStream(MediaStream mediaStream) {
log("onRemoveStream" + mediaStream);
}
@Override
public void onDataChannel(DataChannel dataChannel) {
log("onDataChannel" + dataChannel);
}
@Override
public void onRenegotiationNeeded() {
log("onRenegotiationNeeded");
}
@Override
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
log("onAddTrack" + mediaStreams);
}
}
添加SdpAdapter(继承SdpObserver)作为Sdp的观察者
public class SdpAdapter implements SdpObserver {
private String tag;
public SdpAdapter(String tag){
this.tag = "bo" + tag;
}
public void log(String str){
Log.d(tag, str);
}
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
log("onCreateSuccess " + sessionDescription);
}
@Override
public void onSetSuccess() {
log("onSetSuccess ");
}
@Override
public void onCreateFailure(String s) {
log("onCreateFailure " + s);
}
@Override
public void onSetFailure(String s) {
log("onSetFailure " + s);
}
}
注意: 虽然这里没有真正使用到网络, 但是要添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
Android6.0及以上申请权限
private static final int REQUEST_ALL = 1;
private static String[] PERMISSIONS_ALL = {
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO",
"android.permission.INTERNET",
"android.permission.ACCESS_NETWORK_STATE"
};
public static void verifyStoragePermissions(Activity activity) {
try {
int permission = 0;
for(String temp : PERMISSIONS_ALL){
permission = ActivityCompat.checkSelfPermission(activity, temp);
if (permission != PackageManager.PERMISSION_GRANTED){
break;
}
}
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, PERMISSIONS_ALL,REQUEST_ALL);
}
} catch (Exception e) {
e.printStackTrace();
}
}
网上大部分本地回环(Loopback)的Demo都只用到一个摄像头, 这里使用到同一个手机的前后摄像头, 把它们当做两个客户端, 建立模拟连接, 发送媒体数据. 这跟实际WebRTC工作流程非常接近了, 只有一点差别–这里的数据传输是内存共享, 而实际是通过网络发送