- 需要准备好摄像头,由于本电脑没有摄像头,这里使用虚拟摄像头(VCam)来代替真实摄像头
- 本地台式机使用VCam虚拟摄像头来模拟视频流
- 远程笔记本访问同一链接,从而进行通信
通信效果展示
代码提取- 链接: https://pan.baidu.com/s/13BTXMQWJcPZqihgfN3jgZA
- 提取码: kyfu
- 该文章代码是在 WebRTC本地实现 - Vue与Socket IO通信项目搭建与测试(https)基础上进行更改操作
- 原文章修改的代码内容如下【可查看本文章: 2 - 流程代码】
- 删除原来的发送信息的按钮和test方法
- 增加方法 sendMessage
// 发送消息给 Socket IO 服务器
sendMessage(msg) {
this.$socket.emit('message', sendMessage)
}
代码编写流程
- 客户Peer - A 和 Peer - B进行通信,有可能是A先发起 或者 B先发起,所以代码发起端跟接收端都得写
- Connect Signal Server:2端 连接到信令服务器,也就是之前与Socket IO进行通信过程
- 流程图片
1 - Connect Signal Server
- 流程说明 A用户连接到Sinal Server(也就是Socket IO 服务器) B用户连接到Sinal Server(也就是Socket IO 服务器)
- getUserMedia:2端 获取本地媒体流
- 流程图片
2 - getUserMedia
-
流程说明 A用户本地的媒体流,并设置localStream B用户本地的媒体流,并设置localStream
-
流程代码(this.$lib.logInfo等可直接替换为console.log)
video {
max-width: 100%;
width: 320px;
border: 1px solid;
}
本地视频流
远程视频流
export default {
data() {
return {
// 本地媒体流
localStream: null,
// 是否为创始者
isInitiator: false
}
},
mounted() {
// Socket IO 存在 && 未连接到服务器
if (this.$socket != null && !this.$socket.connected) {
this.$socket.connect();
}
window.onload = () => {
// 获取本地用户媒体流
this.getUserMedia();
};
},
methods: {
// 获取本地用户媒体流
getUserMedia() {
// navigator.mediaDevices.getUserMedia 3个工作域:http://localhost/ 、HTTPS://或file://,否则会是undefined
navigator.mediaDevices
.getUserMedia({
audio: false,
video: true
})
.then((stream) => {
this.$lib.logInfo("获取本地用户媒体流");
// 赋值本地媒体流
this.localStream = stream;
// 在浏览器中进行展示
this.$refs.localVideo.srcObject = stream;
})
.catch((err) => {
this.$lib.logError('获取navigator.mediaDevices.getUserMedia()失败,错误原因如下:');
this.$lib.logError(err);
});
},
// 发送消息给 Socket IO 服务器
sendMessage(msg) {
this.$socket.emit('message', msg)
}
},
sockets: {
connect() {
this.$lib.logSuccess("连接socket io服务器成功");
},
message(msg) {
this.$lib.logInfo("接收到服务端的消息:" + msg);
}
},
}
- 效果展示(如果 VCam播放视频,这里显示的是视频的内容)
2 - 效果展示
- 1端 create,1端 join:
- 流程图片
image.png
-
流程说明 A为创建者,B为加入者(2个扮演的角色可调换) A用户创建房间(自定义房间名称:room_webrtc) 同时Signal Server(Socket IO服务器)通知A用户,你是创建者 B用户加入到room_webrtc 同时Signal Server(Socket IO服务器)通知A用户有新的用户加入,通知B用户加入成功
-
流程代码
data() return 中增加变量
// 房间名称
roomName: 'room_webrtc',
// 通道是否准备就绪
isChannelReady: false
methods:中增加方法:创建加入房间
// 试图创建或加入Socket IO的房间
createOrJoinRoom() {
this.$lib.logInfo('试图创建或加入Socket IO 的房间:' + this.roomName);
this.$socket.emit('create or join', this.roomName);
}
sockets:增加监听多个方法
通知用户(A)为创建者(created) 通知房间其他用户(A)有新伙伴(B)准备加入(join) 通知用户(B)加入成功(joined) 通知房间其他用户(A)新伙伴(B)加入成功(ready) 房间满了(full)
created(room) {
this.$lib.logInfo('您是房间:' + room + ' 的创建者');
// 赋值创建者标志
this.isInitiator = true;
},
join(room) {
this.$lib.logInfo('房间:' + room + ' 有新的用户准备加入');
},
joined(room) {
this.$lib.logInfo('房间:' + room + ' 已经加入成功');
// 加入者,设定通道准备就绪
this.isChannelReady = true;
},
ready(){
this.$lib.logInfo('房间:' + room + ' 新的用户已经加入成功');
// 创建者,设定通道准备就绪
this.isChannelReady = true;
},
full(){
this.$lib.logInfo('房间:' + room + ' 已经满员了');
}
- create 端(创建者端,发送offer)
- 流程图片
创建 RTCPeerConnection
-
流程说明 当远程伙伴加入成功后,创建端需开始创建 RTCPeerConnection(本地端机器与远端机器的一条对等连接) 创建Offer 并 设置本地描述(SetLocalDescription)
-
流程代码 data() return 中增加变量
// RTCPeerConnection,本地端机器与远端机器的一条对等连接
pc: null,
// coturn配置供 RTCPeerConnection 使用,(如果没有coturn,可参照demo的step-05中的代码获取)
pcConfiguration: {
iceServers: [{
urls: 'turn:wzeros.cn',
username: 'wzeros',
credential: '123456'
}]
},
// 远程媒体流
remoteStream: null,
methods:中增加方法:创建RTCPeerConnection
// 创建本地端机器与远端机器的一条对等连接
createPeerConnection() {
try {
this.$lib.logInfo('创建 RTCPeerConnection');
this.pc = new RTCPeerConnection(this.pcConfiguration);
this.pc.onicecandidate = this.handleIceCandidate;
this.pc.onaddstream = this.handleRemoteStreamAdded;
this.pc.onremovestream = this.handleRemoteStreamRemoved;
this.$lib.logInfo('增加本地媒体流');
this.pc.addStream(this.localStream);
} catch (e) {
this.$lib.logError('创建 RTCPeerConnection 出错,错误原因如下');
this.$lib.logObj(e.message);
return;
}
},
// 事件触发器
// 只要本地代理ICE 需要通过信令服务器传递信息给其他对等端时就会触发
// 功能说明地址:https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/onicecandidate
handleIceCandidate(event) {
this.$lib.logInfo('触发 icecandidate 事件,事件内容如下');
this.$lib.logInfo(event);
if (event.candidate) {
this.sendMessage({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
this.$lib.logInfo('End of candidates');
}
},
// 处理远程流添加
handleRemoteStreamAdded(event) {
this.$lib.logInfo('处理远程媒体流添加');
// 赋值远程媒体流
this.remoteStream = event.stream;
// 展示远程媒体流
this.$refs.remoteVideo.srcObject = this.remoteStream;
},
// 处理远程流撤掉
handleRemoteStreamRemoved(event) {
this.$lib.logInfo('Remote stream removed. Event: ', event);
},
// 创建 offer
createOffer() {
this.$lib.logInfo('发送offer给远程的伙伴');
this.pc.createOffer((sessionDescription) => {
this.$lib.logInfo('设置本地描述,描述内容如下:');
this.$lib.logInfo(sessionDescription);
this.pc.setLocalDescription(sessionDescription);
// 通过Signal Server 发送(offer) sessionDescription 到远程伙伴
this.sendMessage(sessionDescription);
}, (event) => {
this.$lib.logError('createOffer()出错,错误原因如下:');
this.$lib.logError(event);
});
}
sockets:ready中增加调用createPeerConnection
ready(room) {
this.$lib.logInfo('房间:' + room + ' 新的用户已经加入成功');
// 创建者 | 加入者,设定通道准备就绪
this.isChannelReady = true;
// 创建者
if (this.isInitiator) {
// 创建本地端机器与远端机器的一条对等连接
this.createPeerConnection();
// 创建 offer
this.createOffer();
}
}
- join端(加入者端,接收offer)
- 流程图片
接收offer
-
流程说明 创建者端通过Signal Server(Socket IO服务器)发送出了offer 加入者端在监听(message)消息后,需要初始化RTCPeerConnection等操作 再发送一个回应信息(answer)给create端 并 设置本地描述(SetLocalDescription)
-
流程代码
methods:中增加方法:创建回复
// 创建 answer
createAnswer() {
this.$lib.logInfo('发送answer给远程的创建者');
this.pc.createAnswer().then((sessionDescription) => {
this.$lib.logInfo('设置会话描述,描述内容如下:');
this.$lib.logObj(sessionDescription);
this.pc.setLocalDescription(sessionDescription);
// 通过Signal Server 发送(answer) sessionDescription 到远程的创建者
this.sendMessage(sessionDescription);
}, (error) => {
this.$lib.logError('创建会话描述出错,错误原因如下:');
this.$lib.logObj(error);
});
}
sockets:修改message监听方法
message(msg) {
this.$lib.logInfo('接收到服务端的消息,消息如下:');
this.$lib.logObj(msg);
if (msg.type) {
switch (msg.type) {
// 加入者接收到创建者的offer
case 'offer':
// 创建本地端机器与远端机器的一条对等连接
this.createPeerConnection();
// 设置远程回话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
// 给创建者回复
this.createAnswer();
break;
default:
break;
}
}
}
- create端(创建者端,接收answer)
- 流程图片
接收answer
-
流程说明 加入者接收到offer后,返回answer,创建者端接收到answer后设定远程会话描述 设置完会话描述后,此时在创建RTCPeerConnection使用了coturn服务配置的话,将会触发onicecandidate事件。如果建立连接成功后将会触发onaddstream(添加远程媒体流) 在onicecandidate事件中做了一件事情发送type:'candidate'的内容到Signal Server服务器,继而推送到2端
-
流程代码
sockets:修改message监听方法
message(msg) {
this.$lib.logInfo('接收到服务端的消息,消息如下:');
this.$lib.logObj(msg);
if (msg.type) {
switch (msg.type) {
case 'offer': // 加入者接收到创建者的offer
// 创建本地端机器与远端机器的一条对等连接
this.createPeerConnection();
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
// 给创建者回复
this.createAnswer();
break;
case 'answer': // 创建者接收到加入者的answer
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
break;
default:
break;
}
}
}
- 2端candidate处理
- 流程图片
candidate处理
-
流程说明 双方只需要做的一件事情就是对RTCPeerConnection添加一个Ice代理后,之后自动触发添加远程媒体流,双方就能够进行通信了
-
流程代码
sockets:修改message监听方法
message(msg) {
this.$lib.logInfo('接收到服务端的消息,消息如下:');
this.$lib.logObj(msg);
if (msg.type) {
switch (msg.type) {
case 'offer': // 加入者接收到创建者的offer
// 创建本地端机器与远端机器的一条对等连接
this.createPeerConnection();
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
// 给创建者回复
this.createAnswer();
break;
case 'answer': // 创建者接收到加入者的answer
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
break;
case 'candidate': // candidate处理
this.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: msg.label,
candidate: msg.candidate
}));
break;
default:
break;
}
}
}
- 2端退出通信处理
- 流程图片
退出通信处理
-
流程说明 只要有一方退出系统,发送离开消息到Signal Server服务器,信令服务器通知双方结束通信 关闭RTCPeerConnection,暂停本地视频流和远程视频流展示
-
流程代码 methods:中增加方法:离开通信
leave() {
if(this.remoteStream !== null) {
this.$refs.remoteVideo.srcObject = null;
this.remoteStream = null;
}
if(this.localStream !== null) {
this.$refs.localVideo.srcObject = null;
this.localStream = null;
}
if (this.pc !== null) {
this.pc.close();
this.pc = null;
}
}
export default :中增加beforeDestroy监听
beforeDestroy() {
this.sendMessage('leave');
}
sockets:修改message监听方法
message(msg) {
this.$lib.logInfo('接收到服务端的消息,消息如下:');
this.$lib.logObj(msg);
if (msg.type) {
switch (msg.type) {
case 'offer': // 加入者接收到创建者的offer
// 创建本地端机器与远端机器的一条对等连接
this.createPeerConnection();
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
// 给创建者回复
this.createAnswer();
break;
case 'answer': // 创建者接收到加入者的answer
// 设置远程会话描述
this.pc.setRemoteDescription(new RTCSessionDescription(msg));
break;
case 'candidate': // candidate处理
this.pc.addIceCandidate(new RTCIceCandidate({
sdpMLineIndex: msg.label,
candidate: msg.candidate
}));
break;
case 'leave': // 离开通信
this.leave();
break;
default:
break;
}
}
}