Android中利用WebRTC搭建点对点视频通话方案

1,134 阅读9分钟

让我先深呼吸一下。其实从很早就开始看视频通话和直播相关的一些内容,但由于这些东西太深奥了,涉及到很多编解码的知识,让我们很多程序员都是可望而不可即。后来我看了一些关于WebRTC的资料,也了解了一些相关的用发,暂时感觉这方面的门槛稍微低了一些。所以我不自量力的搭建了一套视频通话的Demo,算是一个小小的入门吧 ,为今后如果更加深入的研究Android算是立一个Flag。好了,言归正传。今天我搭建的这套通话方案,是超超超级简化版的。为什么这么说,因为没有后台支撑,没有信令传递。说白了,就是单纯整P2P视频传输。有人就要说了,那我看你这个能得到什么。我觉得这是一个单纯学习的博客,我们还是要真正自己学习会了,从而结和自己应用的已有功能来实现相关的业务。比如,我们已经做了文字的聊天,要接入一个点对点视频通话,就没有必要再做对应的信令服务器了。

言归正传,开始WebRTC点对点视频直播方案的讲解。

再开始代码之前,我要先说一下关于信令的传递。第一个要传递的信令就是Candidate,这个是在我们创建PeerConnection的时候生成的。第二个要传递的信令是sdp,这个是在CreateOffer和CreateAnswer时生成的。这两个数据由于没有信令服务器,我们暂时使用生成后放入EditText,通过微信或其他通信工具发到另一方,另一方设置给自己的软件方式,实现这个功能。至于生产环境中要用的时候,我会在后面说明需要什么改造。

这里不一步一步说了,直接上代码。

1.导入依赖

implementation 'org.webrtc:google-webrtc:1.0.+'
implementation 'com.google.code.gson:gson:2.6.2'

这里第一个依赖是WebRTC的依赖,是必须的。第二个是Gson的依赖,这个不是必须的,主要是方便我这里数据的传递。

2.最低支持版本修改

minSdkVersion 21

3.权限设置

清单文件中加入如下权限

<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"
    tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.NFC"/>
<!-- 局域网通信临时用-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_LOGS"
    tools:ignore="ProtectedPermissions" />

别忘了在Manifest节点下加入

xmlns:tools="http://schemas.android.com/tools"

4.接着我们搭建一下布局文件

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbars="none"
    android:background="@android:color/white">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:background="@android:color/white">

        <org.webrtc.SurfaceViewRenderer
            android:id="@+id/LocalSurfaceView"
            android:layout_width="match_parent"
            android:layout_height="400dp"/>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@android:color/white" />

        <org.webrtc.SurfaceViewRenderer
            android:id="@+id/RemoteSurfaceView"
            android:layout_width="match_parent"
            android:layout_height="400dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/et_offer"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_offer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置offer" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/et_answer"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_answer"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置answer" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate1"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate2"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate3"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate4"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate5"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate5"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:orientation="horizontal">

            <EditText
                android:id="@+id/tv_candicate6"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <Button
                android:id="@+id/bt_candidate6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="设置candidate" />
        </LinearLayout>
    </LinearLayout>
</ScrollView>

这里有很多输入框EditText和Button,大家可以不要理会,是我设置Candidate和sdp用的。真实项目中不需要。大家主要注意一下org.webrtc.SurfaceViewRenderer控件。这里我写了两个。这个控件就是完了要承载我们本地视频和远端视频的帧布局控件了。视频布局具体什么样子,只要改变这两个控件大小就好。

5.MainActivity即通话页面的全局变量

private EglBase mRootEglBase;
private SurfaceViewRenderer mLocalSurfaceView;
private SurfaceViewRenderer mRemoteSurfaceView;
private PeerConnectionFactory mPeerConnectionFactory;
private VideoCapturer mVideoCapturer;
private SurfaceTextureHelper mSurfaceTextureHelper;
private VideoTrack mVideoTrack;
private AudioTrack mAudioTrack;
public static final String VIDEO_TRACK_ID = "1";//"ARDAMSv0";
public static final String AUDIO_TRACK_ID = "2";//"ARDAMSa0";
private static final int VIDEO_RESOLUTION_WIDTH = 1280;
private static final int VIDEO_RESOLUTION_HEIGHT = 720;
private static final int VIDEO_FPS = 30;
private PeerConnection mPeerConnection;
private AudioManager audioManager;

private static String TAG = "aaaaaaaaaaaaaaaaaaaa";
private EditText et_offer;
private Button bt_offer;
private EditText et_answer;
private Button bt_answer;
private TextView tv_candicate1;
private TextView tv_candicate2;
private TextView tv_candicate3;
private TextView tv_candicate4;
private TextView tv_candicate5;
private TextView tv_candicate6;
private int count = 0;
private Button bt_candidate1;
private Button bt_candidate2;
private Button bt_candidate3;
private Button bt_candidate4;
private Button bt_candidate5;
private Button bt_candidate6;

空行上方是必备的WebRTC搭建相关的属性,空行下方是该Demo使用的一些属性,和视频通话没有关系。

6.接着我们要在该页面用自己的方法申请动态权限,这里我提供用郭霖大神的框架请求动态权限的一个方案,大家可以用自己项目里自带的 。

添加框架依赖

implementation 'com.permissionx.guolindev:permission-support:1.4.0'

在OnCreate中调用

PermissionX.init(this)
                .permissions(Manifest.permission.READ_PHONE_STATE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.CAMERA,
                        Manifest.permission.RECORD_AUDIO
                )
                .onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {
                    @Override
                    public void onExplainReason(ExplainScope scope, List<String> deniedList, boolean beforeRequest) {
                        scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白");
                    }
                })
                .onForwardToSettings(new ForwardToSettingsCallback() {
                    @Override
                    public void onForwardToSettings(ForwardScope scope, List<String> deniedList) {
                        scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白");
                    }
                })
                .request(new RequestCallback() {
                    @Override
                    public void onResult(boolean allGranted, List<String> grantedList, List<String> deniedList) {
                        if (allGranted) {
                            // TODO: 2020/10/14 权限申请成功
                        } else {
                            Toast.makeText(MainActivity.this, "您拒绝了如下权限:" + deniedList, Toast.LENGTH_SHORT).show();
                        }
                    }
                });

TODO的地方是一会儿我们要进行下一步的代码编写的地方。

这里主要申请了五个动态权限。

7.在Activity中添加如下代码

 /**
     * 开始webRtc的初始化
     */
    private void init() {
        //初始化视频必要对象
        mRootEglBase = EglBase.create();
        //两个视频画面
        mLocalSurfaceView = findViewById(R.id.LocalSurfaceView);
        mRemoteSurfaceView = findViewById(R.id.RemoteSurfaceView);
        et_offer = findViewById(R.id.et_offer);
        bt_offer = findViewById(R.id.bt_offer);
        et_answer = findViewById(R.id.et_answer);
        bt_answer = findViewById(R.id.bt_answer);
        tv_candicate1 = findViewById(R.id.tv_candicate1);
        tv_candicate2 = findViewById(R.id.tv_candicate2);
        tv_candicate3 = findViewById(R.id.tv_candicate3);
        tv_candicate4 = findViewById(R.id.tv_candicate4);
        tv_candicate5 = findViewById(R.id.tv_candicate5);
        tv_candicate6 = findViewById(R.id.tv_candicate6);
        bt_candidate1 = findViewById(R.id.bt_candidate1);
        bt_candidate2 = findViewById(R.id.bt_candidate2);
        bt_candidate3 = findViewById(R.id.bt_candidate3);
        bt_candidate4 = findViewById(R.id.bt_candidate4);
        bt_candidate5 = findViewById(R.id.bt_candidate5);
        bt_candidate6 = findViewById(R.id.bt_candidate6);
        //初始化本地视频界面
        mLocalSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
        mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        mLocalSurfaceView.setMirror(true);
        mLocalSurfaceView.setEnableHardwareScaler(false /* enabled */);
        //初始化对方视频界面
        mRemoteSurfaceView.init(mRootEglBase.getEglBaseContext(), null);
        mRemoteSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        mRemoteSurfaceView.setMirror(true);
        mRemoteSurfaceView.setEnableHardwareScaler(false /* enabled */);
        mRemoteSurfaceView.setZOrderMediaOverlay(true);
        //创建PeerConnectionFactory对象
        mPeerConnectionFactory = createPeerConnectionFactory(this);
        //日志输出设置
        Logging.enableLogToDebugOutput(Logging.Severity.LS_VERBOSE);
        //VideoCapturer对象初始化
        mVideoCapturer = createVideoCapturer();
        //相关视频源初始化,必备
        mSurfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
        VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);
        mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());
        //视频轨初始化,并设置到本地画布上
        mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
        mVideoTrack.setEnabled(true);
        mVideoTrack.addSink(mLocalSurfaceView);
        //音频轨初始化
        AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
        mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
        mAudioTrack.setEnabled(true);
        //初始化音频控制器
        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        setSpeakerphoneOn(true);
        bt_offer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                //接收到对放信令后的一个设置
                try {
                    String description = et_offer.getText().toString();
                    et_offer.setText("");
                    mPeerConnection.setRemoteDescription(
                            new SimpleSdpObserver() {
                                @Override
                                public void onSetSuccess() {
                                    Log.i(TAG, "set remote Description ok");
                                    doAnswerCall();
                                }

                                @Override
                                public void onSetFailure(String msg) {
                                    Log.i(TAG, "set remote Description unok");
                                }
                            },
                            new SessionDescription(
                                    SessionDescription.Type.OFFER,
                                    description));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        bt_answer.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String answer = et_answer.getText().toString();
                et_answer.setText("");
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                mPeerConnection.setRemoteDescription(
                        new SimpleSdpObserver() {
                            @Override
                            public void onSetSuccess() {
                            }

                            @Override
                            public void onSetFailure(String msg) {
                            }
                        },
                        new SessionDescription(
                                SessionDescription.Type.ANSWER,
                                answer));
            }
        });
        bt_candidate1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate1.getText().toString().trim();
                tv_candicate1.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        bt_candidate2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate2.getText().toString().trim();
                tv_candicate2.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        bt_candidate3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate3.getText().toString().trim();
                tv_candicate3.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        bt_candidate4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate4.getText().toString().trim();
                tv_candicate4.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        bt_candidate5.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate5.getText().toString().trim();
                tv_candicate5.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        bt_candidate6.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mPeerConnection == null) {
                    mPeerConnection = createPeerConnection();
                }
                String json = tv_candicate6.getText().toString().trim();
                tv_candicate6.setText("");
                CandidateBean candidateBean = new Gson().fromJson(json, CandidateBean.class);
                IceCandidate remoteIceCandidate =
                        new IceCandidate(candidateBean.getId(),
                                candidateBean.getLabel(),
                                candidateBean.getCandidate());
                mPeerConnection.addIceCandidate(remoteIceCandidate);
            }
        });
        mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, VIDEO_FPS);
        if (mPeerConnection == null) {
            mPeerConnection = createPeerConnection();
        }
//        doStartCall();
    }

    /**
     * 回复
     */
    public void doAnswerCall() {
        if (mPeerConnection == null) {
            mPeerConnection = createPeerConnection();
        }
        MediaConstraints sdpMediaConstraints = new MediaConstraints();
        mPeerConnection.createAnswer(new SimpleSdpObserver() {
            @Override
            public void onCreateSuccess(final SessionDescription sessionDescription) {
                mPeerConnection.setLocalDescription(new SimpleSdpObserver() {
                                                        @Override
                                                        public void onSetFailure(String msg) {
                                                        }

                                                        @Override
                                                        public void onSetSuccess() {
                                                            runOnUiThread(new Runnable() {
                                                                @Override
                                                                public void run() {
                                                                    et_answer.setText(sessionDescription.description);
                                                                }
                                                            });
                                                        }
                                                    },
                        sessionDescription);
            }

            @Override
            public void onCreateFailure(String msg) {
            }
        }, sdpMediaConstraints);
    }

    /**
     * 创建PeerConnectionFactory对象
     *
     * @param context
     * @return
     */
    public PeerConnectionFactory createPeerConnectionFactory(Context context) {
        final VideoEncoderFactory encoderFactory;
        final VideoDecoderFactory decoderFactory;
        encoderFactory = new DefaultVideoEncoderFactory(
                mRootEglBase.getEglBaseContext(),
                false /* enableIntelVp8Encoder */,
                true);
        decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
        PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context)
                .setEnableInternalTracer(true)
                .createInitializationOptions());
        PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder()
                .setVideoEncoderFactory(encoderFactory)
                .setVideoDecoderFactory(decoderFactory);
        builder.setOptions(null);
        return builder.createPeerConnectionFactory();
    }

    /**
     * 创建VideoCapturer对象
     *
     * @return
     */
    private VideoCapturer createVideoCapturer() {
        if (Camera2Enumerator.isSupported(this)) {
            return createCameraCapturer(new Camera2Enumerator(this));
        } else {
            return createCameraCapturer(new Camera1Enumerator(true));
        }
    }

    /**
     * 根据不同摄像头初始化VideoCapturer对象
     *
     * @param enumerator
     * @return
     */
    private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
        final String[] deviceNames = enumerator.getDeviceNames();
        for (String deviceName : deviceNames) {
            if (enumerator.isFrontFacing(deviceName)) {
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }
        for (String deviceName : deviceNames) {
            if (!enumerator.isFrontFacing(deviceName)) {
                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
                if (videoCapturer != null) {
                    return videoCapturer;
                }
            }
        }
        return null;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // TODO: 2021/1/6 缺一个挂电话的动作
        mLocalSurfaceView.release();
        mRemoteSurfaceView.release();
        mVideoCapturer.dispose();
        mSurfaceTextureHelper.dispose();
        PeerConnectionFactory.stopInternalTracingCapture();
        PeerConnectionFactory.shutdownInternalTracer();
        mPeerConnectionFactory.dispose();
    }

    @Override
    protected void onResume() {
        super.onResume();
//        mVideoCapturer.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, VIDEO_FPS);
//        //开始创建PeerConnection
//        //默认开启本地视频后就创建这个PeerConnection
//        if (mPeerConnection == null) {
//            mPeerConnection = createPeerConnection();
//        }
//        doStartCall();
    }

    /**
     * 创建PeerConnection
     *
     * @return
     */
    public PeerConnection createPeerConnection() {
        LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<PeerConnection.IceServer>();
//        PeerConnection.IceServer ice_server =
//                    PeerConnection.IceServer.builder("turn:xxxx:3478")
//                                            .setPassword("xxx")
//                                            .setUsername("xxx")
//                                            .createIceServer();
        PeerConnection.IceServer ice_server = PeerConnection
                .IceServer
                .builder("stun:stun.l.google.com:19302")
                .createIceServer();
        iceServers.add(ice_server);
        PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
        //TCP候选策略控制开关
        rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
        //只使用TCP中转模式,不使用p2p直连
        //rtcConfig.iceTransportsType = PeerConnection.IceTransportsType.RELAY;
        //rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
        //rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
        rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
        // Use ECDSA encryption.
        //rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
        // Enable DTLS for normal calls and disable for loopback calls.
        rtcConfig.enableDtlsSrtp = true;
        //rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
        PeerConnection connection =
                mPeerConnectionFactory.createPeerConnection(rtcConfig,
                        mPeerConnectionObserver);
        if (connection == null) {
            return null;
        }

        List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
        connection.addTrack(mVideoTrack, mediaStreamLabels);
        connection.addTrack(mAudioTrack, mediaStreamLabels);
        connection.setAudioPlayout(true);
        return connection;
    }

    /**
     * 创建PeerConnection的回调
     */
    private PeerConnection.Observer mPeerConnectionObserver = new PeerConnection.Observer() {
        @Override
        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
        }

        @Override
        public void onIceConnectionReceivingChange(boolean b) {
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
        }

        /**
         * 获取到当前设备的Candidate
         * @param iceCandidate
         */
        @Override
        public void onIceCandidate(IceCandidate iceCandidate) {
            JSONObject message = new JSONObject();
            try {
                message.put("type", "candidate");
                message.put("label", iceCandidate.sdpMLineIndex);
                message.put("id", iceCandidate.sdpMid);
                message.put("candidate", iceCandidate.sdp);
                Log.i(TAG, "candidate:" + message.toString());
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        count++;
                        if (count == 1) {
                            tv_candicate1.setText(message.toString());
                        } else if (count == 2) {
                            tv_candicate2.setText(message.toString());
                        } else if (count == 3) {
                            tv_candicate3.setText(message.toString());
                        } else if (count == 4) {
                            tv_candicate4.setText(message.toString());
                        } else if (count == 5) {
                            tv_candicate5.setText(message.toString());
                        } else if (count == 6) {
                            tv_candicate6.setText(message.toString());
                        }
                    }
                });
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
            if (iceCandidates != null) {
                for (int i = 0; i < iceCandidates.length; i++) {
                }
                mPeerConnection.removeIceCandidates(iceCandidates);
            }
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
//            VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0);
//            remoteVideoTrack.setEnabled(true);
//            remoteVideoTrack.addSink(mRemoteSurfaceView);

//            VideoTrack videoTrack = mediaStream.videoTracks.get(0);
//            videoTrack.setEnabled(true);
//            videoTrack.addSink(mRemoteSurfaceView);
        }

        @Override
        public void onRemoveStream(MediaStream mediaStream) {
        }

        @Override
        public void onDataChannel(DataChannel dataChannel) {
        }

        @Override
        public void onRenegotiationNeeded() {
        }

        /**
         * 接收到对方的视频
         * @param rtpReceiver
         * @param mediaStreams
         */
        @Override
        public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
            MediaStreamTrack track = rtpReceiver.track();
            if (track instanceof VideoTrack) {
                Log.i(TAG, "有视频");
                VideoTrack remoteVideoTrack = (VideoTrack) track;
                remoteVideoTrack.setEnabled(true);
                remoteVideoTrack.addSink(mRemoteSurfaceView);
            }
        }
    };

    @Override
    protected void onPause() {
        super.onPause();
//        try {
//            mVideoCapturer.stopCapture();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
    }

    /**
     * 开始呼叫
     */
    public void doStartCall() {
        if (mPeerConnection == null) {
            mPeerConnection = createPeerConnection();
        }
        MediaConstraints mediaConstraints = new MediaConstraints();
        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
        mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
        mPeerConnection.createOffer(new SimpleSdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                mPeerConnection.setLocalDescription(new SimpleSdpObserver() {
                    @Override
                    public void onSetSuccess() {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                et_offer.setText(sessionDescription.description);
                            }
                        });
                    }

                    @Override
                    public void onSetFailure(String msg) {
                    }
                }, sessionDescription);
            }
        }, mediaConstraints);
    }

    public static class SimpleSdpObserver implements SdpObserver {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
        }

        @Override
        public void onSetSuccess() {
        }

        @Override
        public void onCreateFailure(String msg) {
        }

        @Override
        public void onSetFailure(String msg) {
        }
    }

    private void setSpeakerphoneOn(boolean on) {
        if (on) {
            audioManager.setSpeakerphoneOn(true);
        } else {
            audioManager.setSpeakerphoneOn(false);//关闭扬声器
            //把声音设定成Earpiece(听筒)出来,设定为正在通话中
            audioManager.setMode(AudioManager.MODE_IN_CALL);
        }
    }

8.大家可能发现少一个CandidateBean,我们加入,代码如下

public class CandidateBean {

    private String type;
    private int label;
    private String id;
    private String candidate;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public int getLabel() {
        return label;
    }

    public void setLabel(int label) {
        this.label = label;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getCandidate() {
        return candidate;
    }

    public void setCandidate(String candidate) {
        this.candidate = candidate;
    }
}

9.在app的build.gradle中android节点下添加java 8依赖

compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}

10.到这里就没有报错了,我们最后在动态权限全部申请完成的地方,调用init()方法,我们这里是第6步中TODO的位置

11.到这里我们的代码就写完了。我们运行的时候,一个调用doStartCall,一个不调用。调用的为呼叫方。如果用了我的Demo,你也没有做信令服务器,那么要注意一下两点,第一,主叫一打开生成Candidate和sdp,发给被叫方后,先设置candidate再设置sdp;生成被叫方candidate和sdp后,发给主叫方,先设置sdp再设置candidate。第二,我们sdp通过任何聊天工具发到另一方后要注意,最后都是回车结尾的,没有的话自己回车,否则会报错。

到此我们的WebRTC点对点视频就搭建完成了。这里我们使用了谷歌提供的stun服务器。如果想在生产环境中应用,一个是要修改我们的信令传递方式,用聊天的方式通过长连接传递。一个是我们要有一个中继服务器,配置给我们的IceService。

小伙伴们有什么心得和问题都可以给我评论区留言。