观感度:????????????????????
口味:新疆炒米粉
烹饪工夫: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. 点赞、评论、转发 === 催更!