WebRTC 基础
目录
什么是 WebRTC
WebRTC(Web Real-Time Communication)是一项技术,它使浏览器和移动应用程序能够通过简单的 API 直接进行点对点(Peer-to-Peer,P2P)的音频、视频和数据传输,而无需借助中间服务器。WebRTC 主要用于以下场景:
- 视频通话:在浏览器之间建立实时的视频聊天功能。
- 音频通话:支持高质量的语音通话。
- 数据传输:用于传输任意数据,如文件、消息等。
- 实时协作:在多个客户端之间同步文档、白板等应用。
WebRTC 具有以下几个关键特点:
- 实时性:提供低延迟的音视频通信,适用于需要即时反馈的场景。
- 跨平台:支持在不同设备和浏览器间进行通信,广泛适用于桌面和移动端。
- 安全性:通过 SRTP(Secure Real-time Transport Protocol)对传输的数据进行加密,确保通信的安全性。
- 开源性:WebRTC 是一个开源项目,开发者可以自由使用和修改。
WebRTC 的基本概念
在深入了解 WebRTC 的使用之前,理解以下几个关键概念至关重要:
1. 媒体流(MediaStream)
媒体流表示音频或视频流,是由多个媒体轨道(MediaTrack)组成的。每个轨道可以是音频轨道或视频轨道。WebRTC 通过
getUserMedia()
API 获取媒体流并将其传输给对等端。
navigator.mediaDevices.getUserMedia({video:true,audio:true}).then(stream=>{// 使用获取到的媒体流}).catch(error=>{
console.error('Error accessing media devices.', error);});
- 关于媒体流,可以参考:MediaStream 的媒体流对象 (stream) 和流媒体轨道 (track) 详解
2. 信令(Signaling)
信令是指在两个对等端之间交换元数据的过程。元数据包括:
- SDP(Session Description Protocol):描述会话的格式信息,如媒体类型、编解码器、网络参数等。
- ICE 候选(ICE Candidates):用于 P2P 连接的网络候选信息。
WebRTC 并不规定如何实现信令,通常由开发者通过
WebSocket
、
XHR
或其他协议来完成。完成信令交换相关操作的服务称作
信令服务器
3. 会话描述协议(SDP)
SDP 是一种文本格式,用于描述多媒体通信会话。它包含有关媒体格式、编解码器、带宽、网络等信息。SDP 是 WebRTC 连接建立过程中的关键要素之一。
4. ICE(Interactive Connectivity Establishment)
ICE 是一个用于帮助 P2P 连接在不同网络环境下建立的框架。ICE 会尝试使用各种网络候选(candidate),包括本地、STUN 和 TURN 服务器提供的候选,以确保 P2P 连接的建立。
- STUN(Session Traversal Utilities for NAT):帮助客户端发现其外部网络地址。
- TURN(Traversal Using Relays around NAT):用于在 P2P 连接无法直接建立时,通过中继服务器转发流量。
上面的过程强烈建议参考这位作者的文章:
- 一文详解 WebRTC 基础
5. 数据通道(DataChannel)
数据通道 是 WebRTC 提供的一种机制,用于在对等端之间传输任意数据,如文本、文件等。它通过
RTCDataChannel
对象来创建,具有低延迟和高吞吐量的特点。
const dataChannel = pc.createDataChannel("myDataChannel");// 监听消息接收事件
dataChannel.onmessage=(event)=>{
console.log("Received Message:", event.data);};// 发送消息
dataChannel.send("Hello, WebRTC!");
WebRTC 的基本流程
WebRTC 的连接建立过程分为以下几个步骤:
1. 获取本地媒体流
使用
getUserMedia()
API 获取本地的音频和视频流,并将其添加到 RTCPeerConnection 中。
2. 创建 RTCPeerConnection 对象
实例化
RTCPeerConnection
对象,并设置 ICE 服务器配置。RTCPeerConnection 是 WebRTC 的核心对象,用于管理和控制 P2P 连接。
3. 创建 Offer 并设置本地描述
调用
createOffer()
创建一个会话描述(SDP),并使用
setLocalDescription()
将其设置为本地描述。
4. 通过信令通道发送 Offer
将生成的 SDP 通过信令通道发送给远端。
5. 远端接收 Offer 并生成 Answer
远端使用
setRemoteDescription()
接收 SDP,并调用
createAnswer()
生成一个 Answer,然后通过信令通道发送回给本地。
6. 本地接收 Answer 并设置远程描述
本地使用
setRemoteDescription()
接收远端的 Answer,并将其设置为远程描述。
7. 交换 ICE 候选
通过
onicecandidate
事件监听 ICE 候选的生成,并通过信令通道交换候选信息,确保 P2P 连接的建立。
8. 连接建立并开始传输数据
当双方都交换完 SDP 和 ICE 候选后,P2P 连接建立,可以开始音视频和数据的传输。
连接建立流程图
#mermaid-svg-MVIa2sSazDRVuKar {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-MVIa2sSazDRVuKar .error-icon{fill:#552222;}#mermaid-svg-MVIa2sSazDRVuKar .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MVIa2sSazDRVuKar .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-MVIa2sSazDRVuKar .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MVIa2sSazDRVuKar .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MVIa2sSazDRVuKar .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MVIa2sSazDRVuKar .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MVIa2sSazDRVuKar .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MVIa2sSazDRVuKar .marker.cross{stroke:#333333;}#mermaid-svg-MVIa2sSazDRVuKar svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MVIa2sSazDRVuKar .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MVIa2sSazDRVuKar .cluster-label text{fill:#333;}#mermaid-svg-MVIa2sSazDRVuKar .cluster-label span{color:#333;}#mermaid-svg-MVIa2sSazDRVuKar .label text,#mermaid-svg-MVIa2sSazDRVuKar span{fill:#333;color:#333;}#mermaid-svg-MVIa2sSazDRVuKar .node rect,#mermaid-svg-MVIa2sSazDRVuKar .node circle,#mermaid-svg-MVIa2sSazDRVuKar .node ellipse,#mermaid-svg-MVIa2sSazDRVuKar .node polygon,#mermaid-svg-MVIa2sSazDRVuKar .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MVIa2sSazDRVuKar .node .label{text-align:center;}#mermaid-svg-MVIa2sSazDRVuKar .node.clickable{cursor:pointer;}#mermaid-svg-MVIa2sSazDRVuKar .arrowheadPath{fill:#333333;}#mermaid-svg-MVIa2sSazDRVuKar .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MVIa2sSazDRVuKar .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MVIa2sSazDRVuKar .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-MVIa2sSazDRVuKar .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-MVIa2sSazDRVuKar .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MVIa2sSazDRVuKar .cluster text{fill:#333;}#mermaid-svg-MVIa2sSazDRVuKar .cluster span{color:#333;}#mermaid-svg-MVIa2sSazDRVuKar div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MVIa2sSazDRVuKar :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
获取本地媒体流
创建 RTCPeerConnection
创建 Offer 并设置本地描述
通过信令通道发送 Offer
远端接收 Offer 并生成 Answer
本地接收 Answer 并设置远程描述
交换 ICE 候选
连接建立并开始传输数据
信令交换流程图
#mermaid-svg-H1fruSSJV9h8FFHl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-H1fruSSJV9h8FFHl .error-icon{fill:#552222;}#mermaid-svg-H1fruSSJV9h8FFHl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-H1fruSSJV9h8FFHl .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-H1fruSSJV9h8FFHl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-H1fruSSJV9h8FFHl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-H1fruSSJV9h8FFHl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-H1fruSSJV9h8FFHl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-H1fruSSJV9h8FFHl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-H1fruSSJV9h8FFHl .marker.cross{stroke:#333333;}#mermaid-svg-H1fruSSJV9h8FFHl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-H1fruSSJV9h8FFHl .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H1fruSSJV9h8FFHl text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-H1fruSSJV9h8FFHl .actor-line{stroke:grey;}#mermaid-svg-H1fruSSJV9h8FFHl .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-H1fruSSJV9h8FFHl .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-H1fruSSJV9h8FFHl #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-H1fruSSJV9h8FFHl .sequenceNumber{fill:white;}#mermaid-svg-H1fruSSJV9h8FFHl #sequencenumber{fill:#333;}#mermaid-svg-H1fruSSJV9h8FFHl #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-H1fruSSJV9h8FFHl .messageText{fill:#333;stroke:#333;}#mermaid-svg-H1fruSSJV9h8FFHl .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H1fruSSJV9h8FFHl .labelText,#mermaid-svg-H1fruSSJV9h8FFHl .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-H1fruSSJV9h8FFHl .loopText,#mermaid-svg-H1fruSSJV9h8FFHl .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-H1fruSSJV9h8FFHl .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-H1fruSSJV9h8FFHl .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-H1fruSSJV9h8FFHl .noteText,#mermaid-svg-H1fruSSJV9h8FFHl .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-H1fruSSJV9h8FFHl .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H1fruSSJV9h8FFHl .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H1fruSSJV9h8FFHl .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H1fruSSJV9h8FFHl .actorPopupMenu{position:absolute;}#mermaid-svg-H1fruSSJV9h8FFHl .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-H1fruSSJV9h8FFHl .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H1fruSSJV9h8FFHl .actor-man circle,#mermaid-svg-H1fruSSJV9h8FFHl line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-H1fruSSJV9h8FFHl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
Offerer
Signaling Server
Answerer
Send SDP Offer
Forward SDP Offer
Send SDP Answer
Forward SDP Answer
Set Remote Description
Set Local Description
Offerer
Signaling Server
Answerer
WebRTC 的基本对象
RTCPeerConnection
- 可参考:RTCPeerConnection API 详细介绍(含RTCDataChannel)
RTCPeerConnection
是 WebRTC 的核心对象,用于管理 P2P 连接。它提供了以下主要功能:
- 创建和维护 P2P 连接。
- 处理 ICE 候选。
- 发送和接收媒体流。
构造函数
const pc =newRTCPeerConnection(configuration);
configuration
:包含 ICE 服务器配置的对象。可以设置 STUN 和 TURN 服务器。 -iceServers
: 一个包含 STUN 和 TURN 服务器地址的数组,例如:const configuration ={iceServers:[{urls:'stun:stun.l.google.com:19302'},{urls:'turn:your-turn-server.com',username:'user',credential:'pass'}]};
主要方法和属性
createOffer(options)
:创建一个 SDP Offer,用于发起连接。 -options
: 可选参数,用于指定创建 Offer 的约束条件,如是否仅包含视频轨道等。createAnswer(options)
:创建一个 SDP Answer,用于回应对方的 Offer。 -options
: 可选参数,用于指定创建 Answer 的约束条件。setLocalDescription(description)
:设置本地 SDP 描述。 -description
: 一个RTCSessionDescription
对象,通常是由createOffer
或createAnswer
方法生成的。setRemoteDescription(description)
:设置远程 SDP 描述。 -description
: 一个RTCSessionDescription
对象,表示远端的 SDP 描述。addIceCandidate(candidate)
:向连接中添加一个 ICE 候选。 -candidate
: 一个RTCIceCandidate
对象,表示新的 ICE 候选。
事件处理
onicecandidate
:当新的 ICE 候选生成时触发,用于向远端发送 ICE 候选。pc.onicecandidate=(event)=>{if(event.candidate){// 发送候选信息给远端}};
ontrack
:当新的媒体轨道(音频或视频)添加到连接时触发。pc.ontrack=(event)=>{const remoteStream = event.streams[0];// 将远程媒体流添加到视频元素};
ondatachannel
:当远程对等端创建一个数据通道时触发。pc.ondatachannel=(event)=>{const receiveChannel = event.channel; receiveChannel.onmessage=(e)=>{ console.log("Data Channel Message:", e.data);};};
RTCSessionDescription
RTCSessionDescription
对象用于表示 WebRTC 连接的 SDP 信息。它包含连接的媒体类型、编解码器和网络配置等信息。
构造函数
const description =newRTCSessionDescription({ type, sdp });
type
:描述的类型,可以是"offer"
、"answer"
或"rollback"
。sdp
:包含 SDP 的字符串。
示例
const offer =await pc.createOffer();await pc.setLocalDescription(offer);const description =newRTCSessionDescription({type:"offer",sdp: offer.sdp });
RTCIceCandidate
RTCIceCandidate
对象表示 WebRTC 的 ICE 候选信息。它用于描述潜在的 P2P 连接路径。
构造函数
const candidate =newRTCIceCandidate({ candidate, sdpMid, sdpMLineIndex });
candidate
:表示候选的网络地址。sdpMid
:与候选相关的媒体流标识符。sdpMLineIndex
:与候选相关的媒体流索引。
示例
pc.addIceCandidate(newRTCIceCandidate(candidate));
示例
const configuration ={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};const pc =newRTCPeerConnection(configuration);// 添加媒体轨道
navigator.mediaDevices.getUserMedia({video:true,audio:true}).then(stream=>{
stream.getTracks().forEach(track=> pc.addTrack(track, stream));});// 处理 ICE 候选
pc.onicecandidate=(event)=>{if(event.candidate){// 发送 ICE 候选给远端}};// 处理远端媒体流
pc.ontrack=(event)=>{const remoteStream = event.streams[0];
document.querySelector("#remoteVideo").srcObject = remoteStream;};// 创建数据通道const dataChannel = pc.createDataChannel("chat");
dataChannel.onmessage=(event)=>{
console.log("Received Message:", event.data);};
pc.createOffer().then(offer=>{return pc.setLocalDescription(offer);}).then(()=>{// 发送 offer 到远端}).catch(console.error);
详细的代码示例
基本连接示例
以下是一个简单的 WebRTC 连接示例,展示了如何建立一个基本的音视频连接。
// 创建 RTCPeerConnection 对象const configuration ={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};const pc =newRTCPeerConnection(configuration);// 获取本地媒体流
navigator.mediaDevices.getUserMedia({video:true,audio:true}).then(stream=>{
document.querySelector("#localVideo").srcObject = stream;
stream.getTracks().forEach(track=> pc.addTrack(track, stream));});// 处理 ICE 候选
pc.onicecandidate=(event)=>{if(event.candidate){// 发送 ICE 候选给远端}};// 处理远端媒体流
pc.ontrack=(event)=>{const remoteStream = event.streams[0];
document.querySelector("#remoteVideo").srcObject = remoteStream;};// 通过信令通道发送和接收 Offer/Answer
pc.createOffer().then(offer=>{return pc.setLocalDescription(offer);}).then(()=>{// 发送 offer 到远端}).catch(console.error);// 处理远端的 Answer// pc.setRemoteDescription(new RTCSessionDescription(answer));
完整的 WebRTC 实现示例
以下是一个更复杂的完整 WebRTC 示例,涵盖了音视频连接、数据通道和错误处理。
const configuration ={iceServers:[{urls:"stun:stun.l.google.com:19302"}]};const pc =newRTCPeerConnection(configuration);// 获取本地媒体流
navigator.mediaDevices.getUserMedia({video:true,audio:true}).then(stream=>{
document.querySelector("#localVideo").srcObject = stream;
stream.getTracks().forEach(track=> pc.addTrack(track, stream));}).catch(error=>{
console.error("Error accessing media devices.", error);});// 处理 ICE 候选
pc.onicecandidate=(event)=>{if(event.candidate){// 发送 ICE 候选给远端}};// 处理远端媒体流
pc.ontrack=(event)=>{const remoteStream = event.streams[0];
document.querySelector("#remoteVideo").srcObject = remoteStream;};// 创建数据通道const dataChannel = pc.createDataChannel("chat");
dataChannel.onopen=()=> console.log("Data channel is open");
dataChannel.onmessage=(event)=> console.log("Received message:", event.data);
dataChannel.onclose=()=> console.log("Data channel is closed");// 创建 Offer 并设置本地描述
pc.createOffer().then(offer=>{return pc.setLocalDescription(offer);}).then(()=>{// 通过信令通道发送 Offer}).catch(console.error);// 处理远端的 Answer// pc.setRemoteDescription(new RTCSessionDescription(answer)).catch(console.error);// 添加错误处理
pc.oniceconnectionstatechange=()=>{if(pc.iceConnectionState ==="failed"){
console.error("ICE connection failed.");}};
总结
WebRTC 为实时通信提供了强大的功能,能够在不依赖中介服务器的情况下,建立跨平台的 P2P 连接。本文介绍了WebRTC 基本概念和相关流程,下文将主要介绍 WebRTC 创建端与接收端的代码实现。
参考来源
- 一文详解 WebRTC 基础
版权归原作者 Wu Youlu 所有, 如有侵权,请联系我们删除。