WebRTC实现音视频通话
最近闲暇时间(摸鱼时间很难熬)都在搞这个仿微信的项目,于是乎今天就接了个音视频通话。
在线体验:https://chat.juenfy.cn
已存在用户:13006789001~13006789010 密码都是123456
前端项目
github:https://github.com/Juenfy/cover-wechat-client
后端项目
github:https://github.com/Juenfy/cover-wechat-api
客户端采用WebRTC技术(推流),通讯用websocket。
WebRTC像是一个面试过程:
第一步:拨打电话
发起方(拨打电话者)点击拨打电话时,获取本地媒体流并推流给接收方同时捕获接收方推过来的流,捕获到后把流设置到dom上,监听ICE候选确保能点对连接,生成offer,通过websocket告知接收方并拉起等待接听界面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| stream.value = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
peerConnection.value = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] });
stream.value.getTracks().forEach((track) => { peerConnection.value.addTrack(track, stream.value); });
peerConnection.value.ontrack = (event) => { remoteStream.value = event.streams[0]; if (callType.value === TypeVideo) { remoteVideo.value.srcObject = remoteStream.value; } else { remoteAudio.value.srcObject = remoteStream.value; } };
peerConnection.value.onicecandidate = (event) => { if (event.candidate) { ws.send(event.candidate); } };
const offer = await peerConnection.value.createOffer(); await peerConnection.value.setLocalDescription(offer);
ws.send(offer);
showCall.value = true;
callStatus.value = 'wating';
|
第二步:接收电话
接收方收到offer后,先拉起来电界面,再选择接听或者挂断。
1)拉起来电接听界面
1 2 3 4 5 6
| showCall.vue = true;
callStatus.value = 'coming';
....
|
2)挂断
告诉发起方我挂断了,发起方就把RTC关掉、停止推流,dom置空就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| showCall.value = false; callStatus.value = 'closing'; ws.send('reject');
if (peerConnection.value) { peerConnection.value.close(); peerConnection.value = null; } if (stream.value) { const tracks = stream.value.getTracks(); tracks.forEach((track) => track.stop()); } if (localVideo.value) localVideo.value.srcObject = null; if (remoteVideo.value) remoteVideo.value.srcObject = null; if (remoteAudio.value) remoteAudio.value.srcObject = null; showCall.value = false; callStatus.value = 'closing';
|
2)或者接听
操作跟拨打流程差不多,需要设置远端SDP(发起方的offer),添加ICE候选(发起方的ice,这里需要注意的是只有远端SDP初始化完毕状态下才能设置ice)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ...同发起方
...同发起方
...同发起方
...同发起方
...同发起方
await peerConnection.value.setRemoteDescription(new RTCSessionDescription(caller.value.offer));
iceCandidateQueue.value.forEach(async (candidate) => { await peerConnection.value.addIceCandidate(candidate); }); iceCandidateQueue.value = [];
const answer = await peerConnection.value.createAnswer(); await peerConnection.value.setLocalDescription(answer);
ws.send(answer);
callStatus.value = 'calling';
|
关于ice的处理,就是远端SDP初始化完毕状态可以直接设置,未初始化完毕就存到iceCandidateQueue队列备用
1 2 3 4 5 6 7 8 9
| const handleNewICECandidate = async (candidate) => { const iceCandidate = new RTCIceCandidate(candidate); if (peerConnection.value?.signalingState === 'have-remote-offer' || peerConnection.value?.signalingState === 'stable') { peerConnection.value.addIceCandidate(iceCandidate); } else { iceCandidateQueue.value.push(iceCandidate); } };
|
3)设置远端SDP、ICE
发起方收到接收方的答复(接收方接听了),设置远端SDP(接收方的answer),设置ICE(接收方的ice)
1 2 3 4 5 6 7 8 9 10
| await peerConnection.value.setRemoteDescription(new RTCSessionDescription(caller.value.answer));
iceCandidateQueue.value.forEach(async (candidate) => { await peerConnection.value.addIceCandidate(candidate); }); iceCandidateQueue.value = [];
callStatus.value = 'calling';
|
这就是WebRTC视频通话的关键代码跟流程!
我的项目实现效果图: