前端基于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
// 获取本地音视频流constgetLocalStream=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 =newRTCPeerConnection({// 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
:
npminstall-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
:
npminstall 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
// 发起方发起视频请求constcallRemote=async()=>{
console.log('发起视频');
caller.value =true;
calling.value =true;awaitgetLocalStream()// 向信令服务器发送发起请求的事件
socket.value?.emit('callRemote', roomId)}
- 用户B同意视频请求,并且通过信令服务器通知用户A
// 接收方同意视频请求constacceptCall=()=>{
console.log('同意视频邀请');
socket.value?.emit('acceptCall', roomId)}
开始交换 SDP 信息和 candidate 信息:
- 用户A创建创建RTCPeerConnection,添加本地音视频流,生成offer,并且通过信令服务器将offer发送给用户B
// 创建RTCPeerConnection
peer.value =newRTCPeerConnection()// 添加本地音视频流
peer.value.addStream(localStream.value)// 生成offerconst offer =await peer.value.createOffer({offerToReceiveAudio:1,offerToReceiveVideo:1})
console.log('offer', offer);// 设置本地描述的offerawait 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 =newRTCPeerConnection()// 添加本地音视频流const stream =awaitgetLocalStream()
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()}
挂断视频
// 挂断视频consthangUp=()=>{
console.log('挂断视频');
socket.value?.emit('hangUp', roomId)}// 状态复原constreset=()=>{
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 =newPeer({// 连接信令服务器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
});})// 获取本地音视频流asyncfunctiongetLocalStream(constraints: MediaStreamConstraints){// 获取媒体流const stream =await navigator.mediaDevices.getUserMedia(constraints)// 将媒体流设置到 video 标签上播放
localVideo.value!.srcObject = stream;
localVideo.value!.play();return stream
}constacceptCalled=async()=>{// 接收视频const stream =awaitgetLocalStream({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();});}// 开启视频constcallRemote=async()=>{if(!remoteId.value){alert('请输入对方ID')return}const stream =awaitgetLocalStream({video:true,audio:true})// 将本地媒体流发送给远程 Peerconst 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 =newCallSession(context, room, audioOnly, mEvent);
mCurrentCallSession.setTargetId(targetId);
mCurrentCallSession.setIsComing(false);
mCurrentCallSession.setCallState(EnumType.CallState.Outgoing);// 创建房间
mCurrentCallSession.createHome(room,2);
发起方先请求创建房间
publicvoidcreateRoom(String room, int roomSize){if(webSocket !=null){
webSocket.createRoom(room, roomSize, myId);}}
服务器端创建房间并直接将发起方加入到该房间,发送创建成功的消息给发起方:
privatevoidcreateRoom(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 =newRoomInfo();
roomInfo.setMaxSize(size);
roomInfo.setRoomId(room);
roomInfo.setUserId(userId);// 将房间储存起来
rooms.put(room, roomInfo);
CopyOnWriteArrayList<UserBean> copy =newCopyOnWriteArrayList<>();// 将自己加入到房间里
UserBean my = MemCons.userBeans.get(userId);
copy.add(my);
rooms.get(room).setUserBeans(copy);// 发送给自己
EventData send =newEventData();
send.setEventName("__peers");
Map<String, Object> map =newHashMap<>();
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 =newArrayList<>();
inviteList.add(mTargetId);
mEvent.sendInvite(mRoomId, inviteList, mIsAudioOnly);
发起方开启视频预览,关联相机画面到,本地的视屏流和音频流开启,在会话创建的时候就应该开启,PeerConnectionFactory是一个很核心关键的类,用于生成各种核心功能对象
publicvoidcreateLocalStream(){// 音频
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邀请方,这里实际业务应该是全局都可以收到这个通知,所以考虑使用广播进行通知
publicvoidonInvite(String room, boolean audioOnly, String inviteId, String userList){
Intent intent =newIntent();
intent.putExtra("room", room);
intent.putExtra("audioOnly", audioOnly);
intent.putExtra("inviteId", inviteId);
intent.putExtra("userList", userList);
intent.setAction(Consts.ACTION_VOIP_RECEIVER);
intent.setComponent(newComponentName(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");returnfalse;}// 忙线中if(mCurrentCallSession !=null&& mCurrentCallSession.getState()!= EnumType.CallState.Idle){// 发送->忙线中...
Log.i(TAG,"startInCall busy,currentCallSession is exist,start sendBusyRefuse!");
mCurrentCallSession.sendBusyRefuse(room, targetId);returnfalse;}this.isAudioOnly = audioOnly;// 初始化会话
mCurrentCallSession =newCallSession(context, room, audioOnly, mEvent);
mCurrentCallSession.setTargetId(targetId);
mCurrentCallSession.setIsComing(true);
mCurrentCallSession.setCallState(EnumType.CallState.Incoming);// 开始响铃并回复
mCurrentCallSession.shouldStartRing();
mCurrentCallSession.sendRingBack(targetId, room);returntrue;}
进入呼叫界面,点击拒绝或者接受,接受后发送进入房间的消息给服务器,服务器添加用户到房间后发送进入成功的消息给本人,并且发送给所在房间的其他人新人进入的消息。
- SDP 和 candidate消息交换,建立数据连接通道
PeerConnection 该对象的用处就是与远端建立联接,并最终为双方通讯提供网络通道。这个在双方都确认进入房间之后就要创建,多人通话就创建多个。
被邀请人进入房间后,其他人会收到一条新人进入的消息.
Peer peer =newPeer(_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(); 就是开启交换的起点,交换的开启者应该是先进房间的人发起的,如果是一个多人的房间则新进来的人是没有建立连接的对象,其他已经在房间里建立连接的人都需要主动和他建立连接请求。
创建成功后会回调方法:
publicvoidonCreateSuccess(SessionDescription origSdp){
Log.d(TAG,"sdp创建成功 "+ origSdp.type);
String sdpString = origSdp.description;
final SessionDescription sdp =newSessionDescription(origSdp.type, sdpString);
localSdp = sdp;setLocalDescription(sdp);}
setLocalDescription设置成功后会调用onSetSuccess,在这个方法中发送SDP给对方,接着上面的场景就是B被呼叫方
publicvoidonSetSuccess(){
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");}}}
被叫方收到服务器消息处理:
publicvoidreceiveOffer(String userId, String description){
Peer peer = peers.get(userId);if(peer !=null){
SessionDescription sdp =newSessionDescription(SessionDescription.Type.OFFER, description);
peer.setOffer(false);
peer.setRemoteDescription(sdp);
peer.createAnswer();}}/......publicvoidcreateAnswer(){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的设置。
constcandidate:RTCIceCandidateInit =({candidate: jsonObject.data.candidate,sdpMid: jsonObject.data.id,sdpMLineIndex: jsonObject.data.label,});await peer.value.addIceCandidate(candidate);
constdes:RTCSessionDescriptionInit ={sdp: sdp,type:"offer",}// 设置远端描述信息await peer.value.setRemoteDescription(des);
这两处的数据结构不同
版权归原作者 Aramis_twoY 所有, 如有侵权,请联系我们删除。