一篇文章教會(huì)你---怎樣實(shí)現(xiàn)Android-WebRtc視頻通話

先看一下核心代碼

public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity------";
    private EditText etWsUrl;//ws地址
    private Button btnConnectSever;//連接ws
    private EditText etLoginUserId;//登錄userId
    private Button btnLogin;//登錄
    private EditText etUserInfo;//在線用戶信息
    private EditText etCallUserId;//呼叫userId
    private Button btnCall;//呼叫
    private SurfaceViewRenderer localView;//本地?cái)z像頭預(yù)覽(本地視頻)
    private SurfaceViewRenderer remoteView;//服務(wù)器傳來的攝像頭預(yù)覽(對(duì)端視頻)

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        checkPermission();
    }

    private void checkPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//適配6.0權(quán)限
            if (ContextCompat.checkSelfPermission(getApplication(),
                    Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                    || ContextCompat.checkSelfPermission(getApplication(),
                    Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
                    || ContextCompat.checkSelfPermission(getApplication(),
                    Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                    || ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.ACCESS_WIFI_STATE) != PackageManager.PERMISSION_GRANTED
                    || ContextCompat.checkSelfPermission(getApplication(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED
            ) {
                ActivityCompat.requestPermissions(this,
                        new String[]{
                                Manifest.permission.READ_EXTERNAL_STORAGE,
                                Manifest.permission.RECORD_AUDIO,
                                Manifest.permission.CAMERA,
                                Manifest.permission.READ_EXTERNAL_STORAGE,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                Manifest.permission.ACCESS_WIFI_STATE,
                                Manifest.permission.RECORD_AUDIO
                        }, 1);
            } else {
                //已經(jīng)有權(quán)限
                havePermission();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        Log.e(TAG, "onRequestPermissionsResult: ");
        for (int i = 0; i < permissions.length; i++) {
            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, permissions[i] + "權(quán)限未打開", Toast.LENGTH_SHORT).show();
                finish();
                return;
            }
        }
        //已經(jīng)有權(quán)限
        havePermission();
    }


    private EglBase.Context eglBaseContext;
    private PeerConnectionFactory peerConnectionFactory;
    private PeerConnection callPeerConnection;
    private PeerConnection receivePeerConnection;
    private MediaStream mMediaStream;

    private void havePermission() {
        Log.e(TAG, "havePermission: ");

        //創(chuàng)建EglBase對(duì)象 并獲取上下文環(huán)境
        eglBaseContext = EglBase.create().getEglBaseContext();
        //1.初始化p2p連接工廠
        PeerConnectionFactory.initialize(
                PeerConnectionFactory.InitializationOptions
                        .builder(getApplicationContext())
                        .createInitializationOptions()
        );
        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
        DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory(eglBaseContext, true, true);//視頻編碼工廠
        DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(eglBaseContext);//視頻解碼工廠

//        JavaAudioDeviceModule.Builder admbuilder = JavaAudioDeviceModule.builder(this);
//        admbuilder.setAudioSource(MediaRecorder.AudioSource.MIC);//控制錄音來源
//        JavaAudioDeviceModule audioDeviceModule = admbuilder.createAudioDeviceModule();
        //2.創(chuàng)建p2p連接工廠
        peerConnectionFactory = PeerConnectionFactory.builder()
                .setOptions(options)
                .setVideoEncoderFactory(defaultVideoEncoderFactory)
                .setVideoDecoderFactory(defaultVideoDecoderFactory)
//                .setAudioDeviceModule(audioDeviceModule)
                .createPeerConnectionFactory();
        //3.創(chuàng)建SurfaceTextureHelper
        SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
        //4.創(chuàng)建視頻捕獲器
        VideoCapturer videoCapturer = createCameraCapturer(false);//是否正面攝像頭
        //5.創(chuàng)建視頻源
        VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
        //6.初始化視頻捕獲器
        videoCapturer.initialize(surfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
        //7.開始捕獲
        videoCapturer.startCapture(480, 640, 30);
        localView.setMirror(false);//是否鏡像
        localView.init(eglBaseContext, null);//初始化SurfaceView
        remoteView.setMirror(false);//是否鏡像
        remoteView.init(eglBaseContext, null);//初始化SurfaceView
        //8.創(chuàng)建視頻軌道
        VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
        videoTrack.addSink(localView);//展示本地視頻
        //9.創(chuàng)建本地媒體流
        mMediaStream = peerConnectionFactory.createLocalMediaStream("mMediaStream");
        mMediaStream.addTrack(videoTrack);//媒體流添加視頻軌道
        mMediaStream.addTrack(createAudioTrack());//媒體流添加音頻軌道
        //連接websocket服務(wù)
        btnConnectSever.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(etWsUrl.getText().toString())) {
                    Toast.makeText(MainActivity.this, "請輸入wsUrl", Toast.LENGTH_SHORT).show();
                    return;
                }
                RtcWebSokcetHelper.getInstance().connectServer(etWsUrl.getText().toString().trim(), new RtcWebSokcetHelper.OnReciveServerMgsListener() {
                    @Override
                    public void onConnect() {
                        Log.e(TAG, "onConnect: ");
                        Toast.makeText(MainActivity.this, "服務(wù)連接成功", Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onRecive(String json) {
                        RtcWebSokcetHelper.WebsocketDealEvent websocketDealEvent;
                        Log.e(TAG, "onRecive: json=" + json);
                        try {
                            int code = new JSONObject(json).getInt("code");
                            switch (code) {
                                case Constant.SendMsg_Code://收到轉(zhuǎn)發(fā)消息
                                    NetBean.SendMsgRequestBean sendMsgRequestBean = GsonUtil.GsonToBean(json, NetBean.SendMsgRequestBean.class);
                                    if (sendMsgRequestBean.message.contains("不在線")) {
                                        Toast.makeText(MainActivity.this, "目標(biāo)用戶不在線", Toast.LENGTH_SHORT).show();
                                        return;
                                    }
                                    int turnCode = new JSONObject(sendMsgRequestBean.message).getInt("code");
                                    switch (turnCode) {
                                        case Constant.Message_Call_Code:
                                            Log.e(TAG, "onRecive: Message_Call_Code");
                                            showCustomeDialog("是否同意接聽 " + sendMsgRequestBean.fromUserId + " 的來電?",
                                                    "同意", new View.OnClickListener() {
                                                        @Override
                                                        public void onClick(View v) {
                                                            NetBean.SendMsgResponseBean sendMsgResponseBean = new NetBean.SendMsgResponseBean(Constant.Message_Call_Result_Code, sendMsgRequestBean.sequenceId, sendMsgRequestBean.toUserId, sendMsgRequestBean.fromUserId, true, "同意接聽");
                                                            NetBean.SendMsgRequestBean sendMsgRequestBean1 = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, sendMsgRequestBean.sequenceId, sendMsgRequestBean.toUserId, sendMsgRequestBean.fromUserId, GsonUtil.BeanToJson(sendMsgResponseBean));
                                                            RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean1));
                                                            call(sendMsgRequestBean.toUserId, sendMsgRequestBean.fromUserId);//建立通話
                                                        }
                                                    }, "拒絕", new View.OnClickListener() {
                                                        @Override
                                                        public void onClick(View v) {
                                                            NetBean.SendMsgResponseBean sendMsgResponseBean = new NetBean.SendMsgResponseBean(Constant.Message_Call_Result_Code, sendMsgRequestBean.sequenceId, sendMsgRequestBean.toUserId, sendMsgRequestBean.fromUserId, false, "拒絕接聽");
                                                            NetBean.SendMsgRequestBean sendMsgRequestBean1 = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, sendMsgRequestBean.sequenceId, sendMsgRequestBean.toUserId, sendMsgRequestBean.fromUserId, GsonUtil.BeanToJson(sendMsgResponseBean));
                                                            RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean1));
                                                        }
                                                    });
                                            break;
                                        case Constant.Message_SDP_Offer_Code:
                                            Log.e(TAG, "--------onRecive: Message_SDP_Offer_Code 1");
                                            NetBean.SdpMessage sdpMessage = GsonUtil.GsonToBean(sendMsgRequestBean.message, NetBean.SdpMessage.class);
                                            receive(sdpMessage.description, sendMsgRequestBean.fromUserId, sendMsgRequestBean.toUserId);
                                            break;
                                        case Constant.Message_SDP_Answer_Code:
                                            Log.e(TAG, "--------onRecive: Message_SDP_Answer_Code 2");
                                            sdpMessage = GsonUtil.GsonToBean(sendMsgRequestBean.message, NetBean.SdpMessage.class);
                                            SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdpMessage.description);
                                            //11. receive sdp setRemoteDescription
                                            callPeerConnection.setRemoteDescription(new SdpAdapter("setRemoteDescription"), sessionDescription);
                                            break;
                                        case Constant.Message_IceCandidate_Request_Code:
                                            Log.e(TAG, "--------onRecive: Message_IceCandidate_Request_Code 3");
                                            NetBean.IceCandidateMessage iceCandidateMessage = GsonUtil.GsonToBean(sendMsgRequestBean.message, NetBean.IceCandidateMessage.class);
                                            IceCandidate iceCandidate = new IceCandidate(iceCandidateMessage.sdpMid, iceCandidateMessage.sdpMLineIndex, iceCandidateMessage.sdp);
                                            //14.receive  iceCandidate
                                            receivePeerConnection.addIceCandidate(iceCandidate);
                                            break;
                                        case Constant.Message_IceCandidate_Response_Code:
                                            Log.e(TAG, "--------onRecive: Message_IceCandidate_Response_Code 4");
                                            iceCandidateMessage = GsonUtil.GsonToBean(sendMsgRequestBean.message, NetBean.IceCandidateMessage.class);
                                            iceCandidate = new IceCandidate(iceCandidateMessage.sdpMid, iceCandidateMessage.sdpMLineIndex, iceCandidateMessage.sdp);
                                            //16.receive iceCandidate addIceCandidate
                                            callPeerConnection.addIceCandidate(iceCandidate);
                                            break;
                                    }
                                    callBackEvent(json);
                                    break;
                                case Constant.Login_Code://登錄通知
                                    callBackEvent(json);
                                    break;
                                case Constant.OFFLine_Code://斷線通知
                                    NetBean.OffLineResponseBean offLineResponseBean = GsonUtil.GsonToBean(json, NetBean.OffLineResponseBean.class);
                                    Toast.makeText(MainActivity.this, offLineResponseBean.message, Toast.LENGTH_SHORT).show();
                                    break;
                                case Constant.OnLineUserInfo_Code://登錄在線人源信息通知
                                    NetBean.OnLineUserInfoResponseBean onLineUserInfoResponseBean = GsonUtil.GsonToBean(json, NetBean.OnLineUserInfoResponseBean.class);
                                    List<String> userIdList = onLineUserInfoResponseBean.userIdList;
                                    StringBuilder sb = new StringBuilder();
                                    for (int i = 0; i < userIdList.size(); i++) {
                                        String userId = userIdList.get(i);
                                        if (userId.equals(etLoginUserId.getText().toString()))
                                            sb.append(userId + "(自己)\n");
                                        else
                                            sb.append(userId + "\n");
                                    }
                                    etUserInfo.setText(sb.toString());
                                    break;
                            }
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onClose(int code, String reason, boolean remote) {
                        Log.e(TAG, "onClose: reason=" + reason);
                    }
                });
            }
        });
        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (TextUtils.isEmpty(etLoginUserId.getText().toString())) {
                    Toast.makeText(MainActivity.this, "請先輸入登錄UserId", Toast.LENGTH_SHORT).show();
                    return;
                }
                NetBean.LoginRequsetBean loginRequsetBean = new NetBean.LoginRequsetBean(Constant.Login_Code, System.currentTimeMillis() + "", etLoginUserId.getText().toString());
                RtcWebSokcetHelper.getInstance().putWebsocketDealEvent(GsonUtil.BeanToJson(loginRequsetBean), new RtcWebSokcetHelper.Runnable() {
                    @Override
                    public void run() {
                        String responseJson = websocketDealEvent.responseJson;
                        NetBean.LoginResponseBean loginResponseBean = GsonUtil.GsonToBean(responseJson, NetBean.LoginResponseBean.class);
                        if (loginResponseBean.isSucceed) {
                            Toast.makeText(MainActivity.this, "登錄成功", Toast.LENGTH_SHORT).show();
                        } else {
                            Toast.makeText(MainActivity.this, "登錄失敗祟牲," + loginResponseBean.message, Toast.LENGTH_SHORT).show();
                        }
                    }
                });
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(loginRequsetBean));
            }
        });
        btnCall.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String fromUserId = etLoginUserId.getText().toString();
                String toUserId = etCallUserId.getText().toString();
                if (TextUtils.isEmpty(fromUserId) || TextUtils.isEmpty(toUserId)) {
                    Toast.makeText(MainActivity.this, "請先填寫fromUserId,toUserId", Toast.LENGTH_SHORT).show();
                    return;
                }
                String message = GsonUtil.GsonToString(new NetBean.NormalMessage(Constant.Message_Call_Code));
                NetBean.SendMsgRequestBean sendMsgRequestBean = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, System.currentTimeMillis() + "", fromUserId, toUserId, message);
                RtcWebSokcetHelper.getInstance().putWebsocketDealEvent(GsonUtil.BeanToJson(sendMsgRequestBean), new RtcWebSokcetHelper.Runnable() {
                    @Override
                    public void run() {
                        String responseJson = websocketDealEvent.responseJson;
                        try {
                            responseJson = new JSONObject(websocketDealEvent.responseJson).getString("message");
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        NetBean.SendMsgResponseBean sendMsgResponseBean = GsonUtil.GsonToBean(responseJson, NetBean.SendMsgResponseBean.class);
                        if (sendMsgResponseBean.isSucceed) {
                            Log.e(TAG, "run: 呼叫成功");
                            Toast.makeText(MainActivity.this, "呼叫成功", Toast.LENGTH_SHORT).show();
                        } else {
                            Log.e(TAG, "run: 呼叫失敗");
                            Toast.makeText(MainActivity.this, "呼叫失敗," + sendMsgResponseBean.message, Toast.LENGTH_SHORT).show();
                        }
                    }
                });
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean));
            }
        });
    }

    private void callBackEvent(String json) {
        RtcWebSokcetHelper.WebsocketDealEvent websocketDealEvent;
        websocketDealEvent = RtcWebSokcetHelper.getInstance().getWebsocketDealEvent(json);
        if (websocketDealEvent != null && websocketDealEvent.dealRunnable != null) {
            websocketDealEvent.responseJson = json;
            websocketDealEvent.dealRunnable.run();
        }
        RtcWebSokcetHelper.getInstance().removeWebsocketDealEvent(json);
    }

    /**
     * Create local audio track
     *
     * @return AudioTrack
     */
    public AudioTrack createAudioTrack() {
        AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
        WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
        WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true);

        AudioTrack audioTrack = peerConnectionFactory.createAudioTrack("200", audioSource);
        audioTrack.setEnabled(true);
        mMediaStream.addTrack(audioTrack);
        return audioTrack;
    }

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

    //建立WebRtc通話
    private void call(String fromUserId, String toUserId) {
        List<PeerConnection.IceServer> iceServers = new ArrayList<>();//turn/sTurn服務(wù)器集合
        PeerConnection.IceServer iceServer1 = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
                .createIceServer();
        PeerConnection.IceServer iceServer2 = PeerConnection.IceServer.builder("stun:stun.ekiga.net")
                .createIceServer();
        PeerConnection.IceServer iceServer3 = PeerConnection.IceServer.builder("stun:stun.schlund.de")
                .createIceServer();
        PeerConnection.IceServer iceServer4 = PeerConnection.IceServer.builder("stun:stun.voxgratia.org")
                .createIceServer();
        PeerConnection.IceServer iceServer5 = PeerConnection.IceServer.builder("turn:1.117.194.160:3478?transport=tcp")
                .setUsername("test")
                .setPassword("123456")
                .createIceServer();
        PeerConnection.IceServer iceServer6 = PeerConnection.IceServer.builder("turn:1.117.194.160:3478?transport=udp")
                .setUsername("test")
                .setPassword("123456")
                .createIceServer();
        iceServers.add(iceServer1);
        iceServers.add(iceServer2);
        iceServers.add(iceServer3);
        iceServers.add(iceServer4);
        iceServers.add(iceServer5);
        iceServers.add(iceServer6);
        //1.createPeerConnection
        callPeerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("localconnection") {
            //12.onIceCandidate
            @Override
            public void onIceCandidate(IceCandidate iceCandidate) {
                super.onIceCandidate(iceCandidate);
//                String sdpMid;
//                int sdpMLineIndex;
//                String sdp; //todo---WS轉(zhuǎn)發(fā)
                //13.send iceCandidate
                String message = GsonUtil.GsonToString(new NetBean.IceCandidateMessage(Constant.Message_IceCandidate_Request_Code, iceCandidate.sdpMid, iceCandidate.sdpMLineIndex, iceCandidate.sdp));
                NetBean.SendMsgRequestBean sendMsgRequestBean = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, System.currentTimeMillis() + "", fromUserId, toUserId, message);
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean));
            }

            @Override
            public void onAddStream(MediaStream mediaStream) {
                super.onAddStream(mediaStream);
                VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
                runOnUiThread(() -> {
                    remoteVideoTrack.addSink(remoteView);//展示對(duì)端的視頻
                });
            }
        });
        //2.addStream
        callPeerConnection.addStream(mMediaStream);//添加流
        //3.createOffer
        callPeerConnection.createOffer(new SdpAdapter("local offer sdp") {//發(fā)送offer
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                super.onCreateSuccess(sessionDescription);
                //4.setLocalDescription
                callPeerConnection.setLocalDescription(new SdpAdapter("local set local"), sessionDescription);//服務(wù)器中轉(zhuǎn)設(shè)置會(huì)話描述
                //5.send sdp
                //  String description;todo---WS發(fā)送
                String message = GsonUtil.GsonToString(new NetBean.SdpMessage(Constant.Message_SDP_Offer_Code, sessionDescription.description));
                NetBean.SendMsgRequestBean sendMsgRequestBean = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, System.currentTimeMillis() + "", fromUserId, toUserId, message);
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean));
            }
        }, new MediaConstraints());

//        //11. receive sdp setRemoteDescription
//        callPeerConnection.setRemoteDescription(new SdpAdapter("setRemoteDescription"), null);
        //16.receive iceCandidate addIceCandidate
//        callPeerConnection.addIceCandidate(null);
    }

    private void receive(String description, String fromUserId, String toUserId) {
        List<PeerConnection.IceServer> iceServers = new ArrayList<>();//turn/sTurn服務(wù)器集合
        PeerConnection.IceServer iceServer1 = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
                .createIceServer();
        PeerConnection.IceServer iceServer2 = PeerConnection.IceServer.builder("stun:stun.ekiga.net")
                .createIceServer();
        PeerConnection.IceServer iceServer3 = PeerConnection.IceServer.builder("stun:stun.schlund.de")
                .createIceServer();
        PeerConnection.IceServer iceServer4 = PeerConnection.IceServer.builder("stun:stun.voxgratia.org")
                .createIceServer();
        PeerConnection.IceServer iceServer5 = PeerConnection.IceServer.builder("turn:1.117.194.160:3478?transport=tcp")
                .setUsername("test")
                .setPassword("123456")
                .createIceServer();
        PeerConnection.IceServer iceServer6 = PeerConnection.IceServer.builder("turn:1.117.194.160:3478?transport=udp")
                .setUsername("test")
                .setPassword("123456")
                .createIceServer();
        iceServers.add(iceServer1);
        iceServers.add(iceServer2);
        iceServers.add(iceServer3);
        iceServers.add(iceServer4);
        iceServers.add(iceServer5);
        iceServers.add(iceServer6);
        //6.createPeerConnection
        receivePeerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnectionAdapter("localconnection") {
            @Override
            public void onIceCandidate(IceCandidate iceCandidate) {
                super.onIceCandidate(iceCandidate);
//                String sdpMid;
//                int sdpMLineIndex;
//                String sdp; //todo---WS轉(zhuǎn)發(fā)
                //15.send iceCandidate
                String message = GsonUtil.GsonToString(new NetBean.IceCandidateMessage(Constant.Message_IceCandidate_Response_Code, iceCandidate.sdpMid, iceCandidate.sdpMLineIndex, iceCandidate.sdp));
                NetBean.SendMsgRequestBean sendMsgRequestBean = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, System.currentTimeMillis() + "", toUserId, fromUserId, message);
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean));

            }

            @Override
            public void onAddStream(MediaStream mediaStream) {
                super.onAddStream(mediaStream);
                VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
                runOnUiThread(() -> {
                    remoteVideoTrack.addSink(remoteView);//展示對(duì)端的視頻
                });
            }
        });
        receivePeerConnection.addStream(mMediaStream);//添加流
        SessionDescription fromSdp = new SessionDescription(SessionDescription.Type.OFFER, description);
        //7.setRemoteDescription
        receivePeerConnection.setRemoteDescription(new SdpAdapter("Remote"), fromSdp);//被叫方設(shè)置會(huì)話描述
        //8.createAnswer
        receivePeerConnection.createAnswer(new SdpAdapter("remote answer sdp") {//發(fā)送sdp
            @Override
            public void onCreateSuccess(SessionDescription sdp) {
                super.onCreateSuccess(sdp);
                //9.setLocalDescription
                receivePeerConnection.setLocalDescription(new SdpAdapter("Local"), sdp);//服務(wù)器中轉(zhuǎn)設(shè)置會(huì)話描述
                //10.發(fā)送sdp給對(duì)方 對(duì)方設(shè)置遠(yuǎn)端sdp
                String message = GsonUtil.GsonToString(new NetBean.SdpMessage(Constant.Message_SDP_Answer_Code, sdp.description));
                NetBean.SendMsgRequestBean sendMsgRequestBean = new NetBean.SendMsgRequestBean(Constant.SendMsg_Code, System.currentTimeMillis() + "", toUserId, fromUserId, message);
                RtcWebSokcetHelper.getInstance().sendRequest(GsonUtil.BeanToJson(sendMsgRequestBean));
            }
        }, new MediaConstraints());
//        //14.receive  iceCandidate
//        receivePeerConnection.addIceCandidate(null);

    }


    private void initView() {
        etWsUrl = (EditText) findViewById(R.id.et_wsUrl);
        btnConnectSever = (Button) findViewById(R.id.btn_connectSever);
        etLoginUserId = (EditText) findViewById(R.id.et_loginUserId);
        btnLogin = (Button) findViewById(R.id.btn_login);
        etUserInfo = (EditText) findViewById(R.id.et_userInfo);
        etCallUserId = (EditText) findViewById(R.id.et_callUserId);
        btnCall = (Button) findViewById(R.id.btn_call);
        localView = (SurfaceViewRenderer) findViewById(R.id.localView);
        remoteView = (SurfaceViewRenderer) findViewById(R.id.remoteView);
    }

    private void showCustomeDialog(String title, String leftButtonText, View.OnClickListener leftOnClickListener, String rightButtonText, View.OnClickListener rightOnClickListener) {
        Dialog dialog = new Dialog(this);
        ConstraintLayout constraintLayout = new ConstraintLayout(this);
        constraintLayout.setLayoutParams(new ConstraintLayout.LayoutParams(1000, 600));
        TextView textView = new TextView(this);
        textView.setText(title);
        textView.setTextSize(20);
        constraintLayout.addView(textView);
        ConstraintLayout.LayoutParams textViewLayoutParams = new ConstraintLayout.LayoutParams(-2, -2);
        textViewLayoutParams.startToStart = 0;
        textViewLayoutParams.endToEnd = 0;
        textViewLayoutParams.topToTop = 0;
        textViewLayoutParams.topMargin = 80;
        textView.setLayoutParams(textViewLayoutParams);
        Button leftButton = new Button(this);
        Button rightButton = new Button(this);
        leftButton.setText(leftButtonText);
        rightButton.setText(rightButtonText);
        leftButton.setTextSize(20);
        rightButton.setTextSize(20);
        constraintLayout.addView(leftButton);
        constraintLayout.addView(rightButton);
        ConstraintLayout.LayoutParams leftButtonLayoutParams = new ConstraintLayout.LayoutParams(350, 150);
        ConstraintLayout.LayoutParams rightButtonLayoutParams = new ConstraintLayout.LayoutParams(350, 150);
        leftButtonLayoutParams.startToStart = 0;
        leftButtonLayoutParams.bottomToBottom = 0;
        rightButtonLayoutParams.endToEnd = 0;
        rightButtonLayoutParams.bottomToBottom = 0;
        leftButton.setLayoutParams(leftButtonLayoutParams);
        rightButton.setLayoutParams(rightButtonLayoutParams);
        leftButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (leftOnClickListener != null) {
                    leftOnClickListener.onClick(v);
                }
                dialog.dismiss();
            }
        });
        rightButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (rightOnClickListener != null) {
                    rightOnClickListener.onClick(v);
                }
                dialog.dismiss();
            }
        });
        dialog.setContentView(constraintLayout);
        dialog.getWindow().setLayout(1000, 600);
        dialog.show();
    }


}
@ServerEndpoint("/ws")
@Component
public class WebSocketServer extends BaseWebSocketServer {

    @OnOpen
    public void onOpen(Session session) {
        Logger.e("onOpen ");
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        Logger.e("onMessage:message=" + message);
        int code = (int) getJsonValue(message, "code");
        switch (code) {
            case Constant.Login_Code://登錄
                NetBean.LoginRequsetBean wsRequestBean = GsonUtil.GsonToBean(message, NetBean.LoginRequsetBean.class);
                if (getSession(wsRequestBean.userId) != null) {
                    //發(fā)送斷線通知
                    NetBean.LoginResponseBean wsResponseBean = new NetBean.LoginResponseBean(Constant.OFFLine_Code, wsRequestBean.sequenceId, false, "該賬戶被其他用戶登錄");
                    sendMessage(getSession(wsRequestBean.userId), GsonUtil.BeanToJson(wsResponseBean));
                    removeSession(wsRequestBean.userId);
                }
                if (session.isOpen())
                    putSession(wsRequestBean.userId, session);
                //登錄響應(yīng)
                sendMessage(session, GsonUtil.BeanToJson(new NetBean.LoginResponseBean(wsRequestBean.code, wsRequestBean.sequenceId, true, "登錄成功,當(dāng)前在線人數(shù):" + getSessionSize())));
                //每次有人登錄起意,廣播所有人當(dāng)前在線用戶信息
                broadcastAllSession(GsonUtil.BeanToJson(new NetBean.OnLineUserInfoResponseBean(Constant.OnLineUserInfo_Code, getUserIdList())));
                Logger.e(wsRequestBean.userId + "登錄成功");
                break;
            case Constant.SendMsg_Code://轉(zhuǎn)發(fā)消息
                Logger.e("SendMsg_Code------"+message);
                NetBean.SendMsgRequestBean sendMsgRequestBean = GsonUtil.GsonToBean(message, NetBean.SendMsgRequestBean.class);
                if (getSession(sendMsgRequestBean.toUserId) == null) {
                    Logger.e("SendMsg_Code------該用戶不在線");
                    sendMessage(session, GsonUtil.BeanToJson(new NetBean.SendMsgResponseBean(Constant.SendMsg_Code, sendMsgRequestBean.sequenceId, sendMsgRequestBean.fromUserId, sendMsgRequestBean.toUserId, false, "該用戶不在線")));
                    return;
                }
                Logger.e("SendMsg_Code------getSession成功");
                Session session1 = getSession(sendMsgRequestBean.toUserId);
                Logger.e("SendMsg_Code------sendMessage成功");
                sendMessage(session1, message);
                break;
            default://直接斷線
                if (session.isOpen()) {
                    try {
                        session.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
    }

    @OnClose
    public void onClose(Session session) {
        Logger.e("onClose");
        removeSession(session);
        //有人斷線,廣播所有人當(dāng)前在線用戶信息
        broadcastAllSession(GsonUtil.BeanToJson(new NetBean.OnLineUserInfoResponseBean(Constant.OnLineUserInfo_Code, getUserIdList())));
    }

    @OnError
    public void onError(Session session, Throwable error) {
        Logger.e("onError:" + error.getMessage());
    }
}

了解過WebRtc技術(shù)的大家都知道病瞳,他是一個(gè)點(diǎn)對(duì)點(diǎn)(p2p)揽咕,解決端到端之間悲酷,直接進(jìn)行一個(gè)數(shù)據(jù)傳輸?shù)募夹g(shù),這樣可以節(jié)省很大的服務(wù)器帶寬成本心褐√蛳眩可是我們都知道生活中常用的手機(jī)或電腦,他們一般情況下并沒有一個(gè)固定的公網(wǎng)ip逗爹,然而這項(xiàng)技術(shù)剛好可以一定程度上解決這個(gè)問題。他的關(guān)鍵是利用了udp打洞技術(shù)(這個(gè)其實(shí)我以前也研究過嚎于,大概原理就是利用一個(gè)公網(wǎng)服務(wù)器掘而,兩個(gè)客戶端都給這個(gè)公網(wǎng)服務(wù)器發(fā)數(shù)據(jù)包,然后公網(wǎng)服務(wù)器于购,獲取經(jīng)過層層轉(zhuǎn)換的公網(wǎng)ip和端口號(hào)袍睡,并告訴兩個(gè)客戶端對(duì)方的公網(wǎng)ip端口信息,然后兩個(gè)客戶端同時(shí)不斷給另一個(gè)客戶端發(fā)數(shù)據(jù)肋僧,理論上這樣就可以成功打洞了斑胜,但是我測試時(shí)不知道是不是忽略了別的什么因素(防火墻關(guān)了,兩個(gè)不同網(wǎng)絡(luò)的客戶端也試了嫌吠,或者是當(dāng)時(shí)寬帶nat網(wǎng)絡(luò)轉(zhuǎn)換未達(dá)到要求)止潘,最后也沒成功打洞,無耐辫诅,暫時(shí)放棄)

首先我們需要一個(gè)信令服務(wù)器凭戴,說白了就是個(gè)中間人,用來交換打洞通信需要的一些會(huì)話數(shù)據(jù)炕矮,理論上你用websocket么夫,socket,http實(shí)現(xiàn)都可以肤视。這里我是用websocket實(shí)現(xiàn)的档痪,里面主要的作用就是讓指定兩個(gè)客戶端可以互相通信,互發(fā)SessionDescription(SDP信息)和IceCandidate邢滑,這兩個(gè)是webrtc中通信的關(guān)鍵信息腐螟。然后他還需要有多個(gè)公網(wǎng)ip的sturn服務(wù)器或者多添加幾個(gè)sturn服務(wù)器地址也可以(俗話中繼服務(wù)器:就是獲取外網(wǎng)ip用的),如果遇到打洞失敗的情況怎么辦殊鞭,那我們再用一個(gè)turn服務(wù)器(這就是個(gè)數(shù)據(jù)中轉(zhuǎn)服務(wù)器遭垛,不過這個(gè)一般都得自己搭建,網(wǎng)上基本沒免費(fèi)讓大家測試使用的(畢竟如果打洞不成功用到這個(gè)服務(wù)器會(huì)很占帶寬)操灿,我代碼中有一個(gè)我已經(jīng)搭好的turn服務(wù)器地址锯仪,有需要的可以給大家測試試試,這里最好要用火狐瀏覽器趾盐,谷歌的根本不行庶喜,測試sturn/turn服務(wù)器有沒有配置成功的網(wǎng)站地址https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/)小腊,如果打洞失敗,只能用這個(gè)服務(wù)器轉(zhuǎn)發(fā)數(shù)據(jù)了久窟。

最后貼上一張網(wǎng)上搜到的webrtc流程圖

流程圖.png

最后再上一張視頻通話效果圖

演示.gif

完整代碼地址(Android和web服務(wù)器代碼)

https://github.com/dxh104/WebRtcDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秩冈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子斥扛,更是在濱河造成了極大的恐慌入问,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稀颁,死亡現(xiàn)場離奇詭異芬失,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)匾灶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門棱烂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人阶女,你說我怎么就攤上這事颊糜。” “怎么了秃踩?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵衬鱼,是天一觀的道長。 經(jīng)常有香客問我吞瞪,道長馁启,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任芍秆,我火速辦了婚禮惯疙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘妖啥。我一直安慰自己霉颠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布荆虱。 她就那樣靜靜地躺著蒿偎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怀读。 梳的紋絲不亂的頭發(fā)上诉位,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音菜枷,去河邊找鬼苍糠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛啤誊,可吹牛的內(nèi)容都是我干的岳瞭。 我是一名探鬼主播拥娄,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼瞳筏!你這毒婦竟也來了稚瘾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤姚炕,失蹤者是張志新(化名)和其女友劉穎摊欠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柱宦,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凄硼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捷沸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狐史,死狀恐怖痒给,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情骏全,我是刑警寧澤苍柏,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站姜贡,受9級(jí)特大地震影響试吁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楼咳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一熄捍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧母怜,春花似錦余耽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至轨域,卻和暖如春袱耽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背干发。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國打工朱巨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铐然。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓蔬崩,卻偏偏與公主長得像恶座,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子沥阳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容