webrtc端对端音视频连接

WebRTC 全称为:Web Real-Time Communication。它是为了解决 Web 端无法捕获音视频的能力,并且提供了 peer-to-peer(就是浏览器间)的视频交互。实际上,细分看来,它包含三个部分:

MediaStream:捕获音视频流
RTCPeerConnection:传输音视频流(一般用在 peer-to-peer 的场景)
RTCDataChannel: 用来上传音视频二进制数据(一般用到流的上传)

MediaStream用于获取摄像头,麦克风,或屏幕图像。

var constraints = {
        audio: true,
        video: true
    }
    navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())

这是一个异步方法,获取设备成功之后执行回调函数handleSuccess,这个函数的参数是一个音视频流,在回调函数内部实现将音视频流放进video标签播放。

function handleSuccess(stream) {
        document.getElementById("video").srcObject = stream
        }

这就是最基本的获取音视频图像在浏览器进行播放。

RTCPeerConnection 用于建立连接进行UDP音视频传输(在用STUN服务器的情况下),在STUN防火墙打洞失败的情况下就需要使用TCP传输(需要TURN服务器作为中继进行转发),当然这些操作不需要自己来进行,webtrc的API已经帮我们实现,那就是ICE。
ICE:说白了ICE就是帮你选出你能支持的传输方式(可能有多种,会全部发送给对端,由对端根据自己也支持的传输方式选择最优的传输方式,这个过程就叫做网络协商),但是需要配置ICE的STUN服务器地址,和TURN的服务器地址。

除了网络协商还有媒体协商SDP,媒体协商就是告诉双方自己所支持的视频编解码能力,媒体格式等信息。这个过程webrtc的API也已经进行了封装,拿点对点传输来说,A方获取自己的SDP信息,设置自己的本地SDP,然后发送给B端(这个过程A端作为发起方,发送的是offer),B端收到A端的SPD(offer),B端保存对端的SPD,然后B端获取自己的SDP,保存自己的本地SPD,然后B端对A端的offer进行回应(就是将B端自己的SDP告诉A端,这个回应就是answer,其实也就是SDP),这时A端收到B端的回应,A端就会保存B端的SDP。
至此媒体协商完成。

上述的两个过程不分先后,但是都需要websocket作为中间服务器进行对话传输,实现方式有很多种,我是用python实现django+channels实现websocket

前端代码如下(没有写过多的逻辑控制,所以需要先打开接收端,保证websocket在传输发起方的信息时房间里有接收方。)

我的websocket服务会转发自己的消息到自己(用于聊天室),所以这里加了判断,如果不判断,webrtc协商时会造成自己和自己协商。

接收端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>接收端</title>
</head>
<body>
<video id="video" autoplay style="height: 600px;width: 800px" muted></video>
<video id="video2" autoplay style="height: 600px;width: 800px"></video>
<script>

    //生成唯一id,用于websocket判断是否是自己
    //生成唯一ID
    function guid() {
        return Number(Math.random().toString().substr(3, 3) + Date.now()).toString(36);
    }

    var id = guid();
    //首先建立websocket连接
    socket = new WebSocket("ws://这里就用自己的websocket服务器地址/");
    socket.onopen = onOpen;
    socket.onclose = onClose;

    // 连接成功
    function onOpen() {
        console.log("websocket连接成功")
    }

    function onClose() {
        console.log("websocket已经断开")
    }

    function sendCandidate(ICE) {
        socket.send(JSON.stringify({
            'message': {'ICE': ICE, 'ID': id}
        }))
        console.log("B端发送ICE到服务器")
    }

    function sendAnswer(answer) {
        socket.send(JSON.stringify({
            'message': {'SDP': answer, 'ID': id}
        }))
        console.log("B端收到A端offer(SDP),回发answer(SDP)成功")
    }

    socket.onmessage = function (e) {
        const data = JSON.parse(e.data).message;
        if (data.ID != id) {
            try {
                var ice = data.ICE
                remoteConnection.addIceCandidate(new RTCIceCandidate(ice))
                console.log("收到远端ICE候选,并添加")
            } catch (e) {
                var sdp = data.SDP
                let desc = new RTCSessionDescription(sdp)
                remoteConnection.setRemoteDescription(desc).then(function () {
                    remoteConnection.createAnswer().then((answer) => {
                        remoteConnection.setLocalDescription(answer).then(sendAnswer(answer))
                    })
                })
            }
        }
    }
    var iceServer = {
        iceServer: [
            {url: "stun:stun.l.google.com:19302"},//谷歌的公共服务器
        ]
    }
    var remoteConnection
    var constraints = {
        audio: true,
        video: true
    }
    navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())

    function handleSuccess(stream) {
        document.getElementById("video").srcObject = stream
        remoteConnection = new RTCPeerConnection(iceServer)
        remoteConnection.addStream(stream)
        remoteConnection.onaddstream = function (e) {
            console.log('获得发起方的视频流' + e.stream)
            document.getElementById("video2").srcObject = e.stream
        }
        remoteConnection.onicecandidate = function (event) {
            if (event.candidate) {
                sendCandidate(event.candidate)
            }
        }
    }
</script>
</body>
</html>

发起端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>发起端</title>
</head>
<body>
<video id="video" autoplay style="height: 600px;width: 800px" muted></video>
<video id="video2" autoplay style="height: 600px;width: 800px"></video>
<script>
    //生成唯一id,用于websocket判断是否是自己
    //生成唯一ID
    function guid() {
        return Number(Math.random().toString().substr(3, 3) + Date.now()).toString(36);
    }
    var id = guid();
    //首先建立websocket连接
    socket = new WebSocket("ws://这里就用自己的websocket服务器地址/");
    socket.onopen = onOpen;
    socket.onclose = onClose;

    // 连接成功
    function onOpen() {
        console.log("websocket连接成功")
    }

    // 关闭连接
    function onClose() {
        console.log("websocket已经断开")
    }

    //发送ICE
    function sendCandidate(ICE) {
        //获取到本地ice之后发送到websocket
        socket.send(JSON.stringify({
            'message': {'ICE': ICE, 'ID': id}
        }))
        console.log("A端发送ICE到服务器")
    }

    //发送SDP
    function sendOffer(offer) {
        //设置完本地的SDP之后发送到websocket
        socket.send(JSON.stringify({
            'message': {'SDP': offer, 'ID': id}
        }))
        console.log("A端发送offer(SDP)到服务器")
    }

    // 收到消息,判断消息类型是ICE还是SDP
    socket.onmessage = function (e) {
        const data = JSON.parse(e.data).message;
        if (data.ID != id) {
            try {
                var ice = data.ICE
                localConnection.addIceCandidate(new RTCIceCandidate(ice))
            } catch (e) {
                var sdp = data.SDP
                let desc = new RTCSessionDescription(sdp) //构建SDP对象
                localConnection.setRemoteDescription(desc).then(() => { //设置远端SDP
                    console.log('端对端连接成功')
                })
            }
        }
    }

    var iceServer = {
        iceServer: [
            {url: "stun:stun.l.google.com:19302"},//谷歌的公共服务器
        ]
    }
    var localConnection
    var constraints = {
        audio: true,
        video: true
    }
    navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(console.log())

    function handleSuccess(stream) {
        document.getElementById("video").srcObject = stream
        localConnection = new RTCPeerConnection(iceServer) //获取连接对象 ,应填写stun服务器
        localConnection.addStream(stream) //将本地流添加进连接对象
        localConnection.onaddstream = function (e) { //获得对方流触发函数
            console.log('获得应答方的视频流' + e.stream)
            document.getElementById("video2").srcObject = e.stream
        }
        localConnection.onicecandidate = function (event) {  // 用来寻找合适的ICE,获得合适的ICE时触发
            if (event.candidate) {
                sendCandidate(event.candidate) // 通过websocket将ICE发送给端,自己实现的函数
                console.log(event.candidate)
            }
        }
        localConnection.createOffer().then((offer) => { //创建offer 成功之后更改与对端关联的本地描述(SDP),包括本端属性包括媒体格式
            localConnection.setLocalDescription(offer).then(sendOffer(offer)) // 之后发送SDP信息到对端,sendOffer自己实现
        })
    }
</script>
</body>
</html>

这个过程用一张图来说明就很直观了
在这里插入图片描述
在我使用的例子中STUN服务器用的是免费的,如需要TURN服务器可以用coturn实现(coturn包含了STUN和TURN实现)。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值