webrtc-kurento

WebRTC

  1. 呼叫者通过 navigator.mediaDevices.getUserMedia() (en-US) 捕捉本地媒体。
  2. 呼叫者创建一个RTCPeerConnection 并调用 RTCPeerConnection.addTrack() (注: addStream 已经过时。)
  3. 呼叫者调用 RTCPeerConnection.createOffer() 来创建一个提议(offer).
  4. 呼叫者调用 RTCPeerConnection.setLocalDescription() (en-US) 将提议(Offer) 设置为本地描述 (即,连接的本地描述).
  5. setLocalDescription()之后, 呼叫者请求 STUN 服务创建ice候选(ice candidates)
  6. 呼叫者通过信令服务器将提议(offer)传递至 本次呼叫的预期的接受者.
  7. 接受者收到了提议(offer) 并调用 RTCPeerConnection.setRemoteDescription() 将其记录为远程描述 (也就是连接的另一端的描述).
  8. 接受者做一些可能需要的步骤结束本次呼叫:捕获本地媒体,然后通过RTCPeerConnection.addTrack()添加到连接中。
  9. 接受者通过 RTCPeerConnection.createAnswer() (en-US) 创建一个应答。
  10. 接受者调用 RTCPeerConnection.setLocalDescription() (en-US) 将应答(answer) 设置为本地描述. 此时,接受者已经获知连接双方的配置了.
  11. 接受者通过信令服务器将应答传递到呼叫者.
  12. 呼叫者接受到应答.
  13. 呼叫者调用 RTCPeerConnection.setRemoteDescription() 将应答设定为远程描述. 如此,呼叫者已经获知连接双方的配置了.

WebRTC - KMS

img

kurento docker 部署

docker run -d --name kms --network host kurento/kurento-media-server

群组和一对一架构设计

群组通话和一对一kms架构图

WebRTC Android

申请权限

静态申请权限

在 Android 项目中的 AndroidManifest.xml 中增加以下代码:

    <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.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

动态申请权限

引入依赖

  implementation 'com.karumi:dexter:5.0.0'

在MainActivity中添加代码如下:

    private void permission() {
        Dexter.withActivity(MainActivity.this)
                .withPermissions(
                        Manifest.permission.CAMERA,
                        Manifest.permission.READ_EXTERNAL_STORAGE,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.RECORD_AUDIO
                )
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        if (report.areAllPermissionsGranted()) {
                            // 权限通过
                            Log.d("checkpermission", "granted");
                        } else if (report.isAnyPermissionPermanentlyDenied()) {
                            // 权限拒绝
                            Log.d("checkpermission", "not granted");
                        }
                    }

                    @Override
                    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
                        // 重新获取权限
                        token.continuePermissionRequest();
                    }
                }).onSameThread().check();
    }

引入库

引入比较重要的两个库,

第一个当然就是 WebRTC 库了,第二个 JWebSocket 库,用它来与信令服务器互联。

implementation 'org.webrtc:google-webrtc:1.0.32006'
// 引入webSocket
implementation "org.java-websocket:Java-WebSocket:1.4.0"
// alibaba 的fastjson
implementation 'com.alibaba:fastjson:1.2.75'

外物的开始

我们都知道万物有个起源,我们在开发 WebRTC 程序时也不例外,WebRTC程序的起源就是PeerConnectionFactory。这也是与使用 JS 开发 WebRTC 程序最大的不同点之一,因为在 JS 中不需要使用 PeerConnectionFactory 来创建 PeerConnection 对象。

image-20210904150545739

WebRTC中的核心对象 PeerConnection、LocalMediaStream、LocalVideoTrack、LocalAudioTrack 都是通过 PeerConnectionFactory 创建出来的。

PeerConnectionFactory的初始化与构造

在 WebRTC 中使用了大量的设计模式,对于 PeerConnectionFactory 也是如此。它本身就是工厂模式,而这个构造 PeerConnection 等核心对象的工厂又是通过 builder 模式构建出来的。

下面我们就来看看如何构造 PeerConectionFactory。在我们构造 PeerConnectionFactory 之前,首先要对其进行初始化,其代码如下:

PeerConnectionFactory.initialize(PeerConnectionFactory.
                                 InitializationOptions.
                                 builder(context).
                                 setEnableInternalTracer(true).
                                 createInitializationOptions());

初始化之后,就可以通过 builder 模式来构造 PeerConnecitonFactory 对象了。

//编码启用H264编码器(支持硬件加速), Vp8不支持硬件加速
encoderFactory = new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), false, true);
decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());

PeerConnectionFactory.Builder builder = PeerConnectionFactory.
    builder().
    setVideoEncoderFactory(encoderFactory).
    setVideoDecoderFactory(decoderFactory);
builder.setOptions(null);

PeerConnectionFactory peerConnectionFactory = builder.createPeerConnectionFactory();

完整代码

    // 创建PeerConnectionFactory工厂对象
    private PeerConnectionFactory createPeerConnectionFactory(Context context) {
        final VideoEncoderFactory encoderFactory;
        final VideoDecoderFactory decoderFactory;

        //编码启用H264编码器(支持硬件加速), Vp8不支持硬件加速
        encoderFactory = new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), false, 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();;
    }

通过上面的代码,大家也就能够理解为什么 WebRTC 要使用 buider 模式来构造 PeerConnectionFactory 了吧?主要是方便调整建造 PeerConnectionFactory的组件,如编码器、解码器等。

从另外一个角度我们也可以了解到,要更换WebRTC引警的编解码器该从哪里设置了哈!

音视频数据源

有了PeerConnectionFactory对象,我们就可以创建数据源了。实际上,数据源是 WebRTC 对音视频数据的一种抽象,表式数据可以从这里获取。

使用过 JS WebRTC API的同学都非常清楚,在 JS中 VideoTrack 和 AudioTrack 就是数据源。而在 Android 开发中我们可以知道 VideoTrack/AudioTrack 就是 VideoSource/AudioSource的封装,可以认为他们是等同的。

// VideoSource 为视频源, 通过核心类PeerConnectionFactory创建
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(false);
mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);

audioConstraints = new MediaConstraints();
//回声消除
audioConstraints.mandatory.
    add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
//自动增益
audioConstraints.mandatory.
    add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
//高音过滤
audioConstraints.mandatory.
    add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
//噪音处理
audioConstraints.mandatory.
    add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
// 音频源
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
mAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);

数据源只是对数据的一种抽象,它是从哪里获取的数据呢?对于音频来说,在创建 AudioSource时,就开始从音频设备捕获数据了。对于视频来说我们可以指定采集视频数据的设备,然后使用观察者模式从指定设备中获取数据。

接下来我们就来看一下如何指定视频设备。

视频采集

在 Android 系统下有两种 Camera,一种称为 Camera1, 是一种比较老的采集视频数据的方式,别一种称为 Camera2, 是一种新的采集视频的方法。它们之间的最大区别是 Camera1使用同步方式调用API,Camera2使用异步方式,所以Camera2更高效。

 // 创建捕获视频对象
    private VideoCapturer createVideoCapturer() {
        if (Camera2Enumerator.isSupported(this)) {
            Camera2Enumerator camera2Enumerator = new Camera2Enumerator(this);
            String[] deviceNames = camera2Enumerator.getDeviceNames();
            for (String name : deviceNames) {
                if (camera2Enumerator.isFrontFacing(name)) { //mobilePhone支持前置摄像头
                    VideoCapturer videoCapturer = camera2Enumerator.createCapturer(name, null);
                    if (videoCapturer != null) {
                        return videoCapturer;
                    }
                }
            }
        } else {
            Camera1Enumerator camera1Enumerator = new Camera1Enumerator(true);
            String[] deviceNames = camera1Enumerator.getDeviceNames();
            for (String name : deviceNames) {
                if (!camera1Enumerator.isFrontFacing(name)) {
                    mobilePhone调用非前置摄像头
                    VideoCapturer videoCapturer = camera1Enumerator.createCapturer(name, null);
                    if (videoCapturer != null) {
                        return videoCapturer;
                    }
                }
            }
        }
        return null;
    }

上面代码的逻辑也比较简单:

  • 首先看 Android 设备是否支持 Camera2.
  • 如果支持就使用 Camera2, 如果不支持就使用 Camera1.
  • 在获到到具体的设备后,再看其是否有前置摄像头,如果有就使用
  • 如果没有有效的前置摄像头,则选一个非前置摄像头。

通过上面的方法就可以拿到使用的摄像头了,然后将摄像头与视频源连接起来,这样从摄像头获取的数据就源源不断的送到 VideoTrack 里了。

下面我们来看看 VideoCapture 是如何与 VideoSource 关联到一起的

 // VideoCapturer 视频捕捉器的一个顶级接口, 的的子接口为CameraVideoCapturer,封装了安卓相机的使用方法,使用它们可以轻松的获取设备相机数据,切换摄像头,获取摄像头数量等
mVideoCapturer = createVideoCapturer();

mSurfaceTextureHelper = SurfaceTextureHelper
    .create("CaptureThread",mRootEglBase.getEglBaseContext());

//将videoSource注册为mVideoCapturer的观察者
mVideoCapturer.initialize(mSurfaceTextureHelper, getApplicationContext(), videoSource.getCapturerObserver());

//从source中获取track (VideoTrack是对VideoSource的包装,可以方便的将视频源在本地进行播放,添加到MediaStream中进行网络传输)
mVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
mVideoTrack.setEnabled(true);   //打开track

上面的代码中,在初始化 VideoCaptuer 的时候,可以过观察者模式将 VideoCapture 与 VideoSource 联接到了一起。因为 VideoTrack 是 VideoSouce 的一层封装,所以此时我们开启 VideoTrack 后就可以拿到视频数据了

当然,最后还要调用一下 VideoCaptuer 对象的 startCapture 方法真正的打开摄像头,这样 Camera 才会真正的开始工作哈,代码如下:

   
    @Override
    protected void onResume() {
        super.onResume();
        mVideoCapturer.startCapture(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS);
    }

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


渲染视频

在 Android 下 WebRTC 使用OpenGL ES 进行视频渲染,用于展示视频的控件是 WebRTC 对 Android 系统控件 SurfaceView 的封装。

WebRTC 封装后的 SurfaceView 类为 org.webrtc.SurfaceViewRenderer。在界面定义中应该定义两个SurfaceViewRenderer,一个用于显示本地视频,另一个用于显示远端视频。

其定义如下:

    <org.webrtc.SurfaceViewRenderer
        android:id="@+id/localView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"/>

    <org.webrtc.SurfaceViewRenderer
        android:id="@+id/remoteView"
        android:layout_width="120dp"
        android:layout_height="160dp"
        android:layout_margin="16dp"
        android:layout_gravity="top|right"/>

通过上面的代码我们就将显示视频的 View 定义好了。光定义好这两个View 还不够,还要对它做进一步的设置:

        mLocalSurfaceView = findViewById(R.id.localView);   //初始化surface view的时候先通过OpenGL计算
        mLocalSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL); //缩放按比例填充
        mLocalSurfaceView.setMirror(true);  //镜像翻转
        mLocalSurfaceView.setEnableHardwareScaler(false);   //不采用硬件缩放器

        mRemoteSurfaceView = findViewById(R.id.remoteView);
        mRemoteSurfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        mRemoteSurfaceView.setMirror(true);
        mRemoteSurfaceView.setEnableHardwareScaler(true);
        mRemoteSurfaceView.setZOrderMediaOverlay(true);

其含义是:

  • 使用 OpenGL ES 的上下文初始化 View。
  • 设置图像的拉伸比例。
  • 设置图像显示时反转,不然视频显示的内容与实际内容正好相反。
  • 是否打开便件进行拉伸。

接下来将从摄像头采集的数据设置到该view里就可以显示了。设置非常的简单,代码如下:

mVideoTrack.addSink(mLocalSurfaceView);

信令驱动

在整个 WebRTC 双方交互的过程中,其业务逻辑的核心是信令, 所有的模块都是通过信令串联起来的。

以 PeerConnection 对象的创建为例,该在什么时候创建 PeerConnection 对象呢?最好的时机当然是在用户加入房间之后了 。

下面我们就来看一下,对于两人通讯的情况,信令该如何设计。在我们这个例子中,可以将信令分成两大类。第一类为客户端命令;第二类为服务端命令;

image-20210928105254532

android客户端

   // 广播接收信息
    private class MessageReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            String message = intent.getStringExtra("message");

            String type = null;
            JSONObject messageObj = JSONObject.parseObject(message);

            try {
                type = messageObj.getString("id");
                switch (type) {
                    case "registerResponse":
                        // 注册响应
                        String response = messageObj.getString("response");
                        if (response.equals("accepted")) {
                        } else {
                        }
                        break;
                    case "callResponse":
                        // 通话响应
                        String callResponse = messageObj.getString("response");
                        logcatOnUI("callResponse: " + callResponse);
                        if (callResponse.equals("accepted")) {
                            stopFlag = false;
                            // 同意通话
                            handleProcessSdpAnswer(messageObj);
                        } else if (callResponse.equals("rejected")) {
                            // 对方拒绝接听
//                            stop();
                            finish();
                        }
                        break;
                    case "incomingCall":
                        // 来电
                        break;
                    case "incomingCallResponse":
                        // 来电响应
                        break;
                    case "onIceCandidate":
                        // 接收ice候选
                        handleAddIceCandidate(messageObj);
                        break;
                    case "stopCommunication":
                        // 结束通话
                        finish();
                        break;
                    case "startCommunication":
                        // 开始沟通
                        handleProcessSdpAnswer(messageObj);
                        break;
                    case "revokeResponse":
                        // 取消打电话
                        finish();
                        break;
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

服务端

 @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class);
        UserSession user = registry.getBySession(session);

        if (user != null) {
            log.debug("Incoming message from user '{}': {}", user.getName(), jsonMessage);
        } else {
            log.debug("Incoming message from new user: {}", jsonMessage);
        }

        switch (jsonMessage.get("id").getAsString()) {
            case "register":
                try {
                    // 用户注册
                    register(session, jsonMessage);
                } catch (Throwable t) {
                    handleErrorResponse(t, session, "registerResponse");
                }
                break;
            case "call":
                try {
                    // 打电话
                    call(user, jsonMessage);
                } catch (Throwable t) {
                    handleErrorResponse(t, session, "callResponse");
                }
                break;
            case "incomingCallResponse":
                incomingCallResponse(user, jsonMessage);
                break;
            case "onIceCandidate": {
                JsonObject candidate = jsonMessage.get("candidate").getAsJsonObject();
                if (user != null) {
                    IceCandidate cand =
                            new IceCandidate(candidate.has("candidate") ?
                                    candidate.get("candidate").getAsString() : candidate.get("sdp").getAsString(),
                                    candidate.get("sdpMid").getAsString(),
                                    candidate.get("sdpMLineIndex").getAsInt());
                    user.addCandidate(cand);
                }
                break;
            }
            case "stop":
                // 挂断
                stop(session);
                break;
            case "revoke":
                // 撤销
                revoke(session);
                break;
            default:
                break;
        }
    }

web前端

ws.onmessage = function(message) {
	var parsedMessage = JSON.parse(message.data);
	console.info('Received message: ' + message.data);

	switch (parsedMessage.id) {
	case 'registerResponse':
		registerResponse(parsedMessage);
		break;
	case 'callResponse':
		callResponse(parsedMessage);
		break;
	case 'incomingCall':
		incomingCall(parsedMessage);
		break;
	case 'startCommunication':
		startCommunication(parsedMessage);
		break;
	case 'stopCommunication':
		console.info('Communication ended by remote peer');
		// stop(true);
		stop()
		break;
	case 'iceCandidate':
		webRtcPeer.addIceCandidate(parsedMessage.candidate, function(error) {
			if (error)
				return console.error('Error adding candidate: ' + error);
		});
		break;
	default:
		console.error('Unrecognized message', parsedMessage);
	}
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值