关于webrtc:前端音视频WebRTC实时通讯的核心

3次阅读

共计 6799 个字符,预计需要花费 17 分钟才能阅读完成。

观感度:????????????????????

口味:新疆炒米粉

烹饪工夫:10min

本文已收录在前端食堂同名仓库 Github github.com/Geekhyt,欢迎光临食堂,如果感觉酒菜还算可口,赏个 Star 对食堂老板来说是莫大的激励。

通过上两个系列专栏的学习,咱们对前端音视频及 WebRTC 有了初步的理解,是时候敲代码实现一个 Demo 来实在感触下 WebRTC 实时通信的魅力了。还没有看过的同学请移步:

  • 前端音视频的那些名词
  • 前端音视频之 WebRTC 初探

RTCPeerConnection

RTCPeerConnection 类是在浏览器下应用 WebRTC 实现实时互动音视频零碎中最外围的类,它代表一个由本地计算机到远端的 WebRTC 连贯。该接口提供了创立、放弃、监控及敞开连贯的办法的实现。

想要对这个类理解更多能够移步这个链接,https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection

其实,如果你有做过 socket 开发的话,你会更容易了解 RTCPeerConnection,它其实就是一个增强版本的 socket。

在上个系列专栏 前端音视频之 WebRTC 初探 中,咱们理解了 WebRTC 的通信原理,在实在场景下须要进行媒体协商、网络协商、架设信令服务器等操作,我画了一张图,将 WebRTC 的通信过程总结如下:

不过明天咱们为了单纯的搞清楚 RTCPeerConnection,先不思考开发架设信令服务器的问题,简略点,咱们这次尝试在同一个页面中模仿两端进行音视频的互通。

在此之前,咱们先理解一些将要用到的 API 以及 WebRTC 建设连贯的步骤。

相干 API

  • RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连贯。该接口提供了创立、放弃、监控、敞开连贯的办法的实现。
  • PC.createOffer 创立提议 Offer 办法,此办法会返回 SDP Offer 信息。
  • PC.setLocalDescription 设置本地 SDP 形容信息。
  • PC.setRemoteDescription 设置远端 SDP 形容信息,即对方发过来的 SDP 数据。
  • PC.createAnswer 创立应答 Answer 办法,此办法会返回 SDP Answer 信息。
  • RTCIceCandidate WebRTC 网络信息(IP、端口等)
  • PC.addIceCandidate PC 连贯增加对方的 IceCandidate 信息,即增加对方的网络信息。

WebRTC 建设连贯步骤

  • 1. 为连贯的两端创立一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象增加本地流。
  • 2. 获取本地媒体形容信息(SDP),并与对端进行替换。
  • 3. 获取网络信息(Candidate,IP 地址和端口),并与远端进行替换。

Demo 实战

首先,咱们增加视频元素及管制按钮,引入 adpater.js 来适配各浏览器。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
    <style>
        video {width: 320px;}
    </style>
</head>
<body>
    <video id="localVideo" autoplay playsinline></video>
    <video id="remoteVideo" autoplay playsinline></video>

    <div>
        <button id="startBtn"> 关上本地视频 </button>
        <button id="callBtn"> 建设连贯 </button>
        <button id="hangupBtn"> 断开连接 </button>
    </div>
    <!-- 适配各浏览器 API 不对立的脚本 -->
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="./webrtc.js"></script>
</body>
</html>

而后,定义咱们将要应用到的对象。

// 本地流和远端流
let localStream;
let remoteStream;

// 本地和远端连贯对象
let localPeerConnection;
let remotePeerConnection;

// 本地视频和远端视频
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');

// 设置束缚
const mediaStreamConstraints = {video: true}

// 设置仅替换视频
const offerOptions = {offerToReceiveVideo: 1}

接下来,给按钮注册事件并实现相干业务逻辑。

function startHandle() {
    startBtn.disabled = true;
    // 1. 获取本地音视频流
    // 调用 getUserMedia API 获取音视频流
    navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
        .then(gotLocalMediaStream)
        .catch((err) => {console.log('getUserMedia 谬误', err);
        });
}

function callHandle() {
    callBtn.disabled = true;
    hangupBtn.disabled = false;

    // 视频轨道
    const videoTracks = localStream.getVideoTracks();
    // 音频轨道
    const audioTracks = localStream.getAudioTracks();
    // 判断视频轨道是否有值
    if (videoTracks.length > 0) {console.log(` 应用的设施为: ${videoTracks[0].label}.`);
    }
    // 判断音频轨道是否有值
    if (audioTracks.length > 0) {console.log(` 应用的设施为: ${audioTracks[0].label}.`);
    }
    const servers = null;

    // 创立 RTCPeerConnection 对象
    localPeerConnection = new RTCPeerConnection(servers);
    // 监听返回的 Candidate
    localPeerConnection.addEventListener('icecandidate', handleConnection);
    // 监听 ICE 状态变动
    localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange)

    remotePeerConnection = new RTCPeerConnection(servers);
    remotePeerConnection.addEventListener('icecandidate', handleConnection);
    remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
    remotePeerConnection.addEventListener('track', gotRemoteMediaStream);

    // 将音视频流增加到 RTCPeerConnection 对象中
    // 留神:新的协定中曾经不再举荐应用 addStream 办法来增加媒体流,应应用 addTrack 办法
    // localPeerConnection.addStream(localStream);
    // 遍历本地流的所有轨道
    localStream.getTracks().forEach((track) => {localPeerConnection.addTrack(track, localStream)
    })

    // 2. 替换媒体形容信息
    localPeerConnection.createOffer(offerOptions)
    .then(createdOffer).catch((err) => {console.log('createdOffer 谬误', err);
    });
}

function hangupHandle() {
    // 敞开连贯并设置为空
    localPeerConnection.close();
    remotePeerConnection.close();
    localPeerConnection = null;
    remotePeerConnection = null;
    hangupBtn.disabled = true;
    callBtn.disabled = false;
}

// getUserMedia 取得流后,将音视频流展现并保留到 localStream
function gotLocalMediaStream(mediaStream) {
    localVideo.srcObject = mediaStream; 
    localStream = mediaStream; 
    callBtn.disabled = false;
}

function createdOffer(description) {console.log(` 本地创立 offer 返回的 sdp:\n${description.sdp}`)
    // 本地设置形容并将它发送给远端
    // 将 offer 保留到本地
    localPeerConnection.setLocalDescription(description) 
        .then(() => {console.log('local 设置本地形容信息胜利');
        }).catch((err) => {console.log('local 设置本地形容信息谬误', err)
        });
    // 远端将本地给它的形容设置为远端形容
    // 远端将 offer 保留
    remotePeerConnection.setRemoteDescription(description) 
        .then(() => {console.log('remote 设置远端形容信息胜利');
        }).catch((err) => {console.log('remote 设置远端形容信息谬误', err);
        });
    // 远端创立应答 answer
    remotePeerConnection.createAnswer() 
        .then(createdAnswer)
        .catch((err) => {console.log('远端创立应答 answer 谬误', err);
        });
}

function createdAnswer(description) {console.log(` 远端应答 Answer 的 sdp:\n${description.sdp}`)
    // 远端设置本地形容并将它发给本地
    // 远端保留 answer
    remotePeerConnection.setLocalDescription(description)
        .then(() => {console.log('remote 设置本地形容信息胜利');
        }).catch((err) => {console.log('remote 设置本地形容信息谬误', err);
        });
    // 本地将远端的应答形容设置为远端形容
    // 本地保留 answer
    localPeerConnection.setRemoteDescription(description) 
        .then(() => {console.log('local 设置远端形容信息胜利');
        }).catch((err) => {console.log('local 设置远端形容信息谬误', err);
        });
}

// 3. 端与端建设连贯
function handleConnection(event) {
    // 获取到触发 icecandidate 事件的 RTCPeerConnection 对象 
    // 获取到具体的 Candidate
    const peerConnection = event.target;
    const iceCandidate = event.candidate;

    if (iceCandidate) {
        // 创立 RTCIceCandidate 对象
        const newIceCandidate = new RTCIceCandidate(iceCandidate);
        // 失去对端的 RTCPeerConnection
        const otherPeer = getOtherPeer(peerConnection);

        // 将本地取得的 Candidate 增加到远端的 RTCPeerConnection 对象中
        // 为了简略,这里并没有通过信令服务器来发送 Candidate,间接通过 addIceCandidate 来达到调换 Candidate 信息的目标
        otherPeer.addIceCandidate(newIceCandidate)
            .then(() => {handleConnectionSuccess(peerConnection);
            }).catch((error) => {handleConnectionFailure(peerConnection, error);
            });
    }
}

// 4. 显示远端媒体流
function gotRemoteMediaStream(event) {if (remoteVideo.srcObject !== event.streams[0]) {remoteVideo.srcObject = event.streams[0];
        remoteStream = mediaStream;
        console.log('remote 开始承受远端流')
    }
}

最初,还须要注册一些 Log 函数及工具函数。

function handleConnectionChange(event) {
    const peerConnection = event.target;
    console.log('ICE state change event:', event);
    console.log(`${getPeerName(peerConnection)} ICE state: ` + `${peerConnection.iceConnectionState}.`);
}

function handleConnectionSuccess(peerConnection) {console.log(`${getPeerName(peerConnection)} addIceCandidate 胜利 `);
}

function handleConnectionFailure(peerConnection, error) {console.log(`${getPeerName(peerConnection)} addIceCandidate 谬误:\n`+ `${error.toString()}.`);
}

function getPeerName(peerConnection) {return (peerConnection === localPeerConnection) ? 'localPeerConnection' : 'remotePeerConnection';
}

function getOtherPeer(peerConnection) {return (peerConnection === localPeerConnection) ? remotePeerConnection : localPeerConnection;
}

其实当你相熟整个流程后能够将所有的 Log 函数对立抽取并封装起来,上文为了便于你在读代码的过程中更容易的了解整个 WebRTC 建设连贯的过程,并没有进行抽取。

好了,到这里一切顺利的话,你就胜利的建设了 WebRTC 连贯,成果如下:

(顺手抓起桌边的鼠年企鹅公仔)

参考

  • 《从 0 打造音视频直播零碎》李超
  • 《WebRTC 音视频开发 React+Flutter+Go 实战》亢少军
  • https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection

❤️爱心三连击

1. 如果你感觉食堂酒菜还合胃口,就点个赞反对下吧,你的 是我最大的能源。

2. 关注公众号前端食堂,吃好每一顿饭!

3. 点赞、评论、转发 === 催更!

正文完
 0