webrtc end-to-end audio and video connection

The full name of WebRTC is : Web Real-Time Communication. It is to solve the ability of the Web terminal to capture audio and video, and provides peer-to-peer (that is, between browsers) video interaction. In fact, the breakdown looks like it contains three parts:

MediaStream: capture audio and video streams
RTCPeerConnection: transmit audio and video streams (usually used in peer-to-peer scenarios)
RTCDataChannel: used to upload audio and video binary data (usually used to upload streams)

MediaStream is used to obtain camera, microphone, or screen images.

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

This is an asynchronous method. The callback function handleSuccess is executed after the device is successfully obtained. The parameter of this function is an audio and video stream. The audio and video stream is put into the video tag to play in the callback function.

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

This is the most basic way to obtain audio and video images for playback in the browser.

RTCPeerConnection is used to establish a connection for UDP audio and video transmission (in the case of a STUN server). In the case of a STUN firewall failure, TCP transmission is required (the TURN server is required as a relay for forwarding). Of course, these operations are not required Do it yourself, webtrc's API has been implemented for us, that is ICE.
ICE : To put it bluntly, ICE is to help you choose the transmission method you can support (there may be multiple, all of which will be sent to the opposite end, and the opposite end selects the best transmission method according to the transmission method it supports. This process is called Network negotiation), but need to configure the STUN server address of ICE and the server address of TURN.

In addition to network negotiation, there is also media negotiation SDP. Media negotiation is to inform both parties about their supported video codec capabilities, media format and other information. The webrtc API of this process has also been encapsulated. Take point-to-point transmission as an example. Party A obtains its own SDP information, sets its own local SDP, and then sends it to end B (in this process, end A acts as the initiator and sends an offer) , End B receives the SPD (offer) from end A, end B saves the SPD of the opposite end, then end B obtains its own SDP, saves its own local SPD, and then end B responds to end A's offer (that is, end B's own SDP Tell end A that this response is answer, which is actually SDP). When end A receives the response from end B, end A will save the SDP of end B.
So far the media negotiation is complete.

The above two processes are in no particular order, but both require websocket as an intermediate server for dialogue transmission. There are many ways to implement it. I use python to implement django+channels to implement websocket .

The front-end code is as follows (do not write too much logic control, so you need to open the receiver first to ensure that there is a receiver in the room when the websocket transmits the information of the initiator.)

My websocket service will forward its own messages to itself (used in chat rooms), so judgment is added here. If no judgment is made, webrtc will cause itself to negotiate with itself.

Receiving end

<!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>

Initiator

<!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>

This process is very intuitive to illustrate with a picture.
Insert picture description here
In the example I used, the STUN server is free. If you need a TURN server, you can use coturn to implement it (coturn includes STUN and TURN implementations).

Guess you like

Origin blog.csdn.net/weixin_44784018/article/details/105729994