基于WebRTC实现音视频通话

前端基于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 的工作原理如下:

  1. 首先,通信双方收集本地网络地址(包括私有地址和公共地址)以及通过 STUN 和 TURN 服务器获取的候选地址。
  2. 接下来,双方通过信令服务器交换这些候选地址。
  3. 通信双方使用这些候选地址进行连接测试,确定最佳的可用地址。
  4. 一旦找到可用的地址,通信双方就可以开始实时音视频通话。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在 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

  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

整个媒体协商过程可以简化为三个步骤对应上述四个媒体协商方法:

  1. 呼叫端创建 Offer(createOffer)并将 offer 消息(内容是呼叫端的 SDP 信息)通过信令服务器传送给接收端,同时调用 setLocalDesccription 将含有本地 SDP 信息的 Offer 保存起来
  2. 接收端收到对端的 Offer 信息后调用 setRemoteDesccription 方法将含有对端 SDP 信息的 Offer 保存起来,并创建 Answer(createAnswer)并将 Answer 消息(内容是接收端的 SDP 信息)通过信令服务器传送给呼叫端
  3. 呼叫端收到对端的 Answer 信息后调用 setRemoteDesccription 方法将含有对端 SDP 信息的 Answer 保存起来

经过上述三个步骤,则完成了 P2P 通信过程中的媒体协商部分,实际上在呼叫端以及接收端调用setLocalDesccription 同时也开始了收集各端自己的网络信息(candidate),然后各端通过监听事件 onicecandidate 收集到各自的 candidate 并通过信令服务器传送给对端,进而打通 P2P 通信的网络通道,并通过监听 onaddstream 事件拿到对方的视频流进而完成了整个视频通话过程。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实践

项目搭建

前端项目
  1. 项目使用vue3+ts,运行如下命令:
npm create vite@latest webrtc-client -- --template vue-ts
  1. 并且引入tailwindcss:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
  1. 在生成的tailwind.config.js配置文件中添加所有模板文件的路径。
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
  1. 修改style.css中的内容如下:
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. 自定义修改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.ionodemon

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:
  1. 用户A发起视频请求,并且通过信令服务器通知用户B
// 发起方发起视频请求
const callRemote = async () => {
  console.log('发起视频');
  caller.value = true;
  calling.value = true;
  await getLocalStream()
    // 向信令服务器发送发起请求的事件
  socket.value?.emit('callRemote', roomId)
}
  1. 用户B同意视频请求,并且通过信令服务器通知用户A
// 接收方同意视频请求
const acceptCall = () => {
  console.log('同意视频邀请');
  socket.value?.emit('acceptCall', roomId)
}

开始交换 SDP 信息和 candidate 信息:
  1. 用户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 })
  1. 用户B收到用户A的offer
sock.on('sendOffer', (offer) => {
  if (called.value) { // 判断接收方
    console.log('收到offer', offer);
  }
})
  1. 用户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 })
  1. 用户A收到用户B的answer
sock.on('sendAnswer', (answer) => {
  if (caller.value) { // 判断是否是发送方
    // 设置远端answer信息
    peer.value.setRemoteDescription(answer);
  }
})
  1. 用户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
    })
  }
}
  1. 用户B添加用户A的candidate信息
// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
  await peer.value.addIceCandidate(candidate);
})
  1. 用户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
    })
  }
}
  1. 用户A添加用户B的candidate信息(如上)
// 添加candidate信息
sock.on('sendCandidate', async (candidate) => {
  await peer.value.addIceCandidate(candidate);
})
  1. 接下来用户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;
    }

进入呼叫界面,点击拒绝或者接受,接受后发送进入房间的消息给服务器,服务器添加用户到房间后发送进入成功的消息给本人,并且发送给所在房间的其他人新人进入的消息。

  1. 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);

这两处的数据结构不同

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值