上一篇: Android WebRTC完整入门教程02: 本地回环
上一篇完成了两个人在同一个手机中的模拟连接, 这一篇在此基础上给两个手机建立真正的连接. 这就需要一个信令服务器, 其实就是用来给双方交换信息, 并不需要对信息进行处理. 因此服务器和信息的数据格式都可以自己选择, 这里用官方Demo提供的Nodejs服务器, 用soket.io建立连接.
信令服务端
简单介绍下, Node.js工程中主要文件是 index.js 和 js/main.js , index.js 负责启动Node.js服务器, 初始化socket.io服务端, 等待给客户端发送数据. 而 js/main.js 是网页客户端(详细使用方法请参考官方教程).
这里对 index.js 稍作修改, 添加https支持(新的WebRTC不支持http)和添加控制台日志. 需要自己生成https证书, 并复制key.pem和cert.pem文件到Node.js工程的根目录. 在log()方法中加上console.log('chao', array);
就能在控制台看到日志了.
var fs = require('fs');
var options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var fileServer = new(nodeStatic.Server)();
var app = https.createServer(options, function(req, res) {
fileServer.serve(req, res);
}).listen(8080);
var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {
// convenience function to log server messages on the client
function log() {
var array = ['Message from server:'];
array.push.apply(array, arguments);
socket.emit('log', array);
console.log('chao', array);
}
信令客户端
- 在module的build.gradle添加socket.io依赖
implementation('io.socket:socket.io-client:0.8.3') {
// excluding org.json which is provided by Android
exclude group: 'org.json', module: 'json'
}
SignalingClient.java
通过socket.io连接信令服务器, 然后收发数据. 把SDP和IceCandidate转换成json.
public class SignalingClient {
private static SignalingClient instance;
private SignalingClient(){
init();
}
public static SignalingClient get() {
if(instance == null) {
synchronized (SignalingClient.class) {
if(instance == null) {
instance = new SignalingClient();
}
}
}
return instance;
}
private Socket socket;
private String room = "OldPlace";
private Callback callback;
private final TrustManager[] trustAll = new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
};
public void setCallback(Callback callback) {
this.callback = callback;
}
private void init() {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAll, null);
IO.setDefaultHostnameVerifier((hostname, session) -> true);
IO.setDefaultSSLContext(sslContext);
socket = IO.socket("https://192.168.1.97:8080");
socket.connect();
socket.emit("create or join", room);
socket.on("created", args -> {
Log.e("chao", "room created");
callback.onCreateRoom();
});
socket.on("full", args -> {
Log.e("chao", "room full");
});
socket.on("join", args -> {
Log.e("chao", "peer joined");
callback.onPeerJoined();
});
socket.on("joined", args -> {
Log.e("chao", "self joined");
callback.onSelfJoined();
});
socket.on("log", args -> {
Log.e("chao", "log call " + Arrays.toString(args));
});
socket.on("bye", args -> {
Log.e("chao", "bye " + args[0]);
callback.onPeerLeave((String) args[0]);
});
socket.on("message", args -> {
Log.e("chao", "message " + Arrays.toString(args));
Object arg = args[0];
if(arg instanceof String) {
} else if(arg instanceof JSONObject) {
JSONObject data = (JSONObject) arg;
String type = data.optString("type");
if("offer".equals(type)) {
callback.onOfferReceived(data);
} else if("answer".equals(type)) {
callback.onAnswerReceived(data);
} else if("candidate".equals(type)) {
callback.onIceCandidateReceived(data);
}
}
});
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
public void sendIceCandidate(IceCandidate iceCandidate) {
JSONObject jo = new JSONObject();
try {
jo.put("type", "candidate");
jo.put("label", iceCandidate.sdpMLineIndex);
jo.put("id", iceCandidate.sdpMid);
jo.put("candidate", iceCandidate.sdp);
socket.emit("message", jo);
} catch (JSONException e) {
e.printStackTrace();
}
}
public void sendSessionDescription(SessionDescription sdp) {
JSONObject jo = new JSONObject();
try {
jo.put("type", sdp.type.canonicalForm());
jo.put("sdp", sdp.description);
socket.emit("message", jo);
} catch (JSONException e) {
e.printStackTrace();
}
}
public interface Callback {
void onCreateRoom();
void onPeerJoined();
void onSelfJoined();
void onPeerLeave(String msg);
void onOfferReceived(JSONObject data);
void onAnswerReceived(JSONObject data);
void onIceCandidateReceived(JSONObject data);
}
}
MainActivity.java
跟上一篇的差别就是, 把原来直接共享的数据通过SignalingClient发送给服务端, 服务端再发给接收端. 此外, 服务端有一个房间的概念, 连接上服务端就相当于进入房间, 先进入房间的人是房主. 由后进入房间的人发送Offer, 房主接受Offer并回复Answer.
public class MainActivity extends AppCompatActivity implements SignalingClient.Callback {
PeerConnectionFactory peerConnectionFactory;
PeerConnection peerConnection;
SurfaceViewRenderer localView;
SurfaceViewRenderer remoteView;
MediaStream mediaStream;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EglBase.Context eglBaseContext = EglBase.create().getEglBaseContext();
// create PeerConnectionFactory
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 surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
// create VideoCapturer
VideoCapturer videoCapturer = createCameraCapturer(true);
VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
videoCapturer.initialize(surfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
videoCapturer.startCapture(480, 640, 30);
localView = findViewById(R.id.localView);
localView.setMirror(true);
localView.init(eglBaseContext, null);
// create VideoTrack
VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
// // display in localView
videoTrack.addSink(localView);
remoteView = findViewById(R.id.remoteView);
remoteView.setMirror(false);
remoteView.init(eglBaseContext, null);
mediaStream = peerConnectionFactory.createLocalMediaStream("mediaStream");
mediaStream.addTrack(videoTrack);
SignalingClient.get().setCallback(this);
call();
}
private void call() {
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("localconnection") {
@Override
public void onIceCandidate(IceCandidate iceCandidate) {
super.onIceCandidate(iceCandidate);
SignalingClient.get().sendIceCandidate(iceCandidate);
}
@Override
public void onAddStream(MediaStream mediaStream) {
super.onAddStream(mediaStream);
VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
runOnUiThread(() -> {
remoteVideoTrack.addSink(remoteView);
});
}
});
peerConnection.addStream(mediaStream);
}
private VideoCapturer createCameraCapturer(boolean isFront) {
Camera1Enumerator enumerator = new Camera1Enumerator(false);
final String[] deviceNames = enumerator.getDeviceNames();
// First, try to find front facing camera
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;
}
@Override
public void onCreateRoom() {
}
@Override
public void onPeerJoined() {
}
@Override
public void onSelfJoined() {
peerConnection.createOffer(new SdpAdapter("local offer sdp") {
@Override
public void onCreateSuccess(SessionDescription sessionDescription) {
super.onCreateSuccess(sessionDescription);
peerConnection.setLocalDescription(new SdpAdapter("local set local"), sessionDescription);
SignalingClient.get().sendSessionDescription(sessionDescription);
}
}, new MediaConstraints());
}
@Override
public void onPeerLeave(String msg) {
}
@Override
public void onOfferReceived(JSONObject data) {
runOnUiThread(() -> {
peerConnection.setRemoteDescription(new SdpAdapter("localSetRemote"),
new SessionDescription(SessionDescription.Type.OFFER, data.optString("sdp")));
peerConnection.createAnswer(new SdpAdapter("localAnswerSdp") {
@Override
public void onCreateSuccess(SessionDescription sdp) {
super.onCreateSuccess(sdp);
peerConnection.setLocalDescription(new SdpAdapter("localSetLocal"), sdp);
SignalingClient.get().sendSessionDescription(sdp);
}
}, new MediaConstraints());
});
}
@Override
public void onAnswerReceived(JSONObject data) {
peerConnection.setRemoteDescription(new SdpAdapter("localSetRemote"),
new SessionDescription(SessionDescription.Type.ANSWER, data.optString("sdp")));
}
@Override
public void onIceCandidateReceived(JSONObject data) {
peerConnection.addIceCandidate(new IceCandidate(
data.optString("id"),
data.optInt("label"),
data.optString("candidate")
));
}
}
STUN/TURN服务器
STUN服务器用于寻找客户端的公网IP, 让两个服务端通过公网IP直接发送音视频数据, 这些数据不经过STUN服务器. 因此STUN服务器的数据流量很小, 免费的服务器很多. 不保证所有情况下都能建立WebRTC连接.
TURN服务器用于直接转发音视频数据, 当客户端网络情况特殊, 无法相互发送数据时. 经过它的数据量很大, 基本没有免费的. 只要客户端能访问到TURN服务器就能建立WebRTC连接.
2222222222.png
其实STUN与TURN的区别就在于 R-Relay-转发, 需要转发音视频数据的就是TURN服务器. 关于它们这篇文章有详细的介绍.
这里使用的是Google的免费STUN服务器: stun:stun.l.google.com:19302,
创建PeerConnection时传入就可以, 不需要额外的配置. 当然你也可以自己搭建.通话在电脑上启动Node.js服务器,
把SignalingClient.java中的socket地址改成你电脑的内网地址. 在两个安卓手机上安装客户端,
确保手机和电脑在同一个WiFi网络下, 先后启动客户端.
不出意外的话在服务端控制台能看到客户端进入房间和发送信令的日志, 随后两个手机上能看到对方的画面.
本项目Gitee地址/webrtc-android-tutorial-master/step3signaling
本项目Gitee地址/webrtc-android-tutorial-master/step3web
下一篇: Android WebRTC完整入门教程04: 多人视频
作者:rome753
链接:https://www.jianshu.com/p/eb5fd116e6c8
网友评论