认识WebRTC

414 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

WebRTC是什么

WebRTC是一种Web实时通信技术,它允许在两个设备之间进行实时,完全点对点的通信,使Web应用程序和站点能够捕获和选择性地流式传输音频和视频媒体,以及在浏览器之间交换任意数据,而无需中介。

WebRTC的用途

WebRTC有多种用途,与媒体捕获和流 API一起,它们为 Web 提供了强大的多媒体功能,包括对音频视频会议文件交换屏幕共享身份管理以及与传统电话系统接口的支持,包括支持发送 DTMF(按键拨号)信号。

WebRTC组成

构建WebRTC API的协议主要有以下几个:

  • ICE:交互式连接建立(ICE)是一个框架,允许您的Web浏览器与对等方连接。
  • STUN:NAT 会话遍历实用程序 (STUN)是一种协议,用于发现您的公共地址并确定路由器中阻止与对等体直接连接的任何限制。
  • NAT:网络地址转换 (NAT)用于为设备提供公共 IP 地址。
  • TURN:某些使用 NAT 的路由器采用称为“对称 NAT”的限制。这意味着路由器将只接受来自您之前连接到的对等体的连接。在 NAT 周围使用中继 (TURN) 进行遍历意味着通过打开与 TURN 服务器的连接并通过该服务器中继所有信息来绕过对称 NAT 限制。
  • SDP:会话描述协议(SDP)是描述连接的多媒体内容(如分辨率,格式,编解码器,加密等)的标准,以便两个对等方在数据传输后都可以相互理解。从本质上讲,这是描述内容的元数据,而不是媒体内容本身。

建立WebRTC连接

过程描述

提供/应答过程在首次建立呼叫时执行,而且在呼叫的格式或其他配置需要更改时执行。无论是新呼叫,还是重新配置现有呼叫,这些都是交换报价和应答必须执行的基本步骤,暂时省略了ICE层:

  • 调用方通过 MediaDevices.getUserMedia 捕获本地媒体。

  • 调用方创建并调用 RTCPeerConnection.addTrack()(正在弃用),可用addStream

  • 调用方调用 RTCPeerConnection.createOffer() 来创建产品/服务。

  • 调用方调用 RTCPeerConnection.setLocalDescription()将该产品/服务设置为本地描述(即连接本地端的描述)。

  • setLocalDescription() 之后,调用方要求 STUN 服务器生成icecandidate

  • 调用方使用信令服务器将报价传输到呼叫的预期接收方。

  • 接收者收到报价并调用 RTCPeerConnection.setRemoteDescription() 将其记录为远程描述(连接另一端的描述)。

  • 接收者为其呼叫结束执行所需的任何设置:捕获其本地媒体,并通过 RTCPeerConnection.addTrack()将每个媒体轨道附加到对等连接中。

  • 然后,收件人通过调用 RTCPeerConnection.createAnswer() 来创建答案。

  • 接收者调用 RTCPeerConnection.setLocalDescription(),传入创建的答案,以将答案设置为其本地描述。收件人现在知道连接两端的配置。

  • 接收方使用信令服务器将应答发送给呼叫者。

  • 呼叫者收到应答。

  • 调用方调用 RTCPeerConnection.setRemoteDescription() 以将应答设置为其呼叫结束的远程描述。它现在知道两个对等体的配置。媒体开始按配置流动。

创建WebRTC连接代码实现

创建对等连接

createPeerConnection()由调用方和被调用方用于构造RTCPeerConnection对象,即WebRTC连接各自的端点。

function createPeerConnection() { 
    myPeerConnection = new RTCPeerConnection({ 
        iceServers: [ 
            // Information about ICE servers - Use your own! 
            { 
                urls: "stun:stun.stunprotocol.org" 
            } 
        ] 
    }); 
    myPeerConnection.onicecandidate = handleICECandidateEvent; 
    myPeerConnection.ontrack = handleTrackEvent; 
    myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
    myPeerConnection.onremovetrack = handleRemoveTrackEvent;
    myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
    myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
    myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
}

开始谈判

调用方创建其 RTCPeerConnection、创建媒体流并将其磁道添加到连接(如启动调用中所示)后,浏览器将向 RTCPeerConnection 传递协商所需的事件,以指示它已准备好开始与另一个对等方协商。

function handleNegotiationNeededEvent() { 
    myPeerConnection.createOffer() 
      .then((offer) => myPeerConnection.setLocalDescription(offer)) 
      .then(() => { 
          sendToServer({ name: myUsername, target: targetUsername, type: "video-offer", sdp: myPeerConnection.localDescription }); 
      }) 
      .catch(reportError); 
}

一旦 setLocalDescription() 的履行处理程序运行完毕, ICE 代理就开始向 RTCPeerConnection 发送icecandidate事件, 它发现的每个潜在配置都有一个事件。我们的事件处理程序负责将候选项传输到另一个对等方。

会话协商

- 处理邀请

当报价到达时,将使用收到的消息调用被调用方的函数。此函数需要执行两项操作。首先,它需要创建自己的RTCPeerConnection,并将包含来自其麦克风和网络摄像头的音频和视频的曲目添加到其中。其次,它需要处理收到的报价,构建和发送其答案。

- 派遣ICE候选人

ICE谈判过程涉及每个对等体反复向另一个对等体发送候选人,直到它用尽了可以支持媒体传输需求的潜在方式。由于 ICE 不知道您的信令服务器,因此您的代码将处理处理程序中每个候选者的传输,以用于 icecandidate 事件。

function handleICECandidateEvent(event) { 
    if (event.candidate) { 
        sendToServer({ type: "new-ice-candidate", target: targetUsername, candidate: event.candidate }); 
    } 
}
- 收ICE候选人

信令服务器使用它选择的任何方法将每个ICE候选者传送到目标对等体;

function handleNewICECandidateMsg(msg) { 
    const candidate = new RTCIceCandidate(msg.candidate);
    myPeerConnection.addIceCandidate(candidate) 
    .catch(reportError); 
}

每个对等体向另一个对等体发送它认为对所交换的媒体可能可行的每个可能的传输配置的候选项。在某些时候,两个同行同意给定的候选人是一个不错的选择,他们打开连接并开始共享媒体。然而,重要的是要注意,一旦媒体流动,ICE谈判就不会停止。相反,候选人在对话开始后可能仍然继续被交换,要么是在试图找到更好的连接方法时,要么是因为当对等体成功建立连接时,他们已经在运输中。

此外,如果发生某些事情导致流式处理方案发生变化,协商将再次开始,协商需要的事件将发送到 RTCPeerConnection,整个过程将按前面所述再次开始。

- 处理track的移除

当远程对等体通过调用 RTCPeerConnection.removeTrack() 从连接中删除轨道时,您的代码会收到一个 removetrack 事件。

function handleRemoveTrackEvent(event) { 
    const stream = document.getElementById("received_video").srcObject; 
    const trackList = stream.getTracks(); 
    if (trackList.length === 0) { 
        closeVideoCall(); 
    } 
}

结束通话

通话可能结束的原因有很多。呼叫可能已完成,一方或双方已挂断电话。可能发生了网络故障,或者一个用户可能已退出浏览器,或者系统崩溃。

- 挂断电话

当用户单击“挂断”按钮结束调用时

function hangUpCall() { 
    closeVideoCall(); 
    sendToServer({ name: myUsername, target: targetUsername, type: "hang-up" }); 
}

hangUpCall()执行以关闭和重置连接并释放资源。然后,它生成一条消息并将其发送到调用的另一端,以告诉另一个对等体整齐地关闭自身。

- 结束通话

该函数(如下所示)负责停止流、清理和释放 RTCPeerConnection 对象。

function closeVideoCall() { 
    const remoteVideo = document.getElementById("received_video"); 
    const localVideo = document.getElementById("local_video"); 
    if (myPeerConnection) { 
        myPeerConnection.ontrack = null; 
        myPeerConnection.onremovetrack = null; 
        myPeerConnection.onremovestream = null; 
        myPeerConnection.onicecandidate = null;
        myPeerConnection.oniceconnectionstatechange = null;
        myPeerConnection.onsignalingstatechange = null; 
        myPeerConnection.onicegatheringstatechange = null;
        myPeerConnection.onnegotiationneeded = null; 
        if (remoteVideo.srcObject) { 
            remoteVideo.srcObject.getTracks().forEach((track) => track.stop()); 
        } 
        if (localVideo.srcObject) { 
            localVideo.srcObject.getTracks().forEach((track) => track.stop()); 
        } myPeerConnection.close(); myPeerConnection = null; 
    }
    remoteVideo.removeAttribute("src"); 
    remoteVideo.removeAttribute("srcObject"); 
    localVideo.removeAttribute("src"); 
    remoteVideo.removeAttribute("srcObject"); 
    document.getElementById("hangup-button").disabled = true; 
    targetUsername = null; 
}

最后

个人学习笔记分享,仅供参考!

垮脸.webp