Master WebRTC: Build P2P Video Calls with Vue, Node.js, and TURN
This tutorial explains WebRTC fundamentals, signaling via WebSocket, NAT traversal with STUN/TURN, peer‑to‑peer connection flow, media capture, screen sharing, recording, and deployment tips, providing complete Vue and Node.js code examples for building robust 1‑to‑1 and multi‑user video applications.
Prerequisites
WebRTC (Web Real‑Time Communication) is a W3C standard API that enables browsers to perform real‑time audio, video, and screen sharing. It functions both as an API and a protocol, and requires a signaling channel—commonly implemented with WebSocket—to exchange offers, answers, and ICE candidates.
Key Concepts
WebRTC uses peer‑to‑peer (P2P) connections, meaning media streams flow directly between endpoints without server relay, although a TURN server may be used as a fallback when NAT traversal fails. NAT traversal techniques such as STUN (hole punching) and TURN (relay) are essential for establishing connections across different networks.
Connection Workflow
Both peers first open a WebSocket connection to a signaling server. They exchange SDP offers/answers and ICE candidates to negotiate media capabilities and network paths. The process includes creating an
RTCPeerConnection, adding local streams, handling
onicecandidate, and rendering remote streams.
<code>A and B connect via WebSocket, join the same channel, and can receive each other's messages and signaling.
A creates RTCPeerConnection, adds local stream, creates an offer (SDP), sets local description, and sends the offer through the signaling server.
B receives the offer, creates its own RTCPeerConnection, adds its local stream, sets remote description, creates an answer, sets local description, and sends the answer back.
Both peers exchange ICE candidates (STUN/TURN) to establish the optimal path, then render each other's media streams.</code>Media Negotiation Completion
<code>STUN/TURN servers respond with ICE candidates containing public IP and port.
A and B add each other's candidates via addIceCandidate.
After candidate exchange, the best P2P path is selected.
If NAT traversal fails, TURN relays the media streams.
Both peers then play the received audio/video streams.</code>Deep Dive
<code>Local LAN: No TURN needed; direct P2P works.
Online: STUN may succeed (≈70% abroad, <50% China). If it fails, TURN relays media.
Reverse connections can also establish P2P when one side has a public address.</code>Implementation Details
Signaling with Socket.IO
<code>async connectSocket({ commit, state, dispatch }) {
// LAN
let socket = io.connect("http://172.28.74.16:3004");
// Online
// let socket = io.connect("https://www.codeting.top:3004");
socket.on("connect", () => {
commit(types.SET_SOCKET, socket);
dispatch("handleChatData", state.friends);
});
socket.on("friendMessage", (res) => {
if (res) {
commit(types.SET_MESSAGE, res);
} else {
console.log("有问题");
}
});
// ... other event listeners for apply, reply, ICE, offer, answer
}
</code> <code>io.on("connect", function (socket) {
socket.on('friendMessage', async function (res) {
const roomId = res.userId > res.friendId ? res.userId + res.friendId : res.friendId + res.userId;
io.to(roomId).emit('friendMessage', res);
});
socket.on('apply', async function (res) {
io.to(res.roomId).emit('apply', res);
});
// ... other handlers for reply, ICE, offer, answer
});
</code>Media Capture
<code>const constraints = {
audio: { noiseSuppression: true, echoCancellation: true },
video: true,
};
this.localstream = await navigator.mediaDevices.getUserMedia(constraints);
let video = document.querySelector("#rtcA");
video.srcObject = this.localstream;
</code>For audio‑only, set
video: false. For screen sharing, replace
getUserMediawith
getDisplayMedia.
Recording and Screenshot
Use
canvas.getContext('2d').drawImageto capture a frame, and
MediaRecorderto record audio/video streams.
<code>let picture = document.querySelector("#picture");
let rtcA = document.querySelector("#rtcA");
picture.getContext("2d").drawImage(rtcA, 0, 0, 200, 120);
</code> <code>let options = { mimeType: "video/webm;codecs=vp8" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not supported`);
}
this.mediaRecorder = new MediaRecorder(window.streamFriend, options);
this.mediaRecorder.ondataavailable = function (e) {
if (e && e.data && e.data.size > 0) {
that.bufferFriend.push(e.data);
}
};
this.mediaRecorder.start(10);
</code>Deployment Considerations
Production requires a domain name and HTTPS for both the web page and WebSocket (WSS). NAT traversal in the wild depends on a TURN server (e.g., coturn) configured with proper firewall rules.
Limitations and Optimizations
Common issues include environmental noise, echo, and network jitter. Enabling
noiseSuppressionand
echoCancellationin the audio constraints mitigates some problems; further improvements rely on advanced algorithms, better hardware, and network quality.
Advanced Topics
Video effects can be added via canvas filters, though they do not modify the underlying media stream. For true stream‑level effects, processing must occur before the stream is sent, which is limited by JavaScript performance.
Multi‑User Video
For n participants, each client creates n‑1
RTCPeerConnectioninstances. Mesh topology (full peer‑to‑peer) is simple but scales poorly; alternatives include MCU (media mixing) and SFU (selective forwarding) for larger rooms.
Conclusion
The article provides a step‑by‑step guide to understanding, building, and deploying a WebRTC‑based 1‑to‑1 video chat with Vue and Node.js, covering signaling, NAT traversal, media handling, recording, and scaling considerations, encouraging further exploration of quality‑of‑service techniques.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.