Android WebRTC Complete Introductory Tutorial 03: Signaling

The previous article completed the simulated connection of two people in the same mobile phone, and this article establishes a real connection for the two mobile phones on this basis. This requires a signaling server, which is actually used to exchange information for both parties, and There is no need to process the information. Therefore, the server and the data format of the information can be selected by yourself. Here, the Nodejs server provided by the official Demo is used to establish a connection with socket.io.

Signaling server

Briefly, the main files in the Node.js project are index.js and js/main.js . index.js is responsible for starting the Node.js server, initializing the socket.io server, and waiting to send data to the client. And js/main .js is the web client (please refer to the official tutorial for detailed usage ).

Here we slightly modify index.js , add https support (the new WebRTC does not support http) and add console logs. You need to generate https certificates yourself, and copy the key.pem and cert.pem files to the root directory of the Node.js project . Add in the log() method console.log('chao', array);to see the log in the console.

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);
  }

signaling client

Add socket.io dependency in module's build.gradle

    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
connects to the signaling server through socket.io, and then sends and receives data. Convert SDP and IceCandidate into 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);
    }
}

The difference between MainActivity.java
and the previous article is that the original directly shared data is sent to the server through SignalingClient, and then the server sends it to the receiving end. In addition, the server has a concept of a room, and connecting to the server is equivalent to entering The person who enters the room first is the owner of the room. The person who enters the room later sends an Offer, and the owner accepts the Offer and replies with an 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 server

The STUN server is used to find the public network IP of the client, so that the two servers can directly send audio and video data through the public network IP, and these data do not pass through the STUN server. Therefore, the data flow of the STUN server is very small, and there are many free servers. Not guaranteed In any case, a WebRTC connection can be established.

insert image description here

STUN.png

The TURN server is used to directly transfer audio and video data. When the client network is special and cannot send data to each other. The amount of data passing through it is very large, and there is basically no free one. As long as the client can access the TURN server, a WebRTC connection can be established.


insert image description here

TURN.png

In fact, the difference between STUN and TURN lies in the R-Relay-forwarding , and the TURN server needs to transmit audio and video data. This article has a detailed introduction about them.

Google's free STUN server is used here: stun:stun.l.google.com:19302, which can be passed in when creating PeerConnection, no additional configuration is required. Of course, you can also build it yourself.

call

Start the Node.js server on the computer, and change the socket address in SignalingClient.java to the intranet address of your computer. Install the client on two Android phones, make sure that the phone and the computer are in the same WiFi network, and start the client successively If
there is no accident, you can see the log of the client entering the room and sending signaling on the server console, and then you can see each other's screen on the two mobile phones.

Reprint: https://www.jianshu.com/p/505bb98aaea7

Guess you like

Origin blog.csdn.net/gqg_guan/article/details/130605853