想象一下,自由软件开发员 Sarah 正在开发一个雄心勃勃的项目——为远程医疗应用程序提供无缝视频通话功能。她面临的挑战是如何在不依赖第三方服务的情况下集成实时视频通信。经过几天的研究,她发现了 WebRTC 的强大功能。掌握了这些知识后,Sarah 利用 React 的前端优势和 Node.js 的后端优势,进入了点对点连接的世界。在她的旅程中,她不仅实现了强大的视频通话功能,还将她的项目转变成了一个可扩展的高性能应用程序,让她的客户赞叹不已,也让她在激烈的技术竞争中脱颖而出。加入 Sarah 的探索之旅,了解如何利用 WebRTC 创建最先进的实时通信解决方案。
WebRTC 即Web实时通信,它是一种核心协议,使浏览器能够直接促进实时媒体通信。与依赖外部插件或中介的传统方法不同,WebRTC 实现了点对点连接,允许用户之间即时共享音频、视频和数据。该协议原生集成在浏览器中,是一种无缝、高效的实时通信解决方案。
WebRTC 架构和术语
WebRTC 无需插件即可在Web应用程序中实现实时音频、视频和数据交换。下面简要介绍其主要架构组件和术语:
1. 对等连接(PeerConnection):
- 管理点对点连接的核心组件。
- 这意味着你可以直接将媒体发送给对方,而无需中央服务器
2. 信令:
- 两个浏览器在开始通话前需要交换地址
- 涉及会话描述协议(SDP)和 ICE 候选者的交换。
- WebRTC 本身没有定义;通常使用 WebSockets、HTTP 或其他机制实现。
3. 会话描述协议(SDP):
- 一种用于描述多媒体通信会话的格式
- Ice Candidates、想要发送什么媒体,使用什么协议来对媒体进行编码。
4. ICE(交互式连接建立):
- 用于寻找连接对等网络最佳路径的框架。
- 收集并测试多个候选地址(IP/端口组合)以建立连接。
- 如果两个朋友试图在宿舍的 wifi 上互相连接,那么他们可以通过各自的私人路由器 ice 候选地址进行连接。
- 如果两个来自不同国家的人试图互相连接,那么他们会通过各自的公共 IP 进行连接。
5. STUN(用于 NAT 的会话遍历实用程序):
- 一种发现公共 IP 地址和端口映射的协议。
- 可帮助对等方跨越 NAT(网络地址转换)设备进行通信。
6. TURN(使用中继绕过 NAT)
- 当直接对等通信失败时通过中间服务器中继媒体流量的协议。
- 即使在受限的网络环境中也能确保连接。
- 由于
ice candidate
被stun server
发现,您的网络可能会阻止来自browser 2
的传入数据,并且只允许来自stun server
的数据
7. 媒体流
- 代表音频或视频等媒体内容流。
- 使用 getUserMedia API 捕捉。
8. RTCPeerConnection:
- 创建和管理连接的主要 API。
- 它集成了设置、维护和中断点对点通信所需的所有组件。
9. Tracks and Streams:
- MediaStreamTrack:代表单个媒体轨道(音频或视频)。
- MediaStream:MediaStreamTracks 的集合。
WebRTC 的架构旨在高效、安全地处理实时通信,利用这些组件在各种网络配置中建立稳健的点对点连接。
连接双方
在双方之间创建 webrtc 连接的步骤包括:
- 浏览器 1 创建
RTCPeerConnection
- 浏览器 1 创建
offer
- 浏览器 1 将设置
local description
为offer
- 浏览器 1 通过
signaling server
- 浏览器 2 收到
offer
来自signaling server
- 浏览器 2 将设置
remote description
为offer
- 浏览器 2 创建
answer
- 浏览器 2 将设置
local description
为answer
- 浏览器2
answer
通过signaling server
- 浏览器 1 接收
answer
并设置remote description
这只是为了在双方之间建立点对点连接
要发送媒体,我们必须:
- 请求相机/麦克风权限
- 获取
audio
和video
流 - 调用电脑上的
addTrack
- 这将触发
onTrack
另一端的回调
实施
我们将用Node.js 编写代码。它将是一个 websocket 服务器,支持 3 种类型的消息
- 创建
Offer
- 创建
Answer
- 添加
IceCandidate
- 前端使用 React + PeerConnectionObject
后端
- 创建一个空的TS项目,向其中添加ws
npm init -y
npx tsc -- init
npm install ws @types /ws
// 在 tsconfig 中更改 rootDir 和 outDir
"rootDir" : "./src" ,
"outDir" : "./dist" ,
- 创建一个简单的 websocket 服务器
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
let senderSocket: null | WebSocket = null;
let receiverSocket: null | WebSocket = null;
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data: any) {
const message = JSON.parse(data);
});
ws.send('something');
});
- 尝试运行服务器
tsc -b
node dist/index.js
- 添加消息处理程序
import { WebSocket, WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
let senderSocket: null | WebSocket = null;
let receiverSocket: null | WebSocket = null;
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data: any) {
const message = JSON.parse(data);
if (message.type === 'sender') {
senderSocket = ws;
} else if (message.type === 'receiver') {
receiverSocket = ws;
} else if (message.type === 'createOffer') {
if (ws !== senderSocket) {
return;
}
receiverSocket?.send(JSON.stringify({ type: 'createOffer', sdp: message.sdp }));
} else if (message.type === 'createAnswer') {
if (ws !== receiverSocket) {
return;
}
senderSocket?.send(JSON.stringify({ type: 'createAnswer', sdp: message.sdp }));
} else if (message.type === 'iceCandidate') {
if (ws === senderSocket) {
receiverSocket?.send(JSON.stringify({ type: 'iceCandidate', candidate: message.candidate }));
} else if (ws === receiverSocket) {
senderSocket?.send(JSON.stringify({ type: 'iceCandidate', candidate: message.candidate }));
}
}
});
});
这就是您在两个标签页之间进行简单的单向通信所需的全部功能
前端
添加两个路由,一个用于发送方,一个用于接收方
import { useState } from 'react'
import './App.css'
import { Route, BrowserRouter, Routes } from 'react-router-dom'
import { Sender } from './components/Sender'
import { Receiver } from './components/Receiver'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/sender" element={<Sender />} />
<Route path="/receiver" element={<Receiver />} />
</Routes>
</BrowserRouter>
)
}
export default App
- 为发送者(Sender )创建组件
import { useEffect, useState } from "react";
export const Sender = () => {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [pc, setPC] = useState<RTCPeerConnection | null>(null);
useEffect(() => {
const socket = new WebSocket("ws://localhost:8080");
setSocket(socket);
socket.onopen = () => {
socket.send(
JSON.stringify({
type: "sender",
})
);
};
}, []);
const initiateConn = async () => {
if (!socket) {
alert("Socket not found");
return;
}
socket.onmessage = async (event) => {
const message = JSON.parse(event.data);
if (message.type === "createAnswer") {
await pc.setRemoteDescription(message.sdp);
} else if (message.type === "iceCandidate") {
pc.addIceCandidate(message.candidate);
}
};
const pc = new RTCPeerConnection();
setPC(pc);
pc.onicecandidate = (event) => {
if (event.candidate) {
socket?.send(
JSON.stringify({
type: "iceCandidate",
candidate: event.candidate,
})
);
}
};
pc.onnegotiationneeded = async () => {
console.error("onnegotiateion needed");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
socket?.send(
JSON.stringify({
type: "createOffer",
sdp: pc.localDescription,
})
);
};
getCameraStreamAndSend(pc);
};
const getCameraStreamAndSend = (pc: RTCPeerConnection) => {
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
const video = document.createElement("video");
video.srcObject = stream;
video.play();
document.body.appendChild(video);
stream.getTracks().forEach((track) => {
console.error("track added");
console.log(track);
console.log(pc);
pc?.addTrack(track);
});
});
};
return (
<div>
Sender
<button onClick={initiateConn}> Send data </button>
</div>
);
};
- 为接收者(receiver)创建组件
import { useEffect } from "react"
export const Receiver = () => {
useEffect(() => {
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'receiver'
}));
}
startReceiving(socket);
}, []);
function startReceiving(socket: WebSocket) {
const video = document.createElement('video');
document.body.appendChild(video);
const pc = new RTCPeerConnection();
pc.ontrack = (event) => {
video.srcObject = new MediaStream([event.track]);
video.play();
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'createOffer') {
pc.setRemoteDescription(message.sdp).then(() => {
pc.createAnswer().then((answer) => {
pc.setLocalDescription(answer);
socket.send(JSON.stringify({
type: 'createAnswer',
sdp: answer
}));
});
});
} else if (message.type === 'iceCandidate') {
pc.addIceCandidate(message.candidate);
}
}
}
return <div>
</div>
}
现在,您可以启动后端和前端服务器查看视频,也可以在 about:webrtc-internals 中查看大量统计数据/sdps。
所有代码如下 :https://github.com/codeakki/WebRtc/tree/main
作者:Akshay Sharma
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/48757.html