WebRTC Android对等连接(webrtc入门七)

本文继续分享WebRTC android 原生对等连接教程。首先需要构建 WebRTC Android 库,还需要服务器端的信号代码,可以通过数据通道这篇文章找到。

设置好所有内容后,让我们开始创建项目。

创建和设置项目

启动 Android Studio,创建一个新的 “项目”,给它取一个你喜欢的名字,然后点击完成。

加载项目后,添加一个名为 webrtc 的新包,这是本教程所需的唯一包。接下来我们需要添加权限以允许我们访问设备的摄像头和麦克风。将以下内容添加到“AndroidManifest.xml”。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

接下来我们需要导入我们之前构建的 WebRTC Android 库。将项目窗口从“Android”更改为“Project”并将库添加到“app/libs”目录。

最后我们需要将依赖项添加到 gradle 文件中,打开模块的“build.gradle”并将以下内容添加到依赖项部分:

// WebRTC
implementation(name: 'libwebrtc', ext: 'aar')

// WebSocket
implementation 'org.java-websocket:Java-WebSocket:1.5.2'

// Easy Permissions
implementation 'pub.devrel:easypermissions:3.0.0'

完成,接下来我们需要提供资源和视图。

设置资源/视图

首先,我们要处理 “strings.xml” 文件,打开它并添加以下内容:

<resources>
    <string name="app_name">Android WebRTC</string>

    <string name="peer_id_placeholder">Enter remote peer id</string>
    <string name="call_button_text">Call</string>
    <string name="request_camera_mic_permissions_text">Please allow access to your camera and mic.</string>
    <string name="logout_button">Logout</string>
</resources>

在这里,我们基本上设置了示例应用程序所需的字符串。

接下来我们将创建一个简单的视图,打开“activity_main.xml”并添加以下内容:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingTop="20dp"
        android:layout_marginStart="5dp"
        android:layout_marginEnd="5dp"
        android:orientation="vertical">

        <EditText
            android:id="@+id/peerIdEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/peer_id_placeholder"
            android:autofillHints="test"
            android:inputType="text" />

        <Button
            android:id="@+id/callButton"
            android:text="@string/call_button_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:enabled="false"/>

    </LinearLayout>

    <org.webrtc.SurfaceViewRenderer
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/localRenderer"
        android:visibility="invisible"
        />

    <org.webrtc.SurfaceViewRenderer
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/remoteRenderer"
        android:visibility="invisible"
        />

    <Button
        android:id="@+id/logoutButton"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:text="@string/logout_button"
        android:visibility="invisible"
        android:gravity="bottom"
        android:layout_alignParentEnd="true"
        />
</RelativeLayout>

这里我们创建了一个简单的表单来调用远程用户,在底部我们有 2 个 SurfaceRenderer,一个用于本地视图,一个用于远程视图。最后一个简单的注销按钮。

接下来我们终于可以开始写代码了!

创建 webrtc package

首先,我们将在 webrtc package 下创建所需的文件,我将从较容易的一个开始,因为它只是一个简单的接口。

创建一个名为“ConnectionListener”的接口文件并添加以下代码:

import org.webrtc.IceCandidate;
import org.webrtc.MediaStreamTrack;
import org.webrtc.SessionDescription;

public interface ConnectionListener {
    void onIceCandidateReceived(IceCandidate iceCandidate);
    void onAddStream(MediaStreamTrack mediaStreamTrack);
    void onLocalOffer(SessionDescription offer);
    void onLocalAnswer(SessionDescription answer);
}

这个侦听器基本上侦听 ICE 候选事件、流事件和本地报价/回答。

下一个文件更复杂,但我会尽力解释它,创建一个名为“Connection”的新文件,首先我们添加所需的导入文件:

import android.content.Context;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera1Enumerator;
import org.webrtc.Camera2Enumerator;
import org.webrtc.CameraEnumerator;
import org.webrtc.DataChannel;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
import org.webrtc.RtpReceiver;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SoftwareVideoDecoderFactory;
import org.webrtc.SoftwareVideoEncoderFactory;
import org.webrtc.SurfaceTextureHelper;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoDecoderFactory;
import org.webrtc.VideoEncoderFactory;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import java.util.ArrayList;

我们使用了很多来自 WebRTC 库的导入,接下来我们需要让 Connection 类实现 PeerConnection.Observer,所以将其改为以下内容:

public class Connection implements PeerConnection.Observer {

我们要做的第一件事是初始化一些成员变量,所以添加以下内容:

private static final String TAG = "Connection";

private static final String MEDIA_STREAM_ID = "ARDAMS";
private static final String VIDEO_TRACK_ID = "ARDAMSv0";
private static final String AUDIO_TRACK_ID = "ARDAMSa0";
private static final int VIDEO_HEIGHT = 480;
private static final int VIDEO_WIDTH = 640;
private static final int VIDEO_FPS = 30;

private static final String STUN_SERVER_URL = "stun:stun.l.google.com:19302";

private static Connection INSTANCE = null;
private final PeerConnectionFactory mFactory;

private PeerConnection mPeerConnection;
private MediaStream mMediaStream;
private VideoCapturer mVideoCapturer;
private final ConnectionListener mListener;

这里我们为媒体流、视频轨道和音频轨道设置了一些常量。然后我们设置视频分辨率和 FPS。

我们还设置了 STUN 服务器 url。之后,我们准备一些以后需要的变量。

接下来我们将创建连接构造函数:

private Connection(final Context context, final ConnectionListener listener) {
    final PeerConnectionFactory.InitializationOptions options = PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions();
    final EglBase.Context eglContext = EglBase.create().getEglBaseContext();
    final VideoEncoderFactory encoderFactory = new SoftwareVideoEncoderFactory();
    final VideoDecoderFactory decoderFactory = new SoftwareVideoDecoderFactory();

    PeerConnectionFactory.initialize(options);
    mFactory = PeerConnectionFactory.builder()
            .setVideoEncoderFactory(encoderFactory)
            .setVideoDecoderFactory(decoderFactory)
            .createPeerConnectionFactory();
     mListener = listener;
};

这里我们初始化 PeerConnectionFactory 对象,我们还初始化视频编码器和解码器工厂,最后我们初始化并创建对等连接工厂对象。请注意,初始化对等连接工厂应该只进行一次,这就是我将此类设为单例的原因。

接下来我们需要创建方法来创建我们的 Connection 类:

public static synchronized Connection initialize(final Context context, final ConnectionListener listener) {
    if (INSTANCE != null) {
        return INSTANCE;
    }

    INSTANCE = new Connection(context, listener);
    return INSTANCE;
}

如果 INSTANCE 变量为 null,我们在这里创建一个新的 Connection,如果它不为 null,我们返回 Singleton 实例。

接下来我们需要一个方法来初始化设备的摄像头和麦克风,并开始捕捉内容,添加以下方法:

public void initializeMediaDevices(final Context context, final SurfaceViewRenderer localRenderer) throws Exception {
    mMediaStream = mFactory.createLocalMediaStream(MEDIA_STREAM_ID);

    mVideoCapturer = createVideoCapturer(context);

    final VideoSource videoSource = mFactory.createVideoSource(false);
    final EglBase.Context eglContext = EglBase.create().getEglBaseContext();
    final SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("captureThread", eglContext);

        // Video capturer and localRenderer needs to be initialized
    mVideoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
    localRenderer.init(eglContext, new RendererCommon.RendererEvents() {
        @Override
        public void onFirstFrameRendered() {
            Log.d(TAG, "onFirstFrameRendered");
        }

            @Override
        public void onFrameResolutionChanged(int i, int i1, int i2) {
            Log.d(TAG, "Frame resolution changed");
        }
    });

    mVideoCapturer.startCapture(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS);

    final VideoTrack videoTrack = mFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
    videoTrack.setEnabled(true);
    videoTrack.addSink(localRenderer);

    final AudioSource audioSource = mFactory.createAudioSource(new MediaConstraints());
    final AudioTrack audioTrack = mFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
    audioTrack.setEnabled(true);

    mMediaStream.addTrack(videoTrack);
    mMediaStream.addTrack(audioTrack);
    Log.d(TAG, "media devices initialized");
}

相当大的方法,但我会试着解释这里发生的一切,如果有任何方法还不存在,也不要介意,因为我们以后会编写它们。

首先,我们使用在文件顶部定义的 id 创建一个新的 MediaStream。

接下来我们创建一个传递上下文的 VideoCapturer 对象。接下来我们创建一个 SurfaceTextureHelper 来创建一个传入 eglContext 的“捕获”线程。

接下来我们初始化视频捕获器对象并初始化表面视图,你实际上不需要“onFirstFrameRendered”,但是“onFrameResoulutionChanged”在你想要将 UI 更新为新的视频分辨率时非常有用。

我们实际上开始捕捉用户的摄像头,并传递视频宽度/高度/帧数等变量。

接下来我们创建一个新的视频轨道,传入视频轨道 ID,将其设置为启用并将接收器设置为 SurfaceView。

接下来我们通过创建 AudioSource 和 AudioTrack 开始捕获音频。

最后,我们需要将轨道添加到 MediaStream。

接下来我们处理创建报价等。创建以下方法:

public void createOffer() {
    final MediaConstraints mediaConstraints = new MediaConstraints();

    mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
    mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));


    for (final MediaStreamTrack videoTrack: mMediaStream.videoTracks) {
        mPeerConnection.addTrack(videoTrack);
    }

    for (final MediaStreamTrack audioTrack: mMediaStream.audioTracks) {
        mPeerConnection.addTrack(audioTrack);
    }

    mPeerConnection.createOffer(new SdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
            Log.d(TAG, "Local offer created:" + sessionDescription.description);
            mPeerConnection.setLocalDescription(this, sessionDescription);
        }

        @Override
        public void onSetSuccess() {
            Log.d(TAG, "Local description set success");

            mListener.onLocalOffer(mPeerConnection.getLocalDescription());
        }

        @Override
        public void onCreateFailure(String s) {
            Log.e(TAG, "Failed to create local offer error:" + s);
        }

        @Override
        public void onSetFailure(String s) {
            Log.e(TAG, "Failed to set local description error:" + s);
        }
    }, mediaConstraints);
}

首先,我们创建 MediaConstraints,在 Android 系统中,你需要将 “OfferToReveiveVideo “和 “OfferToReceiveAudio “键设置为true,以便从远程对等体接收媒体。

接下来,我们将本地媒体轨道添加到对等连接,并创建一个新的 SDP 提议。创建 SDP 时,我们设置对等连接的本地描述,设置后我们通过侦听器将其传回。

接下来我们需要一种方法来处理远程对等点的 SDP 提议,添加以下方法:

public void createAnswerFromRemoteOffer(final String remoteOffer) {
    // Reuse the SdpObserver
    final SdpObserver observer = new SdpObserver() {
        @Override
        public void onCreateSuccess(SessionDescription sessionDescription) {
            Log.d(TAG, "Local answer created");
            mListener.onLocalAnswer(sessionDescription);
            mPeerConnection.setLocalDescription(this);
        }

        @Override
        public void onSetSuccess() {
            Log.d(TAG, "Set description was successful");
        }

        @Override
        public void onCreateFailure(String s) {
            Log.e(TAG, "Failed to create local answer error:" + s);
        }

        @Override
        public void onSetFailure(String s) {
            Log.e(TAG, "Failed to set description error:" + s);
        }
    };

    final SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, remoteOffer);
    final MediaConstraints mediaConstraints = new MediaConstraints();

    mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
    mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));

    mPeerConnection.setRemoteDescription(observer, sessionDescription);
    mPeerConnection.createAnswer(observer, mediaConstraints);
}

我们在这里所做的与 createOffer 方法没有太大区别,我们将创建的答案传递给侦听器并设置对等连接的本地描述。

接下来我们需要创建一个方法来处理远程 ICE 候选人:

public void addRemoteIceCandidate(final JSONObject iceCandidateData) throws JSONException {
    Log.d(TAG, "Check " + iceCandidateData.toString());
    final String sdpMid = iceCandidateData.getString("sdpMid");
    final int sdpMLineIndex = iceCandidateData.getInt("sdpMLineIndex");
    String sdp = iceCandidateData.getString("candidate");

    final IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
    Log.d(TAG, "add remote candidate " + iceCandidate.toString());
    mPeerConnection.addIceCandidate(iceCandidate);
}

在这里,我们将 ICE 候选 JSON 转换为新的 IceCandidate 对象,然后将 ICE 候选添加到对等连接。

接下来我们将创建一个方法来处理关闭对等连接和释放媒体:

public void close() {
    for (AudioTrack audioTrack : mMediaStream.audioTracks) {
        audioTrack.setEnabled(false);
    }

    for (VideoTrack videoTrack : mMediaStream.videoTracks) {
        videoTrack.setEnabled(false);
    }

     try {
        mVideoCapturer.stopCapture();
    } catch (InterruptedException ie) {
        Log.e(TAG, "Failed to stop capture", ie);
    }

    mPeerConnection.close();
    mFactory.dispose();
}

这里我们将所有本地track的状态改为disabled,停止抓拍camera,最后关闭连接,对factory进行处理。

接下来我们将创建一个方法来获取用户的相机设备进行捕获:

private VideoCapturer createVideoCapturer(final Context context) throws Exception {
    final boolean isUseCamera2 = Camera2Enumerator.isSupported(context);
    final CameraEnumerator cameraEnumerator = isUseCamera2 ? new Camera2Enumerator(context) : new Camera1Enumerator(true);

    final String[] deviceNames = cameraEnumerator.getDeviceNames();

    for (final String deviceName : deviceNames) {
        Log.d(TAG, "Found device: " + deviceName);
        if (cameraEnumerator.isFrontFacing(deviceName)) {
            Log.d(TAG, "Found front device");
            return cameraEnumerator.createCapturer(deviceName, null);
        }
    }

    throw new Exception("Failed to get camera device");
}

这里我们简单的获取用户的第一个前置摄像头,如果没有找到摄像头则发现异常。现在大多数设备都有多个设备,所以可以随意尝试不同的设备。

接下来我们将创建一个方法来创建对等连接对象:

public void createPeerConnection() {
    if (mPeerConnection != null) return;

    final ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();
	        iceServers.add(PeerConnection.IceServer.builder(STUN_SERVER_URL).createIceServer());

    mPeerConnection = mFactory.createPeerConnection(iceServers, this);
    Log.d(TAG, "Peer Connection created");
}

这里我们简单地创建一个 ICE 服务器列表,并使用 ice 服务器数组创建对等连接。

最后,我们现在需要做的就是覆盖 Observer 侦听器:

@Override
public void onAddStream(MediaStream mediaStream) {
    Log.d(TAG, "onAddStream");
}

@Override
public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) {
    Log.d(TAG, "onAddTrack");
    mListener.onAddStream(receiver.track());
}

@Override
public void onIceConnectionReceivingChange(boolean b) {
    Log.d(TAG, "onIceConnectionReceivingChange");
}

@Override
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
    Log.d(TAG, "onIceGatheringChange state=" + iceGatheringState.toString());
}

@Override
public void onDataChannel(DataChannel dataChannel) {
    Log.d(TAG, "onDataChannel");
}

@Override
public void onRenegotiationNeeded() {
    Log.d(TAG, "onRenegotiationNeeded");
}

@Override
public void onIceCandidate(IceCandidate iceCandidate) {
    Log.d(TAG, "onIceCandidate");

    mListener.onIceCandidateReceived(iceCandidate);
}

@Override
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
    Log.d(TAG, "onSignalingChange state=" + signalingState.toString());
}

@Override
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
    Log.d(TAG, "onIceCandidatesRemoved");
}

@Override
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
    Log.d(TAG, "onIceConnectionChange state=" + iceConnectionState.toString());
}

@Override
public void onRemoveStream(MediaStream mediaStream) {
    Log.d(TAG, "onRemoveStream");
}

我们唯一关注的是 onTrack 和 onIceCandidate 方法,可以随意编辑它们。

连接文件就完成了!😃 顺序随机性请见谅。

接下来我们可以充实 MainActivity 文件。

创建 MainActivity 文件

最后,我们可以开始使用新的 Connection 类,打开 MainActivity 并添加以下导入:

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.example.androidwebrtc.webrtc.Connection;
import com.example.androidwebrtc.webrtc.ConnectionListener;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaStreamTrack;
import org.webrtc.RendererCommon;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoTrack;

import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import pub.devrel.easypermissions.AfterPermissionGranted;
import pub.devrel.easypermissions.EasyPermissions;

确保 MainActivity 也实现了 ConnectionListener:

public class MainActivity extends AppCompatActivity implements ConnectionListener {

接下来添加将要使用的成员变量:

private static final String TAG = "MainActivity";
private static final String WS_URI = "wss://192.168.0.109:8888";
// WARNING: Turn this to false for production
private static final boolean IS_DEBUG = true;
private static final int CAMERA_AND_MIC = 1001;

private WebSocketClient socket;
private SurfaceViewRenderer mLocalRenderer;
private SurfaceViewRenderer mRemoteRenderer;
private EditText mPeerIdEditText;
private Button mCallButton;
private Button mLogoutButton;
private Connection mConnection;
private String mRemoteId;

确保将 WS_URI 更改为您自己的网络地址!

首先在 onCreate 方法中添加以下内容:

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

    mLocalRenderer = findViewById(R.id.localRenderer);
    mRemoteRenderer = findViewById(R.id.remoteRenderer);
    mPeerIdEditText = findViewById(R.id.peerIdEditText);
    mCallButton = findViewById(R.id.callButton);
    mLogoutButton = findViewById(R.id.logoutButton);

    mConnection = Connection.initialize(this, this);

    initializeCallButton();
    connectToWebsocketServer();
    initializeLogoutButton();
}

在这里我们设置视图的元素(按钮等),然后初始化按钮并连接到 WebSocket 服务器。

首先需要实现 initializeCallButton 方法:

private void initializeCallButton() {
    mCallButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mPeerIdEditText.getText().toString().trim().length() == 0) return;

            mRemoteId = mPeerIdEditText.getText().toString();
            mPeerIdEditText.setVisibility(View.INVISIBLE);
            mCallButton.setVisibility(View.INVISIBLE);
            mRemoteRenderer.setVisibility(View.VISIBLE);
            mLocalRenderer.setVisibility(View.VISIBLE);
            mLogoutButton.setVisibility(View.VISIBLE);
            Log.d(TAG, "Remote id " + mRemoteId);

            mConnection.createPeerConnection();
            mConnection.createOffer();

            view.clearFocus();
        }
    });
}

在这里,更新 ui 元素可见性,创建新的对等连接并调用 create offer 来创建本地报价。

接下来需要连接到 WebSocket 服务器的方法:

private void connectToWebsocketServer() {
    try {
        this.socket = new WebSocketClient(new URI(WS_URI)) {
            @Override
            public void onOpen(ServerHandshake handshakedata) {
                Log.d(TAG, "onOpen");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mCallButton.setEnabled(true);
                    }
                });
                requestCameraAndMicAccess();
            }

            @Override
            public void onMessage(String message) {
                Log.d(TAG, "onMessage message=" + message);
                handleWebSocketMessage(message);
            }

            @Override
            public void onClose(int code, String reason, boolean remote) {
                Log.d(TAG, "onClose reason=" + reason);
                MainActivity.this.closeConnection();
            }

            @Override
            public void onError(Exception ex) {
                Log.e(TAG, "onError", ex);
            }
        };

        if (IS_DEBUG) {
            Log.w(TAG, "Enabling debug mode");
            final SSLSocketFactory factory = supportSelfSignedCert();
            HttpsURLConnection.setDefaultSSLSocketFactory(factory);
            this.socket.setSocketFactory(factory);
        }

        this.socket.connect();
    } catch (Exception e) {
        Log.e(TAG, e.getMessage());
    }
}

在这里,设置 websocket 侦听器并连接到服务器,一旦建立连接,用户就可以点击呼叫按钮。

接下来将实现注销按钮调用处理程序:

private void initializeLogoutButton() {
    mLogoutButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            closeConnection();
        }
    });
}

所有这一切都是调用 closeConnection,这是编写的下一个方法:

private void closeConnection() {
    mConnection.close();

    mLocalRenderer.release();
    mRemoteRenderer.release();

    mPeerIdEditText.setVisibility(View.VISIBLE);
    mCallButton.setVisibility(View.VISIBLE);
    mRemoteRenderer.setVisibility(View.INVISIBLE);
    mLocalRenderer.setVisibility(View.INVISIBLE);
    mLogoutButton.setVisibility(View.INVISIBLE);
}

在这里,释放渲染器并还原 UI。

接下来需要一个辅助方法,因为我们使用的是自签名证书:

private SSLSocketFactory supportSelfSignedCert() throws NoSuchAlgorithmException, KeyManagementException {
    final TrustManager[] trustManagers = new TrustManager[] {
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { }

                @Override
                public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[]{};
                }
            }
    };

    HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
        @Override
        public boolean verify(String s, SSLSession sslSession) {
            return true;
        }
    });
    final SSLContext context = SSLContext.getInstance("SSL");
    context.init(null, trustManagers, new SecureRandom());

    return context.getSocketFactory();

请注意,不应在生产中使用此来源。

接下来我们将创建一个方法来处理来自服务器的远程消息:

private void handleWebSocketMessage(final String message) {
    Log.d(TAG, "Got server message:" + message);
    try {
        final JSONObject jsonMessage = new JSONObject(message);
        final String action = jsonMessage.getString("action");

        switch(action) {
            case "start":
                Log.d(TAG, "WebSocket::start");
                // TODO: Deplace in text
                Log.d(TAG, "Local ID = " + jsonMessage.getString("id"));
                break;
            case "offer":
                Log.d(TAG, "WebSocket::offer " + jsonMessage.getJSONObject("data"));
                mRemoteId = jsonMessage.getJSONObject("data").getString("remoteId");

                   mConnection.createAnswerFromRemoteOffer(jsonMessage.getJSONObject("data").getJSONObject("offer").getString("name"));
                break;
            case "answer":
                Log.d(TAG, "WebSocket::answer");
                   mConnection.createAnswerFromRemoteOffer(jsonMessage.getJSONObject("data").getJSONObject("answer").getString("sdp"));
                break;
            case "iceCandidate":
                Log.d(TAG, "WebSocket::iceCandidate " + jsonMessage.getJSONObject("data").getJSONObject("candidate").toString());
                    mConnection.addRemoteIceCandidate(jsonMessage.getJSONObject("data").getJSONObject("candidate"));
                break;
            default: Log.w(TAG, "WebSocket unknown action" + action);
        }
    } catch (JSONException je) {
        Log.e(TAG, "Failed to handle WebSocket message", je);
    }
}

这里只是处理 websocket 消息,在这里所做的与我之前的 WebRTC 教程没有太大区别,所以我不会深入解释。

接下来我们将创建一个向服务器发送数据的方法:

private void sendSocketMessage(final String action, final JSONObject data) {
    try {
        final JSONObject message = new JSONObject();
        message.put("action", action);
        message.put("data", data);

        socket.send(message.toString());
    } catch (JSONException je) {
        Log.e(TAG, je.toString());
    }
}

在这里,所做的只是发送一个字符串化的 JSON 对象。

接下来将创建一个从用户那里获取权限的方法,请注意我使用了一个外部库以方便使用,你可以不必这样做。

@AfterPermissionGranted(CAMERA_AND_MIC)
private void requestCameraAndMicAccess() {
    String[] permissions = { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO };
    if(EasyPermissions.hasPermissions(this, permissions)) {
        Log.d(TAG, "media permissions granted");
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                getUserMedia();
            }
        });
    } else {
        EasyPermissions.requestPermissions(this, getString(R.string.request_camera_mic_permissions_text), CAMERA_AND_MIC, permissions);
    }
}

在这里,我们只是获得使用用户摄像头和麦克风的权限。因为捕获需要 ui 线程,所以我们将在 ui 线程上运行 getUserMedia,这是我们将要实现的下一个方法:

private void getUserMedia() {
    try {
        Log.d(TAG, "getUserMedia");
        mConnection.initializeMediaDevices(this, mLocalRenderer);

        final JSONObject data = new JSONObject();
        data.put("action", "start");

        sendSocketMessage("start", data);
    } catch (Exception e) {
        Log.e(TAG, "Failed to get camera device", e);
    }
}

在这里,开始获取用户的媒体设备,然后向服务器发送消息以初始化调用。

如果你像我一样使用 EasyPermissions,还需要重写以下方法:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

最后需要处理 ConnectionListener 事件,第一个是 onAddStream:

@Override
public void onAddStream(MediaStreamTrack mediaStreamTrack) {
    Log.d(TAG, "onAddStream " + mediaStreamTrack.kind());
    mediaStreamTrack.setEnabled(true);

    if (mediaStreamTrack.kind().equals("video")) {
        Log.d(TAG, "add video");
        final VideoTrack videoTrack = (VideoTrack) mediaStreamTrack;

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                final EglBase.Context eglContext = EglBase.create().getEglBaseContext();

                mRemoteRenderer.init(eglContext, new RendererCommon.RendererEvents() {
                    @Override
                    public void onFirstFrameRendered() {

                    }

                    @Override
                    public void onFrameResolutionChanged(int i, int i1, int i2) {

                    }
                });

                videoTrack.addSink(mRemoteRenderer);
            }
        });
    }
}

这里启用远程媒体轨道,如果轨道是音频它应该自动播放,如果轨道是视频则需要初始化表面视图并将视频轨道的接收器设置为表面视图。

接下来处理 onIceCandidateReceived 事件:

@Override
public void onIceCandidateReceived(IceCandidate iceCandidate) {
    try {
        final JSONObject candidate = new JSONObject();
        candidate.put("sdp", iceCandidate.sdp);
        candidate.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);
        candidate.put("sdpMid", iceCandidate.sdpMid);

        final JSONObject data = new JSONObject();
        data.put("action", "iceCandidate");
        data.put("remoteId", mRemoteId);
        data.put("candidate", candidate);

        sendSocketMessage("iceCandidate", data);
    } catch (JSONException je) {
        Log.e(TAG, "Failed to handle onIceCandidate event", je);
    }
}

这里将 candidate 解析成一个 JSON 对象,然后设置数据,然后将数据发送到服务器。

接下来将处理 onLocalOffer 事件:

@Override
public void onLocalOffer(SessionDescription offer) {
    Log.d(TAG, "onLocalOffer offer=" + offer);
    try {
        final JSONObject sdp = new JSONObject();
        sdp.put("type", "offer");
        sdp.put("sdp", offer.description);

        final JSONObject data = new JSONObject();
        data.put("action", offer.type);
        data.put("remoteId", mRemoteId);
        data.put("offer", sdp);

        sendSocketMessage("offer", data);
    } catch (JSONException je) {
        Log.e(TAG, "Failed to handle onLocalOffer", je);
    }
}

与候选事件一样,我们只是创建数据对象,然后将其发送到服务器。

最后处理 onLocalAnswer:

@Override
public void onLocalAnswer(SessionDescription answer) {
    Log.d(TAG, "onLocalAnswer answer=" + answer);
    try {
        final JSONObject sdp = new JSONObject();
        sdp.put("type", "answer");
        sdp.put("sdp", answer.description);

        final JSONObject data = new JSONObject();
        data.put("action", "answer");
        data.put("remoteId", mRemoteId);
        data.put("answer", sdp);

        sendSocketMessage("answer", data);
    } catch (JSONException je) {
        Log.e(TAG, "Failed to handle onLocalAnswer", je);
    }
}

完毕!

运行示例

首先通过以下命令启动节点服务器:

node run src/server.js

接下来访问https://localhost:3000/ 点击开始,记住本地id。

接下来启动应用程序(注意,如果你的两个设备都使用同一个摄像头,这可能无法在模拟器上工作,为此我建议使用实际设备。)

输入远程对等 ID 并点击通话,你们应该可以看到对方的数据流。

总结

在这里我展示了如何建立 WebRTC 连接并与原生 Android SDK 交换媒体。如果我有什么遗漏,可以留言告知。

你可以通过以下网址找到这个项目的源代码:https://github.com/ethand91/webrtc-android

作者:Ethan Denvir

系列阅读

使用 JavaScript 和 Nodejs 搭建 webrtc信令服务器(webrtc入门一)

WebRTC MediaDevices API 获取媒体设备的访问权限(webrtc入门二)

WebRTC建立P2P连接和发送/接收媒体(webrtc入门三)

WebRTC如何实现屏幕共享(webrtc入门四)

WebRTC数据通道基础知识(webrtc入门五)

如何构建 WebRTC Android 库?(webrtc入门六)

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/22988.html

(0)

相关推荐

发表回复

登录后才能评论