WebRTC-H5视频通话
什么是webRTC?
Real-Time Communication (RTC) 是一种专为实现跨Web浏览器实时音频、视频及数据传输而设计的标准协议集合。 RTC 通过提供一组 API 和协议体系框架,在开发者的应用中实现了端到端的实时通信连接,并完全去除了对第三方插件或应用程序的需求。其核心在于利用网页浏览器内置的音频、视频以及数据传输通道机制,在保障用户体验的同时实现了高效的音视频流传输与数据传输过程。 RTC 主要特性与功能包括:
实时音视频通信方面:WebRTC能够实现实时语音通话与视频通话,并实现不同浏览器之间的音频与视频流直接传输。数据传输方面:WebRTC提供了一个可靠的DataChannel数据传输通道(DataChannel),能够高效地传递各种类型的媒体数据。点对点通信目标之一是通过即时建立连接实现无服务器转发式的实时沟通方式。安全性方面:WebRTC内置了双重保障机制——加密技术和身份验证技术——以确保通讯过程中的机密性和完整性。跨平台支持方面:WebRTC具备广泛的兼容性,在桌面浏览器、移动浏览器以及各类移动应用中均可实现无缝式实时通信
WebRTC 在各个实时通信服务、视频会议服务、在线教育平台以及物联网设备之间实现了无缝连接与数据传输。它通过提供丰富的工具和技术规范大大简化了开发者的开发流程并提升了系统的稳定性。
音视频采集
详细信息请参考以下链接:https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/UserMedia
WebRTC通过getUserMedia来获取对应于摄像头和麦克风的媒体流对象MediaStream。这些媒体流能够通过WebRTC进行传输,并被其他对等端接收。将该 MediaStream 赋给视频元素的 srcObject 以实现本地音频视频播放。

function getUserMedia() {
navigator.mediaDevices.getUserMedia({
video: {facingMode: facingMode},
audio: true
}).then(stream => {
localStream = stream
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = stream;
});
}
连接管理
未采用原生webrtc 进行管理连接而选择使用 peerJs 组件来实现管理功能的原因在于原生API操作相对繁琐;通过采用 peerJs 组件能够显著简化开发流程并提升应用效率
什么是 PeerJS?
该平台的主要资源位置位于(peerjs)的官方网站上,请访问peerjs以获取更多信息
GitHub上的具体项目位置位于(peers)项目页面,请访问github-peers查看详细信息
建立在 WebRTC 技术基础之上, PeerJS 是一个用于实现 point-to-point 通信的功能库,旨在简化实现过程中的 point-to-point 通信.作为一项支持浏览器之间实时音频和视频通信的标准方案,WebRTC 以其开放性和广泛适用性著称,但其实现细节往往需要较高的技术门槛.而 PeerJS 则通过提供更高层次的抽象界面来简化开发流程,从而帮助开发者更加便捷地构建基于 point-to-point 通信的应用程序. PeerJS 提供了以下功能和特性:
- 轻量级的API:PeerJS提供了一个轻量级的API接口,显著简化了对等连接(peer connection)的相关操作流程。
- 自动化协作功能:PeerJS内置了一个协作服务器,能够自动协调团队成员之间的协作工作,使团队协作更加高效。
- 稳定的数据传输:该解决方案通过可靠的网络协议实现了数据传输过程中的端到端防护,确保数据传输的安全性和可靠性。
- 广泛兼容性: PeerJS 支持主流浏览器环境,并且提供了丰富的跨平台功能,包括桌面应用和移动应用版本。
PeerJS 的使用示例:
// 创建 Peer 对象
const peer = new Peer('my-peer-id', {
host: 'peerjs-server.com',
port: 9000,
path: '/myapp'
});
// 监听连接建立事件
peer.on('open', id => {
console.log('Connected with ID:', id);
});
// 建立对等连接
const conn = peer.connect('another-peer-id');
// 监听连接打开事件
conn.on('open', () => {
conn.send('Hello from peer 1!');
});
// 监听接收到消息事件
conn.on('data', data => {
console.log('Received:', data);
});
peerjs-server
PeerJS 服务器是 PeerJS 库基于的通讯服务器。它辅助创建与维护对等连接。 github 地址:[github.com/peers/peerj…](https://link.zhihu.com/?target=https%3A//link.juejin.cn/%3Ftarget%3Dhttps%252F%252Fgithub.com%252Fpeers%252Fpeerj-server "github.com/peers/peerj…”)
PeerJS Server 可以部署在您的本地服务器上, 或者您可以选择使用 PeerJS 提供的公共 signaling server. 该服务免费提供给开发者使用, 但如果希望拥有更高的灵活性与控制权, 您也可以自行托管一个 signaling server. 使用 PeerJS Server 的主要优势在于: 其应用架构设计具有高度灵活, 开发过程中可以直接拖放所需的组件进行集成, 支持模块化扩展性, 并基于现代后端框架快速构建高性能应用. 此外, 该平台还支持多种主流通信协议的选择与配置, 并且能够方便地集成到现有系统中.
- 信令交换:请阐述 PeerJS Server 在搭建过程中是如何负责传输各类信令数据的?具体包括哪些元数据信息、ICE选项以及 SDP 协议。
- 中继服务器:通常会遇到无法直接完成对等连接的情况,在这种情况下如何进行中转?请说明 PeerJS Server 如何作为中间设备实现这一功能。
- 认证和安全性:从安全性和认证角度来看,请问 PeerJS Server 是否具备相关的安全机制?这些机制如何保障用户身份验证与数据完整性?
如何使用 PeerJS Server:
- 通过 PeerJS 提供的一个默认的公共信力服务器实现通信功能。
- 安装指定参数
peer -g后运行以下命令:peerjs --port 9000 --key peerjs --path /myapp - 您也可以下载 PeerJS Server 的源代码并自行部署于自己的服务器上。该服务器基于 Node.js 开发环境,请按照官方文档指导完成配置与部署操作。
PeerJS Server 的源代码位于 PeerJS 官方 GitHub 仓库中,请您在此处获取 PeerJS Server 的完整技术文档和配置指导手册。特别提示:如果您决定采用 PeerJS 提供的默认公共信令服务器,请务必仔细阅读并遵守 PeerJS 使用协议条款,并确保该服务器配置完全符合您的特定应用场景需求。
广域网连接
基于NAT(网络地址转换)技术和防火墙机制的影响,在两个独立设备之间建立直接的实时通信连接通常面临诸多挑战。为了克服这一局限性,WebRTC创新性地采用了‘穿洞技术’方案。该方案通过引入中间服务器节点作为通信中继节点,在两端设备间架起了一条跨越障碍的信息传输通道。具体而言,在穿洞服务架构下:首先通过穿透层协议与目标服务器进行对接;其次借助控制平面发起穿孔请求;随后由数据平面自动完成穿孔过程并完成数据转发;最后通过应用层协议将处理结果反馈至源端设备完成整个流程
- 两台机器分别向打洞服务发送了连通请求。
- 该服务接收并传输了两台机器提供的连通参数。
- 两台机器试图建立直连通信,在网络 NAT 和防火墙设置下构建映射以实现两者间的连通。
- 当直连通信受阻时,则两台机器将转而使用中继服务器进行交流。
打洞服务一般依赖一些技术手段来建立这种直接连接,在实际应用中被广泛采用。这些技术包括STUN(会话遍历实用工具)和TURN(中继通信)。其中,STUN协议负责获取设备的公共IP地址和端口号信息;而TURN协议则在直接连接不通时提供备用中继服务器以维持通信路径。在WebRTC应用中实现实时通信方面发挥着关键作用,在实际操作过程中需要特别注意的是,在WebRTC连接建立过程中起辅助作用的是其他组件而不是核心的技术方案本身。The actual data transmission remains a direct point-to-point connection.
免费分享
免费分享
免费提供

示例Demo
使用webRTC实现一个1V1网页视频通话
引入PeerJs组件
此处采用CDN形式进行资源引入,并可参考官方文档以了解其他资源加载方式
规范化的API:adapter-latest.js通过支持规范化的API接口让开发者能够以统一的方式访问WebRTC API,在包括Chrome、Firefox、Safari在内的所有WebRTC兼容的浏览器中都能方便使用。
自动适配功能:adapter-latest.js可以根据不同浏览器的类型和版本自动适应相应的WebRTC API实现细节从而消除不同浏览器之间的差异。
Polyfill功能:adapter-latest.js为无法实现某些WebRTC API的浏览器提供了Polyfill功能通过模拟这些API的行为确保在这些情况下也能达到良好的兼容性。
adapter-latest.js 直接引入就OK了。
在前后摄像头之间进行切换时(即从前置摄像头切换至后置摄像头或反之),需要重新获取视频流并替换原有的视频流。通过 facingMode 属性控制前后摄像头(其中 facingMode :user表示前置摄像头;environment表示后置摄像头),关键步骤包括:调用replaceTrack()函数以完成当前正在使用的轨道的替换操作(详情请参阅开发者文档:developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender)。
stream.getVideoTracks().forEach(track => {
call.peerConnection.getSenders().forEach((sender) => {
if (sender.track.kind === 'video') {
sender.replaceTrack(track).then(() => console.log('replace'));
}
});
})
ios 微信自带浏览器上webrtc不能正常使用的解决方法
解决方法:
window.onload = () => {
document.addEventListener("WeixinJSBridgeReady", function () {
document.getElementById("localVideo").play();
}, false);
}
<video id="localVideo" preload="auto" autoplay="autoplay"
x-webkit-airplay="true"
playsinline="true" webkit-playsinline="true" x5-video-player-type="h5" x5-video-player-fullscreen="true"
x5-video-orientation="portraint" muted></video>
值得注意的是,在我们开发过程中可能会遇到一个关键问题:WeixinJSBridgeReady这个事件会在页面加载后马上触发(该事件会在页面加载后立即触发)。因此,在我们开发过程中可能会遇到一个关键问题:上面的这个代码最好写在window.οnlοad=>(){}函数体中(因此,在我们开发过程中可能会遇到一个关键问题:上面的这个代码最好写在window.onload(){}函数体内)。这段代码最好放置于window.onload(){}函数体内(所以视频标签也需要提前放置于html网页中的适当位置)。因此,在html网页中需要提前放置视频标签(不要等到webrtc通道建立后再动态创建视频)。)
完整代码
<!DOCTYPE html>
<html lang="zh">
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<head>
<title>WebRTC 视频通话演示</title>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
</head>
<body>
<h1>WebRTC 视频通话演示</h1>
<div>
<p>
我的peerId:<span id="myPid"></span>
</p>
<label for="remotePeerIdInput">对方 Peer ID:</label>
<input type="text" id="remotePeerIdInput">
<button id="callButton">呼叫</button>
<button id="hangupButton">挂断</button>
<button id="muteButton">静音</button>
<button id="switchButton">切换摄像头</button>
</div>
<div style="display: flex">
<div style="background: aquamarine">
<h2>本地视频</h2>
<video id="localVideo" width="720" height="240" width="100%" height="300" preload="auto" autoplay="autoplay"
x-webkit-airplay="true"
playsinline="true" webkit-playsinline="true" x5-video-player-type="h5" x5-video-player-fullscreen="true"
x5-video-orientation="portraint" muted></video>
</div>
<div style="background: bisque">
<h2>远程视频</h2>
<video id="remoteVideo" width="720" height="240" autoplay></video>
</div>
</div>
<script src="https://unpkg.com/peerjs@1.4.7/dist/peerjs.min.js"></script>
<script>
let peer = null;
// 处理来自远程 Peer 的呼叫请求
let call;
let dataConn;
// 初始化摄像头方向为前置
let facingMode = 'user';
let localStream = null
const localVideo = document.getElementById('localVideo');
const init = () => {
// 初始化 Peer 对象,指定唯一的 ID
peer = new Peer({
/* host: "xxxxxxxx",
path: "/peerjswss/myapp",
port: 443,
debug: 2,
secure: true,*/
});
// 输出 Peer ID 到控制台
peer.on('open', function (peerId) {
console.log('我的 Peer ID 是: ' + peerId);
document.getElementById("myPid").innerText = peerId
});
peer.on('connection', (conn) => {
dataConn = conn
conn.send("连接成功...............")
conn.on('data', function (data) {
console.log('被呼叫方-接收消息--》', data);
if (data === "挂断") {
console.log("被呼叫方信息——》对方挂断电话")
call.close()
call = null
dataConn.close()
dataConn = null
console.log("call", call)
console.log("dataConn", dataConn)
// 清空远程视频元素
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
alert("对方已挂断............")
}
});
});
peer.on('call', async incomingCall => {
console.log("监听呼叫")
// 如果已有呼叫正在进行中,则拒接新的呼叫
if (call && call.open) {
console.log('拒绝新的呼叫');
incomingCall.answer();
incomingCall.close();
return;
}
// 显示确认对话框,以便用户确定是否接听呼叫
const confirmed = window.confirm('来自 ' + incomingCall.peer + ' 的呼叫。是否接听?');
if (!confirmed) {
console.log('呼叫被用户拒绝');
incomingCall.answer();
incomingCall.close();
return;
}
await getUserMedia()
// 接听呼叫,并发送本地媒体流
console.log('接听呼叫');
call = incomingCall;
incomingCall.answer(localStream);
// 处理来自远程 Peer 的媒体流
incomingCall.on('stream', remoteStream => {
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = remoteStream;
});
// 处理呼叫关闭事件
incomingCall.on('close', () => {
console.log('呼叫被远程 Peer 关闭');
if (call && call.open) {
console.log('关闭已有呼叫');
call.close();
call = null;
}
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
});
});
}
// 获取用户媒体流的函数,根据facingMode参数获取不同摄像头的流
function getUserMedia() {
return new Promise((resolve, reject) => {
// 检查浏览器是否支持 getUserMedia API
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert("getUserMedia is not supported");
reject(new Error("getUserMedia is not supported"));
}
navigator.mediaDevices.getUserMedia({
video: {
// enviroment 后置 | user 前置摄像头
facingMode: facingMode,
// 视频帧率
frameRate: 30,
},
audio: {}
}).then(stream => {
resolve(stream);
localStream = stream
localVideo.srcObject = stream;
// 如果已经存在call,则需要将新的流替换到call中
if (call) {
console.log("---> ", call.peerConnection.getSenders())
stream.getVideoTracks().forEach(track => {
call.peerConnection.getSenders().forEach((sender) => {
if (sender.track.kind === 'video') {
sender.replaceTrack(track).then(() => console.log('replace'));
}
});
})
}
}).catch(error => {
reject(error);
});
})
}
// 切换摄像头的函数
function switchCamera() {
// 切换facingMode
facingMode = facingMode === 'user' ? 'environment' : 'user';
// 重新获取用户媒体流
getUserMedia();
}
// 在需要切换摄像头的地方调用switchCamera函数,例如:
const switchButton = document.getElementById('switchButton');
switchButton.addEventListener('click', switchCamera);
// 当点击“呼叫”按钮时,向远程 Peer 发起呼叫请求
const callButton = document.getElementById('callButton');
const remotePeerIdInput = document.getElementById('remotePeerIdInput');
callButton.addEventListener('click', async () => {
const remotePeerId = remotePeerIdInput.value.trim();
if (!remotePeerId) return;
await getUserMedia()
// 如果已有呼叫正在进行中,则关闭它,并发起新的呼叫请求
if (call && call.open) {
console.log('关闭已有呼叫');
call.close();
}
dataConn = peer.connect(remotePeerId);
dataConn.on('open', function () {
// Send messages
dataConn.send('我要给你打电话了............!');
// Receive messages
dataConn.on('data', function (data) {
console.log('呼叫方接收消息--》', data);
if (data === "挂断") {
console.log("呼叫方接收消息->对方挂断电话")
call.close()
call = null
dataConn.close()
dataConn = null
console.log("call", call)
console.log("dataConn", dataConn)
// 清空远程视频元素
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
alert("对方已挂断............")
}
});
});
// 创建新的呼叫请求,并注册事件监听器
console.log('发起呼叫', localStream);
call = peer.call(remotePeerId, localStream);
console.log("call-->", call)
// 处理来自远程 Peer 的媒体流
call.on('stream', remoteStream => {
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = remoteStream;
});
// 处理呼叫关闭事件
call.on('close', () => {
console.log('呼叫被远程 Peer 关闭');
if (call && call.open) {
console.log('关闭已有呼叫');
call.close();
call = null;
}
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
// 停止本地媒体流
localStream.getTracks().forEach(track => track.stop());
localVideo.srcObject = null;
});
// 处理呼叫错误事件
call.on('error', error => {
console.error('呼叫错误:', error);
});
});
// 当点击“挂断”按钮时,关闭呼叫并停止本地媒体流
const hangupButton = document.getElementById('hangupButton');
hangupButton.addEventListener('click', () => {
dataConn.send("挂断")
// 如果已有呼叫正在进行中,则关闭它
if (call && call.open) {
console.log('挂断呼叫');
call.close();
call = null;
}
// 停止本地媒体流
localStream.getTracks().forEach(track => track.stop());
localVideo.srcObject = null;
// 清空远程视频元素
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
});
// 当点击“静音”按钮时,切换音频静音状态
const muteButton = document.getElementById('muteButton');
muteButton.addEventListener('click', () => {
const audioTracks = localStream.getAudioTracks();
audioTracks.forEach(track => {
track.enabled = !track.enabled;
});
muteButton.innerText = audioTracks[0].enabled ? "静音" : "取消静音";
});
init()
</script>
</body>
</html>
