WebRTC是实时音视频通信,需要实现双向的通信,并且还要求有很低的延迟,不同于单向的直播。对于通信双方PeerA和PeerB,在通信之前需要进行媒体协商,即了解对方支持的媒体格式(编码格式),用什么格式编的码,就需要用相同的格式解码。所以在通信传输之前,要找双方支持的媒体格式,然后使用都支持的格式来进行视频传输。
SDP(Session Description Protocol)可以用于描述上述信息,在webrtc中,参与通信的双方必须先交换SDP信息,交换SDP的过程就称为媒体协商。实现webrtc音视频通话的基本过程如下:
1,引入webrtc库和websocket库。
dependencies {
implementation("org.jitsi:webrtc:124.0.0")
implementation("org.java-websocket:Java-WebSocket:1.5.3")
}
2,自定义一个继承于org.webrtc.SurfaceViewRenderer的类,在布局文件中使用该自定义类作为视频的SurfaceView。
import android.content.Context
import android.graphics.Outline
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import org.webrtc.SurfaceViewRenderer
// 该类的主要功能是将 SurfaceViewRenderer 渲染的视图裁剪成圆角矩形
// RoundSurfaceViewRender 类继承自 SurfaceViewRenderer,表明它是一个自定义的渲染视图.
// @JvmOverloads 注解允许 Kotlin 编译器生成多个构造函数重载,以便在 Java 代码中使用。
class RoundSurfaceViewRender @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : SurfaceViewRenderer(context, attrs) {
private var radius: Float = 0f
init {
// outlineProvider 是 View 的一个属性,用于提供视图的轮廓信息。
// 使用了一个匿名对象实现 ViewOutlineProvider 接口,并重写了 getOutline 方法。
// 在 getOutline 方法中,创建了一个 Rect 对象,其范围为视图的整个区域,
// 然后使用 outline.setRoundRect 方法将轮廓设置为圆角矩形,圆角半径为 radius。
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val rect = Rect(0, 0, view.measuredWidth, view.measuredHeight)
outline.setRoundRect(rect, radius)
}
}
// clipToOutline = true 表示将视图裁剪为其轮廓的形状,这样就实现了将视图裁剪成圆角矩形的效果。
clipToOutline = true
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 将 radius 设置为视图宽度和高度的一半中的较小值,这样可以保证圆角的半径不会超过视图的最小边长的一半,避免出现异常的圆角效果。
radius = Math.min(w / 2f, h / 2f)
}
}
3,初始化WebRTC的PeerConnectionFactory,用于创建和管理WebRTC会话。
fun initWebRtc() {
Log.v(TAG, "initWebRtc...")
// PeerConnectionFactory.initialize是 WebRTC 库中用于初始化 PeerConnectionFactory 的静态方法。
// PeerConnectionFactory 是 WebRTC 中的核心工厂类,用于创建 PeerConnection、MediaStream 等对象
PeerConnectionFactory.initialize(
// 使用 InitializationOptions 的构建器模式,传入 mContext(上下文对象),用于创建初始化选项。
PeerConnectionFactory.InitializationOptions
.builder(mContext)
// 调用构建器的 createInitializationOptions 方法,生成最终的初始化选项对象,并将其传递给 initialize 方法
.createInitializationOptions()
)
// 调用 EglBase 类的 create 方法,创建一个新的 EglBase 实例。
// EglBase 是 WebRTC 中用于管理 EGL(Embedded-System Graphics Library)上下文的类。
// 通过.eglBaseContext获取 EglBase 实例的 EGL 上下文,
// 并将其赋值给 eglBaseContext 变量,已声明 var eglBaseContext: EglBase.Context? = null,后续视频编解码等操作会使用到这个上下文。
eglBaseContext = EglBase.create().eglBaseContext
// 创建一个 PeerConnectionFactory.Options 对象,用于配置 PeerConnectionFactory 的行为。
val options = PeerConnectionFactory.Options()
// 设置 disableEncryption 属性为 true,表示禁用 WebRTC 连接中的加密功能。
options.disableEncryption = true
// 设置 disableNetworkMonitor 属性为 true,表示禁用网络状态监测功能。这可以减少一些不必要的网络开销,但可能会影响 WebRTC 对网络变化的自适应能力。
options.disableNetworkMonitor = true
// 使用 PeerConnectionFactory 的构建器模式创建一个构建器实例。
peerConnectionFactory = PeerConnectionFactory.builder()
// 设置视频解码器工厂,使用 DefaultVideoDecoderFactory 并传入之前创建的 EGL 上下文。这将指定 WebRTC 使用的视频解码器。
.setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBaseContext))
// 设置视频编码器工厂,使用 DefaultVideoEncoderFactory 并传入 EGL 上下文以及两个布尔参数。
// 第二个参数enableIntelVp8Encoder,用于控制是否启用英特尔 VP8 编码器。VP8 是一种开放的视频编码格式,由 Google 开发,具有良好的压缩效率和低延迟特性,广泛应用于 WebRTC 等实时通信场景。英特尔公司为其特定的硬件平台(如部分 Intel CPU)提供了优化的 VP8 编码器实现。
// 第三个参数enableH264HighProfile,用于控制是否启用 H.264 高级配置文件(High Profile)。H.264 是一种广泛使用的视频编码标准,有多种配置文件(Profile)可供选择,不同的配置文件提供不同的编码效率和功能。高级配置文件(High Profile)通常提供更高的压缩效率和更好的视频质量,但需要更多的计算资源。
// 当 enableH264HighProfile 设置为 true 时,DefaultVideoEncoderFactory 会尝试使用 H.264 高级配置文件进行视频编码。
// 当 enableH264HighProfile 设置为 false 时,DefaultVideoEncoderFactory 会使用 H.264 的其他配置文件(如基线配置文件 Baseline Profile 或主配置文件 Main Profile)进行编码,这些配置文件通常对计算资源的要求较低,更适合在资源受限的设备上使用。
.setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBaseContext, true, true))
// 将之前配置好的 options 对象传递给构建器,应用之前设置的加密和网络监测选项。
.setOptions(options)
// 调用构建器的 createPeerConnectionFactory 方法,创建一个 PeerConnectionFactory 实例
.createPeerConnectionFactory()
// 创建一个可变的列表 iceServers,用于存储 ICE(Interactive Connectivity Establishment)服务器信息。
// ICE 服务器用于帮助 WebRTC 客户端在不同网络环境下建立连接。
// 已声明 iceServers: MutableList<PeerConnection.IceServer>
iceServers = mutableListOf()
// 使用 PeerConnection.IceServer 的构建器模式,传入 STUN(Session Traversal Utilities for NAT)服务器的地址(STUN 是一个常量,可能定义为 stun:stun.l.google.com:19302),创建一个 ICE 服务器对象。
val iceServer = PeerConnection.IceServer.builder(STUN).createIceServer()
// 将创建好的 ICE 服务器对象添加到 iceServers 列表中。
iceServers.add(iceServer)
}
4,通过nodejs构建信令服务器
// 引入 ws 库,它是 Node.js 中用于创建 WebSocket 服务器和客户端的库。
const WebSocket = require('ws');
// 创建一个 WebSocket 服务器实例 wss,并指定服务器监听的端口为 8080。
const wss = new WebSocket.Server({ port: 8080 });
console.log('WebSocket server listening on port 8080');
// 为 WebSocket 服务器的 connection 事件添加监听器。
//当有新的客户端连接到服务器时,会触发该事件,并执行回调函数。
// 第二个参数 function connection(ws) 回调函数接收一个参数 ws,它代表与客户端建立的 WebSocket 连接。
wss.on('connection', function connection(ws) {
// 当客户端与服务器建立连接后,服务器向客户端发送一条连接成功的消息。
ws.send('Have connected to the server!');
// 为 WebSocket 连接的 message 事件添加监听器。当客户端向服务器发送消息时,会触发该事件,并执行回调函数。
ws.on('message', function incoming(message) {
console.log('received: %s', message);
try {
// 尝试将客户端发送的消息解析为 JSON 对象。
let receiveObj = JSON.parse(message);
console.log('received: receiveObj:%s', receiveObj);
// 根据解析后的 JSON 对象中的 cmdType 属性,调用不同的处理函数。
switch (receiveObj.cmdType) {
case "cmd_join_room":
handleJoinRoom(receiveObj, ws)
break
case "cmd_offer":
handleOffer(receiveObj)
break
case "cmd_answer":
handleAnswer(receiveObj)
break
case "cmd_ice":
handleIce(receiveObj)
break
case "cmd_hang_up":
handleHangup(receiveObj)
break
}
} catch (e) {
console.error('Failed to parse message:', e);
}
// 将客户端发送的消息原样返回给客户端。
ws.send('Hello, you sent -> ' + message);
});
// 为 WebSocket 连接的 close 事件添加监听器。当客户端与服务器断开连接时,会触发该事件,并执行回调函数。
ws.on('close', function close() {
// 遍历 roomMaps 中的所有房间,查找包含该客户端的房间。
roomMaps.forEach((room, roomId) => {
if (room.has(ws.uid)) {
// 从房间中移除该客户端
room.delete(ws.uid);
if (room.size === 0) {
// 如果房间中没有客户端了,则从 roomMaps 中移除该房间。
roomMaps.delete(roomId);
}
}
});
console.log('connection closed');
});
// 为 WebSocket 连接的 error 事件添加监听器。当 WebSocket 连接发生错误时,会触发该事件,并执行回调函数。
ws.on('error', function error(error) {
console.error('WebSocket Error: ', error);
});
});
// 引入 Node.js 的 child_process 模块中的 exec 函数,用于执行系统命令。
const { exec } = require('child_process');
console.log('WebSocket server exec arp -a');
// 执行 arp -a 命令,该命令用于显示当前系统的 ARP 缓存表
exec('arp -a', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
// 定义一个正则表达式,用于匹配 IP 地址。
const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g;
// 从命令执行结果中提取所有匹配的 IP 地址
const matches = stdout.match(ipRegex);
if (matches) {
console.log(matches);
}
console.log(`stdout: ${stdout}`);
if (stderr) {
console.log(`stderr: ${stderr}`);
}
});
// 定义一个 Client 类用于表示一个客户端
class Client {
// 构造函数,接收客户端的唯一标识符 uid、WebSocket 连接对象 ws 和房间 ID roomId 作为参数。
constructor(uid,ws,roomId) {
this.uid = uid;
this.ws = ws;
this.ws.uid = this.uid;
this.roomId = roomId;
this.ws.roomId = roomId;
}
}
// 定义一个 Map 对象 roomMaps,用于存储所有房间的信息,每个房间是一个 Map 对象,存储该房间中的所有客户端。
let roomMaps = new Map();
// 处理 cmd_join_room 命令的函数,接收解析后的 JSON 对象 receiveObj 和 WebSocket 连接对象 ws 作为参数。
function handleJoinRoom(receiveObj, ws) {
console.log('handleJoinRoom...');
let uid = receiveObj.uid;
let roomId = receiveObj.roomId;
// 从 roomMaps 中获取指定房间 ID 的房间
let room = roomMaps.get(roomId);
// 借助模板字符串(用反引号 包裹),你能够把变量直接嵌入字符串之中。使用${}语法来插入变量的值,最终实现将uid、roomId和room合并在同一句console.log` 里输出
console.log(`handleJoinRoom...uid: ${uid}, roomId: ${roomId}, room: ${room}`);
if (!room) {
console.log('handleJoinRoom...new room:');
room = new Map()
}
if (room.has(uid)) {
console.log('Already in room');
return
}
// 创建一个新的 Client 对象
let client = new Client(uid,ws,roomId)
console.log('handleJoinRoom...client:' + client);
// 将客户端添加到房间中
room.set(uid, client)
// 将房间添加到 roomMaps 中
roomMaps.set(roomId, room)
console.log('Entered into room');
if (room.size > 1){
// 如果房间中已经有其他客户端,则向其他客户端发送 cmd_new_peer 消息。
console.log('handleJoinRoom...room.keys():', room.keys());
// Array.from() 是 JavaScript 的一个静态方法,用于将类数组对象或可迭代对象转换为真正的数组。这里将 room.keys() 返回的 MapIterator 对象转换为一个包含所有客户端 uid 的数组 clients,方便后续遍历操作。
let clients = Array.from(room.keys())
// forEach 是数组的一个迭代方法,它会对数组中的每个元素执行一次提供的回调函数。
// 这里对 clients 数组中的每个 remoteUid(即其他客户端的唯一标识)执行回调函数。
clients.forEach(remoteUid => {
if (remoteUid !== uid) {
// 创建了一个 JavaScript 对象 sendObj,用于存储要发送给其他客户端的消息内容。cmdType 字段表明消息的类型是 cmd_new_peer,表示有新的客户端加入房间;uid 字段存储当前加入房间的客户端的唯一标识;remoteUid 字段存储接收消息的客户端的唯一标识。
let sendObj = {
cmdType: "cmd_new_peer",
uid: uid,
remoteUid
}
console.log('handleJoinRoom...room size > 1,sendObj:',sendObj);
// 根据键 remoteUid 获取 room 这个 Map 中对应的客户端对象。如果找到了对应的客户端对象,就将其赋值给 remoteClient 变量。
let remoteClient = room.get(remoteUid)
console.log('handleJoinRoom...room size > 1,remoteClient:',remoteClient);
if (remoteClient) {
// 如果 remoteClient 存在,就调用其 ws 属性(通常是该客户端对应的 WebSocket 连接对象)的 send 方法,将 sendObj 对象转换为 JSON 字符串后发送给该客户端。
remoteClient.ws.send(JSON.stringify(sendObj));
console.log("new peer send to remotes successful");
} else {
console.log("Failed to send new peer message: remoteClient not found");
}
}
})
}
}
function handleOffer(receiveObj) {
console.log('handleOffer...receiveObj:', receiveObj);
let remoteUid = receiveObj.remoteUid
let roomId = receiveObj.roomId
let room = roomMaps.get(roomId)
let client = room.get(remoteUid)
console.log(`handleOffer...remoteUid: ${remoteUid}, roomId: ${roomId}, room: ${room}, client: ${client}`);
if (client) {
client.ws.send(JSON.stringify(receiveObj))
}
}
function handleAnswer(receiveObj) {
console.log('handleAnswer...receiveObj:', receiveObj);
let remoteUid = receiveObj.remoteUid
let roomId = receiveObj.roomId
let room = roomMaps.get(roomId)
let client = room.get(remoteUid)
console.log(`handleAnswer...remoteUid: ${remoteUid}, roomId: ${roomId}, room: ${room}, client: ${client}`);
if (client) {
client.ws.send(JSON.stringify(receiveObj))
}
}
function handleIce(receiveObj) {
console.log('handleIce...receiveObj:', receiveObj);
let remoteUid = receiveObj.remoteUid
let roomId = receiveObj.roomId
let room = roomMaps.get(roomId)
let client = room.get(remoteUid)
console.log(`handleIce...remoteUid: ${remoteUid}, roomId: ${roomId}, room: ${room}, client: ${client}`);
if (client) {
client.ws.send(JSON.stringify(receiveObj))
}
}
function handleHangup(receiveObj) {
console.log('handleHangup...receiveObj:', receiveObj);
let uid = receiveObj.uid
let roomId = receiveObj.roomId
let room = roomMaps.get(roomId);
console.log(`handleOffer...uid: ${uid}, roomId: ${roomId}, room: ${room}`);
if (room.size > 1){
let clients = Array.from(room.keys())
clients.forEach(remoteUid => {
if (remoteUid !== uid){
let sendObj = {
cmdType: "cmd_hang_up",
uid: uid,
remoteUid
}
console.log('handleHangup...sendObj:',sendObj);
let remoteClient = room.get(remoteUid)
console.log('handleHangup...remoteClient:',remoteClient);
if (remoteClient) {
remoteClient.ws.send(JSON.stringify(sendObj));
console.log("new peer send successful");
} else {
console.log("Failed to send new peer message: remoteClient not found");
}
console.log('handleHangup...delete remoteUid:', remoteUid);
room.delete(remoteUid);
}
})
}
console.log('handleHangup...delete uid:', uid);
room.delete(uid);
}
上面的javaScript文件命名为ericServer.js,在在命令行通过执行 node ericServer.js 即可启动node服务器。
5, 自定义继承于org.java_websocket.client.WebSocketClient的类MyWebSocketClient,并重载onMessage(String msg)函数。
class MyWebSocketClient(serverUri: URI) : WebSocketClient(serverUri) {
// 重写了 WebSocketClient 类的 onOpen 方法,当 WebSocket 连接成功建立时会调用该方法。
override fun onOpen(handshakedata: ServerHandshake) {
Log.d(TAG, "onOpen,connected to server: ${uri}")
}
// 重写了 WebSocketClient 类的 onMessage 方法,当接收到服务器发送的消息时会调用该方法。
override fun onMessage(message: String) {
Log.v(TAG, "onMessage,received from server")
try {
val jsonStr = checkJsonObject(message)
if (TextUtils.isEmpty(jsonStr)) {
return
}
// 将提取的 JSON 字符串转换为 JSONObject 对象
val jsonObject = JSONObject(jsonStr)
val cmdType = jsonObject.getString("cmdType")
Log.v(TAG, "onMessage,received from server,cmdType: $cmdType")
when (cmdType) {
CMD_SHAKE_PEER -> {
Log.v(TAG, "onMessage,case cmd_shake_peer")
val isDND = true //开启勿扰模式
if (isDND) {
WebRtcManager(mContext).handleCallerHangup()
}
}
CMD_NEW_PEER -> {
Log.v(TAG, "onMessage,case cmd_new_peer")
WebRtcManager(mContext).handleNewPeer(jsonObject)
}
CMD_OFFER -> {
Log.v(TAG, "onMessage,case cmd_offer")
WebRtcManager(mContext).handleOffer(jsonObject)
}
CMD_ANSWER -> {
Log.v(TAG, "onMessage,case cmd_answer")
WebRtcManager(mContext).handleAnswer(jsonObject)
}
CMD_ICE -> {
Log.v(TAG, "onMessage,case cmd_ice")
WebRtcManager(mContext).handleIce(jsonObject)
}
CMD_HANG_UP -> {
Log.v(TAG, "onMessage,case cmd_hang_up")
WebRtcManager(mContext).handleCalledHangup(jsonObject)
}
else -> {
}
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
// 重写了 WebSocketClient 类的 onClose 方法,当 WebSocket 连接关闭时会调用该方法。
override fun onClose(code: Int, reason: String, remote: Boolean) {
Log.v(TAG, "onClose,code: $code,reason: $reason,remote: $remote")
}
// 重写了 WebSocketClient 类的 onError 方法,当 WebSocket 连接出现错误时会调用该方法。
override fun onError(ex: Exception) {
Log.v(TAG, "onError: $ex")
}
private fun checkJsonObject(str: String): String {
Log.d(TAG, "checkJsonObject...$str")
if (TextUtils.isEmpty(str)) {
return "false"
}
var jsonStr = ""
// 如果字符串包含 { 和 },则认为包含 JSON 数据,提取从 { 开始的子字符串作为有效的 JSON 字符串。
if (str.contains("{") && str.contains("}")) {
val index = str.indexOf("{")
Log.d(TAG, "checkJsonObject...index: $index")
jsonStr = str.substring(index)
Log.d(TAG, "checkJsonObject...jsonStr: $jsonStr")
} else {
Log.v(TAG, "checkJsonObject...not Json")
return ""
}
return jsonStr
}
}
6,初始化WebSocket,首先定义URI uri = new URI("ws://ip_addr:port");该uri作为MyWebSocketClient的构造函数参数,执行new MyWebSocketClient(uri).connect()并抛出URISyntaxException异常。
fun initWebsocket() {
Log.v(TAG, "initWebsocket...")
try {
// URI 类用于表示统一资源标识符,它会对传入的地址进行格式验证。
val uri = URI("ws://192.168.5.34:8880")
webSocketClient = MyWebSocketClient(uri)
// 调用 webSocketClient 的 connect 方法来尝试连接到 WebSocket 服务器。?. 是 Kotlin 的安全调用操作符,确保在 webSocketClient 不为 null 的情况下才调用 connect 方法。
webSocketClient?.connect()
} catch (e: java.net.URISyntaxException) {
// 捕获 URISyntaxException 异常,该异常会在传入的 WebSocket 服务器地址格式不正确时抛出。
Log.v(TAG, "initWebsocket...URISyntaxException: $e")
}
}
7,初始化SurfaceViewRenderer类型的surfaceView。
// 函数对传入的 SurfaceViewRenderer 进行初始化和属性设置,确保其能够正确渲染内容,并提供合适的显示效果,如镜像显示、内容缩放、屏幕常亮等。
// SurfaceViewRenderer 通常用于在 Android 中渲染视频流等内容
private fun initSurfaceView(surfaceView: SurfaceViewRenderer?) {
Log.v(TAG, "initSurfaceView...")
if (surfaceView == null) {
Log.v(TAG, "initSurfaceView param is null")
return
}
// 调用 SurfaceViewRenderer 的 init 方法对其进行初始化。
// eglBaseContext 是一个 EGL(Embedded-System Graphics Library)上下文对象,它为 SurfaceViewRenderer 提供了图形渲染所需的上下文环境。第二个参数传入 null,这通常表示不使用自定义的渲染器配置。
surfaceView.init(eglBaseContext, null)
// 将 SurfaceViewRenderer 的镜像属性设置为 true,意味着渲染的内容会进行水平镜像显示,常用于前置摄像头的视频预览,给用户一种照镜子的直观感受。
surfaceView.setMirror(true)
// 设置 SurfaceViewRenderer 的缩放类型为 SCALE_ASPECT_FILL。这种缩放类型会保持内容的宽高比,同时将内容缩放至填满整个 SurfaceViewRenderer 的显示区域,可能会裁剪掉部分超出显示区域的内容。
surfaceView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
// 开启屏幕常亮功能,当 SurfaceViewRenderer 处于显示状态时,屏幕不会自动熄灭,保证用户可以持续看到渲染的内容。
surfaceView.keepScreenOn = true
// 设置 SurfaceViewRenderer 的 Z 顺序,使其显示在其他媒体层之上。这意味着 SurfaceViewRenderer 渲染的内容会覆盖在其他普通视图之上显示。
surfaceView.setZOrderMediaOverlay(true)
// 禁用硬件缩放功能。硬件缩放通常由设备的 GPU 等硬件来完成,禁用后可能会使用软件缩放,可能会影响性能,但在某些情况下可以避免硬件缩放带来的兼容性问题。
surfaceView.setEnableHardwareScaler(false)
}
8,初始化PeerConnection。
fun initPeerConnection() {
Log.v(TAG, "initPeerConnection...")
// 创建一个 RTCConfiguration 对象,该对象用于配置 PeerConnection 的行为。iceServers 是一个包含 ICE(Interactive Connectivity Establishment)服务器信息的列表,ICE 服务器用于帮助客户端在不同网络环境下建立连接。
val configuration = PeerConnection.RTCConfiguration(iceServers)
// val webRtcPeerConnObserver: PeerConnection.Observer = WebRtcPeerConnObserver()
// peerConnection = peerConnectionFactory?.createPeerConnection(configuration, webRtcPeerConnObserver)
// 通过 PeerConnectionFactory 来创建 PeerConnection 对象。
// peerConnectionFactory 是之前初始化好的工厂对象,使用 PeerConnectionFactory 的构建器模式创建的,使用 ?. 操作符确保在 peerConnectionFactory 不为 null 时才调用 createPeerConnection 方法。
peerConnection = peerConnectionFactory?.createPeerConnection(configuration,
// object : WebRtcPeerConnObserver():创建一个匿名内部类,继承自 WebRtcPeerConnObserver,并重写其中的一些方法,用于处理 PeerConnection 的事件。
object : WebRtcPeerConnObserver() {
// 重写 WebRtcPeerConnObserver 中的 onIceCandidate 方法,当 PeerConnection 生成一个新的 ICE 候选者时会调用该方法。
override fun onIceCandidate(iceCandidate: IceCandidate) {
super.onIceCandidate(iceCandidate)
Log.d(TAG, "initPeerConnection...,onIceCandidate,iceCandidate: $iceCandidate")
if (iceCandidate != null) {
val sendObj = JSONObject()
val ownerUid = "uid001"
try {
sendObj.put("cmdType", CMD_ICE)
sendObj.put("uid", ownerUid)
sendObj.put("remoteUid", remoteUid)
sendObj.put("roomId", roomId)
val msgObj = JSONObject()
msgObj.put("sdpMid", iceCandidate.sdpMid)
msgObj.put("sdpMLineIndex", iceCandidate.sdpMLineIndex)
msgObj.put("sdp", iceCandidate.sdp)
sendObj.put("msg", msgObj)
// 通过 webSocketClient 将封装好的消息以字符串形式发送出去。使用 ?. 操作符确保 webSocketClient 不为 null。
// var webSocketClient: MyWebSocketClient? = null
webSocketClient?.send(sendObj.toString())
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
}
// 重写 WebRtcPeerConnObserver 中的 onAddStream 方法,当 PeerConnection 接收到一个新的媒体流时会调用该方法。
override fun onAddStream(mediaStream: MediaStream) {
super.onAddStream(mediaStream)
Log.v(TAG, "initPeerConnection...,onAddStream,mediaStream: $mediaStream")
// 从媒体流中获取视频轨道列表。
val videoTracks = mediaStream.videoTracks
if (videoTracks != null && videoTracks.isNotEmpty()) {
val videoTrack = videoTracks[0]
Log.v(TAG, "initPeerConnection...,onAddStream,videoTrack: $videoTrack")
if (videoTrack != null) {
val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch {
videoTrack.addSink(surfaceViewRemote)
if (surfaceViewRemote != null) {
Log.d(TAG, "setBackground null...")
surfaceViewRemote!!.background = null
}
}
}
}
// 从接收到的媒体流中获取音频轨道列表
val audioTracks = mediaStream.audioTracks
if (audioTracks != null && audioTracks.isNotEmpty()) {
audioTracks[0]?.setVolume(VOLUME)
}
}
})
}
在上面的代码中,有一个继承自WebRtcPeerConnObserver的匿名内部类,定义WebRtcPeerConnObserver类如下:
// 在 Kotlin 里,open 关键字的作用是允许其他类继承使用。
// 在 Kotlin 里,类默认是 final 的,也就是说默认不允许被继承。这和 Java 不同,Java 里的类默认是可以被继承的,除非用 final 关键字修饰。
open class WebRtcPeerConnObserver: PeerConnection.Observer {
companion object {
private const val TAG = "PeerConnectionObserver"
}
override fun onSignalingChange(signalingState: PeerConnection.SignalingState) {
Log.v(TAG, "onSignalingChange,signalingState: $signalingState")
signalingState?.let {
Log.v(TAG, "onSignalingChange,signalingState.name: ${it.name}")
}
}
override fun onIceConnectionChange(iceConnectionState: PeerConnection.IceConnectionState) {
Log.v(TAG, "onIceConnectionChange,iceConnectionState: $iceConnectionState")
}
override fun onIceConnectionReceivingChange(b: Boolean) {
Log.v(TAG, "onIceConnectionReceivingChange,b: $b")
}
override fun onIceGatheringChange(iceGatheringState: PeerConnection.IceGatheringState) {
Log.v(TAG, "onIceGatheringChange,iceGatheringState: $iceGatheringState")
}
override fun onIceCandidate(iceCandidate: IceCandidate) {
Log.v(TAG, "onIceCandidate,iceCandidate: $iceCandidate")
}
override fun onIceCandidatesRemoved(iceCandidates: Array<IceCandidate>) {
Log.v(TAG, "onIceCandidatesRemoved,iceCandidates: ${iceCandidates.contentToString()}")
}
override fun onAddStream(mediaStream: MediaStream) {
Log.v(TAG, "onAddStream,mediaStream: $mediaStream")
}
override fun onRemoveStream(mediaStream: MediaStream) {
Log.v(TAG, "onRemoveStream,mediaStream: $mediaStream")
}
override fun onDataChannel(dataChannel: DataChannel) {
Log.v(TAG, "onDataChannel,dataChannel: $dataChannel")
}
override fun onRenegotiationNeeded() {
Log.v(TAG, "onRenegotiationNeeded...")
}
}
另外,在initPeerConnection函数中向服务器发送了cmd_ice命令,在收到服务器的回复后进行如下处理:
fun handleIce(msgObj: JSONObject) {
Log.v(TAG, "handleIce...")
//received peer ice
Log.d(TAG, "handleIce...$msgObj")
try {
// 从 msgObj 中提取键为 "msg" 的 JSONObject,该对象包含了 ICE 候选者的详细信息。
val iceObj = msgObj.getJSONObject("msg")
// 使用提取到的信息创建一个 IceCandidate 对象
val iceCandidate = IceCandidate(
// 从 iceObj 中获取键为 "sdpMid" 的字符串值,该值表示 SDP 中的媒体流标识。
iceObj.getString("sdpMid"),
// 从 iceObj 中获取键为 "sdpMLineIndex" 的整数值,该值表示 SDP 中的媒体行索引。
iceObj.getInt("sdpMLineIndex"),
// 从 iceObj 中获取键为 "sdp" 的字符串值,该值是 ICE 候选者的 SDP 描述。
iceObj.getString("sdp")
)
// 调用 peerConnection 对象的 addIceCandidate 方法,将创建好的 IceCandidate 对象添加到对等连接中。
peerConnection?.addIceCandidate(iceCandidate)
} catch (e: JSONException) {
Log.e(TAG, "handleIce...jsonException: $e")
}
}
9,启动摄像头捕捉视频和音频。
// 函数的主要功能是依据传入的isFront参数,尝试查找并创建一个前置或后置摄像头的视频捕获器,若成功则返回该捕获器实例,否则返回null。
// 函数的返回类型VideoCapturer为视频捕获器类,?表示该返回值可能为null。
private fun createCameraCapture(isFront: Boolean): VideoCapturer? {
Log.v(TAG, "createCameraCapture...isFront: $isFront")
// 创建一个Camera1Enumerator类的实例,参数true表示使用对旧版相机 API 进行优化的枚举器。
val enumerator = Camera1Enumerator(true)
// 存储枚举器枚举出来的所有相机设备名称的列表。
val deviceNames = enumerator.deviceNames
// 对deviceNames列表中的每个设备名称进行遍历。
for (deviceName in deviceNames) {
// 若isFront为true,则调用enumerator.isFrontFacing(deviceName)判断该设备是否为前置摄像头;
// 反之,调用enumerator.isBackFacing(deviceName)判断是否为后置摄像头。
val checkFront = if (isFront) enumerator.isFrontFacing(deviceName) else enumerator.isBackFacing(deviceName)
Log.d(TAG, "createCameraCapture...deviceName: $deviceName,checkFront: $checkFront")
// 若checkFront为true,表明该设备是所需的摄像头(前置或后置)。
if (checkFront) {
Log.d(TAG, "createCameraCapture...frontFacing: $deviceName")
// 调用enumerator.createCapturer方法,依据设备名称创建一个视频捕获器实例,第二个参数null表示不使用自定义的相机事件监听器。
val videoCapture = enumerator.createCapturer(deviceName, null)
Log.d(TAG, "createCameraCapture...videoCapture: $videoCapture")
// 若视频捕获器实例不为null,就返回该实例。
if (videoCapture != null) {
return videoCapture
}
}
}
// 若遍历完所有设备都没有找到合适的摄像头或者无法创建视频捕获器,就返回null。
return null
}
// startVideoCapture 函数用于启动视频捕获并将视频轨道添加到 peerConnection 中
fun startVideoCapture(surfaceViewRenderer: SurfaceViewRenderer?) {
Log.v(TAG, "startVideoCapture...")
if (surfaceViewRenderer == null) {
return
}
// 通过 peerConnectionFactory 创建一个视频源,参数true 表示启用自动对焦功能。peerConnectionFactory 是可空的,所以使用了安全调用操作符 ?.。
val videoSource = peerConnectionFactory?.createVideoSource(true)
Log.d(TAG, "startVideoCapture...videoSource: $videoSource")
// 创建一个 SurfaceTextureHelper 对象,用于管理视频捕获的纹理。Thread.currentThread().name 是当前线程的名称,eglBaseContext 是 EGL 上下文。
val surfaceTextureHelper = SurfaceTextureHelper.create(
Thread.currentThread().name, eglBaseContext
)
// 调用 createCameraCapture 函数创建一个前置摄像头的视频捕获器。如果创建失败(返回 null),则直接返回。
val videoCapture = createCameraCapture(true) ?: return
// 初始化视频捕获器,传入 SurfaceTextureHelper、上下文 mContext 和视频源的捕获器观察者。
videoCapture.initialize(surfaceTextureHelper, mContext, videoSource?.capturerObserver)
// 开始视频捕获,指定视频的分辨率(宽度和高度)和帧率。这些参数来自 MyWebSocketClient 类。
videoCapture.startCapture(MyWebSocketClient.VIDEO_RES_WIDTH, MyWebSocketClient.VIDEO_RES_HEIGHT, MyWebSocketClient.VIDEO_FPS)
// 通过 peerConnectionFactory 创建一个视频轨道,名称为 "videotrack0",并关联之前创建的视频源。
val videoTrack = peerConnectionFactory?.createVideoTrack("videotrack0", videoSource)
// 将视频轨道的输出添加到 surfaceViewLocal 上进行渲染。
videoTrack?.addSink(surfaceViewLocal)
Log.d(TAG, "startVideoCapture...videoTrack: $videoTrack")
// 通过 peerConnectionFactory 创建一个本地媒体流,名称为 "VideoStream"。
val mediaStream = peerConnectionFactory?.createLocalMediaStream("VideoStream")
Log.d(TAG, "startVideoCapture...mediaStream: $mediaStream")
if (peerConnection != null && videoTrack != null) {
// 检查 peerConnection 和 videoTrack 是否不为 null,如果都不为 null,则将视频轨道添加到 peerConnection 中。
peerConnection!!.addTrack(videoTrack, listOf(videoTrack.id()))
}
}
// startAudioCapture 函数用于启动音频捕获并将音频轨道添加到 peerConnection 中
private fun startAudioCapture() {
Log.v(TAG, "startAudioCapture...")
// 创建一个 MediaConstraints 对象,用于设置音频捕获的约束条件。
val audioConstraints = MediaConstraints()
// 通过 peerConnectionFactory 创建一个音频源,并传入音频约束条件。
val audioSource = peerConnectionFactory?.createAudioSource(audioConstraints)
Log.d(TAG, "startAudioCapture...audioSource: $audioSource")
// 通过 peerConnectionFactory 创建一个音频轨道,名称为 "audiotrack",并关联之前创建的音频源。
val audioTrack = peerConnectionFactory?.createAudioTrack("audiotrack", audioSource)
Log.d(TAG, "startAudioCapture...audioTrack: $audioTrack")
// 设置音频轨道的音量,VOLUME 是一个常量。
audioTrack?.setVolume(VOLUME)
// 将音频轨道添加到 peerConnection 中。
peerConnection?.addTrack(audioTrack)
}
10,开启会话通信过程。
(1)首先要加入会议,构造JSONObject对象sendObj,sendObj.put("cmdType", CMD_JOIN),并且此时开始捕捉音频和本地视频。
fun handleJoinConversation() {
Log.v(TAG, "handleJoinConversation...")
// 创建一个 JSONObject 实例 sendObj,用于构建要发送的 JSON 数据
val sendObj = JSONObject()
val ownerUid = ""
Log.d(TAG, "handleJoinConversation...uid: $ownerUid")
try {
// 向 sendObj 中添加一个键值对
sendObj.put("cmdType", MyWebSocketClient.CMD_JOIN_ROOM)
sendObj.put("uid", ownerUid)
sendObj.put("roomId", roomId)
if (webSocketClient != null) {
// 获取 webSocketClient 的连接状态,isOpen 是一个布尔值,表示 WebSocket 连接是否打开。这里使用了非空断言 !!,因为已经在前面的 if 语句中检查过 webSocketClient 不为 null。
val isOpen = webSocketClient!!.isOpen
Log.v(TAG, "handleJoinConversation...isOpen: $isOpen")
if (isOpen) {
// 将 sendObj 转换为 JSON 字符串,并通过 webSocketClient 发送给服务器。
webSocketClient!!.send(sendObj.toString())
}
}
} catch (e: JSONException) {
// 捕获在 try 块中抛出的 JSONException 异常,将捕获到的 JSONException 包装成 RuntimeException 并重新抛出,以便上层代码可以处理该异常。
throw RuntimeException(e)
}
}
(2)在服务器接收到cmd_join的消息后,则向其它已经加入会议的客户端发送cmd_new_peer的消息。
(3)在客户端MyWebSocketClient的onMessage()中收到cmd_new_peer的消息后调用createOffer创建会话描述提议。
fun handleNewPeer(msgObj: JSONObject) {
Log.v(TAG, "handleNewPeer...")
try {
remoteUid = msgObj.getString("uid")
Log.d(TAG, "handleNewPeer...remoteUid: $remoteUid")
startAudioCapture()
startVideoCapture(surfaceViewLocal)
// 创建一个 MediaConstraints 对象 constraints,用于设置 WebRTC 会话的约束条件。
val constraints = MediaConstraints()
// 在 WebRTC 里,通常借助特定的约束键来区分是接收音频流还是视频流。
// 一般会使用 "OfferToReceiveAudio" 和 "OfferToReceiveVideo" 来分别表示接收音频流和视频流。
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
// 调用 peerConnection 对象的 createOffer 方法,用于创建一个会话描述协议(SDP)提议。
peerConnection?.createOffer(object : EricSdpObserver("createOffer") {
// 定义一个可空的 SessionDescription 类型的变量 localSdp,用于存储创建的 SDP 提议。
var localSdp: SessionDescription? = null
// 重写 EricSdpObserver 类的 onCreateSuccess 方法,当 SDP 提议创建成功时调用。
override fun onCreateSuccess(sessionDescription: SessionDescription) {
Log.v(TAG, "handleNewPeer...onCreateSuccess")
super.onCreateSuccess(sessionDescription)
// 将创建的 SDP 提议赋值给 localSdp 变量
localSdp = sessionDescription
// 调用 peerConnection 的 setLocalDescription 方法,将创建的 SDP 提议设置为本地描述。
peerConnection?.setLocalDescription(this, sessionDescription)
}
// 重写 EricSdpObserver 类的 onSetSuccess 方法,当本地描述设置成功时调用。
override fun onSetSuccess() {
super.onSetSuccess()
Log.v(TAG, "handleNewPeer...onSetSuccess,for setLocalDescription")
val sendObj = JSONObject()
try {
// 向 sendObj 中添加多个键值对,包括命令类型、本地用户 ID、远程用户 ID、房间 ID 和 SDP 描述。
sendObj.put("cmdType", MyWebSocketClient.CMD_OFFER)
sendObj.put("uid", "uid001")
sendObj.put("remoteUid", remoteUid)
sendObj.put("roomId", roomId)
sendObj.put("description", localSdp?.description)
if (webSocketClient != null) {
// 将 sendObj 转换为字符串并通过 WebSocket 发送出去
webSocketClient!!.send(sendObj.toString())
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
}, constraints)
} catch (e: JSONException) {
Log.e(TAG, "handleNewPeer...,JSONException: $e")
}
}
上面的代码调用到了EricSdpObserver类,其定义如下。
import android.util.Log
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
// 在 Kotlin 中,类默认是 final 的,也就是不能被继承。如果想要让一个类能够被其他类继承,就得使用 open 修饰符来标记这个类。
// 通过继承 open 类,可以利用多态特性。多态指的是在运行时根据对象的实际类型来调用相应的方法。
open class EricSdpObserver: SdpObserver {
companion object {
private const val TAG = "ArkSdpObserver--AVC"
}
constructor()
constructor(type: String) {
Log.v(TAG, "ArkSdpObserver,type:$type")
}
override fun onCreateSuccess(sessionDescription: SessionDescription) {
Log.v(TAG, "onCreateSuccess...")
}
override fun onSetSuccess() {
Log.v(TAG, "onSetSuccess...")
}
override fun onCreateFailure(s: String) {
Log.v(TAG, "onCreateFailure...$s")
}
override fun onSetFailure(s: String) {
Log.v(TAG, "onSetFailure...$s")
}
}
(4)在服务器接收到cmd_offer后,根据remoteId从会议室获取远端客户端client,并且通过client.ws.send()将收到的消息发送给remote客户端。
(5)在客户端收到cmd_offer的消息后定义一个函数handleOffer(JSONObject msgObj)来处理消息,此时收到offer,当前的角色是应答者answer。
fun handleOffer(msgObj: JSONObject) {
Log.v(TAG, "handleOffer...")
//received offer,current identity is answer
Log.d(TAG, "handleOffer...$msgObj")
if (msgObj == null) {
return
}
try {
// 从 msgObj 中提取键为 "uid" 的字符串值,并将其赋值给 remoteUid 变量,该变量表示调用者(发送 Offer 的用户)的唯一标识符。
remoteUid = msgObj.getString("uid")//caller's uid
Log.d(TAG, "handleOffer...caller's uid: $remoteUid")
// 从 msgObj 中提取键为 "description" 的字符串值,该值为 Offer 的 SDP(会话描述协议)描述。
var sdpDescription = msgObj.getString("description")
// 使用提取到的 SDP 描述创建一个 SessionDescription 对象,类型为 OFFER。
val sdp = SessionDescription(SessionDescription.Type.OFFER, sdpDescription)
// 获取 peerConnection 对象的信令状态。使用安全调用操作符 ?. 是因为 peerConnection 可能为 null。
val signalingState = peerConnection?.signalingState()
Log.d(TAG, "handleOffer...signalingState: $signalingState")
// 检查信令状态是否为 STABLE,如果不是,则输出详细日志并返回,结束函数执行。
if (signalingState != PeerConnection.SignalingState.STABLE) {
Log.v(TAG, "handleOffer...signalingState NOT STABLE")
return
}
// 调用 peerConnection 的 setRemoteDescription 方法,将接收到的 Offer 的 SDP 描述设置为远程描述。
peerConnection?.setRemoteDescription(object : EricSdpObserver("offerSetRemoteDesc") {
// 定义一个布尔类型的变量 isCreateAnswer,用于标记是否已经创建了 Answer。
var isCreateAnswer = false
var sdpDescription: String? = null
// 重写 EricSdpObserver 类的 onCreateSuccess 方法,当创建 Answer 成功时调用。
override fun onCreateSuccess(sessionDescription: SessionDescription) {
super.onCreateSuccess(sessionDescription)
Log.v(TAG, "handleOffer...onCreateSuccess, create answer succeed")
if (sessionDescription == null) {
return
}
// 将创建的 Answer 的 SDP 描述赋值给 sdpDescription 变量。
sdpDescription = sessionDescription.description
// 调用 peerConnection 的 setLocalDescription 方法,将创建的 Answer 的 SDP 描述设置为本地描述。
peerConnection?.setLocalDescription(this, sessionDescription)
}
// 重写 EricSdpObserver 类的 onSetSuccess 方法,当设置远程描述或本地描述成功时调用。
override fun onSetSuccess() {
super.onSetSuccess()
Log.v(TAG, "handleOffer...onSetSuccess,callback for setRemoteDescription,setLocalDescription")
// 如果 isCreateAnswer 为 false,表示这是设置远程描述成功后的回调
// 如果 isCreateAnswer 为 true,表示这是设置本地描述成功后的回调
if (!isCreateAnswer) {
// isCreateAnswer 为 false
Log.v(TAG, "handleOffer...onSetSuccess1, callback for setRemoteDescription")
isCreateAnswer = true
// 创建 MediaConstraints 对象并设置约束条件,要求接收音频和视频流。
val constraints = MediaConstraints()
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
constraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
// 调用 peerConnection 的 createAnswer 方法,创建 Answer。
peerConnection?.createAnswer(this, constraints)
startVideoCapture(surfaceViewLocal)
} else {
// isCreateAnswer 为 true
Log.v(TAG, "handleOffer...onSetSuccess2, callback for setLocalDescription")
val sendObj = JSONObject()
val ownerUid = "uid001"
Log.d(TAG, "handleOffer...uid: $ownerUid")
try {
sendObj.put("cmdType", MyWebSocketClient.CMD_ANSWER)
sendObj.put("uid", ownerUid)
sendObj.put("remoteUid", remoteUid)
sendObj.put("roomId", roomId)
sendObj.put("description", sdpDescription)
if (webSocketClient != null) {
webSocketClient!!.send(sendObj.toString())
}
} catch (e: JSONException) {
throw RuntimeException(e)
}
}
}
override fun onSetFailure(s: String) {
super.onSetFailure(s)
Log.v(TAG, "handleOffer...onSetFailure: $s")
}
override fun onCreateFailure(s: String) {
super.onCreateFailure(s)
Log.v(TAG, "handleOffer...onCreateFailure$s")
}
}, sdp)
} catch (e: JSONException) {
Log.e(TAG, "handleOffer...jsonException: $e")
}
}
(6)在服务端收到cmd_answer后,将收到的消息发送给remote客户端。
(7)在客户端收到cmd_answer的消息后定义一个函数handleAnswer(JSONObject msgObj)来处理消息,此时收到answer,当前的角色是拨打者caller。
// handleAnswer 函数的主要功能是处理接收到的应答消息,提取其中的 SDP 描述,创建应答类型的会话描述对象,并将其设置为远程会话描述。同时,监听会话描述创建和设置过程中的成功和失败事件,并输出相应的日志。
fun handleAnswer(msgObj: JSONObject) {
Log.v(TAG, "handleAnswer...")
//received answer,current identity is caller
Log.d(TAG, "handleAnswer...$msgObj")
try {
// 从 msgObj 中提取键为 "description" 的字符串值,该值代表应答消息中的 SDP(会话描述协议)描述。
val sdpDescription = msgObj.getString("description")
// 使用提取到的 SDP 描述创建一个 SessionDescription 对象,指定其类型为 ANSWER,表示这是一个应答类型的会话描述。
val sdp = SessionDescription(SessionDescription.Type.ANSWER, sdpDescription)
// 调用 peerConnection 对象的 setRemoteDescription 方法,将接收到的应答的 SDP 描述设置为远程会话描述。
peerConnection?.setRemoteDescription(
// 创建一个匿名内部类,继承自 EricSdpObserver,并传入 "callerSetRemote" 作为构造参数。EricSdpObserver 应该是一个自定义的 SdpObserver 实现类,用于监听会话描述设置过程中的各种事件。
object : EricSdpObserver("callerSetRemote") {
// 重写 onCreateSuccess 方法,当会话描述创建成功时调用。
override fun onCreateSuccess(sessionDescription: SessionDescription) {
super.onCreateSuccess(sessionDescription)
Log.v(TAG, "handleAnswer...onCreateSuccess")
}
// 重写 onSetSuccess 方法,当会话描述设置成功时调用。
override fun onSetSuccess() {
super.onSetSuccess()
Log.v(TAG, "handleAnswer...onSetSuccess")
}
// 重写 onCreateFailure 方法,当会话描述创建失败时调用。
override fun onCreateFailure(s: String) {
super.onCreateFailure(s)
Log.v(TAG, "handleAnswer...onCreateFailure: $s")
}
// 重写 onSetFailure 方法,当会话描述设置失败时调用。
override fun onSetFailure(s: String) {
super.onSetFailure(s)
Log.v(TAG, "handleAnswer...onSetFailure: $s")
}
},
sdp
)
} catch (e: JSONException) {
Log.e(TAG, "handleAnswer...jsonException$e")
}
}