WebRTC(Web 实时通信)是一项强大的技术,可直接在 Web 浏览器和移动应用程序之间实现实时音频、视频和数据共享。无论您是在构建视频会议应用程序、直播平台还是交互式 Web 应用程序,WebRTC 都已成为通信领域的变革者。
在本博客中,我们将从头开始学习 WebRTC,探索其核心概念,了解其架构,并深入研究一些实际的编码示例。最后,您将掌握构建自己的 WebRTC 应用程序的基础知识。
WebRTC(Web 实时通信)是一组开源技术,可实现 Web 浏览器和移动应用程序之间通过互联网直接进行实时通信。它允许点对点音频、视频和数据共享,而无需任何插件或其他软件。WebRTC 广泛用于构建视频会议、语音通话、直播、在线游戏等应用程序。WebRTC 点对点连接
WebRTC 受到主流网络浏览器的支持,包括 Google Chrome、Mozilla Firefox、Safari 和 Microsoft Edge。WebRTC 之所以受到广泛采用,是因为其开源特性、易于实现,并且无需第三方插件即可构建无缝实时通信应用程序。
要使用 WebRTC(Web 实时通信),您需要熟悉促进 Web 浏览器之间实时通信所需的 API 和库。WebRTC 可直接在 Web 应用程序内实现点对点音频、视频和数据流传输,使其成为构建视频会议、语音通话和其他实时通信功能的理想选择。以下是您应该了解的基本组件:
该API可以访问用户的媒体设备(摄像头和麦克风),并提供可与RTCPeerConnection一起使用的MediaStream对象。
该 API 是 WebRTC 的核心,负责建立和管理浏览器之间的点对点连接。它处理 ICE(交互式连接建立)协商、NAT 遍历和媒体流传输。
此 API 提供无需服务器的点对点数据通信功能。它可用于在对等点之间发送任意数据。
WebRTC 需要在建立直接连接之前通过信令在对等端之间交换连接详细信息。此过程不是由 WebRTC 标准定义的,需要单独的信令机制,例如 WebSocket 或服务器端应用程序。
ICE, STUN and TURN
ICE(交互式连接建立)、STUN(NAT 会话遍历实用程序)和 TURN(使用中继绕过 NAT 进行遍历)是 WebRTC 框架的重要组成部分,可实现互联网上的实时通信。它们用于在客户端之间建立点对点连接,即使它们位于防火墙或网络地址转换 (NAT) 设备后面。
ICE(交互式连接建立) ICE 是一种结合 STUN 和 TURN 服务器的技术,用于发现并建立 WebRTC 客户端之间的最佳连接路径,即使在具有挑战性的网络环境中也能实现实时通信。
STUN(NAT 的会话遍历实用程序) STUN 是一种用于发现客户端所处公共 IP 地址和端口的协议。ICE 是一种结合 STUN 和 TURN 服务器的技术,用于发现并建立 WebRTC 客户端之间的最佳连接路径,即使在具有挑战性的网络环境中也能实现实时通信。
TURN(使用中继绕过 NAT)当由于网络配置受限而无法建立直接对等连接时,TURN 服务器可充当中介。它们在客户端之间中继媒体流,确保可靠的通信。
首先,确保你的机器上安装了 Node.js。然后,打开终端或命令提示符并运行以下命令来创建一个新的 React 应用程序:
npx create-react-app simple-webrtc
接下来,导航到项目目录并启动 Web 服务器
cd simple-webrtc
npm start
然后,在代码编辑器中打开该项目。您会在文件夹中找到主要代码文件src。您可以编辑App.js以修改网页内容。
import React from 'react';
import './App.css';
function App() {
return (
<div className="App">
<h1>Welcome to My Simple Web Page</h1>
<p>This is a basic web page built with React.</p>
</div>
);
}
export default App;
在我的网页示例中,我将使用 Ant Design 作为 UI 库,以使我的生活更轻松。编辑后,我的 React 页面将如下所示:
import React from 'react';
import {Button, Typography, Input} from 'antd';
import '../App.css';
const {Title, Paragraph} = Typography;
const {TextArea} = Input;
function App() {
const renderHelper = () => {
return (
<div className="wrapper">
<Input
placeholder="User ID"
style={{width: 240, marginTop: 16}}
/>
<Input
placeholder="Channel Name"
style={{width: 240, marginTop: 16}}
/>
<Button
style={{width: 240, marginTop: 16}}
type="primary"
>
Call
</Button>
<Button
danger
style={{width: 240, marginTop: 16}}
type="primary"
>
Hangup
</Button>
</div>
);
};
const renderTextarea = () => {
return (
<div className="wrapper">
<TextArea
style={{width: 240, marginTop: 16}}
placeholder='Send message'
/>
<TextArea
style={{width: 240, marginTop: 16}}
placeholder='Receive message'
disabled
/>
<Button
style={{width: 240, marginTop: 16}}
type="primary"
disabled={sendButtonDisabled}
>
Send Message
</Button>
</div>
);
};
return (
<div className="App">
<div className="App-header">
<Title>WebRTC</Title>
<Paragraph>This is a simple demo app that demonstrates how to build a WebRTC application from scratch, including a signaling server. It serves as a step-by-step guide to help you understand the process of implementing WebRTC in your own projects.</Paragraph>
<div className='wrapper-row' style={{justifyContent: 'space-evenly', width: '50%'}}>
{renderHelper()}
{renderTextarea()}
</div>
<div
className='playerContainer'
id="playerContainer"
>
<video
id="peerPlayer"
autoPlay
style={{width: 640, height: 480}}
/>
<video
id="localPlayer"
autoPlay
style={{width: 640, height: 480}}
/>
</div>
</div>
</div>
);
}
export default App;
现在,我们已经成功为WebRTC创建了一个基本的网页。
let localStream;
const setupDevice = () => {
console.log('setupDevice invoked');
navigator.getUserMedia({ audio: true, video: true }, (stream) => {
// render local stream on DOM
const localPlayer = document.getElementById('localPlayer');
localPlayer.srcObject = stream;
localStream = stream;
}, (error) => {
console.error('getUserMedia error:', error);
});
};
在 WebRTC 中处理媒体流和约束对于控制实时通信期间的音频和视频行为至关重要。您可以在向用户请求媒体时指定约束,例如分辨率、帧速率或特定设备。约束有助于定制媒体捕获以满足特定要求。
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
},
audio: true,
};
navigator.mediaDevices.getUserMedia(constraints)
.then((stream) => {
// Handle the media stream as needed.
})
.catch((error) => {
// Handle the error if constraints cannot be satisfied.
});
const servers;
const pcConstraints = {
'optional': [
{'DtlsSrtpKeyAgreement': true},
],
};
// When user clicks call button, we will create the p2p connection with RTCPeerConnection
const callOnClick = () => {
console.log('callOnClick invoked');
if (localStream.getVideoTracks().length > 0) {
console.log(`Using video device: ${localStream.getVideoTracks()[0].label}`);
}
if (localStream.getAudioTracks().length > 0) {
console.log(`Using audio device: ${localStream.getAudioTracks()[0].label}`);
}
localPeerConnection = new RTCPeerConnection(servers, pcConstraints);
localPeerConnection.onicecandidate = gotLocalIceCandidateOffer;
localPeerConnection.onaddstream = gotRemoteStream;
localPeerConnection.addStream(localStream);
localPeerConnection.createOffer().then(gotLocalDescription);
};
// async function to handle offer sdp
const gotLocalDescription = (offer) => {
console.log('gotLocalDescription invoked:', offer);
localPeerConnection.setLocalDescription(offer);
};
// async function to handle received remote stream
const gotRemoteStream = (event) => {
console.log('gotRemoteStream invoked');
const remotePlayer = document.getElementById('peerPlayer');
remotePlayer.srcObject = event.stream;
};
// async function to handle ice candidates
const gotLocalIceCandidateOffer = (event) => {
console.log('gotLocalIceCandidateOffer invoked', event.candidate, localPeerConnection.localDescription);
// when gathering candidate finished, send complete sdp
if (!event.candidate) {
const offer = localPeerConnection.localDescription;
// send offer sdp to signaling server via websocket
sendWsMessage('send_offer', {
channelName,
userId,
sdp: offer,
});
}
};
处理 WebRTC 中的媒体流和约束对于控制实时通信期间的音频和视频行为至关重要。以下是如何管理媒体流和约束的简要概述:
我们使用gotLocalIceCandidateOffer函数处理 ICE 候选。候选收集完成后,我们通过信令发送完整的 SDP。如果为event.candidate空,则表示 ICE 候选收集已准备就绪。处理 ICE 候选的方法有两种:一种是将 ICE 候选插入 SDP 并将它们一起发送,另一种是通过信令将每个 ICE 候选发送给远程用户。然后远程用户在其本地对等连接中设置它。
在此阶段,我们已经完成了 RTCPeerConnection 的设置并生成了 offer SDP。但是,为了与远程浏览器建立连接,我们需要一个信令服务器来交换 SDP。
信令服务器在 WebRTC 通信中起着至关重要的作用。它促进对等端之间交换会话信息 (SDP),从而允许它们建立直接的对等连接。信令过程包括将本地浏览器生成的 SDP 请求发送到远程浏览器,反之亦然。
信令服务器
一旦信令服务器收到本地浏览器的 SDP 请求,它就会将其转发到远程浏览器。然后远程浏览器生成其 SDP 应答并通过信令服务器将其发送回本地浏览器。
这种 SDP 提供和答复的交换使得两个浏览器能够协商媒体流的参数,例如编解码器、支持的分辨率以及成功进行点对点通信所需的其他设置。
信令服务器不传输实际的媒体流;它仅充当在对等端之间交换 SDP 的信使。一旦 SDP 交换完成,媒体流就会直接在对等端之间传输,从而为实时通信创建直接且安全的连接。
请记住,您可以使用各种技术来实现信令服务器,例如 WebSockets、HTTP 或任何其他合适的通信协议。信令服务器技术的选择取决于您的 WebRTC 应用程序的具体要求。
const debug = require('debug')(`${process.env.APPNAME}:index`);
const app = require('express')();
const server = require('http').Server(app);
const wss = require ('./wss');
const HTTPPORT = 4000;
const WSSPORT = 8090;
// init the websocket server on 8090
wss.init(WSSPORT)
// init the http server on 4000
server.listen(HTTPPORT, () => {
debug(`${process.env.APPNAME} is running on port: ${HTTPPORT}`);
});
NodeJs实现WebSocket
const debug = require('debug')(`${process.env.APPNAME}:wss`);
const WebSocket = require('ws');
let channels = {}
function init (port) {
debug('ws init invoked, port:', port)
const wss = new WebSocket.Server({ port });
wss.on('connection', (socket) => {
debug('A client has connected!');
socket.on('error', debug);
socket.on('message', message => onMessage(wss, socket, message));
socket.on('close', message => onClose(wss, socket, message));
})
}
function send(wsClient, type, body) {
debug('ws send', body);
wsClient.send(JSON.stringify({
type,
body,
}))
}
function clearClient(wss, socket) {
// clear client by channel name and user id
Object.keys(channels).forEach((cname) => {
Object.keys(channels[cname]).forEach((uid) => {
if (channels[cname][uid] === socket) {
delete channels[cname][uid]
}
})
})
}
function onMessage(wss, socket, message) {
debug(`onMessage ${message}`);
const parsedMessage = JSON.parse(message)
const type = parsedMessage.type
const body = parsedMessage.body
const channelName = body.channelName
const userId = body.userId
switch (type) {
case 'join': {
// join channel
if (channels[channelName]) {
channels[channelName][userId] = socket
} else {
channels[channelName] = {}
channels[channelName][userId] = socket
}
const userIds = Object.keys(channels[channelName])
send(socket, 'joined', userIds)
break;
}
case 'quit': {
// quit channel
if (channels[channelName]) {
channels[channelName][userId] = null
const userIds = Object.keys(channels[channelName])
if (userIds.length === 0) {
delete channels[channelName]
}
}
break;
}
case 'send_offer': {
// exchange sdp to peer
const sdp = body.sdp
let userIds = Object.keys(channels[channelName])
userIds.forEach(id => {
if (userId.toString() !== id.toString()) {
const wsClient = channels[channelName][id]
send(wsClient, 'offer_sdp_received', sdp)
}
})
break;
}
case 'send_answer': {
// exchange sdp to peer
const sdp = body.sdp
let userIds = Object.keys(channels[channelName])
userIds.forEach(id => {
if (userId.toString() !== id.toString()) {
const wsClient = channels[channelName][id]
send(wsClient, 'answer_sdp_received', sdp)
}
})
break;
}
case 'send_ice_candidate': {
const candidate = body.candidate
let userIds = Object.keys(channels[channelName])
userIds.forEach(id => {
if (userId.toString() !== id.toString()) {
const wsClient = channels[channelName][id]
send(wsClient, 'ice_candidate_received', candidate)
}
})
}
default:
break;
}
}
function onClose(wss, socket, message) {
debug('onClose', message);
clearClient(wss, socket)
}
React实现WebSocket
import React, {useRef} from 'react';
import {useEffect} from 'react';
const URL_WEB_SOCKET = 'ws://localhost:8090/ws';
function App() {
const ws = useRef(null);
useEffect(() => {
const wsClient = new WebSocket(URL_WEB_SOCKET);
wsClient.onopen = () => {
console.log('ws opened');
ws.current = wsClient;
// setup camera and join channel after ws opened
join();
setupDevice();
};
wsClient.onclose = () => console.log('ws closed');
wsClient.onmessage = (message) => {
console.log('ws message received', message.data);
const parsedMessage = JSON.parse(message.data);
switch (parsedMessage.type) {
case 'joined': {
const body = parsedMessage.body;
console.log('users in this channel', body);
break;
}
case 'offer_sdp_received': {
const offer = parsedMessage.body;
onAnswer(offer);
break;
}
case 'answer_sdp_received': {
gotRemoteDescription(parsedMessage.body);
break;
}
case 'quit': {
break;
}
default:
break;
}
};
return () => {
wsClient.close();
};
}, []);
const sendWsMessage = (type, body) => {
console.log('sendWsMessage invoked', type, body);
ws.current.send(JSON.stringify({
type,
body,
}));
};
}
使用时要谨慎const ws = useRef(null),并考虑为什么不直接使用wsClient = new WebSocket(URL_WEB_SOCKET)。React Hooks 的行为不同,ws每次页面重新渲染时变量都会被重置。为了确保 WebSocket 连接在渲染过程中保持不变,我们可以使用钩子useRef。这类似于在类上使用实例变量,并且与钩子不同,它不受重新渲染的影响useState。
通过使用useRef,我们可以在组件的整个生命周期内保持对 WebSocket 实例的稳定引用。这使我们能够有效地管理 WebSocket 连接,而不会受到渲染更新的影响。请记住 主要useRef用于处理在渲染过程中持续存在的可变值,使其成为管理 React 组件中 WebSocket 连接的理想选择。
有了信令服务器,您的 WebRTC 应用程序将能够建立连接并实现远程对等点之间的无缝音频和视频通信。
现在,我们几乎已经到达了完整 WebRTC 应用程序的最后一部分,当远程用户接到对方的呼叫时,我们需要处理应答逻辑。该过程与之前类似,但这次我们将生成应答 SDP 并通过信令服务器将其返回给呼叫者。
const onAnswer = (offer) => {
console.log('onAnswer invoked');
setCallButtonDisabled(true);
setHangupButtonDisabled(false);
if (localStream.getVideoTracks().length > 0) {
console.log(`Using video device: ${localStream.getVideoTracks()[0].label}`);
}
if (localStream.getAudioTracks().length > 0) {
console.log(`Using audio device: ${localStream.getAudioTracks()[0].label}`);
}
localPeerConnection = new RTCPeerConnection(servers, pcConstraints);
localPeerConnection.onicecandidate = gotLocalIceCandidateAnswer;
localPeerConnection.onaddstream = gotRemoteStream;
localPeerConnection.addStream(localStream);
localPeerConnection.setRemoteDescription(offer);
localPeerConnection.createAnswer().then(gotAnswerDescription);
};
const gotRemoteStream = (event) => {
console.log('gotRemoteStream invoked');
const remotePlayer = document.getElementById('peerPlayer');
remotePlayer.srcObject = event.stream;
};
const gotAnswerDescription = (answer) => {
console.log('gotAnswerDescription invoked:', answer);
localPeerConnection.setLocalDescription(answer);
};
const gotLocalIceCandidateAnswer = (event) => {
console.log('gotLocalIceCandidateAnswer invoked', event.candidate, localPeerConnection.localDescription);
// gathering candidate finished, send complete sdp
if (!event.candidate) {
const answer = localPeerConnection.localDescription;
sendWsMessage('send_answer', {
channelName,
userId,
sdp: answer,
});
}
};
}
最后,我们成功完成了设置 WebRTC 的复杂过程。现在,让我们通过运行npm start并打开两个网页来启动 Web 应用程序——一个用于呼叫者,另一个用于被呼叫者。单击Call呼叫者页面上的按钮,即可开始通过 WebRTC 进行直播。使用 WebRTC 进行直播
以下是使用 WebRTC API 的典型 10 个步骤:
通过全面的端到端图表,您可以完整了解此 WebRTC 应用程序的整个流程。
WebRTC 中的数据通道是一种功能,允许在点对点连接中两个对等端之间以低延迟方式双向通信任意数据。与媒体流(用于音频和视频)不同,数据通道提供了一种在浏览器之间直接交换非媒体数据的方法,使其适用于各种实时应用程序。
实现数据通道涉及在 RTCPeerConnection 中创建数据通道并处理其状态和消息事件以在对等端之间交换数据。数据通道 API 提供 send() 等方法来发送数据,以及 onmessage、onopen、onclose 和 onerror 等事件来处理通信事件。
const createDataChannel = () => {
try {
console.log('localPeerConnection.createDataChannel invoked');
sendChannel = localPeerConnection.createDataChannel('sendDataChannel', {reliable: true});
} catch (error) {
console.error('localPeerConnection.createDataChannel failed', error);
}
sendChannel.onopen = handleSendChannelStateChange;
sendChannel.onClose = handleSendChannelStateChange;
localPeerConnection.ondatachannel = gotReceiveChannel;
};
const sendOnClick = () => {
console.log('sendOnClick invoked', sendMessage);
sendChannel.send(sendMessage);
setSendMessage('');
};
const gotReceiveChannel = (event) => {
console.log('gotReceiveChannel invoked');
receiveChannel = event.channel;
receiveChannel.onmessage = handleMessage;
receiveChannel.onopen = handleReceiveChannelStateChange;
receiveChannel.onclose = handleReceiveChannelStateChange;
};
const handleMessage = (event) => {
console.log('handleMessage invoked', event.data);
setReceiveMessage(event.data);
setSendMessage('');
};
const handleSendChannelStateChange = () => {
const readyState = sendChannel.readyState;
console.log('handleSendChannelStateChange invoked', readyState);
if (readyState === 'open') {
setSendButtonDisabled(false);
} else {
setSendButtonDisabled(true);
}
};
const handleReceiveChannelStateChange = () => {
const readyState = receiveChannel.readyState;
console.log('handleReceiveChannelStateChange invoked', readyState);
};
}
我们已经成功使用 WebRTC 实现了点对点数据通道。为了查看其实际效果,让我们通过运行npm start并打开两个网页来启动 Web 应用程序。在呼叫者的页面上,单击Call按钮以启动对等连接。
连接后,输入Hello, World!!!呼叫者的文本区域并单击Send按钮。通过数据通道发送消息
您将看到该消息在另一端被实时接收,展示了 WebRTC 的无缝数据传输能力。从数据通道接收消息
选择在质量和带宽消耗之间取得平衡的编解码器。WebRTC 支持各种编解码器,例如视频的 VP8、VP9、H.264,以及音频的 Opus、G.711、G.722。选择编解码器时,请考虑目标设备和网络条件。例如,VP8 受到广泛支持并且质量良好,而 H.264 可能更适合某些设备上的硬件加速解码。
WebRTC 使用数据报传输层安全性 (DTLS) 协议来加密媒体流。DTLS 为 UDP 数据传输提供安全加密。在建立对等连接时,WebRTC 使用 DTLS 协商和交换用于加密媒体流的加密密钥。
如果您使用 WebRTC 数据通道交换非媒体数据,请启用数据通道消息加密。数据通道使用 DTLS 上的 SCTP(流控制传输协议)进行安全数据传输。
确保您的信令服务器和媒体服务器(如果使用)支持安全传输协议,例如 HTTPS 和 WSS(WebSocket Secure)。HTTPS 为交换信令数据提供了安全通道,而 WSS 则确保 WebRTC 中使用的 WebSocket 连接的安全通信。
使用getDisplayMedia或navigator.mediaDevices.getDisplayMediaAPI 来捕获用户的屏幕。此 API 允许用户授予与应用程序共享其屏幕的权限。请务必处理用户拒绝或没有所需权限的情况。
WebAssembly 提供了一种在浏览器中直接执行用 C、C++ 和 Rust 等语言编写的代码以及 JavaScript 的方法。凭借接近原生的性能,它可以更高效地执行视频处理、图像识别和加密/解密等计算密集型任务。
通过将性能关键任务卸载到 WebAssembly 模块,开发人员可以通过多种方式优化他们的 WebRTC 应用程序:
所有代码都在git上,可以完整获取到!
https://github.com/giftedunicorn/webpig/blob/main/src/pages/webrtc.js
https://github.com/giftedunicorn/webpig/blob/main/server/index.js
https://github.com/giftedunicorn/webpig/blob/main/src/pages/agorawebsdk.js
恭喜!您现在已经了解了 WebRTC 的基础知识并构建了一个基本的视频通话应用程序。这只是您的 WebRTC 之旅的开始。借助 WebRTC 的巨大潜力,您可以探索各种应用程序,从视频会议到在线游戏等等。继续尝试,磨练您的技能,并对不断发展的 WebRTC 世界保持好奇心。
请记住,实时通信触手可及,因此请拥抱这项强大的技术,将您的 Web 应用程序提升到新的水平。祝您编码愉快!
提示:请勿发布广告垃圾评论,否则封号处理!!