navigator.mediaDevices.getUserMedia()
(en-US) 捕捉本地媒体。RTCPeerConnection
并调用 RTCPeerConnection.addTrack()
(注: addStream
已经过时。)RTCPeerConnection.createOffer()
来创建一个提议(offer).RTCPeerConnection.setLocalDescription()
(en-US) 将提议(Offer) 设置为本地描述 (即,连接的本地描述).RTCPeerConnection.setRemoteDescription()
将其记录为远程描述 (也就是连接的另一端的描述).RTCPeerConnection.addTrack()
添加到连接中。RTCPeerConnection.createAnswer()
(en-US) 创建一个应答。RTCPeerConnection.setLocalDescription()
(en-US) 将应答(answer) 设置为本地描述. 此时,接受者已经获知连接双方的配置了.RTCPeerConnection.setRemoteDescription()
将应答设定为远程描述. 如此,呼叫者已经获知连接双方的配置了.docker run -d --name kms --network host kurento/kurento-media-server
在 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 对象。
WebRTC中的核心对象 PeerConnection、LocalMediaStream、LocalVideoTrack、LocalAudioTrack 都是通过 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;
}
上面代码的逻辑也比较简单:
通过上面的方法就可以拿到使用的摄像头了,然后将摄像头与视频源连接起来,这样从摄像头获取的数据就源源不断的送到 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);
其含义是:
接下来将从摄像头采集的数据设置到该view里就可以显示了。设置非常的简单,代码如下:
mVideoTrack.addSink(mLocalSurfaceView);
在整个 WebRTC 双方交互的过程中,其业务逻辑的核心是信令, 所有的模块都是通过信令串联起来的。
以 PeerConnection 对象的创建为例,该在什么时候创建 PeerConnection 对象呢?最好的时机当然是在用户加入房间之后了 。
下面我们就来看一下,对于两人通讯的情况,信令该如何设计。在我们这个例子中,可以将信令分成两大类。第一类为客户端命令;第二类为服务端命令;
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);
}
}