# WebRTC音视频通话 # 主要功能 1. 2人/多人音视频通话 2. 静音/闭麦 3. 切换摄像头 4. 暂停/继续视频流 # 集成步骤 1. demo使用的是vue3,HBuilderX导入的时候选择vue3,vue2也是支持的 2. 拷贝demo里的Info.plist和AndroidManifest.xml到项目根目录 3. 集成插件,集成插件步骤请参考[https://www.cnblogs.com/wenrisheng/p/18323027](https://www.cnblogs.com/wenrisheng/p/18323027) 4. demo/static/NodeJS是websocket服务器,使用node app.js命令既可以运行 5. 修改demo的socket服务器地址(webSocketUrl)改为电脑ip 6. socket业务可以使用各自的服务器,支持用来发送接收信令,可以使用socket、webSocket、socket.IO都可以,demo里的socket服务只是配合演示流程 ```javascript socketTask = uni.connectSocket({ url: 'ws://172.16.11.37:8088', complete: () => {} }); ``` 6. 如果socket服务器是采用局域网IP连接,ios某些机型连接局域网时首次访问会弹出局域网授权信息,点击确定后再次点击界面上的"连接socket"按钮,socket连接状态可以查看控制台或代码 7. demo演示流程: 点击界面上的加入房间即可进行视频通话 整体业务流程: 1. 点击"加入房间",会向房间里的其他人发送新成员加入消息 ``` { msgType: "join", userId: "xx" } ``` 2. 其他成员收到join消息时,会与该用户创建一个PeerConnection(同时把本地音视频加入PeerConnection),并生成offer,设置setLocalDescription,然后将offer数据发送给对方,同时会生成onIceCandidate,IceCandidate数据也要发送给对方 ``` offer数据: { msgType: "sdp", fromUserId: "xx", toUserId: "xx", type: "offer", sdp: "xx" } IceCandidate数据: { msgType: "iceCandidate", fromUserId: “xx”, toUserId: "xx", id: candidate.sdpMid, label: candidate.sdpMLineIndex, candidate: candidate.sdp } ``` 3. 用户收到offer消息时,也创建一个PeerConnection(同时把本地音视频加入PeerConnection),并setRemoteDescription,然后生成answer,设置setLocalDescription,然后将answer数据发送给对方,同时会生成onIceCandidate,IceCandidate数据也要发送给对方 ``` answer数据: { msgType: "sdp", fromUserId: "xx", toUserId: "", type: "answer", sdp: "" } IceCandidate数据: { msgType: "iceCandidate", fromUserId: “xx”, toUserId: "xx", id: candidate.sdpMid, label: candidate.sdpMLineIndex, candidate: candidate.sdp } ``` 4. 用户收到answer消息时,设置setRemoteDescription,双方收到对方的iceCandidate消息时,都调用addIceCandidate接口设置 5. 完成以上流程后,就可以进行视频通话了 ## 接口 ```javascript import { UTSWebRTC } from "@/uni_modules/wrs-uts-webrtc" let webRTC = new UTSWebRTC() ``` - 设置webRTC的回调 ```javascript // 设置webRTC的回调 webRTC.onCallback((resp) => { let opt = resp.opt this.showMsg("webRTC.onCallback opt:" + opt) switch (opt) { // 信令状态改变 case "onSignalingChange": { this.showMsg("onSignalingChange:" + JSON.stringify(resp)) let state = resp.state if (state) { switch (state) { case 0: { this.showMsg("RTCSignalingStateStable") } break; case 1: { this.showMsg("RTCSignalingStateHaveLocalOffer") } break; case 2: { this.showMsg("RTCSignalingStateHaveLocalPrAnswer") } break; case 3: { // this.showMsg("RTCSignalingStateHaveRemoteOffer") } break; case 4: { this.showMsg("RTCSignalingStateHaveRemotePrAnswer") } break; case 5: { this.showMsg("RTCSignalingStateClosed") let userId = resp.userId if (userId) { this.userLeave(userId) } } break; default: break; } } } break; case "onIceGatheringChange": { this.showMsg("onIceGatheringChange:" + JSON.stringify(resp)) let state = resp.state if (state) { switch (state) { case 0: { this.showMsg("RTCIceGatheringStateNew") } break; case 1: { this.showMsg("RTCIceGatheringStateGathering") } break; case 2: { this.showMsg("RTCIceGatheringStateComplete") } break; default: break; } } } break; // 生成IceCandidate case "onIceCandidate": { this.showMsg("onIceCandidate") let userId = resp.userId let candidate = resp.iceCandidate this.sendSocketData({ msgType: "iceCandidate", fromUserId: this.userId, toUserId: userId, id: candidate.sdpMid, label: candidate.sdpMLineIndex, candidate: candidate.sdp }) } break; case "onIceConnectionChange": { this.showMsg("onIceConnectionChange:" + JSON.stringify(resp)) let state = resp.state switch (state) { case 0: { this.showMsg("RTCIceConnectionStateNew") } break; case 1: { this.showMsg("RTCIceConnectionStateChecking") } break; case 2: { // 这步没有 this.showMsg("RTCIceConnectionStateConnected") } break; case 3: { this.showMsg("RTCIceConnectionStateCompleted") } break; case 4: { this.showMsg("RTCIceConnectionStateFailed") } break; case 5: { // 通讯被断开,一般是对方掉线或者STUN/TURN 服务器问题:如果 ICE 服务器配置不当,或者 STUN/TURN 服务器不可用,可能会导致连接失败。确保你的 STUN/TURN 服务器正常工作并且可达。 this.showMsg("RTCIceConnectionStateDisconnected") let userId = resp.userId if (userId) { this.userLeave(userId) } } break; case 6: { this.showMsg(" RTCIceConnectionStateClosed") let userId = resp.userId if (userId) { this.userLeave(userId) } } break; case 7: { this.showMsg(" RTCIceConnectionStateCount") } break; default: break; } } break; // 收到其他用户的音频或视频流 case "onAddStream": { let userId = resp.userId this.showMsg("onAddStream:" + JSON.stringify(resp)) if (userId) { let stream = resp.stream if (stream) { // 如果有视频流,则显示其他用户的视频 let videoTracks = stream.videoTracks if (videoTracks) { if (videoTracks.length > 0) { let exist = this.existUser(userId) if (!exist) { console.log("显示远程视频流:" + userId) this.otherPersons.push({ userId: userId }) } } } } } } break; case "onRemoveStream": { } break; default: break; } }) ``` - 初始化本地视频 ```javascript // 初始化视频 webRTC.initVideoTrack({ trackId: "video0", isScreencast: false // 仅对Android生效 }) ``` - 初始化本地音频 ```javascript // 初始化音频 webRTC.initAudioTrack({ trackId: "audio0" }) ``` - 配置音频,仅支持iOS ```javascript webRTC.configureAudioSession({ category: "playAndRecord", mode: "voiceChat" }) ``` - 开启相机抓流/切换摄像头 ```javascript // 开始本地抓流 webRTC.startVideoCapture({ isFront: this.isFront, width: 1280, // width仅支持Android height: 720, // height仅支持Android fps: 30 }) ``` - 暂停抓流 ``` webRTC.stopVideoCapture() ``` - 创建连接 iceServers支持类型: 1. 第一种 ``` { urls: ["xxx"] } ``` 2. 第二种 ``` { urls: ["xxx"], username: "xx", credential: "xx" } ``` ```javascript let iceServers = [{ urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302", "stun:stun3.l.google.com:19302", "stun:stun4.l.google.com:19302" ] }] let params = {} params.userId = userId // params.iceServers = iceServers // params.sdpSemantics = 1 // 0: RTCSdpSemanticsPlanB 1:RTCSdpSemanticsUnifiedPlan // params.continualGatheringPolicy = // 1 // 0: RTCContinualGatheringPolicyGatherOnce 1: RTCContinualGatheringPolicyGatherContinually // params.constraints = { // mandatory: {}, // optional: { // DtlsSrtpKeyAgreement: "true" // } // } userId = webRTC.createPeerConnection(params) ``` - 将本地视频加入连接 ```javascript let videoResp = webRTC.addVideoTrack({ userId: userId, streamIds: ["video0"] }) let videoFlag = videoResp.flag if (!videoFlag) { this.showMsg("添加本地视频出错:" + JSON.stringify(videoFlag)) } ``` - 将本地音频加入连接 ```javascript let audioResp = webRTC.addAudioTrack({ userId: userId, streamIds: ["audio0"] }) let audioFlag = audioResp.flag if (!audioFlag) { this.showMsg("添加本地音频出错:" + JSON.stringify(videoFlag)) } ``` - 创建offer ```javascript webRTC.createOffer({ userId: userId, setLocalDescription: false }, (resp) => { let flag = resp.flag if (flag) { let sessionDescription = resp.sessionDescription let type = sessionDescription.type let sdp = sessionDescription.sdp } } ) ``` - 设置本地LocalDescription ```javascript webRTC.setLocalDescription({ userId: userId, type: type, // 支持offer、pranswer、answer sdp: sdp }, (localDescResp) => { let localFlag = localDescResp.flag if (localFlag) { } } ) ``` - 设置远程RemoteDescription ```javascript webRTC.setRemoteDescription({ userId: userId, type: type, // 支持offer、pranswer、answer sdp: sdp }, (resp) => { let flag = resp.flag if (flag) { } } ) ``` - 创建answer ```javascript webRTC.createAnswer({ userId: userId, setLocalDescription: false }, (answerResp) => { let flag = answerResp.flag if (flag) { // console.log("createAnswer result:" + JSON.stringify()) let sessionDescription = answerResp.sessionDescription let type = sessionDescription.type let sdp = sessionDescription.sdp } } ) ``` - 添加候选人IceCandidate,一般调用offer或answer时会生成多次IceCandidate,可以都发送给对方,对方设置多次 ```javascript webRTC.addIceCandidate({ userId: userId, sdpMid: sdpMid, sdpMLineIndex: sdpMLineIndex, sdp: sdp }, (resp) => { let flag = resp.flag if (!flag) { this.showMsg("addIceCandidate error:" + JSON.stringify(resp)) } }) ``` - 销毁某个用户的连接 ```javascript webRTC.destroyPeerConnection({ userId: userId }) ``` - 销毁所有的连接 ``` webRTC.destroyAllPeerConnection() ``` ### UI组件 使用wrs-uts-webrtc-view组件的页面要用nvue ```vue ``` - 渲染本地视频 ```javascript // 渲染本地视频界面 this.$refs.localView.renderLocalVideo() ``` - 渲染其他用户视频 目前有2种方式: 1. 调用接口,常用于2人通话 ``` this.$refs.remoteView.renderRemoteVideo(userId) ``` 2. 绑定userId属性,常用于多人通话 ``` :userId="userId" ```