基于WebRTC与Socket的Android Java多人视频通话改造咨询
多人WebRTC音视频通话改造代码调整建议
一、信令服务端(Node.js)改造
- 废弃一对一的点对点信令转发逻辑,改用房间(Room)机制:
- 新增房间创建、加入、退出接口,服务端维护房间内用户列表
- 新用户加入时,向房间内所有已有用户推送
user-joined事件,同时向新用户返回房间内在线用户列表 - 信令消息(offer/answer/iceCandidate)需携带
房间ID和目标用户ID,服务端根据标识精准转发
- 核心代码片段:
// 房间存储结构 const rooms = new Map(); // key: roomId, value: { users: Map(userId, socket) } // 处理用户加入房间 socket.on('join-room', (roomId, userId) => { if (!rooms.has(roomId)) { rooms.set(roomId, { users: new Map() }); } const room = rooms.get(roomId); // 通知老用户新成员加入 room.users.forEach((userSocket) => { userSocket.emit('user-joined', userId); }); // 向新用户推送已有成员列表 socket.emit('existing-users', Array.from(room.users.keys())); room.users.set(userId, socket); // 处理用户断开 socket.on('disconnect', () => { room.users.delete(userId); room.users.forEach((userSocket) => { userSocket.emit('user-left', userId); }); if (room.users.size === 0) rooms.delete(roomId); }); }); // 转发信令消息 socket.on('signal', (roomId, targetUserId, data) => { const room = rooms.get(roomId); if (room?.users.has(targetUserId)) { room.users.get(targetUserId).emit('signal', socket.id, data); } });
二、Android客户端(Java)改造
1. 多PeerConnection实例管理
- 用
Map<String, PeerConnection>替代单一PeerConnection对象,key为对方用户ID,value为对应实例 - 收到服务端
existing-users事件时,为每个老用户创建PeerConnection并发起offer;收到user-joined事件时,为新用户创建PeerConnection并处理信令交互 - 核心代码片段:
private Map<String, PeerConnection> peerConnections = new HashMap<>(); private PeerConnectionFactory peerConnectionFactory; // 初始化PeerConnectionFactory(复用一对一逻辑) private void initPeerConnectionFactory(Context context, EglBase eglBase) { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()); peerConnectionFactory = PeerConnectionFactory.builder() .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext())) .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true)) .createPeerConnectionFactory(); } // 为指定用户创建PeerConnection private PeerConnection createPeerConnection(String userId, EglBase eglBase) { List<IceServer> iceServers = Collections.singletonList(new IceServer.Builder("stun:stun.l.google.com:19302").createIceServer()); PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers); PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, new PeerConnectionObserver(userId)); peerConnections.put(userId, peerConnection); // 复用本地音视频轨道 localAudioTrack.addSink(peerConnection); localVideoTrack.addSink(peerConnection); return peerConnection; } // 处理新用户加入 private void handleUserJoined(String userId, EglBase eglBase) { PeerConnection peerConnection = createPeerConnection(userId, eglBase); // 发起offer peerConnection.createOffer(new SdpObserver() { @Override public void onCreateSuccess(SessionDescription sdp) { peerConnection.setLocalDescription(new SdpObserver() {}, sdp); // 发送信令到服务端 socket.emit("signal", roomId, userId, new JSONObject().put("type", "offer").put("sdp", sdp.description)); } // 其他回调实现略 }, new MediaConstraints()); } // 处理收到的信令 private void handleSignal(String fromUserId, JSONObject data) throws JSONException { PeerConnection peerConnection = peerConnections.get(fromUserId); if (peerConnection == null) { peerConnection = createPeerConnection(fromUserId, eglBase); } String type = data.getString("type"); SessionDescription.Type sdpType = "offer".equals(type) ? SessionDescription.Type.OFFER : SessionDescription.Type.ANSWER; if ("offer".equals(type) || "answer".equals(type)) { peerConnection.setRemoteDescription(new SdpObserver() {}, new SessionDescription(sdpType, data.getString("sdp"))); if ("offer".equals(type)) { // 创建answer并发送 peerConnection.createAnswer(new SdpObserver() { @Override public void onCreateSuccess(SessionDescription sdp) { peerConnection.setLocalDescription(new SdpObserver() {}, sdp); try { socket.emit("signal", roomId, fromUserId, new JSONObject().put("type", "answer").put("sdp", sdp.description)); } catch (JSONException e) { e.printStackTrace(); } } // 其他回调实现略 }, new MediaConstraints()); } } else if ("iceCandidate".equals(type)) { IceCandidate candidate = new IceCandidate( data.getString("sdpMid"), data.getInt("sdpMLineIndex"), data.getString("candidate") ); peerConnection.addIceCandidate(candidate); } }
2. 多视频流渲染管理
- 动态创建
SurfaceViewRenderer渲染每个远端视频流,用户离开时移除视图并释放资源 - 核心代码片段:
private Map<String, SurfaceViewRenderer> renderers = new HashMap<>(); private LinearLayout videoContainer; // 布局容器 // 添加远端视频渲染视图 private void addRemoteVideoRenderer(String userId, VideoTrack videoTrack, EglBase eglBase) { SurfaceViewRenderer renderer = new SurfaceViewRenderer(context); renderer.init(eglBase.getEglBaseContext(), null); videoContainer.addView(renderer); videoTrack.addSink(renderer); renderers.put(userId, renderer); } // 处理用户离开 private void handleUserLeft(String userId) { // 释放PeerConnection PeerConnection peerConnection = peerConnections.remove(userId); if (peerConnection != null) peerConnection.close(); // 移除渲染视图 SurfaceViewRenderer renderer = renderers.remove(userId); if (renderer != null) { videoContainer.removeView(renderer); renderer.release(); } }
3. 音视频轨道复用
- 复用同一套
AudioTrack和VideoTrack,添加到所有PeerConnection实例中,避免重复初始化设备导致资源冲突
三、关键注意事项
- ICE候选转发:每个PeerConnection的ICE候选需单独发送给对应目标用户,服务端需精准转发
- 带宽优化:通过
RTCConfiguration设置带宽限制,或启用Simulcast分层编码,缓解多人通话的带宽压力 - 状态同步:服务端需确保房间用户状态实时同步,避免客户端保留无效的PeerConnection实例
- 错误处理:新增PeerConnection创建失败、信令发送失败等场景的异常处理,防止单个用户问题影响全局通话
内容的提问来源于stack exchange,提问作者Bhaskar Jha
相关产品推荐
相关产品推荐

