一篇文章教会你---怎样实现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;//本地摄像头预览(本地视频)
    private SurfaceViewRenderer remoteView;//服务器传来的摄像头预览(对端视频)

    @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权限
            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 {
                //已经有权限
                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] + "权限未打开", Toast.LENGTH_SHORT).show();
                finish();
                return;
            }
        }
        //已经有权限
        havePermission();
    }


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

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

        //创建EglBase对象 并获取上下文环境
        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.创建p2p连接工厂
        peerConnectionFactory = PeerConnectionFactory.builder()
                .setOptions(options)
                .setVideoEncoderFactory(defaultVideoEncoderFactory)
                .setVideoDecoderFactory(defaultVideoDecoderFactory)
//                .setAudioDeviceModule(audioDeviceModule)
                .createPeerConnectionFactory();
        //3.创建SurfaceTextureHelper
        SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBaseContext);
        //4.创建视频捕获器
        VideoCapturer videoCapturer = createCameraCapturer(false);//是否正面摄像头
        //5.创建视频源
        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.创建视频轨道
        VideoTrack videoTrack = peerConnectionFactory.createVideoTrack("100", videoSource);
        videoTrack.addSink(localView);//展示本地视频
        //9.创建本地媒体流
        mMediaStream = peerConnectionFactory.createLocalMediaStream("mMediaStream");
        mMediaStream.addTrack(videoTrack);//媒体流添加视频轨道
        mMediaStream.addTrack(createAudioTrack());//媒体流添加音频轨道
        //连接websocket服务
        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, "服务连接成功", 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://收到转发消息
                                    NetBean.SendMsgRequestBean sendMsgRequestBean = GsonUtil.GsonToBean(json, NetBean.SendMsgRequestBean.class);
                                    if (sendMsgRequestBean.message.contains("不在线")) {
                                        Toast.makeText(MainActivity.this, "目标用户不在线", 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服务器集合
        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转发
                //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);//展示对端的视频
                });
            }
        });
        //2.addStream
        callPeerConnection.addStream(mMediaStream);//添加流
        //3.createOffer
        callPeerConnection.createOffer(new SdpAdapter("local offer sdp") {//发送offer
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                super.onCreateSuccess(sessionDescription);
                //4.setLocalDescription
                callPeerConnection.setLocalDescription(new SdpAdapter("local set local"), sessionDescription);//服务器中转设置会话描述
                //5.send sdp
                //  String description;todo---WS发送
                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服务器集合
        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转发
                //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);//展示对端的视频
                });
            }
        });
        receivePeerConnection.addStream(mMediaStream);//添加流
        SessionDescription fromSdp = new SessionDescription(SessionDescription.Type.OFFER, description);
        //7.setRemoteDescription
        receivePeerConnection.setRemoteDescription(new SdpAdapter("Remote"), fromSdp);//被叫方设置会话描述
        //8.createAnswer
        receivePeerConnection.createAnswer(new SdpAdapter("remote answer sdp") {//发送sdp
            @Override
            public void onCreateSuccess(SessionDescription sdp) {
                super.onCreateSuccess(sdp);
                //9.setLocalDescription
                receivePeerConnection.setLocalDescription(new SdpAdapter("Local"), sdp);//服务器中转设置会话描述
                //10.发送sdp给对方 对方设置远端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) {
                    //发送断线通知
                    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);
                //登录响应
                sendMessage(session, GsonUtil.BeanToJson(new NetBean.LoginResponseBean(wsRequestBean.code, wsRequestBean.sequenceId, true, "登录成功,当前在线人数:" + getSessionSize())));
                //每次有人登录,广播所有人当前在线用户信息
                broadcastAllSession(GsonUtil.BeanToJson(new NetBean.OnLineUserInfoResponseBean(Constant.OnLineUserInfo_Code, getUserIdList())));
                Logger.e(wsRequestBean.userId + "登录成功");
                break;
            case Constant.SendMsg_Code://转发消息
                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);
        //有人断线,广播所有人当前在线用户信息
        broadcastAllSession(GsonUtil.BeanToJson(new NetBean.OnLineUserInfoResponseBean(Constant.OnLineUserInfo_Code, getUserIdList())));
    }

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

了解过WebRtc技术的大家都知道,他是一个点对点(p2p),解决端到端之间,直接进行一个数据传输的技术,这样可以节省很大的服务器带宽成本。可是我们都知道生活中常用的手机或电脑,他们一般情况下并没有一个固定的公网ip,然而这项技术刚好可以一定程度上解决这个问题。他的关键是利用了udp打洞技术(这个其实我以前也研究过,大概原理就是利用一个公网服务器,两个客户端都给这个公网服务器发数据包,然后公网服务器,获取经过层层转换的公网ip和端口号,并告诉两个客户端对方的公网ip端口信息,然后两个客户端同时不断给另一个客户端发数据,理论上这样就可以成功打洞了,但是我测试时不知道是不是忽略了别的什么因素(防火墙关了,两个不同网络的客户端也试了,或者是当时宽带nat网络转换未达到要求),最后也没成功打洞,无耐,暂时放弃)

首先我们需要一个信令服务器,说白了就是个中间人,用来交换打洞通信需要的一些会话数据,理论上你用websocket,socket,http实现都可以。这里我是用websocket实现的,里面主要的作用就是让指定两个客户端可以互相通信,互发SessionDescription(SDP信息)和IceCandidate,这两个是webrtc中通信的关键信息。然后他还需要有多个公网ip的sturn服务器或者多添加几个sturn服务器地址也可以(俗话中继服务器:就是获取外网ip用的),如果遇到打洞失败的情况怎么办,那我们再用一个turn服务器(这就是个数据中转服务器,不过这个一般都得自己搭建,网上基本没免费让大家测试使用的(毕竟如果打洞不成功用到这个服务器会很占带宽),我代码中有一个我已经搭好的turn服务器地址,有需要的可以给大家测试试试,这里最好要用火狐浏览器,谷歌的根本不行,测试sturn/turn服务器有没有配置成功的网站地址https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/),如果打洞失败,只能用这个服务器转发数据了。

最后贴上一张网上搜到的webrtc流程图

流程图.png

最后再上一张视频通话效果图

演示.gif

完整代码地址(Android和web服务器代码)

https://github.com/dxh104/WebRtcDemo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容