1、前言
最近公司做的一个项目需求是实现多人视频会议聊天,查阅资料,决定使用HTML5新支持的WebRtc来作为视频通讯。客户端使用支持HTML5浏览器即可,如chrome,服务器段需要提供两个主要的服务功能,一个是信令服务器(Signaling Server),一个是NAT穿透服务器(ICE Server)。信令服务器使用websocket来实现业务上的功能,如呼叫,加入会议,挂断等等,NAT穿透服务器使用chrome内置的
const peerConnectionConfig = {
iceServers: [
{url: 'stun:stun.l.google.com:19302' }
]
};
简单的框架图如下:
2、webrtc优势和应用场景:
优势:
1 跨平台(Web、Windows、MacOS、Linux、iOS、Android)
2 实时传输
3 音视频引擎
4 免费、免插件、免安装
5 主流浏览器支持
6 强大的打洞能力
应用场景:
在线教育、在线医疗、音视频会议、即时通讯工具、直播、共享远程桌面、P2P网络加速、游戏(狼人杀、线上KTV)等。
3、关于WebRTC 的一些基本概念
传统的视频推流的技术实现一般是这样的:客户端采集视频数据,推流到服务器上,服务器再根据具体情况将视频数据推送到其他客户端上,类似直播 但是 WebRTC 却截然不同,它可以在客户端之间直接搭建基于 UDP 的数据通道,经过简单的握手流程之后,可以在不同设备的两个浏览器内直接传输任意数据。 这其中的流程包括:
1:打开本地自己的摄像头(目的是本地创建视频流,这个视频流是要发送给对方的)
2:初始化双方链接的基础创建PeerConnection
3:开始创建链接基础信息,这一步就开始正式建立和对方的通信基础(创建完成后本地保存一份offer)
4:A 创建 offer 信息后,先调用 setLocalDescription 存储本地 offer 描述,再将其发送给 B。
B 收到 offer 后,先调用 setRemoteDescription 存储远端 offer 描述;
然后又创建 answer 信息,同样需要调用 setLocalDescription 存储本地 answer 描述,再返回 给 A
A 拿到 answer 后,再次调用 setRemoteDescription 设置远端 answer 描述。
5:监听ICE候选信息(这个过程是在发送offer和创建应答之间的)
6:成功创建一个 WebRTC 连接
【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~
4、创建一个 RTCPeerConnection
RTCPeerConnection 对象是 WebRTC API 的入口,它负责创建、维护一个 WebRTC 连接,以及在这个连接中的数据传输。目前新版本的浏览器大都支持了这一对象,但是由于目前 API 还不稳定,所以需要加入各个浏览器内核的前缀,例如 Chrome 中我们使用 webkitRTCPeerConnection 来访问它。 首先我们的目标是在同一个页面中创建两个实时视频,一个的数据直接来自你的摄像头,另一个的数据来自本地创建的 WebRTC 连接。看起来是这样的:
<video id="yours" autoplay></video> <video id="theirs" autoplay></video>
下面我们创建一个 main.js 文件,先封装一下各浏览器的 userMedia 和 RTCPeerConnection 对象:
function hasUserMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerConnection;
return !!window.RTCPeerConnection;
}
然后我们需要浏览器调用系统的摄像头 API getUserMedia 获得媒体流,注意要打开浏览器的摄像头限制。Chrome由于安全的问题,只能在 https 下或者 localhost 下打开摄像头。下面我会介绍如何在开发中如何使用http在本地进行调试
var yourVideo = document.getElementById("yours");
var theirVideo = document.getElementById("theirs");
var yourConnection, theirConnection;
if (hasUserMedia()) {
navigator.getUserMedia({ video: true, audio: false },
stream => {
yourVideo.srcObject = stream;
if (hasRTCPeerConnection()) {
// 稍后我们实现 startPeerConnection
startPeerConnection(stream);
} else {
alert("没有RTCPeerConnection API");
}
},
err => {
console.log(err);
}
)
}else{
alert("没有userMedia API")
}
接下来需要去连接信令服务器
let wsConnection = new WebSocket(url);
wsConnection.onopen = function () {
console.log("信令服务器连接成功");
resolve(wsConnection);
}
wsConnection.onclose = function (evt) {
message.error("连接异常,请关闭页面重新进入")
};
wsConnection.onerror = function (evt) {
console.log("socket error", evt);
};
wsConnection.onmessage = function (evt) {
// 收到信令服务器发过来的信息去进行指定操作
}
没有意外的话,现在应该能在页面中看到一个视频了。
下一步是实现 startPeerConnection 方法,建立传输视频数据所需要的 ICE 通信路径,这里我们以 Chrome 为例:
function startPeerConnection(stream) {
//这里使用了几个公共的stun协议服务器
var config = {
'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
};
yourConnection = new RTCPeerConnection(config);
theirConnection = new RTCPeerConnection(config);
yourConnection.onicecandidate = function(e) {
if (e.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onicecandidate = function(e) {
if (e.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
}
我们使用这个函数创建了两个连接对象,在 config 里,你可以任意指定 ICE 服务器,虽然有些浏览器内置了默认的 ICE 服务器,可以不用配置,但还是建议加上这些配置。下面,我们进行 SDP 的握手。 由于是在同一页面中进行的通信,所以我们可以直接交换双方的 candidate 对象,但在不同页面中,可能需要一个额外的服务器协助这个交换流程。
5、建立 SDP Offer 和 SDP Answer
function startPeerConnection(stream) {
var config = {
'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.l.google.com:19302' }]
};
yourConnection = new RTCPeerConnection(config);
theirConnection = new RTCPeerConnection(config);
yourConnection.onicecandidate = function(e) {
if (e.candidate) {
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
theirConnection.onicecandidate = function(e) {
if (e.candidate) {
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));
}
}
//本方产生了一个offer
yourConnection.createOffer().then(offer => {
yourConnection.setLocalDescription(offer);
//对方接收到这个offer
theirConnection.setRemoteDescription(offer);
//对方产生一个answer
theirConnection.createAnswer().then(answer => {
theirConnection.setLocalDescription(answer);
//本方接收到一个answer
yourConnection.setRemoteDescription(answer);
})
});
}
和 ICE 的连接一样,由于我们是在同一个页面中进行 SDP 的握手,所以不需要借助任何其他的通信手段来交换 offer 和 answer,直接赋值即可。如果需要在两个不同的页面中进行交换,则需要借助一个额外的服务器来协助,可以采用 websocket 或者其它手段进行这个交换过程。
6、加入视频流
现在我们已经有了一个可靠的视频数据传输通道了,下一步只需要向这个通道加入数据流即可。WebRTC 直接为我们封装好了加入视频流的接口,当视频流添加时,另一方的浏览器会通过 onaddstream 来告知用户,通道中有视频流加入。
yourConnection.addStream(stream);
theirConnection.onaddstream = function(e) {
theirVideo.srcObject = e.stream;
}
7、最后效果
8、视频录制上传到服务器
因为公司项目需要对双方视频的内容进行复听,所以就有了将视频录制上传到服务器上,现在主要讲如何实现的
主要是通过这个api来实现 MediaRecorder(),这个方法里面会实时返回对应的流,我们只需要存起来在停止录制的时候进行转换上传到服务器
两个方法start、stop
start()
开始录制媒体,可以设置一个参数,录制的媒体会按照设置的值进行分割成一个个单独的区块, 而不是以默认的方式录制一个非常大的整块内容。
stop()
停止录制,同时触发dataavailable事件,返回一个存储Blob内容的录制数据
ondataavailable
录制结束时同时触发,事件对象中返回录制的媒体数据
获取用户媒体权限,创建媒体录制对象:
//获取用户媒体权限,视频的话参数{audio: true, video: true}
navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {
//创建媒体录制对象
this.recorder = new window.MediaRecorder(stream);
this.bindEvents();
}, error => {
alert('出错,请确保已允许浏览器获取录音权限');
});
视频录制,将流存到this.chunks
bindEvents () {
this.recorder.ondataavailable = this.getRecordingData;
},
getRecordingData (e) {
//录制的数据
this.chunks.push(e.data);
},
//视频的话:保存视频数据
saveRecordingData () {
let blob = new Blob(this.chunks, { 'type' : 'video/webm' }),
videoStream = URL.createObjectURL(blob);
this.chunkList.push({stream: videoStream});
this.chunks = [];
}
上传到服务器,通过formData的方式将blob对象上传到服务器,以上就是整个实现方式了。
9、总结
问题1:Chrome由于安全的问题,只能在 https 下或者 localhost 下打开摄像头。那如何在内网的多台电脑上调试呢
解决1:chrome快捷方式右键->属性->目标 输入框中追加 --unsafely-treat-insecure-origin-as-secure="http://10.250.199.175:8881" ip地址换成自己对应的ip 地址
问题2:在视频通话中存在回声,会有自己说出去的声音,解决方案需要将video标签静音通过muted 属性
问题3:在视频录制上传中,需要在每次上传完后清空存储流的数据,否则上传到服务器文件会很大