WebRTC(Web Real-Time Communication)协议
WebRTC(Web Real-Time Communication)是一种支持浏览器和移动应用程序之间进行 实时音频、视频和数据通信 的协议。它使得开发者能够在浏览器中实现高质量的 P2P(点对点)实时通信,而无需安装插件或第三方软件。WebRTC 主要用于视频通话、语音聊天、在线协作和数据传输等应用场景。
核心功能
WebRTC 提供了以下几个关键功能:
- 音视频通信:WebRTC 支持通过浏览器进行音频和视频的实时传输,用户之间可以进行高质量的语音和视频通话。
- P2P 数据传输:WebRTC 不仅支持音视频流,还支持点对点的数据通道(Data Channel)传输,适合进行文件传输、屏幕共享和实时协作。
- 低延迟:WebRTC 专门优化了数据传输过程,以减少通信中的延迟,使得实时通信更加顺畅。
工作原理
WebRTC 使用了一系列底层协议和技术来实现点对点通信。WebRTC 的工作流程通常包括以下几个关键步骤:
- 建立连接(Signaling)
- 在 WebRTC 中,信令(Signaling) 是客户端用于交换通信所需的元数据(如网络信息、音视频编解码信息、媒体能力等)的一种过程。信令不是 WebRTC 协议的一部分,但它是 WebRTC 通信的必要步骤。
- 信令的内容包括:协商媒体(音视频)格式、网络路径、设备信息等。
- 通常,信令使用 WebSockets、HTTP 或其他协议进行实现。
- 信令过程包括:
- Offer(提议):发起方创建会话请求,发送给接收方。
- Answer(应答):接收方回应发起方的请求,确认会话设置。
- ICE candidates(ICE 候选者):每个端点通过收集网络候选地址来交换,以帮助建立最佳的 P2P 连接。
- 网络连接(ICE、STUN 和 TURN)
- ICE(Interactive Connectivity Establishment):用于在 NAT 后的网络环境中建立端到端的连接。ICE 是 WebRTC 连接的关键组成部分,它帮助客户端发现并连接到彼此。
- STUN(Session Traversal Utilities for NAT):STUN 服务器帮助客户端了解自己在 NAT 后的公网 IP 地址。
- TURN(Traversal Using Relays around NAT):TURN 服务器在 P2P 连接无法直接建立时提供数据转发服务,确保通信的可靠性。TURN 作为最后的解决方案,通常会导致更高的延迟,因此只有在需要时才使用。
- 媒体流传输(RTP/RTCP)
- RTP(Real-Time Transport Protocol):RTP 是 WebRTC 用于音频和视频流的传输协议。它允许在网络中实时地传输数据包,并为这些数据包添加时间戳,确保音视频数据的正确顺序。
- RTCP(Real-Time Control Protocol):RTCP 用于监控 RTP 会话的质量,并提供流控制和同步。
- 数据传输(DataChannel)
- RTCDataChannel:WebRTC 支持数据通道(DataChannel),使得浏览器间可以通过 P2P 传输任意数据(包括文本、文件、图像等)。数据通道提供了低延迟的点对点数据传输能力,常用于文件传输、屏幕共享等应用。
关键技术
WebRTC 由多种技术组成,其中最重要的包括:
-
getUserMedia:用于获取用户的音频和视频输入设备(如麦克风和摄像头)的权限。它会返回一个包含音视频流的对象。
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { // 显示视频流 const videoElement = document.getElementById('my-video'); videoElement.srcObject = stream; }) .catch(error => console.log('Error accessing media devices: ', error));
-
RTCPeerConnection:用于建立、维护和管理 P2P 连接。它负责处理网络连接、音视频编解码、带宽管理等任务。
const peerConnection = new RTCPeerConnection(configuration); peerConnection.addStream(localStream); // 添加本地音视频流 // 建立连接后,发送媒体流 peerConnection.createOffer() .then(offer => peerConnection.setLocalDescription(offer)) .then(() => { // 将 offer 发送给接收方 });
-
RTCDataChannel:用于建立点对点的数据传输通道,可以传输任意类型的数据。
javascript复制编辑const dataChannel = peerConnection.createDataChannel('chat'); dataChannel.onopen = () => console.log('Data channel open'); dataChannel.onmessage = (event) => console.log('Received message: ', event.data); // 发送数据 dataChannel.send('Hello, WebRTC!');
应用场景
- 视频通话:WebRTC 可以用于构建视频会议应用,如 Zoom、Google Meet 等。
- 语音通话:WebRTC 支持语音通话,广泛应用于 IP 电话、语音助手等。
- 文件传输:通过 RTCDataChannel,WebRTC 可以用于点对点的文件传输。
- 实时协作:WebRTC 用于多人在线编辑、白板共享等实时协作工具。
- 直播:WebRTC 可以支持低延迟的视频直播,适用于游戏直播、网络教学等领域。
实现过程
1. 获取音视频流(getUserMedia)
getUserMedia
是 WebRTC 中用于访问用户音频和视频设备的 API。通过它,你可以获取麦克风和摄像头的权限,从而获取用户的音视频流。
示例:获取视频和音频流
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
// 获取视频流后,可以将其显示在视频标签上
const videoElement = document.getElementById('localVideo');
videoElement.srcObject = stream;
// 创建 RTCPeerConnection 实例(将在后面讨论)
const peerConnection = new RTCPeerConnection();
// 将本地流添加到连接
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
})
.catch(error => {
console.error('Error accessing media devices.', error);
});
getUserMedia
:请求用户设备的音视频流。- 返回的
MediaStream
可以用于显示、录制或传输。
2. 创建点对点连接(RTCPeerConnection)
WebRTC 使用 RTCPeerConnection 来管理媒体流的传输。它代表了与另一个客户端的点对点连接。
示例:创建 RTCPeerConnection 并添加本地流
const peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' } // 使用 STUN 服务器
]
});
// 添加本地流到连接
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = stream;
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
});
- STUN 服务器:STUN(Session Traversal Utilities for NAT)帮助客户端发现自己的公共 IP 地址,用于 NAT 穿透。
3. 信令交换(Signal)
WebRTC 协议并不直接定义信令交换的方式,因此你需要自己实现信令交换。信令过程用于交换连接的元数据,如会话描述(SDP)和 ICE 候选者等。
- 创建 Offer(发起方)
peerConnection.createOffer()
.then(offer => {
return peerConnection.setLocalDescription(offer); // 设置本地 SDP
})
.then(() => {
// 将 offer 发送给对方(通过信令服务器)
signalingServer.send({ type: 'offer', offer: peerConnection.localDescription });
});
- SDP(Session Description Protocol):描述了音视频流的编码、传输等信息。
- 设置 Answer(接收方)
接收方收到 Offer 后,创建 Answer 并回复:
signalingServer.on('offer', offer => {
peerConnection.setRemoteDescription(new RTCSessionDescription(offer))
.then(() => peerConnection.createAnswer())
.then(answer => {
return peerConnection.setLocalDescription(answer); // 设置本地 SDP
})
.then(() => {
// 将 answer 发送给发起方
signalingServer.send({ type: 'answer', answer: peerConnection.localDescription });
});
});
- 交换 ICE 候选者
在连接过程中,客户端会收集并交换 ICE 候选者(候选网络路径)。这些候选者用于寻找最佳的连接路径。
peerConnection.onicecandidate = event => {
if (event.candidate) {
// 发送 ICE 候选者到对方
signalingServer.send({ type: 'ice-candidate', candidate: event.candidate });
}
};
// 接收对方的 ICE 候选者
signalingServer.on('ice-candidate', candidate => {
peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
});
- 建立连接并处理媒体流
当信令交换完成并且 ICE 候选者交换完毕后,WebRTC 将会建立一个完整的点对点连接,音视频流会开始传输。
示例:显示远端视频流
peerConnection.ontrack = event => {
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = event.streams[0]; // 获取远程流并显示
};
5. 数据通道(RTCDataChannel)
WebRTC 还支持 RTCDataChannel,用于在两个客户端之间进行低延迟的点对点数据传输(例如文件传输、聊天信息等)。
示例:创建并使用数据通道
const dataChannel = peerConnection.createDataChannel('chat');
// 监听数据通道的消息
dataChannel.onmessage = event => {
console.log('Received message:', event.data);
};
// 发送数据
dataChannel.send('Hello from WebRTC!');
6. 断开连接
当通信结束时,你可以通过关闭 PeerConnection 来断开连接并释放资源。
peerConnection.close();
WebRTC-Streamer开源项目
项目介绍
WebRTC-Streamer 是一个开源工具集,旨在简化实时音视频数据流的传输与集成,主要通过 WebRTC 技术实现低延迟的音视频流传输。开发者无需深入理解复杂的底层协议即可轻松将实时音视频功能集成到自己的应用中。该项目特别设计了高效的音视频流处理功能,支持多种数据来源,如 V4L2 捕获设备、RTSP 流、屏幕捕捉 等,适用于多种实时传输场景。
快速启动
WebRTC-Streamer 提供了简便的集成方式。以下是一个通过 HTML 和 JavaScript 快速搭建基本实时音视频流服务的示例代码:
<html>
<head>
<script src="libs/adapter.min.js"></script>
<script src="webrtcstreamer.js"></script>
</head>
<body>
<script>
var webRtcServer;
window.onload = function() {
webRtcServer = new WebRtcStreamer(document.getElementById("video"), location.protocol+"//" + location.hostname + ":8000");
webRtcServer.connect("rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov");
}
window.onbeforeunload = function() {
if (webRtcServer !== null) {
webRtcServer.disconnect();
}
}
</script>
<video id="video" controls autoplay muted></video>
</body>
</html>
代码解析:
- 引入了
adapter.min.js
和webrtcstreamer.js
两个必要的 JavaScript 库。 - 创建一个
WebRtcStreamer
实例,指定本地服务器地址及目标 RTSP 视频流地址。 - 页面加载时自动连接至 RTSP 流,播放视频。
- 页面卸载时,断开连接,释放资源。
应用案例与最佳实践
示例 1:直播演示
- 使用 WebRTC-Streamer 可以通过简化的 Web 组件方式轻松展示来自 RTSP 源的实时视频流。
<webrtc-streamer url="rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov">
示例 2:地图上的直播流
- 配合 Google Map API,WebRTC-Streamer 可以在地图上显示多个实时视频流,适用于 监控、交通管理 等领域。
其它生态项目
WebRTC-Streamer 还支持一系列相关生态项目,以扩展其功能和适用范围:
- webrtc-streamer-card:为 Home Assistant 提供的卡片插件,允许从 WebRTC-Streamer 服务中拉取零延迟视频流,适用于智能家居。
- rpi-webrtc-streamer:面向 树莓派 系列微控制器的 WebRTC 流媒体软件包,支持在边缘设备上实现高效的音视频处理。
- Live555 Integration:通过整合 Live555 Media Server,增强 WebRTC-Streamer 在处理非标准音视频格式方面的能力,扩展其应用场景。
附录:WebRTC-Streamer项目地址
https://gitcode.com/gh_mirrors/we/webrtc-streamer/?utm_source=artical_gitcode&index=bottom&type=card&webUrl&isLogin=1
附录:webrtcstreamer.js源码
// webrtcstreamer.js
var WebRtcStreamer = (function() {
/**
* Interface with WebRTC-streamer API
* @constructor
* @param {string} videoElement - id of the video element tag
* @param {string} srvurl - url of webrtc-streamer (default is current location)
*/
var WebRtcStreamer = function WebRtcStreamer (videoElement, srvurl) {
if (typeof videoElement === "string") {
this.videoElement = document.getElementById(videoElement);
} else {
this.videoElement = videoElement;
}
this.srvurl = srvurl || location.protocol+"//"+window.location.hostname+":"+window.location.port;
this.pc = null;
this.mediaConstraints = { offerToReceiveAudio: true, offerToReceiveVideo: true };
this.iceServers = null;
this.earlyCandidates = [];
}
WebRtcStreamer.prototype._handleHttpErrors = function (response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
/**
* Connect a WebRTC Stream to videoElement
* @param {string} videourl - id of WebRTC video stream
* @param {string} audiourl - id of WebRTC audio stream
* @param {string} options - options of WebRTC call
* @param {string} stream - local stream to send
*/
WebRtcStreamer.prototype.connect = function(videourl, audiourl, options, localstream) {
this.disconnect();
// getIceServers is not already received
if (!this.iceServers) {
console.log("Get IceServers");
fetch(this.srvurl + "/api/getIceServers")
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream))
.catch( (error) => this.onError("getIceServers " + error ))
} else {
this.onReceiveGetIceServers(this.iceServers, videourl, audiourl, options, localstream);
}
}
/**
* Disconnect a WebRTC Stream and clear videoElement source
*/
WebRtcStreamer.prototype.disconnect = function() {
if (this.videoElement?.srcObject) {
this.videoElement.srcObject.getTracks().forEach(track => {
track.stop()
this.videoElement.srcObject.removeTrack(track);
});
}
if (this.pc) {
fetch(this.srvurl + "/api/hangup?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.catch( (error) => this.onError("hangup " + error ))
try {
this.pc.close();
}
catch (e) {
console.log ("Failure close peer connection:" + e);
}
this.pc = null;
}
}
/*
* GetIceServers callback
*/
WebRtcStreamer.prototype.onReceiveGetIceServers = function(iceServers, videourl, audiourl, options, stream) {
this.iceServers = iceServers;
this.pcConfig = iceServers || {"iceServers": [] };
try {
this.createPeerConnection();
var callurl = this.srvurl + "/api/call?peerid=" + this.pc.peerid + "&url=" + encodeURIComponent(videourl);
if (audiourl) {
callurl += "&audiourl="+encodeURIComponent(audiourl);
}
if (options) {
callurl += "&options="+encodeURIComponent(options);
}
if (stream) {
this.pc.addStream(stream);
}
// clear early candidates
this.earlyCandidates.length = 0;
// create Offer
this.pc.createOffer(this.mediaConstraints).then((sessionDescription) => {
console.log("Create offer:" + JSON.stringify(sessionDescription));
this.pc.setLocalDescription(sessionDescription)
.then(() => {
fetch(callurl, { method: "POST", body: JSON.stringify(sessionDescription) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.catch( (error) => this.onError("call " + error ))
.then( (response) => this.onReceiveCall(response) )
.catch( (error) => this.onError("call " + error ))
}, (error) => {
console.log ("setLocalDescription error:" + JSON.stringify(error));
});
}, (error) => {
alert("Create offer error:" + JSON.stringify(error));
});
} catch (e) {
this.disconnect();
alert("connect error: " + e);
}
}
WebRtcStreamer.prototype.getIceCandidate = function() {
fetch(this.srvurl + "/api/getIceCandidate?peerid=" + this.pc.peerid)
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => this.onReceiveCandidate(response))
.catch( (error) => this.onError("getIceCandidate " + error ))
}
/*
* create RTCPeerConnection
*/
WebRtcStreamer.prototype.createPeerConnection = function() {
console.log("createPeerConnection config: " + JSON.stringify(this.pcConfig));
this.pc = new RTCPeerConnection(this.pcConfig);
var pc = this.pc;
pc.peerid = Math.random();
pc.onicecandidate = (evt) => this.onIceCandidate(evt);
pc.onaddstream = (evt) => this.onAddStream(evt);
pc.oniceconnectionstatechange = (evt) => {
console.log("oniceconnectionstatechange state: " + pc.iceConnectionState);
if (this.videoElement) {
if (pc.iceConnectionState === "connected") {
this.videoElement.style.opacity = "1.0";
}
else if (pc.iceConnectionState === "disconnected") {
this.videoElement.style.opacity = "0.25";
}
else if ( (pc.iceConnectionState === "failed") || (pc.iceConnectionState === "closed") ) {
this.videoElement.style.opacity = "0.5";
} else if (pc.iceConnectionState === "new") {
this.getIceCandidate();
}
}
}
pc.ondatachannel = function(evt) {
console.log("remote datachannel created:"+JSON.stringify(evt));
evt.channel.onopen = function () {
console.log("remote datachannel open");
this.send("remote channel openned");
}
evt.channel.onmessage = function (event) {
console.log("remote datachannel recv:"+JSON.stringify(event.data));
}
}
pc.onicegatheringstatechange = function() {
if (pc.iceGatheringState === "complete") {
const recvs = pc.getReceivers();
recvs.forEach((recv) => {
if (recv.track && recv.track.kind === "video") {
console.log("codecs:" + JSON.stringify(recv.getParameters().codecs))
}
});
}
}
try {
var dataChannel = pc.createDataChannel("ClientDataChannel");
dataChannel.onopen = function() {
console.log("local datachannel open");
this.send("local channel openned");
}
dataChannel.onmessage = function(evt) {
console.log("local datachannel recv:"+JSON.stringify(evt.data));
}
} catch (e) {
console.log("Cannor create datachannel error: " + e);
}
console.log("Created RTCPeerConnnection with config: " + JSON.stringify(this.pcConfig) );
return pc;
}
/*
* RTCPeerConnection IceCandidate callback
*/
WebRtcStreamer.prototype.onIceCandidate = function (event) {
if (event.candidate) {
if (this.pc.currentRemoteDescription) {
this.addIceCandidate(this.pc.peerid, event.candidate);
} else {
this.earlyCandidates.push(event.candidate);
}
}
else {
console.log("End of candidates.");
}
}
WebRtcStreamer.prototype.addIceCandidate = function(peerid, candidate) {
fetch(this.srvurl + "/api/addIceCandidate?peerid="+peerid, { method: "POST", body: JSON.stringify(candidate) })
.then(this._handleHttpErrors)
.then( (response) => (response.json()) )
.then( (response) => {console.log("addIceCandidate ok:" + response)})
.catch( (error) => this.onError("addIceCandidate " + error ))
}
/*
* RTCPeerConnection AddTrack callback
*/
WebRtcStreamer.prototype.onAddStream = function(event) {
console.log("Remote track added:" + JSON.stringify(event));
this.videoElement.srcObject = event.stream;
var promise = this.videoElement.play();
if (promise !== undefined) {
promise.catch((error) => {
console.warn("error:"+error);
this.videoElement.setAttribute("controls", true);
});
}
}
/*
* AJAX /call callback
*/
WebRtcStreamer.prototype.onReceiveCall = function(dataJson) {
console.log("offer: " + JSON.stringify(dataJson));
var descr = new RTCSessionDescription(dataJson);
this.pc.setRemoteDescription(descr).then(() => {
console.log ("setRemoteDescription ok");
while (this.earlyCandidates.length) {
var candidate = this.earlyCandidates.shift();
this.addIceCandidate(this.pc.peerid, candidate);
}
this.getIceCandidate()
}
, (error) => {
console.log ("setRemoteDescription error:" + JSON.stringify(error));
});
}
/*
* AJAX /getIceCandidate callback
*/
WebRtcStreamer.prototype.onReceiveCandidate = function(dataJson) {
console.log("candidate: " + JSON.stringify(dataJson));
if (dataJson) {
for (var i=0; i<dataJson.length; i++) {
var candidate = new RTCIceCandidate(dataJson[i]);
console.log("Adding ICE candidate :" + JSON.stringify(candidate) );
this.pc.addIceCandidate(candidate).then( () => { console.log ("addIceCandidate OK"); }
, (error) => { console.log ("addIceCandidate error:" + JSON.stringify(error)); } );
}
this.pc.addIceCandidate();
}
}
/*
* AJAX callback for Error
*/
WebRtcStreamer.prototype.onError = function(status) {
console.log("onError:" + status);
}
return WebRtcStreamer;
})();
if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
window.WebRtcStreamer = WebRtcStreamer;
}
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = WebRtcStreamer;
}