前端基于WebRTC实现音视频通话
背景
随着互联网技术的飞速发展,实时音视频通话已经成为在线教育、远程办公、社交媒体等领域的核心且常用的功能。WebRTC(Web Real-Time Communication)作为一项开放的实时通信标准,为开发者提供了快速构建实时音视频通话系统的能力。
应用场景
- 点对点视频聊天:如 微信视频 等实时视频通话应用。
- 多人视频会议:企业级多人视频会议系统,如飞书、钉钉、腾讯会议等。
- 在线教育:如腾讯课堂、网易云课堂等。
- 直播:游戏直播、课程直播等。
P2P通信原理
P2P 通信即点对点通信。
P2P连接需要进行内网穿透是因为在现代网络环境中,许多设备位于私有网络(如家庭网络)后面,这些设备使用了NAT(Network Address Translation)来共享公共IP地址。NAT会阻止直接从外部访问这些设备,因此需要通过内网穿透技术来克服这一障碍,实现设备之间的直接通信。大多数客户端设备(如智能手机、电脑、智能家居设备等)都连接到了一个局域网(LAN)中,而这个局域网通常由路由器或交换机提供网络连接。在这种局域网环境中,每个设备都被分配了一个私有IP地址,这些私有IP地址只在局域网内部有效,无法直接从公共互联网访问到。
要实现两个客户端的实时音视频通信,并且这两个客户端可能处于不同网络环境,使用不同的设备,都需要解决哪些问题?
主要是下面这 3 个问题:
- 如何发现对方?
- 不同的音视频编解码能力如何沟通?
- 如何联系上对方?
下面我们将逐个讨论这 3 个问题。
如何发现对方?
在 P2P 通信的过程中,双方需要交换一些元数据比如媒体信息、网络数据等等信息,我们通常称这一过程叫做“信令(signaling)”。
对应的服务器即“信令服务器 (signaling server)”,通常也有人将之称为“房间服务器”,因为它不仅可以交换彼此的媒体信息和网络信息,同样也可以管理房间信息。
比如:
1)通知彼此 who 加入了房间;2)who 离开了房间 3)告诉第三方房间人数是否已满是否可以加入房间。
为了避免出现冗余,并最大限度地提高与已有技术的兼容性,WebRTC 标准并没有规定信令方法和协议。在本课程中会使用websocket来搭建一个信令服务器
不同的音视频编解码能力如何沟通?
不同浏览器对于音视频的编解码能力是不同的。
比如: 以日常生活中的例子来讲,小李会讲汉语和英语,而小王会讲汉语和法语。为了保证双方都可以正确的理解对方的意思,最简单的办法即取他们都会的语言,也就是汉语来沟通。
在 WebRTC 中:有一个专门的协议,称为 Session Description Protocol(SDP),可以用于描述上述这类信息。
因此:参与音视频通讯的双方想要了解对方支持的媒体格式,必须要交换 SDP 信息。而交换 SDP 的过程,通常称之为媒体协商。
如何联系上对方?
其实就是网络协商的过程,即参与音视频实时通信的双方要了解彼此的网络情况,这样才有可能找到一条相互通讯的链路。
正常的设备都是局域网内的私有IP设备,不知道双方的互联网IP地址就无法建立连接。
理想的网络情况是每个客户端都有自己的私有公网 IP 地址,这样的话就可以直接进行点对点连接。实际上呢,出于网络安全和其他原因的考虑,大多数客户端之间都是在某个局域网内,需要网络地址转换(NAT)。
在 WebRTC 中我们使用 ICE 机制建立网络连接。ICE 协议通过一系列的技术(如 STUN、TURN 服务器)帮助通信双方发现和协商可用的公共网络地址,从而实现 NAT 穿越。
ICE 的工作原理如下:
- 首先,通信双方收集本地网络地址(包括私有地址和公共地址)以及通过 STUN 和 TURN 服务器获取的候选地址。
- 接下来,双方通过信令服务器交换这些候选地址。
- 通信双方使用这些候选地址进行连接测试,确定最佳的可用地址。
- 一旦找到可用的地址,通信双方就可以开始实时音视频通话。
在 WebRTC 中网络信息通常用candidate来描述
针对上面三个问题的总结:就是通过 WebRTC 提供的 API 获取各端的媒体信息 SDP 以及 网络信息 candidate ,并通过信令服务器交换,进而建立了两端的连接通道完成实时视频语音通话。
常用的API
音视频采集getUserMedia
// 获取本地音视频流
const getLocalStream = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ // 获取音视频流
audio: true,
video: true
})
localVideo.value!.srcObject = stream
localVideo.value!.play()
return stream
}
核心对象 RTCPeerConnection
RTCPeerConnection 作为创建点对点连接的 API,是我们实现音视频实时通信的关键。
const peer = new RTCPeerConnection({
// iceServers: [
// { url: "stun:stun.l.google.com:19302" }, // 谷歌的公共服务
// {
// urls: "turn:***",
// credential: "***",
// username: "***",
// },
// ],
});
主要会用到以下几个方法:
媒体协商方法:
- createOffer
- createAnswer
- setLocalDesccription
- setRemoteDesccription
重要事件:
-
onicecandidate
-
onaddstream
整个媒体协商过程可以简化为三个步骤对应上述四个媒体协商方法:
- 呼叫端创建 Offer(createOffer)并将 offer 消息(内容是呼叫端的 SDP 信息)通过信令服务器传送给接收端,同时调用 setLocalDesccription 将含有本地 SDP 信息的 Offer 保存起来
- 接收端收到对端的 Offer 信息后调用 setRemoteDesccription 方法将含有对端 SDP 信息的 Offer 保存起来,并创建 Answer(createAnswer)并将 Answer 消息(内容是接收端的 SDP 信息)通过信令服务器传送给呼叫端
- 呼叫端收到对端的 Answer 信息后调用 setRemoteDesccription 方法将含有对端 SDP 信息的 Answer 保存起来
经过上述三个步骤,则完成了 P2P 通信过程中的媒体协商部分,实际上在呼叫端以及接收端调用setLocalDesccription 同时也开始了收集各端自己的网络信息(candidate),然后各端通过监听事件 onicecandidate 收集到各自的 candidate 并通过信令服务器传送给对端,进而打通 P2P 通信的网络通道,并通过监听 onaddstream 事件拿到对方的视频流进而完成了整个视频通话过程。
实践
项目搭建
前端项目
- 项目使用
vue3+ts
,运行如下命令:
npm create vite@latest webrtc-client -- --template vue-ts
- 并且引入
tailwindcss
:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
- 在生成的
tailwind.config.js
配置文件中添加所有模板文件的路径。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
- 修改
style.css
中的内容如下:
@tailwind base;
@tailwind components;
@tailwind utilities;
- 自定义修改
App.vue
中的内容如下:
<script lang="ts" setup>
import { ref } from 'vue'
const called = ref<boolean>(false) // 是否是接收方
const caller = ref<boolean>(false) // 是否是发起方
const calling = ref<boolean>(false) // 呼叫中
const communicating = ref<boolean>(false) // 视频通话中
const localVideo = ref<HTMLVideoElement>() // video标签实例,播放本人的视频
const remoteVideo = ref<HTMLVideoElement>() // video标签实例,播放对方的视频
// 发起方发起视频请求
const callRemote = () => {
console.log('发起视频');
}
// 接收方同意视频请求
const acceptCall = () => {
console.log('同意视频邀请');
}
// 挂断视频
const hangUp = () => {
console.log('挂断视频');
}
</script>
<template>
<div class="flex items-center flex-col text-center p-12 h-screen">
<div class="relative h-full mb-4">
<video
ref="localVideo"
class="w-96 h-full bg-gray-200 mb-4 object-cover"
></video>
<video
ref="remoteVideo"
class="w-32 h-48 absolute bottom-0 right-0 object-cover"
></video>
<div v-if="caller && calling" class="absolute top-2/3 left-36 flex flex-col items-center">
<p class="mb-4 text-white">等待对方接听...</p>
<img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer" alt="">
</div>
<div v-if="called && calling" class="absolute top-2/3 left-32 flex flex-col items-center">
<p class="mb-4 text-white">收到视频邀请...</p>
<div class="flex">
<img @click="hangUp" src="/refuse.svg" class="w-16 cursor-pointer mr-4" alt="">
<img @click="acceptCall" src="/accept.svg" class="w-16 cursor-pointer" alt="">
</div>
</div>
</div>
<div class="flex gap-2 mb-4">
<button
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white"
@click="callRemote"
>发起视频</button>
<button
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white"
@click="hangUp"
>挂断视频</button>
</div>
</div>
</template>
执行完上面的步骤就可以运行npm run dev
来在本地启动项目了
后端项目
创建一个webrtc-server
的文件夹,执行npm init
,一路回车即可,然后运行如下命令安装socket.io
和nodemon
:
npm install socket.io nodemon
创建index.js
的文件,并添加如下内容:
const socket = require('socket.io');
const http = require('http');
const server = http.createServer()
const io = socket(server, {
cors: {
origin: '*' // 配置跨域
}
});
io.on('connection', sock => {
console.log('连接成功...')
// 向客户端发送连接成功的消息
sock.emit('connectionSuccess');
})
server.listen(3000, () => {
console.log('服务器启动成功');
});
在package.json
中添加start
命令,使用nodemon
启动项目:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
执行完后运行npm run start
即在3000端口可启动node服务了
前端连接信令服务器
前端需要安装socket.io-client
, 并连接信令服务器:
<script setup lang="ts">
// App.vue
import { ref, onMounted, onUnmounted } from 'vue'
import { io, Socket } from "socket.io-client";
// ...
const socket = ref<Socket>() // Socket实例
onMounted(() => {
const sock = io('localhost:3000'); // 对应服务的端口
// 连接成功
sock.on('connectionSuccess', () => {
console.log('连接成功')
});
socket.value = sock;
})
// ...
</script>
发起视频请求
角色:用户A–发起方,用户B–接收方
房间:类比聊天窗口
连接成功时加入房间:
// 前端代码
const roomId = '001'
sock.on('connectionSuccess', () => {
console.log('连接服务器成功...');
sock.emit('joinRoom', roomId) // 前端发送加入房间事件
})
// 服务端代码
sock.on('joinRoom', (roomId) => {
sock.join(roomId) // 加入房间
})
用户A发起视频请求并通知用户B:
- 用户A发起视频请求,并且通过信令服务器通知用户B
// 发起方发起视频请求
const callRemote = async () => {
console.log('发起视频');
caller.value = true;
calling.value = true;
await getLocalStream()
// 向信令服务器发送发起请求的事件
socket.value?.emit('callRemote', roomId)
}
- 用户B同意视频请求,并且通过信令服务器通知用户A
// 接收方同意视频请求
const acceptCall = () => {
console.log('同意视频邀请');
socket.value?.emit('acceptCall', roomId)
}
开始交换 SDP 信息和 candidate 信息:
- 用户A创建创建RTCPeerConnection,添加本地音视频流,生成offer,并且通过信令服务器将offer发送给用户B
// 创建RTCPeerConnection
peer.value = new RTCPeerConnection()
// 添加本地音视频流
peer.value.addStream(localStream.value)
// 生成offer
const offer = await peer.value.createOffer({
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
})
console.log('offer', offer);
// 设置本地描述的offer
await peer.value.setLocalDescription(offer);
// 通过信令服务器将offer发送给用户B
socket.value?.emit('sendOffer', { offer, roomId })
- 用户B收到用户A的offer
sock.on('sendOffer', (offer) => {
if (called.value) { // 判断接收方
console.log('收到offer', offer);
}
})
- 用户B需要创建自己的RTCPeerConnection,添加本地音视频流,设置远端描述信息,生成answer,并且通过信令服务器发送给用户A
// 创建自己的RTCPeerConnection
peer.value = new RTCPeerConnection()
// 添加本地音视频流
const stream = await getLocalStream()
peer.value.addStream(stream)
// 设置远端描述信息
await peer.value.setRemoteDescription(offer);
const answer = await peer.value.createAnswer()
console.log(answer);
await peer.value.setLocalDescription(answer);
// 发送answer给信令服务器
socket.value?.emit('sendAnswer', { answer, roomId })
- 用户A收到用户B的answer
sock.on('sendAnswer', (answer) => {
if (caller.value) { // 判断是否是发送方
// 设置远端answer信息
peer.value.setRemoteDescription(answer);
}
})
- 用户A获取candidate信息并且通过信令服务器发送candidate给用户B
// 通过监听onicecandidate事件获取candidate信息
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
console.log('用户A获取candidate信息', event.candidate);
// 通过信令服务器发送candidate信息给用户B
socket.value?.emit('sendCandidate', {
roomId,
candidate: event.candidate
})
}
}
- 用户B添加用户A的candidate信息
// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
await peer.value.addIceCandidate(candidate);
})
- 用户B获取candidate信息并且通过信令服务器发送candidate给用户A(如上)
peer.value.onicecandidate = (event: any) => {
if (event.candidate) {
console.log('用户B获取candidate信息', event.candidate);
// 通过信令服务器发送candidate信息给用户A
socket.value?.emit('sendCandidate', {
roomId,
candidate: event.candidate
})
}
}
- 用户A添加用户B的candidate信息(如上)
// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
await peer.value.addIceCandidate(candidate);
})
- 接下来用户A和用户B就可以进行P2P通信流
// 监听onaddstream来获取对方的音视频流
peer.value.onaddstream = (event: any) => {
calling.value = false;
communicating.value = true;
remoteVideo.value!.srcObject = event.stream
remoteVideo.value!.play()
}
挂断视频
// 挂断视频
const hangUp = () => {
console.log('挂断视频');
socket.value?.emit('hangUp', roomId)
}
// 状态复原
const reset = () => {
called.value = false
caller.value = false
calling.value = false
communicating.value = false
peer.value = null
localVideo.value!.srcObject = null
remoteVideo.value!.srcObject = null
localStream.value = undefined
}
拓展:peerjs
文档:https://peerjs.com/docs/#start
服务端实现
// 使用peer搭建信令服务器
const { PeerServer } = require('peer');
const peerServer = PeerServer({ port: 3001, path: '/myPeerServer' });
前端实现
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Peer } from "peerjs";
const url = ref<string>()
const localVideo = ref<HTMLVideoElement>()
const remoteVideo = ref<HTMLVideoElement>()
const peerId = ref<string>()
const remoteId = ref<string>()
const peer = ref<any>()
const caller = ref<boolean>(false)
const called = ref<boolean>(false)
const callObj = ref<any>(false)
onMounted(() => {
//
peer.value = new Peer({ // 连接信令服务器
host: 'localhost',
port: 3001,
path: '/myPeerServer'
});
peer.value.on('open', (id: string) => {
peerId.value = id
})
// 接收视频请求
peer.value.on('call', async (call: any) => {
called.value = true
callObj.value = call
});
})
// 获取本地音视频流
async function getLocalStream(constraints: MediaStreamConstraints) {
// 获取媒体流
const stream = await navigator.mediaDevices.getUserMedia(constraints)
// 将媒体流设置到 video 标签上播放
localVideo.value!.srcObject = stream;
localVideo.value!.play();
return stream
}
const acceptCalled = async () => {
// 接收视频
const stream = await getLocalStream({ video: true, audio: true })
callObj.value.answer(stream);
callObj.value.on('stream', (remoteStream: any) => {
called.value = false
// 将远程媒体流添加到 video 元素中
remoteVideo.value!.srcObject = remoteStream;
remoteVideo.value!.play();
});
}
// 开启视频
const callRemote = async () => {
if (!remoteId.value) {
alert('请输入对方ID')
return
}
const stream = await getLocalStream({ video: true, audio: true })
// 将本地媒体流发送给远程 Peer
const call = peer.value.call(remoteId.value, stream);
caller.value = true
call.on('stream', (remoteStream: any) => {
caller.value = false
// 将远程媒体流添加到 video 元素中
remoteVideo.value!.srcObject = remoteStream;
remoteVideo.value!.play();
});
}
</script>
Android端webRTC使用
音视屏通话逻辑梳理
1.登陆信令服务,建立socket连接,生成唯一userID,信令服务保存会话和ID的映射,保存在线用户的一些操作
2.A发起语音或视频请求,B接受邀请或拒绝,双发都进入同一个房间:
创建会话,即创建一个两个人或多个人的房间,将双方都放置到该房间,这些工作在服务器端处理
room = UUID.randomUUID().toString() + System.currentTimeMillis();
boolean b = gEngineKit.startOutCall(getApplicationContext(), room, targetId, audioOnly);
mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
mCurrentCallSession.setTargetId(targetId);
mCurrentCallSession.setIsComing(false);
mCurrentCallSession.setCallState(EnumType.CallState.Outgoing);
// 创建房间
mCurrentCallSession.createHome(room, 2);
发起方先请求创建房间
public void createRoom(String room, int roomSize) {
if (webSocket != null) {
webSocket.createRoom(room, roomSize, myId);
}
}
服务器端创建房间并直接将发起方加入到该房间,发送创建成功的消息给发起方:
private void createRoom(String message, Map<String, Object> data) {
String room = (String) data.get("room");
String userId = (String) data.get("userID");
System.out.println(String.format("createRoom:%s ", room));
RoomInfo roomParam = rooms.get(room);
// 没有这个房间
if (roomParam == null) {
int size = (int) Double.parseDouble(String.valueOf(data.get("roomSize")));
// 创建房间
RoomInfo roomInfo = new RoomInfo();
roomInfo.setMaxSize(size);
roomInfo.setRoomId(room);
roomInfo.setUserId(userId);
// 将房间储存起来
rooms.put(room, roomInfo);
CopyOnWriteArrayList<UserBean> copy = new CopyOnWriteArrayList<>();
// 将自己加入到房间里
UserBean my = MemCons.userBeans.get(userId);
copy.add(my);
rooms.get(room).setUserBeans(copy);
// 发送给自己
EventData send = new EventData();
send.setEventName("__peers");
Map<String, Object> map = new HashMap<>();
map.put("connections", "");
map.put("you", userId);
map.put("roomSize", size);
send.setData(map);
System.out.println(gson.toJson(send));
sendMsg(my, -1, gson.toJson(send));
}
}
发送offer邀请对方进入房间,
List<String> inviteList = new ArrayList<>();
inviteList.add(mTargetId);
mEvent.sendInvite(mRoomId, inviteList, mIsAudioOnly);
发起方开启视频预览,关联相机画面到,本地的视屏流和音频流开启,在会话创建的时候就应该开启,PeerConnectionFactory是一个很核心关键的类,用于生成各种核心功能对象
public void createLocalStream() {
// 音频
audioSource = _factory.createAudioSource(createAudioConstraints());
_localAudioTrack = _factory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
// 视频
if (!mIsAudioOnly) {
// 这里对camera1 和camera2做了适配
captureAndroid = createVideoCapture();
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
videoSource = _factory.createVideoSource(captureAndroid.isScreencast());
captureAndroid.initialize(surfaceTextureHelper, mContext, videoSource.getCapturerObserver());
//开启视屏流
captureAndroid.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, FPS);
_localVideoTrack = _factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
}
}
服务器将邀请消息转发给被B邀请方,这里实际业务应该是全局都可以收到这个通知,所以考虑使用广播进行通知
public void onInvite(String room, boolean audioOnly, String inviteId, String userList) {
Intent intent = new Intent();
intent.putExtra("room", room);
intent.putExtra("audioOnly", audioOnly);
intent.putExtra("inviteId", inviteId);
intent.putExtra("userList", userList);
intent.setAction(Consts.ACTION_VOIP_RECEIVER);
intent.setComponent(new ComponentName(App.getInstance().getPackageName(), VoipReceiver.class.getName()));
// 发送广播
App.getInstance().sendBroadcast(intent);
}
B在收到邀请的消息时就创建会话对象:
boolean b = SkyEngineKit.Instance().startInCall(App.getInstance(), room, inviteId, audioOnly);
public boolean startInCall(Context context, final String room, final String targetId,
final boolean audioOnly) {
if (avEngineKit == null) {
Log.e(TAG, "startInCall error,init is not set");
return false;
}
// 忙线中
if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
// 发送->忙线中...
Log.i(TAG, "startInCall busy,currentCallSession is exist,start sendBusyRefuse!");
mCurrentCallSession.sendBusyRefuse(room, targetId);
return false;
}
this.isAudioOnly = audioOnly;
// 初始化会话
mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
mCurrentCallSession.setTargetId(targetId);
mCurrentCallSession.setIsComing(true);
mCurrentCallSession.setCallState(EnumType.CallState.Incoming);
// 开始响铃并回复
mCurrentCallSession.shouldStartRing();
mCurrentCallSession.sendRingBack(targetId, room);
return true;
}
进入呼叫界面,点击拒绝或者接受,接受后发送进入房间的消息给服务器,服务器添加用户到房间后发送进入成功的消息给本人,并且发送给所在房间的其他人新人进入的消息。
- SDP 和 candidate消息交换,建立数据连接通道
PeerConnection 该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。这个在双方都确认进入房间之后就要创建,多人通话就创建多个。
被邀请人进入房间后,其他人会收到一条新人进入的消息.
Peer peer = new Peer(_factory, iceServers, userId, this);
peer.setOffer(true);
// add localStream
List<String> mediaStreamLabels = Collections.singletonList("ARDAMS");
if (_localVideoTrack != null) {
peer.addVideoTrack(_localVideoTrack, mediaStreamLabels);
}
if (_localAudioTrack != null) {
peer.addAudioTrack(_localAudioTrack, mediaStreamLabels);
}
// 添加列表
peers.put(userId, peer);
// createOffer
peer.createOffer();
这里的peer.createOffer(); 就是开启交换的起点,交换的开启者应该是先进房间的人发起的,如果是一个多人的房间则新进来的人是没有建立连接的对象,其他已经在房间里建立连接的人都需要主动和他建立连接请求。
创建成功后会回调方法:
public void onCreateSuccess(SessionDescription origSdp) {
Log.d(TAG, "sdp创建成功 " + origSdp.type);
String sdpString = origSdp.description;
final SessionDescription sdp = new SessionDescription(origSdp.type, sdpString);
localSdp = sdp;
setLocalDescription(sdp);
}
setLocalDescription设置成功后会调用onSetSuccess,在这个方法中发送SDP给对方,接着上面的场景就是B被呼叫方
public void onSetSuccess() {
Log.d(TAG, "sdp连接成功 " + pc.signalingState().toString());
if (pc == null) return;
// 发送者
if (isOffer) {
if (pc.getRemoteDescription() == null) {
Log.d(TAG, "Local SDP set succesfully");
if (!isOffer) {
//接收者,发送Answer
mEvent.onSendAnswer(mUserId, localSdp);
} else {
//发送者,发送自己的offer
mEvent.onSendOffer(mUserId, localSdp);
}
} else {
Log.d(TAG, "Remote SDP set succesfully");
drainCandidates();
}
} else {
if (pc.getLocalDescription() != null) {
Log.d(TAG, "Local SDP set succesfully");
if (!isOffer) {
//接收者,发送Answer
mEvent.onSendAnswer(mUserId, localSdp);
} else {
//发送者,发送自己的offer
mEvent.onSendOffer(mUserId, localSdp);
}
drainCandidates();
} else {
Log.d(TAG, "Remote SDP set succesfully");
}
}
}
被叫方收到服务器消息处理:
public void receiveOffer(String userId, String description) {
Peer peer = peers.get(userId);
if (peer != null) {
SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, description);
peer.setOffer(false);
peer.setRemoteDescription(sdp);
peer.createAnswer();
}
}
/......
public void createAnswer() {
if (pc == null) return;
Log.d(TAG, "createAnswer");
//成功后会调用上面的success回调 发送answer给对方
pc.createAnswer(this, offerOrAnswerConstraint());
}
//
A就会收到answer,同时调用 peer.setRemoteDescription(sdp);
SDP交换完成后会调用onIceCandidate在这里把candidate信息发送给对方
mEvent.onSendIceCandidate(mUserId, candidate);
前端和移动端互通
双方的RTC实现是一样的,但是API和数据结构可能不同,前端的实现相对比较简洁。需要注意的是SDP和candidate的设置。
const candidate :RTCIceCandidateInit = ({
candidate: jsonObject.data.candidate,
sdpMid: jsonObject.data.id,
sdpMLineIndex: jsonObject.data.label,
});
await peer.value.addIceCandidate(candidate);
const des:RTCSessionDescriptionInit = {
sdp: sdp,
type: "offer",
}
// 设置远端描述信息
await peer.value.setRemoteDescription(des);
这两处的数据结构不同