• WebRTC 系列(四、多人通话,H5、Android、iOS)


    WebRTC 系列(三、点对点通话,H5、Android、iOS)

     上一篇博客中,我们已经实现了点对点通话,即一对一通话,这一次就接着实现多人通话。多人通话的实现方式呢也有好几种方案,这里我简单介绍两种方案。

    一、多人通话方案

    1.Mesh

    多个客户端之间建立多个 PeerConnection,即如果有三个客户端 A、B、C,A 有两个 PeerConnection 分别与 B、C 通信,B 也是有两个 PeerConnection,分别与 A、C 通信,C 也是有两个 PeerConnection,分别与 A、B 通信,如图:

    ​​​​​​​​​​​​​​优点:服务端压力小,不需要对音视频数据做处理。
    缺点:客户端编解码压力较大,传输的数据与通话人数成正比,兼容性较差。

    2.Mixer

    客户端只与服务器有一个 PeerConnection,有多个客户端时,服务端增加多个媒体流,由服务端来做媒体数据转发,如图:

    优点:客户端只有一个连接,传输数据减少,服务端可对音视频数据预处理,兼容性好。
    缺点:服务器压力大,通话人数过多时,服务器如果对音视频数据有预处理,可能导致通话延迟。

    3.demo 方案选择

    两种方案各有利弊,感觉在实际业务中,第二种方案更合适,毕竟把更多逻辑放在服务端更可控一点,我为了演示简单,就选用了第一种方案,下面就说说第一种方案的话,第一个人、第二个人、第三个人加入房间的流程是什么样的。

    第一个人 A 加入房间:

    1. A 发送 join;
    2. 服务器向房间内其他所有人发送 otherJoin;
    3. 房间内没有其他人,结束。

    第二个人 B 加入房间:

    1. B 发送 join;
    2. 服务器向房间内其他所有人发送 otherJoin;
    3. A 收到 otherJoin(带有 B 的 userId);
    4. A 检查远端视频渲染控件集合中是否存在 B 的 userId 对应的远端视频渲染控件,没有则创建并初始化,然后保存;
    5. A 检查 PeerConnection 集合中是否存在 B 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    6. A 通过 PeerConnection 创建 offer,获取 sdp;
    7. A 将 offer sdp 作为参数 setLocalDescription;
    8. A 发送 offer sdp(带有 A 的 userId);
    9. B 收到 offer(带有 A 的 userId);
    10. B 检查远端视频渲染控件集合中是否存在 A 的 userId 对应远端视频渲染控件,没有则创建并初始化,然后保存;
    11. B 检查 PeerConnection 集合中是否存在 A 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    12. B 将 offer sdp 作为参数 setRemoteDescription;
    13. B 通过 PeerConnection 创建 answer,获取 sdp;
    14. B 将 answer sdp 作为参数 setLocalDescription;
    15. B 发送 answer sdp(带有 B 的 userId);
    16. A 收到 answer sdp(带有 B 的 userId);
    17. A 通过 userId 找到对应 PeerConnection,将 answer sdp 作为参数 setRemoteDescription。

    第三个人 C 加入房间:

    1. C 发送 join;
    2. 服务器向房间内其他所有人发送 otherJoin;
    3. A 收到 otherJoin(带有 C 的 userId);
    4. A 检查远端视频渲染控件集合中是否存在 C 的 userId 对应的远端视频渲染控件,没有则创建并初始化,然后保存;
    5. A 检查 PeerConnection 集合中是否存在 C 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    6. A 通过 PeerConnection 创建 offer,获取 sdp;
    7. A 将 offer sdp 作为参数 setLocalDescription;
    8. A 发送 offer sdp(带有 A 的 userId);
    9. C 收到 offer(带有 A 的 userId);
    10. C 检查远端视频渲染控件集合中是否存在 A 的 userId 对应远端视频渲染控件,没有则创建并初始化,然后保存;
    11. C 检查 PeerConnection 集合中是否存在 A 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    12. C 将 offer sdp 作为参数 setRemoteDescription;
    13. C 通过 PeerConnection 创建 answer,获取 sdp;
    14. C 将 answer sdp 作为参数 setLocalDescription;
    15. C 发送 answer sdp(带有 C 的 userId);
    16. A 收到 answer sdp(带有 C 的 userId);
    17. A 通过 userId 找到对应 PeerConnection,将 answer sdp 作为参数 setRemoteDescription。
    18. B 收到 otherJoin(带有 C 的 userId);
    19. B 检查远端视频渲染控件集合中是否存在 C 的 userId 对应的远端视频渲染控件,没有则创建并初始化,然后保存;
    20. B 检查 PeerConnection 集合中是否存在 C 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    21. B 通过 PeerConnection 创建 offer,获取 sdp;
    22. B 将 offer sdp 作为参数 setLocalDescription;
    23. B 发送 offer sdp(带有 B 的 userId);
    24. C 收到 offer(带有 B 的 userId);
    25. C 检查远端视频渲染控件集合中是否存在 B 的 userId 对应远端视频渲染控件,没有则创建并初始化,然后保存;
    26. C 检查 PeerConnection 集合中是否存在 B 的 userId 对应的 PeerConnection,没有则创建并添加音轨、视轨,然后保存;
    27. C 将 offer sdp 作为参数 setRemoteDescription;
    28. C 通过 PeerConnection 创建 answer,获取 sdp;
    29. C 将 answer sdp 作为参数 setLocalDescription;
    30. C 发送 answer sdp(带有 C 的 userId);
    31. B 收到 answer sdp(带有 C 的 userId);
    32. B 通过 userId 找到对应 PeerConnection,将 answer sdp 作为参数 setRemoteDescription。

    依此类推,如果还有第四个用户 D 再加入房间的话,D 也会发送 join,然后 A、B、C 也会类似上述 3~17 步处理。

    这期间的 onIceCandidate 回调的处理和之前类似,只是将生成的 IceCandidate 对象传递给对方时需要带上发送方(自己)的 userId,便于对方找到对应的 PeerConnection,以及接收方的 userId,便于服务器找到接收方的长连接。

    这期间的 onAddStream 回调的处理也和之前类似,只是需要通过对方的 userId 找到对应的远端控件渲染控件。

    二、信令服务器

    信令服务器的依赖就不重复了,根据上述流程,我们需要引入用户的概念,但暂时我没有引入房间的概念,所以在测试的时候我认为只有一个房间,所有人都加入的同一个房间。

    多人通话 WebSocket 服务端代码:

    1. package com.qinshou.webrtcdemo_server;
    2. import com.google.gson.Gson;
    3. import com.google.gson.JsonObject;
    4. import com.google.gson.reflect.TypeToken;
    5. import org.java_websocket.WebSocket;
    6. import org.java_websocket.handshake.ClientHandshake;
    7. import org.java_websocket.server.WebSocketServer;
    8. import java.net.InetSocketAddress;
    9. import java.util.LinkedList;
    10. import java.util.List;
    11. import java.util.Map;
    12. /**
    13. * Author: MrQinshou
    14. * Email: cqflqinhao@126.com
    15. * Date: 2023/2/8 9:33
    16. * Description: 多人通话 WebSocketServer
    17. */
    18. public class MultipleWebSocketServerHelper {
    19. public static class WebSocketBean {
    20. private String mUserId;
    21. private WebSocket mWebSocket;
    22. public WebSocketBean() {
    23. }
    24. public WebSocketBean(WebSocket webSocket) {
    25. mWebSocket = webSocket;
    26. }
    27. public String getUserId() {
    28. return mUserId;
    29. }
    30. public void setUserId(String userId) {
    31. mUserId = userId;
    32. }
    33. public WebSocket getWebSocket() {
    34. return mWebSocket;
    35. }
    36. public void setWebSocket(WebSocket webSocket) {
    37. mWebSocket = webSocket;
    38. }
    39. }
    40. private WebSocketServer mWebSocketServer;
    41. private final List mWebSocketBeans = new LinkedList<>();
    42. // private static final String HOST_NAME = "192.168.1.104";
    43. private static final String HOST_NAME = "172.16.2.172";
    44. private static final int PORT = 8888;
    45. private WebSocketBean getWebSocketBeanByWebSocket(WebSocket webSocket) {
    46. for (WebSocketBean webSocketBean : mWebSocketBeans) {
    47. if (webSocket == webSocketBean.getWebSocket()) {
    48. return webSocketBean;
    49. }
    50. }
    51. return null;
    52. }
    53. private WebSocketBean getWebSocketBeanByUserId(String userId) {
    54. for (WebSocketBean webSocketBean : mWebSocketBeans) {
    55. if (userId.equals(webSocketBean.getUserId())) {
    56. return webSocketBean;
    57. }
    58. }
    59. return null;
    60. }
    61. private WebSocketBean removeWebSocketBeanByWebSocket(WebSocket webSocket) {
    62. for (WebSocketBean webSocketBean : mWebSocketBeans) {
    63. if (webSocket == webSocketBean.getWebSocket()) {
    64. mWebSocketBeans.remove(webSocketBean);
    65. return webSocketBean;
    66. }
    67. }
    68. return null;
    69. }
    70. public void start() {
    71. InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST_NAME, PORT);
    72. mWebSocketServer = new WebSocketServer(inetSocketAddress) {
    73. @Override
    74. public void onOpen(WebSocket conn, ClientHandshake handshake) {
    75. System.out.println("onOpen--->" + conn);
    76. // 有客户端连接,创建 WebSocketBean,此时仅保存了 WebSocket 连接,但还没有和 userId 绑定
    77. mWebSocketBeans.add(new WebSocketBean(conn));
    78. }
    79. @Override
    80. public void onClose(WebSocket conn, int code, String reason, boolean remote) {
    81. System.out.println("onClose--->" + conn);
    82. WebSocketBean webSocketBean = removeWebSocketBeanByWebSocket(conn);
    83. if (webSocketBean == null) {
    84. return;
    85. }
    86. // 通知其他用户有人退出房间
    87. JsonObject jsonObject = new JsonObject();
    88. jsonObject.addProperty("msgType", "otherQuit");
    89. jsonObject.addProperty("userId", webSocketBean.mUserId);
    90. for (WebSocketBean w : mWebSocketBeans) {
    91. if (w != webSocketBean) {
    92. w.mWebSocket.send(jsonObject.toString());
    93. }
    94. }
    95. }
    96. @Override
    97. public void onMessage(WebSocket conn, String message) {
    98. System.out.println("onMessage--->" + message);
    99. Map map = new Gson().fromJson(message, new TypeToken>() {
    100. }.getType());
    101. String msgType = map.get("msgType");
    102. if ("join".equals(msgType)) {
    103. // 收到加入房间指令
    104. String userId = map.get("userId");
    105. WebSocketBean webSocketBean = getWebSocketBeanByWebSocket(conn);
    106. // WebSocket 连接绑定 userId
    107. if (webSocketBean != null) {
    108. webSocketBean.setUserId(userId);
    109. }
    110. // 通知其他用户有其他人加入房间
    111. JsonObject jsonObject = new JsonObject();
    112. jsonObject.addProperty("msgType", "otherJoin");
    113. jsonObject.addProperty("userId", userId);
    114. for (WebSocketBean w : mWebSocketBeans) {
    115. if (w != webSocketBean && w.getUserId() != null) {
    116. w.mWebSocket.send(jsonObject.toString());
    117. }
    118. }
    119. return;
    120. }
    121. if ("quit".equals(msgType)) {
    122. // 收到退出房间指令
    123. String userId = map.get("userId");
    124. WebSocketBean webSocketBean = getWebSocketBeanByWebSocket(conn);
    125. // WebSocket 连接解绑 userId
    126. if (webSocketBean != null) {
    127. webSocketBean.setUserId(null);
    128. }
    129. // 通知其他用户有其他人退出房间
    130. JsonObject jsonObject = new JsonObject();
    131. jsonObject.addProperty("msgType", "otherQuit");
    132. jsonObject.addProperty("userId", userId);
    133. for (WebSocketBean w : mWebSocketBeans) {
    134. if (w != webSocketBean && w.getUserId() != null) {
    135. w.mWebSocket.send(jsonObject.toString());
    136. }
    137. }
    138. return;
    139. }
    140. // 其他消息透传
    141. // 接收方
    142. String toUserId = map.get("toUserId");
    143. // 找到接收方对应 WebSocket 连接
    144. WebSocketBean webSocketBean = getWebSocketBeanByUserId(toUserId);
    145. if (webSocketBean != null) {
    146. webSocketBean.getWebSocket().send(message);
    147. }
    148. }
    149. @Override
    150. public void onError(WebSocket conn, Exception ex) {
    151. ex.printStackTrace();
    152. System.out.println("onError");
    153. }
    154. @Override
    155. public void onStart() {
    156. System.out.println("onStart");
    157. }
    158. };
    159. mWebSocketServer.start();
    160. }
    161. public void stop() {
    162. if (mWebSocketServer == null) {
    163. return;
    164. }
    165. for (WebSocket webSocket : mWebSocketServer.getConnections()) {
    166. webSocket.close();
    167. }
    168. try {
    169. mWebSocketServer.stop();
    170. } catch (InterruptedException e) {
    171. throw new RuntimeException(e);
    172. }
    173. mWebSocketServer = null;
    174. }
    175. public static void main(String[] args) {
    176. new MultipleWebSocketServerHelper().start();
    177. }
    178. }

    三、消息格式

    传递的消息的话,相较于点对点通话,sdp 和 iceCandidate 中需要添加 fromUserId 和 toUserId 字段,另外还需要增加 join、otherJoin、quit、ohterQuit 消息:

    1. // sdp
    2. {
    3. "msgType": "sdp",
    4. "fromUserId": userId,
    5. "toUserId": toUserId,
    6. "type": sessionDescription.type,
    7. "sdp": sessionDescription.sdp
    8. }
    9. // iceCandidate
    10. {
    11. "msgType": "iceCandidate",
    12. "fromUserId": userId,
    13. "toUserId": toUserId,
    14. "id": iceCandidate.sdpMid,
    15. "label": iceCandidate.sdpMLineIndex,
    16. "candidate": iceCandidate.candidate
    17. }
    18. // join
    19. {
    20. "msgType": "join"
    21. "userId": userId
    22. }
    23. // otherJoin
    24. {
    25. "msgType": "otherJoin"
    26. "userId": userId
    27. }
    28. // quit
    29. {
    30. "msgType": "quit"
    31. "userId": userId
    32. }
    33. // otherQuit
    34. {
    35. "msgType": "otherQuit"
    36. "userId": userId
    37. }

    四、H5

    代码与 p2p_demo 其实差不了太多,但是我们创建 PeerConnection 的时机需要根据上面梳理流程进行修改,发送的信令也需要根据上面定义的格式进行修改,布局中将远端视频渲染控件去掉,改成一个远端视频渲染控件的容器,每当有新的连接时创建新的远端视频渲染控件放到容器中,另外,WebSocket 需要额外处理 otherJoin 和 otherQuit 信令。

    1.添加依赖

    这个跟前两篇的一样,不需要额外引入。

    2.multiple_demo.html

    1. <html>
    2. <head>
    3. <title>Multiple Demotitle>
    4. <style>
    5. body {
    6. overflow: hidden;
    7. margin: 0px;
    8. padding: 0px;
    9. }
    10. #local_view {
    11. width: 100%;
    12. height: 100%;
    13. }
    14. #remote_views {
    15. width: 9%;
    16. height: 80%;
    17. position: absolute;
    18. top: 10%;
    19. right: 10%;
    20. bottom: 10%;
    21. overflow-y: auto;
    22. }
    23. .remote_view {
    24. width: 100%;
    25. aspect-ratio: 9/16;
    26. }
    27. #left {
    28. width: 10%;
    29. height: 5%;
    30. position: absolute;
    31. left: 10%;
    32. top: 10%;
    33. }
    34. #p_websocket_state,
    35. #input_server_url,
    36. .my_button {
    37. width: 100%;
    38. height: 100%;
    39. display: block;
    40. margin-bottom: 10%;
    41. }
    42. style>
    43. head>
    44. <body>
    45. <video id="local_view" width="480" height="270" autoplay controls muted>video>
    46. <div id="remote_views">
    47. div>
    48. <div id="left">
    49. <p id="p_websocket_state">WebSocket 已断开p>
    50. <input id="input_server_url" type="text" placeholder="请输入服务器地址" value="ws://192.168.1.104:8888">input>
    51. <button id="btn_connect" class="my_button" onclick="connect()">连接 WebSocketbutton>
    52. <button id="btn_disconnect" class="my_button" onclick="disconnect()">断开 WebSocketbutton>
    53. <button id="btn_join" class="my_button" onclick="join()">加入房间button>
    54. <button id="btn_quit" class="my_button" onclick="quit()">退出房间button>
    55. div>
    56. body>
    57. <script type="text/javascript">
    58. /**
    59. * Author: MrQinshou
    60. * Email: cqflqinhao@126.com
    61. * Date: 2023/4/15 11:24
    62. * Description: 生成 uuid
    63. */
    64. function uuid() {
    65. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    66. var r = Math.random() * 16 | 0;
    67. var v = c == 'x' ? r : (r & 0x3 | 0x8);
    68. return v.toString(16);
    69. });
    70. }
    71. script>
    72. <script type="text/javascript">
    73. var localView = document.getElementById("local_view");
    74. var remoteViews = document.getElementById("remote_views");
    75. var localStream;
    76. // let userId = uuid();
    77. let userId = "h5";
    78. let peerConnectionDict = {};
    79. let remoteViewDict = {};
    80. function createPeerConnection(fromUserId) {
    81. let peerConnection = new RTCPeerConnection();
    82. peerConnection.oniceconnectionstatechange = function (event) {
    83. if ("disconnected" == event.target.iceConnectionState) {
    84. let peerConnection = peerConnectionDict[fromUserId];
    85. if (peerConnection != null) {
    86. peerConnection.close();
    87. delete peerConnectionDict[fromUserId];
    88. }
    89. let remoteView = remoteViewDict[fromUserId];
    90. if (remoteView != null) {
    91. remoteView.removeAttribute('src');
    92. remoteView.load();
    93. remoteView.remove();
    94. delete remoteViewDict[fromUserId];
    95. }
    96. }
    97. }
    98. peerConnection.onicecandidate = function (event) {
    99. console.log("onicecandidate--->" + event.candidate);
    100. sendIceCandidate(event.candidate, fromUserId);
    101. }
    102. peerConnection.ontrack = function (event) {
    103. console.log("remote ontrack--->" + event.streams);
    104. let remoteView = remoteViewDict[fromUserId];
    105. if (remoteView == null) {
    106. return;
    107. }
    108. let streams = event.streams;
    109. if (streams && streams.length > 0) {
    110. remoteView.srcObject = streams[0];
    111. }
    112. }
    113. return peerConnection;
    114. }
    115. function createOffer(peerConnection, fromUserId) {
    116. peerConnection.createOffer().then(function (sessionDescription) {
    117. console.log(fromUserId + " create offer success.");
    118. peerConnection.setLocalDescription(sessionDescription).then(function () {
    119. console.log(fromUserId + " set local sdp success.");
    120. var jsonObject = {
    121. "msgType": "sdp",
    122. "fromUserId": userId,
    123. "toUserId": fromUserId,
    124. "type": "offer",
    125. "sdp": sessionDescription.sdp
    126. };
    127. send(JSON.stringify(jsonObject));
    128. }).catch(function (error) {
    129. console.log("error--->" + error);
    130. })
    131. }).catch(function (error) {
    132. console.log("error--->" + error);
    133. })
    134. }
    135. function createAnswer(peerConnection, fromUserId) {
    136. peerConnection.createAnswer().then(function (sessionDescription) {
    137. console.log(fromUserId + " create answer success.");
    138. peerConnection.setLocalDescription(sessionDescription).then(function () {
    139. console.log(fromUserId + " set local sdp success.");
    140. var jsonObject = {
    141. "msgType": "sdp",
    142. "fromUserId": userId,
    143. "toUserId": fromUserId,
    144. "type": "answer",
    145. "sdp": sessionDescription.sdp
    146. };
    147. send(JSON.stringify(jsonObject));
    148. }).catch(function (error) {
    149. console.log("error--->" + error);
    150. })
    151. }).catch(function (error) {
    152. console.log("error--->" + error);
    153. })
    154. }
    155. function join() {
    156. var jsonObject = {
    157. "msgType": "join",
    158. "userId": userId,
    159. };
    160. send(JSON.stringify(jsonObject));
    161. }
    162. function quit() {
    163. var jsonObject = {
    164. "msgType": "quit",
    165. "userId": userId,
    166. };
    167. send(JSON.stringify(jsonObject));
    168. for (var key in peerConnectionDict) {
    169. let peerConnection = peerConnectionDict[key];
    170. peerConnection.close();
    171. delete peerConnectionDict[key];
    172. }
    173. for (var key in remoteViewDict) {
    174. let remoteView = remoteViewDict[key];
    175. remoteView.removeAttribute('src');
    176. remoteView.load();
    177. remoteView.remove();
    178. delete remoteViewDict[key];
    179. }
    180. }
    181. function sendOffer(offer, toUserId) {
    182. var jsonObject = {
    183. "msgType": "sdp",
    184. "fromUserId": userId,
    185. "toUserId": toUserId,
    186. "type": "offer",
    187. "sdp": offer.sdp
    188. };
    189. send(JSON.stringify(jsonObject));
    190. }
    191. function receivedOffer(jsonObject) {
    192. let fromUserId = jsonObject["fromUserId"];
    193. var peerConnection = peerConnectionDict[fromUserId];
    194. if (peerConnection == null) {
    195. // 创建 PeerConnection
    196. peerConnection = createPeerConnection(fromUserId);
    197. // 为 PeerConnection 添加音轨、视轨
    198. for (let i = 0; localStream != null && i < localStream.getTracks().length; i++) {
    199. const track = localStream.getTracks()[i];
    200. peerConnection.addTrack(track, localStream);
    201. }
    202. peerConnectionDict[fromUserId] = peerConnection;
    203. }
    204. var remoteView = remoteViewDict[fromUserId];
    205. if (remoteView == null) {
    206. remoteView = document.createElement("video");
    207. remoteView.className = "remote_view";
    208. remoteView.autoplay = true;
    209. remoteView.control = true;
    210. remoteView.muted = true;
    211. remoteViews.appendChild(remoteView);
    212. remoteViewDict[fromUserId] = remoteView;
    213. }
    214. let options = {
    215. "type": jsonObject["type"],
    216. "sdp": jsonObject["sdp"]
    217. }
    218. // 将 offer sdp 作为参数 setRemoteDescription
    219. let sessionDescription = new RTCSessionDescription(options);
    220. peerConnection.setRemoteDescription(sessionDescription).then(function () {
    221. console.log(fromUserId + " set remote sdp success.");
    222. // 通过 PeerConnection 创建 answer,获取 sdp
    223. peerConnection.createAnswer().then(function (sessionDescription) {
    224. console.log(fromUserId + " create answer success.");
    225. // 将 answer sdp 作为参数 setLocalDescription
    226. peerConnection.setLocalDescription(sessionDescription).then(function () {
    227. console.log(fromUserId + " set local sdp success.");
    228. // 发送 answer sdp
    229. sendAnswer(sessionDescription, fromUserId);
    230. })
    231. })
    232. }).catch(function (error) {
    233. console.log("error--->" + error);
    234. });
    235. }
    236. function sendAnswer(answer, toUserId) {
    237. var jsonObject = {
    238. "msgType": "sdp",
    239. "fromUserId": userId,
    240. "toUserId": toUserId,
    241. "type": "answer",
    242. "sdp": answer.sdp
    243. };
    244. send(JSON.stringify(jsonObject));
    245. }
    246. function receivedAnswer(jsonObject) {
    247. let fromUserId = jsonObject["fromUserId"];
    248. var peerConnection = peerConnectionDict[fromUserId];
    249. if (peerConnection == null) {
    250. // 创建 PeerConnection
    251. peerConnection = createPeerConnection(fromUserId);
    252. // 为 PeerConnection 添加音轨、视轨
    253. for (let i = 0; localStream != null && i < localStream.getTracks().length; i++) {
    254. const track = localStream.getTracks()[i];
    255. peerConnection.addTrack(track, localStream);
    256. }
    257. peerConnectionDict[fromUserId] = peerConnection;
    258. }
    259. var remoteView = remoteViewDict[fromUserId];
    260. if (remoteView == null) {
    261. remoteView = document.createElement("video");
    262. remoteView.className = "remote_view";
    263. remoteView.autoplay = true;
    264. remoteView.control = true;
    265. remoteView.muted = true;
    266. remoteViews.appendChild(remoteView);
    267. remoteViewDict[fromUserId] = remoteView;
    268. }
    269. let options = {
    270. "type": jsonObject["type"],
    271. "sdp": jsonObject["sdp"]
    272. }
    273. let sessionDescription = new RTCSessionDescription(options);
    274. let type = jsonObject["type"];
    275. peerConnection.setRemoteDescription(sessionDescription).then(function () {
    276. console.log(fromUserId + " set remote sdp success.");
    277. }).catch(function (error) {
    278. console.log("error--->" + error);
    279. });
    280. }
    281. function sendIceCandidate(iceCandidate, toUserId) {
    282. if (iceCandidate == null) {
    283. return;
    284. }
    285. var jsonObject = {
    286. "msgType": "iceCandidate",
    287. "fromUserId": userId,
    288. "toUserId": toUserId,
    289. "id": iceCandidate.sdpMid,
    290. "label": iceCandidate.sdpMLineIndex,
    291. "candidate": iceCandidate.candidate
    292. };
    293. send(JSON.stringify(jsonObject));
    294. }
    295. function receivedCandidate(jsonObject) {
    296. let fromUserId = jsonObject["fromUserId"];
    297. let peerConnection = peerConnectionDict[fromUserId];
    298. if (peerConnection == null) {
    299. return
    300. }
    301. let options = {
    302. "sdpMLineIndex": jsonObject["label"],
    303. "sdpMid": jsonObject["id"],
    304. "candidate": jsonObject["candidate"]
    305. }
    306. let iceCandidate = new RTCIceCandidate(options);
    307. peerConnection.addIceCandidate(iceCandidate);
    308. }
    309. function receivedOtherJoin(jsonObject) {
    310. // 创建 PeerConnection
    311. let userId = jsonObject["userId"];
    312. var peerConnection = peerConnectionDict[userId];
    313. if (peerConnection == null) {
    314. peerConnection = createPeerConnection(userId);
    315. for (let i = 0; localStream != null && i < localStream.getTracks().length; i++) {
    316. const track = localStream.getTracks()[i];
    317. peerConnection.addTrack(track, localStream);
    318. }
    319. peerConnectionDict[userId] = peerConnection;
    320. }
    321. var remoteView = remoteViewDict[userId];
    322. if (remoteView == null) {
    323. remoteView = document.createElement("video");
    324. remoteView.className = "remote_view";
    325. remoteView.autoplay = true;
    326. remoteView.control = true;
    327. remoteView.muted = true;
    328. remoteViews.appendChild(remoteView);
    329. remoteViewDict[userId] = remoteView;
    330. }
    331. // 通过 PeerConnection 创建 offer,获取 sdp
    332. peerConnection.createOffer().then(function (sessionDescription) {
    333. console.log(userId + " create offer success.");
    334. // 将 offer sdp 作为参数 setLocalDescription
    335. peerConnection.setLocalDescription(sessionDescription).then(function () {
    336. console.log(userId + " set local sdp success.");
    337. // 发送 offer sdp
    338. sendOffer(sessionDescription, userId);
    339. }).catch(function (error) {
    340. console.log("error--->" + error);
    341. })
    342. }).catch(function (error) {
    343. console.log("error--->" + error);
    344. });
    345. }
    346. function receivedOtherQuit(jsonObject) {
    347. let userId = jsonObject["userId"];
    348. let peerConnection = peerConnectionDict[userId];
    349. if (peerConnection != null) {
    350. peerConnection.close();
    351. delete peerConnectionDict[userId];
    352. }
    353. let remoteView = remoteViewDict[userId];
    354. if (remoteView != null) {
    355. remoteView.removeAttribute('src');
    356. remoteView.load();
    357. remoteView.remove();
    358. delete remoteViewDict[userId];
    359. }
    360. }
    361. navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (mediaStream) {
    362. // 初始化 PeerConnectionFactory;
    363. // 创建 EglBase;
    364. // 创建 PeerConnectionFactory;
    365. // 创建音轨;
    366. // 创建视轨;
    367. localStream = mediaStream;
    368. // 初始化本地视频渲染控件;
    369. // 初始化远端视频渲染控件;
    370. // 开始本地渲染。
    371. localView.srcObject = mediaStream;
    372. }).catch(function (error) {
    373. console.log("error--->" + error);
    374. })
    375. script>
    376. <script type="text/javascript">
    377. var websocket;
    378. function connect() {
    379. let inputServerUrl = document.getElementById("input_server_url");
    380. let pWebsocketState = document.getElementById("p_websocket_state");
    381. let url = inputServerUrl.value;
    382. websocket = new WebSocket(url);
    383. websocket.onopen = function () {
    384. console.log("onOpen");
    385. pWebsocketState.innerText = "WebSocket 已连接";
    386. }
    387. websocket.onmessage = function (message) {
    388. console.log("onmessage--->" + message.data);
    389. let jsonObject = JSON.parse(message.data);
    390. let msgType = jsonObject["msgType"];
    391. if ("sdp" == msgType) {
    392. let type = jsonObject["type"];
    393. if ("offer" == type) {
    394. receivedOffer(jsonObject);
    395. } else if ("answer" == type) {
    396. receivedAnswer(jsonObject);
    397. }
    398. } else if ("iceCandidate" == msgType) {
    399. receivedCandidate(jsonObject);
    400. } else if ("otherJoin" == msgType) {
    401. receivedOtherJoin(jsonObject);
    402. } else if ("otherQuit" == msgType) {
    403. receivedOtherQuit(jsonObject);
    404. }
    405. }
    406. websocket.onclose = function (error) {
    407. console.log("onclose--->" + error);
    408. pWebsocketState.innerText = "WebSocket 已断开";
    409. }
    410. websocket.onerror = function (error) {
    411. console.log("onerror--->" + error);
    412. }
    413. }
    414. function disconnect() {
    415. websocket.close();
    416. }
    417. function send(message) {
    418. if (!websocket) {
    419. return;
    420. }
    421. websocket.send(message);
    422. }
    423. script>
    424. html>

    多人通话至少需要三个端,我们就等所有端都实现了再最后来看效果。

    五、Android

    1.添加依赖

    这个跟前两篇的一样,不需要额外引入。

    2.布局

    1. "1.0" encoding="utf-8"?>
    2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:app="http://schemas.android.com/apk/res-auto"
    4. xmlns:tools="http://schemas.android.com/tools"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent"
    7. android:background="#FF000000"
    8. android:keepScreenOn="true"
    9. tools:context=".P2PDemoActivity">
    10. <org.webrtc.SurfaceViewRenderer
    11. android:id="@+id/svr_local"
    12. android:layout_width="match_parent"
    13. android:layout_height="0dp"
    14. app:layout_constraintBottom_toBottomOf="parent"
    15. app:layout_constraintDimensionRatio="9:16"
    16. app:layout_constraintEnd_toEndOf="parent"
    17. app:layout_constraintStart_toStartOf="parent"
    18. app:layout_constraintTop_toTopOf="parent" />
    19. <androidx.core.widget.NestedScrollView
    20. android:layout_width="90dp"
    21. android:layout_height="wrap_content"
    22. android:layout_marginTop="30dp"
    23. android:layout_marginEnd="30dp"
    24. android:layout_marginBottom="30dp"
    25. app:layout_constraintEnd_toEndOf="parent"
    26. app:layout_constraintTop_toTopOf="parent">
    27. <androidx.appcompat.widget.LinearLayoutCompat
    28. android:id="@+id/ll_remotes"
    29. android:layout_width="match_parent"
    30. android:layout_height="wrap_content"
    31. android:orientation="vertical">
    32. androidx.appcompat.widget.LinearLayoutCompat>
    33. androidx.core.widget.NestedScrollView>
    34. <androidx.appcompat.widget.LinearLayoutCompat
    35. android:layout_width="match_parent"
    36. android:layout_height="wrap_content"
    37. android:layout_marginStart="30dp"
    38. android:layout_marginTop="30dp"
    39. android:layout_marginEnd="30dp"
    40. android:orientation="vertical"
    41. app:layout_constraintStart_toStartOf="parent"
    42. app:layout_constraintTop_toTopOf="parent">
    43. <androidx.appcompat.widget.AppCompatTextView
    44. android:id="@+id/tv_websocket_state"
    45. android:layout_width="match_parent"
    46. android:layout_height="wrap_content"
    47. android:text="WebSocket 已断开"
    48. android:textColor="#FFFFFFFF" />
    49. <androidx.appcompat.widget.AppCompatEditText
    50. android:id="@+id/et_server_url"
    51. android:layout_width="match_parent"
    52. android:layout_height="wrap_content"
    53. android:hint="请输入服务器地址"
    54. android:textColor="#FFFFFFFF"
    55. android:textColorHint="#FFFFFFFF" />
    56. <androidx.appcompat.widget.AppCompatButton
    57. android:id="@+id/btn_connect"
    58. android:layout_width="wrap_content"
    59. android:layout_height="wrap_content"
    60. android:text="连接 WebSocket"
    61. android:textAllCaps="false" />
    62. <androidx.appcompat.widget.AppCompatButton
    63. android:id="@+id/btn_disconnect"
    64. android:layout_width="wrap_content"
    65. android:layout_height="wrap_content"
    66. android:text="断开 WebSocket"
    67. android:textAllCaps="false" />
    68. <androidx.appcompat.widget.AppCompatButton
    69. android:id="@+id/btn_join"
    70. android:layout_width="wrap_content"
    71. android:layout_height="wrap_content"
    72. android:text="加入房间"
    73. android:textSize="12sp" />
    74. <androidx.appcompat.widget.AppCompatButton
    75. android:id="@+id/btn_quit"
    76. android:layout_width="wrap_content"
    77. android:layout_height="wrap_content"
    78. android:text="退出房间"
    79. android:textSize="12sp" />
    80. androidx.appcompat.widget.LinearLayoutCompat>
    81. androidx.constraintlayout.widget.ConstraintLayout>

    布局中将远端视频渲染控件去掉,改成一个远端视频渲染控件的容器,每当有新的连接时创建新的远端视频渲染控件放到容器中。

    3.MultipleDemoActivity.java

    1. package com.qinshou.webrtcdemo_android;
    2. import android.content.Context;
    3. import android.os.Bundle;
    4. import android.text.TextUtils;
    5. import android.view.View;
    6. import android.view.ViewGroup;
    7. import android.widget.EditText;
    8. import android.widget.LinearLayout;
    9. import android.widget.TextView;
    10. import androidx.appcompat.app.AppCompatActivity;
    11. import androidx.appcompat.widget.LinearLayoutCompat;
    12. import org.json.JSONException;
    13. import org.json.JSONObject;
    14. import org.webrtc.AudioSource;
    15. import org.webrtc.AudioTrack;
    16. import org.webrtc.Camera2Capturer;
    17. import org.webrtc.Camera2Enumerator;
    18. import org.webrtc.CameraEnumerator;
    19. import org.webrtc.DataChannel;
    20. import org.webrtc.DefaultVideoDecoderFactory;
    21. import org.webrtc.DefaultVideoEncoderFactory;
    22. import org.webrtc.EglBase;
    23. import org.webrtc.IceCandidate;
    24. import org.webrtc.MediaConstraints;
    25. import org.webrtc.MediaStream;
    26. import org.webrtc.PeerConnection;
    27. import org.webrtc.PeerConnectionFactory;
    28. import org.webrtc.RtpReceiver;
    29. import org.webrtc.SessionDescription;
    30. import org.webrtc.SurfaceTextureHelper;
    31. import org.webrtc.SurfaceViewRenderer;
    32. import org.webrtc.VideoCapturer;
    33. import org.webrtc.VideoDecoderFactory;
    34. import org.webrtc.VideoEncoderFactory;
    35. import org.webrtc.VideoSource;
    36. import org.webrtc.VideoTrack;
    37. import java.util.ArrayList;
    38. import java.util.List;
    39. import java.util.Map;
    40. import java.util.UUID;
    41. import java.util.concurrent.ConcurrentHashMap;
    42. /**
    43. * Author: MrQinshou
    44. * Email: cqflqinhao@126.com
    45. * Date: 2023/3/21 17:22
    46. * Description: P2P demo
    47. */
    48. public class MultipleDemoActivity extends AppCompatActivity {
    49. private static final String TAG = MultipleDemoActivity.class.getSimpleName();
    50. private static final String AUDIO_TRACK_ID = "ARDAMSa0";
    51. private static final String VIDEO_TRACK_ID = "ARDAMSv0";
    52. private static final List STREAM_IDS = new ArrayList() {{
    53. add("ARDAMS");
    54. }};
    55. private static final String SURFACE_TEXTURE_HELPER_THREAD_NAME = "SurfaceTextureHelperThread";
    56. private static final int WIDTH = 1280;
    57. private static final int HEIGHT = 720;
    58. private static final int FPS = 30;
    59. private EglBase mEglBase;
    60. private PeerConnectionFactory mPeerConnectionFactory;
    61. private VideoCapturer mVideoCapturer;
    62. private AudioTrack mAudioTrack;
    63. private VideoTrack mVideoTrack;
    64. private WebSocketClientHelper mWebSocketClientHelper = new WebSocketClientHelper();
    65. // private String mUserId = UUID.randomUUID().toString();
    66. private String mUserId = "Android";
    67. private final Map mPeerConnectionMap = new ConcurrentHashMap<>();
    68. private final Map mRemoteViewMap = new ConcurrentHashMap<>();
    69. @Override
    70. protected void onCreate(Bundle savedInstanceState) {
    71. super.onCreate(savedInstanceState);
    72. setContentView(R.layout.activity_multiple_demo);
    73. ((EditText) findViewById(R.id.et_server_url)).setText("ws://192.168.1.104:8888");
    74. findViewById(R.id.btn_connect).setOnClickListener(new View.OnClickListener() {
    75. @Override
    76. public void onClick(View view) {
    77. String url = ((EditText) findViewById(R.id.et_server_url)).getText().toString().trim();
    78. mWebSocketClientHelper.connect(url);
    79. }
    80. });
    81. findViewById(R.id.btn_disconnect).setOnClickListener(new View.OnClickListener() {
    82. @Override
    83. public void onClick(View view) {
    84. mWebSocketClientHelper.disconnect();
    85. }
    86. });
    87. findViewById(R.id.btn_join).setOnClickListener(new View.OnClickListener() {
    88. @Override
    89. public void onClick(View view) {
    90. join();
    91. }
    92. });
    93. findViewById(R.id.btn_quit).setOnClickListener(new View.OnClickListener() {
    94. @Override
    95. public void onClick(View view) {
    96. quit();
    97. }
    98. });
    99. mWebSocketClientHelper.setOnWebSocketListener(new WebSocketClientHelper.OnWebSocketClientListener() {
    100. @Override
    101. public void onOpen() {
    102. runOnUiThread(new Runnable() {
    103. @Override
    104. public void run() {
    105. ((TextView) findViewById(R.id.tv_websocket_state)).setText("WebSocket 已连接");
    106. }
    107. });
    108. }
    109. @Override
    110. public void onClose() {
    111. runOnUiThread(new Runnable() {
    112. @Override
    113. public void run() {
    114. ((TextView) findViewById(R.id.tv_websocket_state)).setText("WebSocket 已断开");
    115. }
    116. });
    117. }
    118. @Override
    119. public void onMessage(String message) {
    120. ShowLogUtil.debug("message--->" + message);
    121. try {
    122. JSONObject jsonObject = new JSONObject(message);
    123. String msgType = jsonObject.optString("msgType");
    124. if (TextUtils.equals("sdp", msgType)) {
    125. String type = jsonObject.optString("type");
    126. if (TextUtils.equals("offer", type)) {
    127. receivedOffer(jsonObject);
    128. } else if (TextUtils.equals("answer", type)) {
    129. receivedAnswer(jsonObject);
    130. }
    131. } else if (TextUtils.equals("iceCandidate", msgType)) {
    132. receivedCandidate(jsonObject);
    133. } else if (TextUtils.equals("otherJoin", msgType)) {
    134. receivedOtherJoin(jsonObject);
    135. } else if (TextUtils.equals("otherQuit", msgType)) {
    136. receivedOtherQuit(jsonObject);
    137. }
    138. } catch (JSONException e) {
    139. e.printStackTrace();
    140. }
    141. }
    142. });
    143. // 初始化 PeerConnectionFactory
    144. initPeerConnectionFactory(MultipleDemoActivity.this);
    145. // 创建 EglBase
    146. mEglBase = EglBase.create();
    147. // 创建 PeerConnectionFactory
    148. mPeerConnectionFactory = createPeerConnectionFactory(mEglBase);
    149. // 创建音轨
    150. mAudioTrack = createAudioTrack(mPeerConnectionFactory);
    151. // 创建视轨
    152. mVideoCapturer = createVideoCapturer();
    153. VideoSource videoSource = createVideoSource(mPeerConnectionFactory, mVideoCapturer);
    154. mVideoTrack = createVideoTrack(mPeerConnectionFactory, videoSource);
    155. // 初始化本地视频渲染控件,这个方法非常重要,不初始化会黑屏
    156. SurfaceViewRenderer svrLocal = findViewById(R.id.svr_local);
    157. svrLocal.init(mEglBase.getEglBaseContext(), null);
    158. mVideoTrack.addSink(svrLocal);
    159. // 开始本地渲染
    160. // 创建 SurfaceTextureHelper,用来表示 camera 初始化的线程
    161. SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(SURFACE_TEXTURE_HELPER_THREAD_NAME, mEglBase.getEglBaseContext());
    162. // 初始化视频采集器
    163. mVideoCapturer.initialize(surfaceTextureHelper, MultipleDemoActivity.this, videoSource.getCapturerObserver());
    164. mVideoCapturer.startCapture(WIDTH, HEIGHT, FPS);
    165. }
    166. @Override
    167. protected void onDestroy() {
    168. super.onDestroy();
    169. if (mEglBase != null) {
    170. mEglBase.release();
    171. mEglBase = null;
    172. }
    173. if (mVideoCapturer != null) {
    174. try {
    175. mVideoCapturer.stopCapture();
    176. } catch (InterruptedException e) {
    177. e.printStackTrace();
    178. }
    179. mVideoCapturer.dispose();
    180. mVideoCapturer = null;
    181. }
    182. if (mAudioTrack != null) {
    183. mAudioTrack.dispose();
    184. mAudioTrack = null;
    185. }
    186. if (mVideoTrack != null) {
    187. mVideoTrack.dispose();
    188. mVideoTrack = null;
    189. }
    190. for (PeerConnection peerConnection : mPeerConnectionMap.values()) {
    191. peerConnection.close();
    192. peerConnection.dispose();
    193. }
    194. mPeerConnectionMap.clear();
    195. SurfaceViewRenderer svrLocal = findViewById(R.id.svr_local);
    196. svrLocal.release();
    197. for (SurfaceViewRenderer surfaceViewRenderer : mRemoteViewMap.values()) {
    198. surfaceViewRenderer.release();
    199. }
    200. mRemoteViewMap.clear();
    201. mWebSocketClientHelper.disconnect();
    202. }
    203. private void initPeerConnectionFactory(Context context) {
    204. PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions());
    205. }
    206. private PeerConnectionFactory createPeerConnectionFactory(EglBase eglBase) {
    207. VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true);
    208. VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
    209. return PeerConnectionFactory.builder().setVideoEncoderFactory(videoEncoderFactory).setVideoDecoderFactory(videoDecoderFactory).createPeerConnectionFactory();
    210. }
    211. private AudioTrack createAudioTrack(PeerConnectionFactory peerConnectionFactory) {
    212. AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
    213. AudioTrack audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
    214. audioTrack.setEnabled(true);
    215. return audioTrack;
    216. }
    217. private VideoCapturer createVideoCapturer() {
    218. VideoCapturer videoCapturer = null;
    219. CameraEnumerator cameraEnumerator = new Camera2Enumerator(MultipleDemoActivity.this);
    220. for (String deviceName : cameraEnumerator.getDeviceNames()) {
    221. // 前摄像头
    222. if (cameraEnumerator.isFrontFacing(deviceName)) {
    223. videoCapturer = new Camera2Capturer(MultipleDemoActivity.this, deviceName, null);
    224. }
    225. }
    226. return videoCapturer;
    227. }
    228. private VideoSource createVideoSource(PeerConnectionFactory peerConnectionFactory, VideoCapturer videoCapturer) {
    229. // 创建视频源
    230. VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
    231. return videoSource;
    232. }
    233. private VideoTrack createVideoTrack(PeerConnectionFactory peerConnectionFactory, VideoSource videoSource) {
    234. // 创建视轨
    235. VideoTrack videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
    236. videoTrack.setEnabled(true);
    237. return videoTrack;
    238. }
    239. private PeerConnection createPeerConnection(PeerConnectionFactory peerConnectionFactory, String fromUserId) {
    240. // 内部会转成 RTCConfiguration
    241. List iceServers = new ArrayList<>();
    242. PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(iceServers, new PeerConnection.Observer() {
    243. @Override
    244. public void onSignalingChange(PeerConnection.SignalingState signalingState) {
    245. }
    246. @Override
    247. public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
    248. ShowLogUtil.debug("onIceConnectionChange--->" + iceConnectionState);
    249. if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
    250. PeerConnection peerConnection = mPeerConnectionMap.get(fromUserId);
    251. ShowLogUtil.debug("peerConnection--->" + peerConnection);
    252. if (peerConnection != null) {
    253. peerConnection.close();
    254. mPeerConnectionMap.remove(fromUserId);
    255. }
    256. runOnUiThread(new Runnable() {
    257. @Override
    258. public void run() {
    259. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(fromUserId);
    260. if (surfaceViewRenderer != null) {
    261. ((ViewGroup) surfaceViewRenderer.getParent()).removeView(surfaceViewRenderer);
    262. mRemoteViewMap.remove(fromUserId);
    263. }
    264. }
    265. });
    266. }
    267. }
    268. @Override
    269. public void onIceConnectionReceivingChange(boolean b) {
    270. }
    271. @Override
    272. public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
    273. }
    274. @Override
    275. public void onIceCandidate(IceCandidate iceCandidate) {
    276. ShowLogUtil.verbose("onIceCandidate--->" + iceCandidate);
    277. sendIceCandidate(iceCandidate, fromUserId);
    278. }
    279. @Override
    280. public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
    281. }
    282. @Override
    283. public void onAddStream(MediaStream mediaStream) {
    284. ShowLogUtil.verbose("onAddStream--->" + mediaStream);
    285. if (mediaStream == null || mediaStream.videoTracks == null || mediaStream.videoTracks.isEmpty()) {
    286. return;
    287. }
    288. runOnUiThread(new Runnable() {
    289. @Override
    290. public void run() {
    291. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(fromUserId);
    292. if (surfaceViewRenderer != null) {
    293. mediaStream.videoTracks.get(0).addSink(surfaceViewRenderer);
    294. }
    295. }
    296. });
    297. }
    298. @Override
    299. public void onRemoveStream(MediaStream mediaStream) {
    300. }
    301. @Override
    302. public void onDataChannel(DataChannel dataChannel) {
    303. }
    304. @Override
    305. public void onRenegotiationNeeded() {
    306. }
    307. @Override
    308. public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
    309. }
    310. });
    311. return peerConnection;
    312. }
    313. private void join() {
    314. try {
    315. JSONObject jsonObject = new JSONObject();
    316. jsonObject.put("msgType", "join");
    317. jsonObject.put("userId", mUserId);
    318. mWebSocketClientHelper.send(jsonObject.toString());
    319. } catch (JSONException e) {
    320. e.printStackTrace();
    321. }
    322. }
    323. private void quit() {
    324. try {
    325. JSONObject jsonObject = new JSONObject();
    326. jsonObject.put("msgType", "quit");
    327. jsonObject.put("userId", mUserId);
    328. mWebSocketClientHelper.send(jsonObject.toString());
    329. } catch (JSONException e) {
    330. e.printStackTrace();
    331. }
    332. new Thread(new Runnable() {
    333. @Override
    334. public void run() {
    335. for (PeerConnection peerConnection : mPeerConnectionMap.values()) {
    336. peerConnection.close();
    337. }
    338. mPeerConnectionMap.clear();
    339. }
    340. }).start();
    341. for (SurfaceViewRenderer surfaceViewRenderer : mRemoteViewMap.values()) {
    342. ((ViewGroup) surfaceViewRenderer.getParent()).removeView(surfaceViewRenderer);
    343. }
    344. mRemoteViewMap.clear();
    345. }
    346. private void sendOffer(SessionDescription offer, String toUserId) {
    347. try {
    348. JSONObject jsonObject = new JSONObject();
    349. jsonObject.put("msgType", "sdp");
    350. jsonObject.put("fromUserId", mUserId);
    351. jsonObject.put("toUserId", toUserId);
    352. jsonObject.put("type", "offer");
    353. jsonObject.put("sdp", offer.description);
    354. mWebSocketClientHelper.send(jsonObject.toString());
    355. } catch (JSONException e) {
    356. e.printStackTrace();
    357. }
    358. }
    359. private void receivedOffer(JSONObject jsonObject) {
    360. String fromUserId = jsonObject.optString("fromUserId");
    361. PeerConnection peerConnection = mPeerConnectionMap.get(fromUserId);
    362. if (peerConnection == null) {
    363. // 创建 PeerConnection
    364. peerConnection = createPeerConnection(mPeerConnectionFactory, fromUserId);
    365. // 为 PeerConnection 添加音轨、视轨
    366. peerConnection.addTrack(mAudioTrack, STREAM_IDS);
    367. peerConnection.addTrack(mVideoTrack, STREAM_IDS);
    368. mPeerConnectionMap.put(fromUserId, peerConnection);
    369. }
    370. runOnUiThread(new Runnable() {
    371. @Override
    372. public void run() {
    373. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(fromUserId);
    374. if (surfaceViewRenderer == null) {
    375. // 初始化 SurfaceViewRender ,这个方法非常重要,不初始化黑屏
    376. surfaceViewRenderer = new SurfaceViewRenderer(MultipleDemoActivity.this);
    377. surfaceViewRenderer.init(mEglBase.getEglBaseContext(), null);
    378. surfaceViewRenderer.setLayoutParams(new LinearLayout.LayoutParams(dp2px(MultipleDemoActivity.this, 90), dp2px(MultipleDemoActivity.this, 160)));
    379. LinearLayoutCompat llRemotes = findViewById(R.id.ll_remotes);
    380. llRemotes.addView(surfaceViewRenderer);
    381. mRemoteViewMap.put(fromUserId, surfaceViewRenderer);
    382. }
    383. }
    384. });
    385. String type = jsonObject.optString("type");
    386. String sdp = jsonObject.optString("sdp");
    387. PeerConnection finalPeerConnection = peerConnection;
    388. // 将 offer sdp 作为参数 setRemoteDescription
    389. SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp);
    390. peerConnection.setRemoteDescription(new MySdpObserver() {
    391. @Override
    392. public void onCreateSuccess(SessionDescription sessionDescription) {
    393. }
    394. @Override
    395. public void onSetSuccess() {
    396. ShowLogUtil.debug(fromUserId + " set remote sdp success.");
    397. // 通过 PeerConnection 创建 answer,获取 sdp
    398. MediaConstraints mediaConstraints = new MediaConstraints();
    399. finalPeerConnection.createAnswer(new MySdpObserver() {
    400. @Override
    401. public void onCreateSuccess(SessionDescription sessionDescription) {
    402. ShowLogUtil.verbose(fromUserId + "create answer success.");
    403. // 将 answer sdp 作为参数 setLocalDescription
    404. finalPeerConnection.setLocalDescription(new MySdpObserver() {
    405. @Override
    406. public void onCreateSuccess(SessionDescription sessionDescription) {
    407. }
    408. @Override
    409. public void onSetSuccess() {
    410. ShowLogUtil.verbose(fromUserId + " set local sdp success.");
    411. // 发送 answer sdp
    412. sendAnswer(sessionDescription, fromUserId);
    413. }
    414. }, sessionDescription);
    415. }
    416. @Override
    417. public void onSetSuccess() {
    418. }
    419. }, mediaConstraints);
    420. }
    421. }, sessionDescription);
    422. }
    423. private void sendAnswer(SessionDescription answer, String toUserId) {
    424. try {
    425. JSONObject jsonObject = new JSONObject();
    426. jsonObject.put("msgType", "sdp");
    427. jsonObject.put("fromUserId", mUserId);
    428. jsonObject.put("toUserId", toUserId);
    429. jsonObject.put("type", "answer");
    430. jsonObject.put("sdp", answer.description);
    431. mWebSocketClientHelper.send(jsonObject.toString());
    432. } catch (JSONException e) {
    433. e.printStackTrace();
    434. }
    435. }
    436. private void receivedAnswer(JSONObject jsonObject) {
    437. String fromUserId = jsonObject.optString("fromUserId");
    438. PeerConnection peerConnection = mPeerConnectionMap.get(fromUserId);
    439. if (peerConnection == null) {
    440. peerConnection = createPeerConnection(mPeerConnectionFactory, fromUserId);
    441. peerConnection.addTrack(mAudioTrack, STREAM_IDS);
    442. peerConnection.addTrack(mVideoTrack, STREAM_IDS);
    443. mPeerConnectionMap.put(fromUserId, peerConnection);
    444. }
    445. runOnUiThread(new Runnable() {
    446. @Override
    447. public void run() {
    448. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(fromUserId);
    449. if (surfaceViewRenderer == null) {
    450. // 初始化 SurfaceViewRender ,这个方法非常重要,不初始化黑屏
    451. surfaceViewRenderer = new SurfaceViewRenderer(MultipleDemoActivity.this);
    452. surfaceViewRenderer.init(mEglBase.getEglBaseContext(), null);
    453. surfaceViewRenderer.setLayoutParams(new LinearLayout.LayoutParams(dp2px(MultipleDemoActivity.this, 90), dp2px(MultipleDemoActivity.this, 160)));
    454. LinearLayoutCompat llRemotes = findViewById(R.id.ll_remotes);
    455. llRemotes.addView(surfaceViewRenderer);
    456. mRemoteViewMap.put(fromUserId, surfaceViewRenderer);
    457. }
    458. }
    459. });
    460. String type = jsonObject.optString("type");
    461. String sdp = jsonObject.optString("sdp");
    462. // 收到 answer sdp,将 answer sdp 作为参数 setRemoteDescription
    463. SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp);
    464. peerConnection.setRemoteDescription(new MySdpObserver() {
    465. @Override
    466. public void onCreateSuccess(SessionDescription sessionDescription) {
    467. }
    468. @Override
    469. public void onSetSuccess() {
    470. ShowLogUtil.debug(fromUserId + " set remote sdp success.");
    471. }
    472. }, sessionDescription);
    473. }
    474. private void sendIceCandidate(IceCandidate iceCandidate, String toUserId) {
    475. try {
    476. JSONObject jsonObject = new JSONObject();
    477. jsonObject.put("msgType", "iceCandidate");
    478. jsonObject.put("fromUserId", mUserId);
    479. jsonObject.put("toUserId", toUserId);
    480. jsonObject.put("id", iceCandidate.sdpMid);
    481. jsonObject.put("label", iceCandidate.sdpMLineIndex);
    482. jsonObject.put("candidate", iceCandidate.sdp);
    483. mWebSocketClientHelper.send(jsonObject.toString());
    484. } catch (JSONException e) {
    485. e.printStackTrace();
    486. }
    487. }
    488. private void receivedCandidate(JSONObject jsonObject) {
    489. String fromUserId = jsonObject.optString("fromUserId");
    490. PeerConnection peerConnection = mPeerConnectionMap.get(fromUserId);
    491. if (peerConnection == null) {
    492. return;
    493. }
    494. String id = jsonObject.optString("id");
    495. int label = jsonObject.optInt("label");
    496. String candidate = jsonObject.optString("candidate");
    497. IceCandidate iceCandidate = new IceCandidate(id, label, candidate);
    498. peerConnection.addIceCandidate(iceCandidate);
    499. }
    500. private void receivedOtherJoin(JSONObject jsonObject) throws JSONException {
    501. String userId = jsonObject.optString("userId");
    502. PeerConnection peerConnection = mPeerConnectionMap.get(userId);
    503. if (peerConnection == null) {
    504. // 创建 PeerConnection
    505. peerConnection = createPeerConnection(mPeerConnectionFactory, userId);
    506. // 为 PeerConnection 添加音轨、视轨
    507. peerConnection.addTrack(mAudioTrack, STREAM_IDS);
    508. peerConnection.addTrack(mVideoTrack, STREAM_IDS);
    509. mPeerConnectionMap.put(userId, peerConnection);
    510. }
    511. runOnUiThread(new Runnable() {
    512. @Override
    513. public void run() {
    514. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(userId);
    515. if (surfaceViewRenderer == null) {
    516. // 初始化 SurfaceViewRender ,这个方法非常重要,不初始化黑屏
    517. surfaceViewRenderer = new SurfaceViewRenderer(MultipleDemoActivity.this);
    518. surfaceViewRenderer.init(mEglBase.getEglBaseContext(), null);
    519. surfaceViewRenderer.setLayoutParams(new LinearLayout.LayoutParams(dp2px(MultipleDemoActivity.this, 90), dp2px(MultipleDemoActivity.this, 160)));
    520. LinearLayoutCompat llRemotes = findViewById(R.id.ll_remotes);
    521. llRemotes.addView(surfaceViewRenderer);
    522. mRemoteViewMap.put(userId, surfaceViewRenderer);
    523. }
    524. }
    525. });
    526. PeerConnection finalPeerConnection = peerConnection;
    527. // 通过 PeerConnection 创建 offer,获取 sdp
    528. MediaConstraints mediaConstraints = new MediaConstraints();
    529. peerConnection.createOffer(new MySdpObserver() {
    530. @Override
    531. public void onCreateSuccess(SessionDescription sessionDescription) {
    532. ShowLogUtil.verbose(userId + " create offer success.");
    533. // 将 offer sdp 作为参数 setLocalDescription
    534. finalPeerConnection.setLocalDescription(new MySdpObserver() {
    535. @Override
    536. public void onCreateSuccess(SessionDescription sessionDescription) {
    537. }
    538. @Override
    539. public void onSetSuccess() {
    540. ShowLogUtil.verbose(userId + " set local sdp success.");
    541. // 发送 offer sdp
    542. sendOffer(sessionDescription, userId);
    543. }
    544. }, sessionDescription);
    545. }
    546. @Override
    547. public void onSetSuccess() {
    548. }
    549. }, mediaConstraints);
    550. }
    551. private void receivedOtherQuit(JSONObject jsonObject) throws JSONException {
    552. String userId = jsonObject.optString("userId");
    553. PeerConnection peerConnection = mPeerConnectionMap.get(userId);
    554. if (peerConnection != null) {
    555. peerConnection.close();
    556. mPeerConnectionMap.remove(userId);
    557. }
    558. runOnUiThread(new Runnable() {
    559. @Override
    560. public void run() {
    561. SurfaceViewRenderer surfaceViewRenderer = mRemoteViewMap.get(userId);
    562. if (surfaceViewRenderer != null) {
    563. ((ViewGroup) surfaceViewRenderer.getParent()).removeView(surfaceViewRenderer);
    564. mRemoteViewMap.remove(userId);
    565. }
    566. }
    567. });
    568. }
    569. public static int dp2px(Context context, float dp) {
    570. float density = context.getResources().getDisplayMetrics().density;
    571. return (int) (dp * density + 0.5f);
    572. }
    573. }

    其中 WebSocketClientHelper 跟之前一样的,其余逻辑跟 H5 是一样的。多人通话至少需要三个端,我们就等所有端都实现了再最后来看效果。

    六、iOS

    1.添加依赖

    这个跟前两篇的一样,不需要额外引入。

    2.MultipleDemoViewController.swift

    1. //
    2. // LocalDemoViewController.swift
    3. // WebRTCDemo-iOS
    4. //
    5. // Created by 覃浩 on 2023/3/21.
    6. //
    7. import UIKit
    8. import WebRTC
    9. import SnapKit
    10. class MultipleDemoViewController: UIViewController {
    11. private static let AUDIO_TRACK_ID = "ARDAMSa0"
    12. private static let VIDEO_TRACK_ID = "ARDAMSv0"
    13. private static let STREAM_IDS = ["ARDAMS"]
    14. private static let WIDTH = 1280
    15. private static let HEIGHT = 720
    16. private static let FPS = 30
    17. private var localView: RTCEAGLVideoView!
    18. private var remoteViews: UIScrollView!
    19. private var peerConnectionFactory: RTCPeerConnectionFactory!
    20. private var audioTrack: RTCAudioTrack?
    21. private var videoTrack: RTCVideoTrack?
    22. /**
    23. iOS 需要将 Capturer 保存为全局变量,否则无法渲染本地画面
    24. */
    25. private var videoCapturer: RTCVideoCapturer?
    26. /**
    27. iOS 需要将远端流保存为全局变量,否则无法渲染远端画面
    28. */
    29. private var remoteStreamDict: [String : RTCMediaStream] = [:]
    30. // private let userId = UUID().uuidString
    31. private let userId = "iOS"
    32. private var peerConnectionDict: [String : RTCPeerConnection] = [:]
    33. private var remoteViewDict: [String : RTCEAGLVideoView] = [:]
    34. private var lbWebSocketState: UILabel? = nil
    35. private var tfServerUrl: UITextField? = nil
    36. private let webSocketHelper = WebSocketClientHelper()
    37. override func viewDidLoad() {
    38. super.viewDidLoad()
    39. // 表明 View 不要扩展到整个屏幕,而是在 NavigationBar 下的区域
    40. edgesForExtendedLayout = UIRectEdge()
    41. self.view.backgroundColor = UIColor.black
    42. // WebSocket 状态文本框
    43. lbWebSocketState = UILabel()
    44. lbWebSocketState!.textColor = UIColor.white
    45. lbWebSocketState!.text = "WebSocket 已断开"
    46. self.view.addSubview(lbWebSocketState!)
    47. lbWebSocketState!.snp.makeConstraints({ make in
    48. make.left.equalToSuperview().offset(30)
    49. make.right.equalToSuperview().offset(-30)
    50. make.height.equalTo(40)
    51. })
    52. // 服务器地址输入框
    53. tfServerUrl = UITextField()
    54. tfServerUrl!.textColor = UIColor.white
    55. tfServerUrl!.text = "ws://192.168.1.104:8888"
    56. tfServerUrl!.placeholder = "请输入服务器地址"
    57. tfServerUrl!.delegate = self
    58. self.view.addSubview(tfServerUrl!)
    59. tfServerUrl!.snp.makeConstraints({ make in
    60. make.left.equalToSuperview().offset(30)
    61. make.right.equalToSuperview().offset(-30)
    62. make.height.equalTo(20)
    63. make.top.equalTo(lbWebSocketState!.snp.bottom).offset(10)
    64. })
    65. // 连接 WebSocket 按钮
    66. let btnConnect = UIButton()
    67. btnConnect.backgroundColor = UIColor.lightGray
    68. btnConnect.setTitle("连接 WebSocket", for: .normal)
    69. btnConnect.setTitleColor(UIColor.black, for: .normal)
    70. btnConnect.addTarget(self, action: #selector(connect), for: .touchUpInside)
    71. self.view.addSubview(btnConnect)
    72. btnConnect.snp.makeConstraints({ make in
    73. make.left.equalToSuperview().offset(30)
    74. make.width.equalTo(140)
    75. make.height.equalTo(40)
    76. make.top.equalTo(tfServerUrl!.snp.bottom).offset(10)
    77. })
    78. // 断开 WebSocket 按钮
    79. let btnDisconnect = UIButton()
    80. btnDisconnect.backgroundColor = UIColor.lightGray
    81. btnDisconnect.setTitle("断开 WebSocket", for: .normal)
    82. btnDisconnect.setTitleColor(UIColor.black, for: .normal)
    83. btnDisconnect.addTarget(self, action: #selector(disconnect), for: .touchUpInside)
    84. self.view.addSubview(btnDisconnect)
    85. btnDisconnect.snp.makeConstraints({ make in
    86. make.left.equalToSuperview().offset(30)
    87. make.width.equalTo(140)
    88. make.height.equalTo(40)
    89. make.top.equalTo(btnConnect.snp.bottom).offset(10)
    90. })
    91. // 呼叫按钮
    92. let btnCall = UIButton()
    93. btnCall.backgroundColor = UIColor.lightGray
    94. btnCall.setTitle("加入房间", for: .normal)
    95. btnCall.setTitleColor(UIColor.black, for: .normal)
    96. btnCall.addTarget(self, action: #selector(join), for: .touchUpInside)
    97. self.view.addSubview(btnCall)
    98. btnCall.snp.makeConstraints({ make in
    99. make.left.equalToSuperview().offset(30)
    100. make.width.equalTo(160)
    101. make.height.equalTo(40)
    102. make.top.equalTo(btnDisconnect.snp.bottom).offset(10)
    103. })
    104. // 挂断按钮
    105. let btnHangUp = UIButton()
    106. btnHangUp.backgroundColor = UIColor.lightGray
    107. btnHangUp.setTitle("退出房间", for: .normal)
    108. btnHangUp.setTitleColor(UIColor.black, for: .normal)
    109. btnHangUp.addTarget(self, action: #selector(quit), for: .touchUpInside)
    110. self.view.addSubview(btnHangUp)
    111. btnHangUp.snp.makeConstraints({ make in
    112. make.left.equalToSuperview().offset(30)
    113. make.width.equalTo(160)
    114. make.height.equalTo(40)
    115. make.top.equalTo(btnCall.snp.bottom).offset(10)
    116. })
    117. webSocketHelper.setDelegate(delegate: self)
    118. // 初始化 PeerConnectionFactory
    119. initPeerConnectionFactory()
    120. // 创建 EglBase
    121. // 创建 PeerConnectionFactory
    122. peerConnectionFactory = createPeerConnectionFactory()
    123. // 创建音轨
    124. audioTrack = createAudioTrack(peerConnectionFactory: peerConnectionFactory)
    125. // 创建视轨
    126. videoTrack = createVideoTrack(peerConnectionFactory: peerConnectionFactory)
    127. let tuple = createVideoCapturer(videoSource: videoTrack!.source)
    128. let captureDevice = tuple.captureDevice
    129. videoCapturer = tuple.videoCapture
    130. // 初始化本地视频渲染控件
    131. localView = RTCEAGLVideoView()
    132. localView.delegate = self
    133. self.view.insertSubview(localView,at: 0)
    134. localView.snp.makeConstraints({ make in
    135. make.width.equalToSuperview()
    136. make.height.equalTo(localView.snp.width).multipliedBy(16.0/9.0)
    137. make.centerY.equalToSuperview()
    138. })
    139. videoTrack?.add(localView!)
    140. // 开始本地渲染
    141. (videoCapturer as? RTCCameraVideoCapturer)?.startCapture(with: captureDevice!, format: captureDevice!.activeFormat, fps: MultipleDemoViewController.FPS)
    142. // 初始化远端视频渲染控件容器
    143. remoteViews = UIScrollView()
    144. self.view.insertSubview(remoteViews, aboveSubview: localView)
    145. remoteViews.snp.makeConstraints { maker in
    146. maker.width.equalTo(90)
    147. maker.top.equalToSuperview().offset(30)
    148. maker.right.equalToSuperview().offset(-30)
    149. maker.bottom.equalToSuperview().offset(-30)
    150. }
    151. }
    152. override func viewDidDisappear(_ animated: Bool) {
    153. (videoCapturer as? RTCCameraVideoCapturer)?.stopCapture()
    154. videoCapturer = nil
    155. for peerConnection in peerConnectionDict.values {
    156. peerConnection.close()
    157. }
    158. peerConnectionDict.removeAll(keepingCapacity: false)
    159. remoteViewDict.removeAll(keepingCapacity: false)
    160. remoteStreamDict.removeAll(keepingCapacity: false)
    161. webSocketHelper.disconnect()
    162. }
    163. private func initPeerConnectionFactory() {
    164. RTCPeerConnectionFactory.initialize()
    165. }
    166. private func createPeerConnectionFactory() -> RTCPeerConnectionFactory {
    167. var videoEncoderFactory = RTCDefaultVideoEncoderFactory()
    168. var videoDecoderFactory = RTCDefaultVideoDecoderFactory()
    169. if TARGET_OS_SIMULATOR != 0 {
    170. videoEncoderFactory = RTCSimluatorVideoEncoderFactory()
    171. videoDecoderFactory = RTCSimulatorVideoDecoderFactory()
    172. }
    173. return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
    174. }
    175. private func createAudioTrack(peerConnectionFactory: RTCPeerConnectionFactory) -> RTCAudioTrack {
    176. let mandatoryConstraints : [String : String] = [:]
    177. let optionalConstraints : [String : String] = [:]
    178. let audioSource = peerConnectionFactory.audioSource(with: RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints))
    179. let audioTrack = peerConnectionFactory.audioTrack(with: audioSource, trackId: MultipleDemoViewController.AUDIO_TRACK_ID)
    180. audioTrack.isEnabled = true
    181. return audioTrack
    182. }
    183. private func createVideoTrack(peerConnectionFactory: RTCPeerConnectionFactory) -> RTCVideoTrack? {
    184. let videoSource = peerConnectionFactory.videoSource()
    185. let videoTrack = peerConnectionFactory.videoTrack(with: videoSource, trackId: MultipleDemoViewController.VIDEO_TRACK_ID)
    186. videoTrack.isEnabled = true
    187. return videoTrack
    188. }
    189. private func createVideoCapturer(videoSource: RTCVideoSource) -> (captureDevice: AVCaptureDevice?, videoCapture: RTCVideoCapturer?) {
    190. let videoCapturer = RTCCameraVideoCapturer(delegate: videoSource)
    191. let captureDevices = RTCCameraVideoCapturer.captureDevices()
    192. if (captureDevices.count == 0) {
    193. return (nil, nil)
    194. }
    195. var captureDevice: AVCaptureDevice?
    196. for c in captureDevices {
    197. // 前摄像头
    198. if (c.position == .front) {
    199. captureDevice = c
    200. break
    201. }
    202. }
    203. if (captureDevice == nil) {
    204. return (nil, nil)
    205. }
    206. return (captureDevice, videoCapturer)
    207. }
    208. private func createPeerConnection(peerConnectionFactory: RTCPeerConnectionFactory, fromUserId: String) -> RTCPeerConnection {
    209. let configuration = RTCConfiguration()
    210. // configuration.sdpSemantics = .unifiedPlan
    211. // configuration.continualGatheringPolicy = .gatherContinually
    212. // configuration.iceServers = [RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"])]
    213. let mandatoryConstraints : [String : String] = [:]
    214. // let mandatoryConstraints = [kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue,
    215. // kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue]
    216. let optionalConstraints : [String : String] = [:]
    217. // let optionalConstraints = ["DtlsSrtpKeyAgreement" : kRTCMediaConstraintsValueTrue]
    218. let mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints)
    219. return peerConnectionFactory.peerConnection(with: configuration, constraints: mediaConstraints, delegate: self)
    220. }
    221. @objc private func connect() {
    222. webSocketHelper.connect(url: tfServerUrl!.text!.trimmingCharacters(in: .whitespacesAndNewlines))
    223. }
    224. @objc private func disconnect() {
    225. webSocketHelper.disconnect()
    226. }
    227. @objc private func join() {
    228. var jsonObject = [String : String]()
    229. jsonObject["msgType"] = "join"
    230. jsonObject["userId"] = userId
    231. do {
    232. let data = try JSONSerialization.data(withJSONObject: jsonObject)
    233. webSocketHelper.send(message: String(data: data, encoding: .utf8)!)
    234. } catch {
    235. ShowLogUtil.verbose("error--->\(error)")
    236. }
    237. }
    238. @objc private func quit() {
    239. var jsonObject = [String : String]()
    240. jsonObject["msgType"] = "quit"
    241. jsonObject["userId"] = userId
    242. do {
    243. let data = try JSONSerialization.data(withJSONObject: jsonObject)
    244. webSocketHelper.send(message: String(data: data, encoding: .utf8)!)
    245. } catch {
    246. ShowLogUtil.verbose("error--->\(error)")
    247. }
    248. for peerConnection in peerConnectionDict.values {
    249. peerConnection.close()
    250. }
    251. peerConnectionDict.removeAll(keepingCapacity: false)
    252. for (key, value) in remoteViewDict {
    253. remoteViews.removeSubview(view: value)
    254. }
    255. remoteViewDict.removeAll(keepingCapacity: false)
    256. }
    257. private func sendOffer(offer: RTCSessionDescription, toUserId: String) {
    258. var jsonObject = [String : String]()
    259. jsonObject["msgType"] = "sdp"
    260. jsonObject["fromUserId"] = userId
    261. jsonObject["toUserId"] = toUserId
    262. jsonObject["type"] = "offer"
    263. jsonObject["sdp"] = offer.sdp
    264. do {
    265. let data = try JSONSerialization.data(withJSONObject: jsonObject)
    266. webSocketHelper.send(message: String(data: data, encoding: .utf8)!)
    267. } catch {
    268. ShowLogUtil.verbose("error--->\(error)")
    269. }
    270. }
    271. private func receivedOffer(jsonObject: [String : Any]) {
    272. let fromUserId = jsonObject["fromUserId"] as? String ?? ""
    273. var peerConnection = peerConnectionDict[fromUserId]
    274. if (peerConnection == nil) {
    275. // 创建 PeerConnection
    276. peerConnection = createPeerConnection(peerConnectionFactory: peerConnectionFactory, fromUserId: fromUserId)
    277. // 为 PeerConnection 添加音轨、视轨
    278. peerConnection!.add(audioTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    279. peerConnection!.add(videoTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    280. peerConnectionDict[fromUserId] = peerConnection
    281. }
    282. var remoteView = remoteViewDict[fromUserId]
    283. if (remoteView == nil) {
    284. let x = 0
    285. var y = 0
    286. if (remoteViews.subviews.count == 0) {
    287. y = 0
    288. } else {
    289. for i in 0..<remoteViews.subviews.count {
    290. y += Int(remoteViews.subviews[i].frame.height)
    291. }
    292. }
    293. let width = 90
    294. let height = width / 9 * 16
    295. remoteView = RTCEAGLVideoView(frame: CGRect(x: x, y: y, width: width, height: height))
    296. remoteViews.appendSubView(view: remoteView!)
    297. remoteViewDict[fromUserId] = remoteView
    298. }
    299. // 将 offer sdp 作为参数 setRemoteDescription
    300. let type = jsonObject["type"] as? String
    301. let sdp = jsonObject["sdp"] as? String
    302. let sessionDescription = RTCSessionDescription(type: .offer, sdp: sdp!)
    303. peerConnection?.setRemoteDescription(sessionDescription, completionHandler: { _ in
    304. ShowLogUtil.verbose("\(fromUserId) set remote sdp success.")
    305. // 通过 PeerConnection 创建 answer,获取 sdp
    306. let mandatoryConstraints : [String : String] = [:]
    307. let optionalConstraints : [String : String] = [:]
    308. let mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints)
    309. peerConnection?.answer(for: mediaConstraints, completionHandler: { sessionDescription, error in
    310. ShowLogUtil.verbose("\(fromUserId) create answer success.")
    311. // 将 answer sdp 作为参数 setLocalDescription
    312. peerConnection?.setLocalDescription(sessionDescription!, completionHandler: { _ in
    313. ShowLogUtil.verbose("\(fromUserId) set local sdp success.")
    314. // 发送 answer sdp
    315. self.sendAnswer(answer: sessionDescription!, toUserId: fromUserId)
    316. })
    317. })
    318. })
    319. }
    320. private func sendAnswer(answer: RTCSessionDescription, toUserId: String) {
    321. var jsonObject = [String : String]()
    322. jsonObject["msgType"] = "sdp"
    323. jsonObject["fromUserId"] = userId
    324. jsonObject["toUserId"] = toUserId
    325. jsonObject["type"] = "answer"
    326. jsonObject["sdp"] = answer.sdp
    327. do {
    328. let data = try JSONSerialization.data(withJSONObject: jsonObject)
    329. webSocketHelper.send(message: String(data: data, encoding: .utf8)!)
    330. } catch {
    331. ShowLogUtil.verbose("error--->\(error)")
    332. }
    333. }
    334. private func receivedAnswer(jsonObject: [String : Any]) {
    335. let fromUserId = jsonObject["fromUserId"] as? String ?? ""
    336. var peerConnection = peerConnectionDict[fromUserId]
    337. if (peerConnection == nil) {
    338. peerConnection = createPeerConnection(peerConnectionFactory: peerConnectionFactory, fromUserId: fromUserId)
    339. peerConnection!.add(audioTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    340. peerConnection!.add(videoTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    341. peerConnectionDict[fromUserId] = peerConnection
    342. }
    343. DispatchQueue.main.async {
    344. var remoteView = self.remoteViewDict[fromUserId]
    345. if (remoteView == nil) {
    346. let x = 0
    347. var y = 0
    348. if (self.remoteViews.subviews.count == 0) {
    349. y = 0
    350. } else {
    351. for i in 0..<self.remoteViews.subviews.count {
    352. y += Int(self.remoteViews.subviews[i].frame.height)
    353. }
    354. }
    355. let width = 90
    356. let height = width / 9 * 16
    357. remoteView = RTCEAGLVideoView(frame: CGRect(x: x, y: y, width: width, height: height))
    358. self.remoteViews.appendSubView(view: remoteView!)
    359. self.remoteViewDict[fromUserId] = remoteView
    360. }
    361. }
    362. // 收到 answer sdp,将 answer sdp 作为参数 setRemoteDescription
    363. let type = jsonObject["type"] as? String
    364. let sdp = jsonObject["sdp"] as? String
    365. let sessionDescription = RTCSessionDescription(type: .answer, sdp: sdp!)
    366. peerConnection!.setRemoteDescription(sessionDescription, completionHandler: { _ in
    367. ShowLogUtil.verbose(fromUserId + " set remote sdp success.");
    368. })
    369. }
    370. private func sendIceCandidate(iceCandidate: RTCIceCandidate, toUserId: String) {
    371. var jsonObject = [String : Any]()
    372. jsonObject["msgType"] = "iceCandidate"
    373. jsonObject["fromUserId"] = userId
    374. jsonObject["toUserId"] = toUserId
    375. jsonObject["id"] = iceCandidate.sdpMid
    376. jsonObject["label"] = iceCandidate.sdpMLineIndex
    377. jsonObject["candidate"] = iceCandidate.sdp
    378. do {
    379. let data = try JSONSerialization.data(withJSONObject: jsonObject)
    380. webSocketHelper.send(message: String(data: data, encoding: .utf8)!)
    381. } catch {
    382. ShowLogUtil.verbose("error--->\(error)")
    383. }
    384. }
    385. private func receivedCandidate(jsonObject: [String : Any]) {
    386. let fromUserId = jsonObject["fromUserId"] as? String ?? ""
    387. let peerConnection = peerConnectionDict[fromUserId]
    388. if (peerConnection == nil) {
    389. return
    390. }
    391. let id = jsonObject["id"] as? String
    392. let label = jsonObject["label"] as? Int32
    393. let candidate = jsonObject["candidate"] as? String
    394. let iceCandidate = RTCIceCandidate(sdp: candidate!, sdpMLineIndex: label!, sdpMid: id)
    395. peerConnection!.add(iceCandidate)
    396. }
    397. private func receiveOtherJoin(jsonObject: [String : Any]) {
    398. let userId = jsonObject["userId"] as? String ?? ""
    399. var peerConnection = peerConnectionDict[userId]
    400. if (peerConnection == nil) {
    401. // 创建 PeerConnection
    402. peerConnection = createPeerConnection(peerConnectionFactory: peerConnectionFactory, fromUserId: userId)
    403. // 为 PeerConnection 添加音轨、视轨
    404. peerConnection!.add(audioTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    405. peerConnection!.add(videoTrack!, streamIds: MultipleDemoViewController.STREAM_IDS)
    406. peerConnectionDict[userId] = peerConnection
    407. }
    408. DispatchQueue.main.async {
    409. var remoteView = self.remoteViewDict[userId]
    410. if (remoteView == nil) {
    411. let x = 0
    412. var y = 0
    413. if (self.remoteViews.subviews.count == 0) {
    414. y = 0
    415. } else {
    416. for i in 0..<self.remoteViews.subviews.count {
    417. y += Int(self.remoteViews.subviews[i].frame.height)
    418. }
    419. }
    420. let width = 90
    421. let height = width / 9 * 16
    422. remoteView = RTCEAGLVideoView(frame: CGRect(x: x, y: y, width: width, height: height))
    423. self.remoteViews.appendSubView(view: remoteView!)
    424. self.remoteViewDict[userId] = remoteView
    425. }
    426. }
    427. // 通过 PeerConnection 创建 offer,获取 sdp
    428. let mandatoryConstraints : [String : String] = [:]
    429. let optionalConstraints : [String : String] = [:]
    430. let mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints)
    431. peerConnection?.offer(for: mediaConstraints, completionHandler: { sessionDescription, error in
    432. ShowLogUtil.verbose("\(userId) create offer success.")
    433. if (error != nil) {
    434. return
    435. }
    436. // 将 offer sdp 作为参数 setLocalDescription
    437. peerConnection?.setLocalDescription(sessionDescription!, completionHandler: { _ in
    438. ShowLogUtil.verbose("\(userId) set local sdp success.")
    439. // 发送 offer sdp
    440. self.sendOffer(offer: sessionDescription!, toUserId: userId)
    441. })
    442. })
    443. }
    444. private func receiveOtherQuit(jsonObject: [String : Any]) {
    445. let userId = jsonObject["userId"] as? String ?? ""
    446. Thread(block: {
    447. let peerConnection = self.peerConnectionDict[userId]
    448. if (peerConnection != nil) {
    449. peerConnection?.close()
    450. self.peerConnectionDict.removeValue(forKey: userId)
    451. }
    452. }).start()
    453. let remoteView = remoteViewDict[userId]
    454. if (remoteView != nil) {
    455. remoteViews.removeSubview(view: remoteView!)
    456. remoteViewDict.removeValue(forKey: userId)
    457. }
    458. remoteStreamDict.removeValue(forKey: userId)
    459. }
    460. }
    461. // MARK: - RTCVideoViewDelegate
    462. extension MultipleDemoViewController: RTCVideoViewDelegate {
    463. func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
    464. }
    465. }
    466. // MARK: - RTCPeerConnectionDelegate
    467. extension MultipleDemoViewController: RTCPeerConnectionDelegate {
    468. func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
    469. }
    470. func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
    471. ShowLogUtil.verbose("peerConnection didAdd stream--->\(stream)")
    472. var userId: String?
    473. for (key, value) in peerConnectionDict {
    474. if (value == peerConnection) {
    475. userId = key
    476. }
    477. }
    478. if (userId == nil) {
    479. return
    480. }
    481. remoteStreamDict[userId!] = stream
    482. let remoteView = remoteViewDict[userId!]
    483. if (remoteView == nil) {
    484. return
    485. }
    486. if let videoTrack = stream.videoTracks.first {
    487. ShowLogUtil.verbose("video track found.")
    488. videoTrack.add(remoteView!)
    489. }
    490. if let audioTrack = stream.audioTracks.first{
    491. ShowLogUtil.verbose("audio track found.")
    492. audioTrack.source.volume = 8
    493. }
    494. }
    495. func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
    496. }
    497. func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {
    498. }
    499. func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
    500. if (newState == .disconnected) {
    501. DispatchQueue.main.async {
    502. var userId: String?
    503. for (key, value) in self.peerConnectionDict {
    504. if (value == peerConnection) {
    505. userId = key
    506. }
    507. }
    508. if (userId == nil) {
    509. return
    510. }
    511. Thread(block: {
    512. let peerConnection = self.peerConnectionDict[userId!]
    513. if (peerConnection != nil) {
    514. peerConnection?.close()
    515. self.peerConnectionDict.removeValue(forKey: userId!)
    516. }
    517. }).start()
    518. let remoteView = self.remoteViewDict[userId!]
    519. if (remoteView != nil) {
    520. self.remoteViews.removeSubview(view: remoteView!)
    521. self.remoteViewDict.removeValue(forKey: userId!)
    522. }
    523. self.remoteStreamDict.removeValue(forKey: userId!)
    524. }
    525. }
    526. }
    527. func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
    528. }
    529. func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) {
    530. // ShowLogUtil.verbose("didGenerate candidate--->\(candidate)")
    531. var userId: String?
    532. for (key, value) in self.peerConnectionDict {
    533. if (value == peerConnection) {
    534. userId = key
    535. }
    536. }
    537. if (userId == nil) {
    538. return
    539. }
    540. self.sendIceCandidate(iceCandidate: candidate, toUserId: userId!)
    541. }
    542. func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {
    543. }
    544. func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {
    545. }
    546. }
    547. // MARK: - UITextFieldDelegate
    548. extension MultipleDemoViewController: UITextFieldDelegate {
    549. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    550. textField.resignFirstResponder()
    551. return true
    552. }
    553. }
    554. // MARK: - WebSocketDelegate
    555. extension MultipleDemoViewController: WebSocketDelegate {
    556. func onOpen() {
    557. lbWebSocketState?.text = "WebSocket 已连接"
    558. }
    559. func onClose() {
    560. lbWebSocketState?.text = "WebSocket 已断开"
    561. }
    562. func onMessage(message: String) {
    563. do {
    564. let data = message.data(using: .utf8)
    565. let jsonObject: [String : Any] = try JSONSerialization.jsonObject(with: data!) as! [String : Any]
    566. let msgType = jsonObject["msgType"] as? String
    567. if ("sdp" == msgType) {
    568. let type = jsonObject["type"] as? String;
    569. if ("offer" == type) {
    570. receivedOffer(jsonObject: jsonObject);
    571. } else if ("answer" == type) {
    572. receivedAnswer(jsonObject: jsonObject);
    573. }
    574. } else if ("iceCandidate" == msgType) {
    575. receivedCandidate(jsonObject: jsonObject);
    576. } else if ("otherJoin" == msgType) {
    577. receiveOtherJoin(jsonObject: jsonObject)
    578. } else if ("otherQuit" == msgType) {
    579. receiveOtherQuit(jsonObject: jsonObject)
    580. }
    581. } catch {
    582. }
    583. }
    584. }

    其中 UIScrollView 的 appendSubView 和 removeSubView 是我为 UIScrollView 添加的两个扩展方法,方便纵向添加和删除控件:

    1. import UIKit
    2. extension UIScrollView {
    3. func appendSubView(view: UIView) {
    4. let oldShowsHorizontalScrollIndicator = showsHorizontalScrollIndicator
    5. let oldShowsVerticalScrollIndicator = showsVerticalScrollIndicator
    6. showsHorizontalScrollIndicator = false
    7. showsVerticalScrollIndicator = false
    8. var y = 0.0
    9. if (subviews.count == 0) {
    10. y = 0
    11. } else {
    12. for i in 0..<subviews.count {
    13. if ("_UIScrollViewScrollIndicator" == String(reflecting: type(of: subviews[i]))){
    14. continue
    15. }
    16. y += subviews[i].frame.height
    17. }
    18. }
    19. view.frame.origin.y = y
    20. addSubview(view)
    21. let contentSizeWidth = contentSize.width
    22. // 重新计算 UIScrollView 内容高度
    23. var contentSizeHeight = 0.0
    24. for i in 0..<subviews.count {
    25. if ("_UIScrollViewScrollIndicator" == String(reflecting: type(of: subviews[i]))){
    26. continue
    27. }
    28. contentSizeHeight += subviews[i].frame.height
    29. }
    30. contentSize = CGSize(width: contentSizeWidth, height: contentSizeHeight)
    31. showsHorizontalScrollIndicator = oldShowsHorizontalScrollIndicator
    32. showsVerticalScrollIndicator = oldShowsVerticalScrollIndicator
    33. }
    34. func removeSubview(view: UIView) {
    35. let oldShowsHorizontalScrollIndicator = showsHorizontalScrollIndicator
    36. let oldShowsVerticalScrollIndicator = showsVerticalScrollIndicator
    37. showsHorizontalScrollIndicator = false
    38. showsVerticalScrollIndicator = false
    39. var index = -1
    40. for i in 0..<subviews.count {
    41. if (subviews[i] == view) {
    42. index = i
    43. break
    44. }
    45. }
    46. if (index == -1) {
    47. return
    48. }
    49. for i in index+1..<subviews.count {
    50. subviews[i].frame.origin.y = subviews[i].frame.origin.y-view.frame.height
    51. }
    52. view.removeFromSuperview()
    53. let contentSizeWidth = contentSize.width
    54. // 重新计算 UIScrollView 内容高度
    55. var contentSizeHeight = 0.0
    56. for i in 0..<subviews.count {
    57. if ("_UIScrollViewScrollIndicator" == String(reflecting: type(of: subviews[i]))){
    58. continue
    59. }
    60. contentSizeHeight += subviews[i].frame.height
    61. }
    62. contentSize = CGSize(width: contentSizeWidth, height: contentSizeHeight)
    63. showsHorizontalScrollIndicator = oldShowsHorizontalScrollIndicator
    64. showsVerticalScrollIndicator = oldShowsVerticalScrollIndicator
    65. }
    66. }

    好了,现在三端都实现了,我们可以来看看效果了。

    七、效果展示

    运行 MultipleWebSocketServerHelper 的 main() 方法,我们可以看到服务端已经开启,然后我们依次将 H5、Android、iOS 连接 WebSocket,再依次加入房间:

    其中 iOS 在录屏的时候可能是系统限制,画面静止了,但其实跟另外两端是一样的,从另外两端的远端画面可以看到 iOS 是有在采集摄像头画面的。

    八、总结

    实现完成后可以感觉到多人呼叫其实也没有多难,跟点对点 Demo 的流程大致一样,只是我们需要重新定义创建 PeerConnection 的时机,但是流程仍然是不变的。以及信令有些许不同,信令这就是业务层面的,自己按需来设计,上面我定义的消息格式只是一个最简单的实现。

    至此,WebRTC 单人和多人通话的 Demo 全部完成,这就说明它能满足我们基本的视频通话、视频会议等需求,至于丢包处理、美颜滤镜就是网络优化、图像处理相关的了,后续还会记录网络穿透如何去做,以及使用 WebRTC 时的一些小功能,比如屏幕录制、图片投屏、白板等这些视频会议常用的功能。

    九、Demo

    Demo 传送门

  • 相关阅读:
    ctfshow web入门(21-28爆破)
    Codechef [June Long Two 2022] 题解
    MySQL进阶篇(五)- 锁
    批量修改/插入数据库的时候究竟该怎么选择?
    java桌面程序
    Java如何解决浮点数计算不精确问题
    leetCode 583.两个字符串的删除操作 动态规划 + 优化空间复杂度(二维dp、一维dp)
    kingbase SQL优化案例 (union+join层级查询优化 )
    Java声明式事务实战!工作中用这几种就够了!
    [附源码]计算机毕业设计JAVA课堂点名系统
  • 原文地址:https://blog.csdn.net/zgcqflqinhao/article/details/130459346