在上一篇中,介绍了局域网内的单项通信。这次我们再进一步,进行一个局域网内的1v1视频通话,可以相互看到对方并进行语音交流
效果
image.png
效果如上图
- 左侧是chrome浏览器
○ 页面中左侧Video是自己的画面,使用的是本机的摄像头。
○ 页面中右侧Video是另一个网页(Safari)的画面 - 右侧是safari浏览器
○ 页面中左侧Video是自己的画面,使用的是safari自带的虚拟摄像头。
○ 页面中右侧Video是另一个网页(Chrome)的画面
流程
image.png
代码
可以通过gitee直接获取代码
服务端
var ws = require("nodejs-websocket");
var offer_client = null;
var answer_client = null;
function checkStart() {
if (offer_client && answer_client) {
offer_client.send(JSON.stringify({
type: "create_offer"
}))
}
}
ws.createServer(function (conn) {
// 收到websocket连接
conn.on("text", function (str) {
let obj = JSON.parse(str);
console.log("recv:", obj);
if ("connect" === obj.type) {
if (!offer_client) {
offer_client = conn;
conn.send(JSON.stringify({
type: "connect",
code: 200,
message: 'connect success'
}))
checkStart();
} else if (!answer_client) {
answer_client = conn;
conn.send(JSON.stringify({
type: "connect",
code: 200,
message: 'connect success'
}))
checkStart();
} else {
conn.send(JSON.stringify({
type: "connect",
code: -1,
message: 'connect failed'
}))
conn.close();
}
} else if ("offer" === obj.type) {
answer_client.send(str);
} else if ("answer" === obj.type) {
offer_client.send(str);
} else if ("offer_ice" === obj.type) {
answer_client.send(str);
} else if ("answer_ice" === obj.type) {
offer_client.send(str);
}
})
conn.on("error", function (event) {
});
conn.on("close", function (code, reason) {
if (conn === offer_client) {
console.log("remove offer_client")
offer_client = null;
} else if (conn === answer_client) {
console.log("remove answer_client")
answer_client = null;
}
})
}).listen(9000);
网页
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>1v1</title>
</head>
<body>
<video id="localVideo" style="width: 320px; height: 240px;" autoplay muted></video>
<video id="remoteVideo" style="width: 320px; height: 240px;" autoplay muted></video>
<button id="startBtn" disabled>开始连接</button>
<script>
// 获取摄像头返回的MediaStream
let localStream = null;
// 显示本地画面的VideoElement
let localVideo = document.getElementById("localVideo");
let remoteVideo = document.getElementById("remoteVideo");
let iceType;
let ws;
// 推流用的MediaStream
let pc = new RTCPeerConnection();
pc.addEventListener('icecandidate', (event) => {
if (event.candidate) {
ws.send(JSON.stringify({
type: iceType,
candidate: event.candidate
}));
}
})
pc.addEventListener("track", (event) => {
remoteVideo.srcObject = event.streams[0];
})
// 建立连接按钮
let startBtn = document.getElementById("startBtn");
startBtn.addEventListener('click', () => {
ws = new WebSocket('ws://127.0.0.1:9000');
// websocket 连接成功消息
ws.addEventListener('open', () => {
// 向服务端发送连接消息
ws.send(JSON.stringify({
type: "connect"
}))
})
// 收到服务端消息
ws.addEventListener('message', (event) => {
let msg = JSON.parse(event.data);
switch (msg.type) {
case "connect":
if (200 === msg.code) {
console.log("连接成功,等待其他用户");
startBtn.disabled = true;
} else {
console.log("连接失败,已经满员")
}
break;
case "create_offer":
sendOffer();
break;
case "offer":
recvOffer(msg);
break;
case "answer":
recvAnswer(msg);
break;
case "offer_ice":
case "answer_ice":
pc.addIceCandidate(msg.candidate);
break;
default:
break;
}
})
ws.addEventListener('close', () => {
console.log("websocket 连接断开")
startBtn.disabled = false;
})
})
/**
* 获取摄像头
*/
function getDevice() {
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then((mediaStream) => {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
resolve(mediaStream);
}).catch((err) => {
reject(err);
})
})
}
function sendOffer() {
iceType = "offer_ice";
pc.addTrack(localStream.getVideoTracks()[0], localStream);
pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }).then((offer) => {
pc.setLocalDescription(offer).then(() => {
ws.send(JSON.stringify(offer));
})
})
}
function recvOffer(offer) {
iceType = "answer_ice";
pc.addTrack(localStream.getVideoTracks()[0], localStream);
pc.setRemoteDescription(offer).then(() => {
pc.createAnswer().then((answer) => {
pc.setLocalDescription(answer).then(() => {
ws.send(JSON.stringify(answer));
})
})
})
}
function recvAnswer(answer) {
pc.setRemoteDescription(answer);
}
getDevice().then((mediastream) => {
// 获取摄像头成功
startBtn.disabled = false;
}).catch((err) => {
console.error("获取摄像头失败")
})
</script>
</body>
</html>
其他
如果你也是专注前端多媒体或者对前端多媒体感兴趣,可以搜索微信公众号"前端多媒体"