WebRtc实现多人视频会议以及视频录制上传

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:在视频录制上传中,需要在每次上传完后清空存储流的数据,否则上传到服务器文件会很大

原文链接:WebRtc实现多人视频会议以及视频录制上传 - 掘金

猜你喜欢

转载自blog.csdn.net/irainsa/article/details/129992888