前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >前端音视频WebRTC实时通讯的核心

前端音视频WebRTC实时通讯的核心

作者头像
童欧巴
发布于 2020-11-02 03:36:27
发布于 2020-11-02 03:36:27
2.8K02
代码可运行
举报
文章被收录于专栏:前端食堂前端食堂
运行总次数:2
代码可运行

这是前端食堂的第46篇原创

「观感度:?????」

「口味:新疆炒米粉」

「烹饪时间:10min」

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

通过上两个系列专栏的学习,我们对前端音视频及 WebRTC 有了初步的了解,是时候敲代码实现一个 Demo 来真实感受下 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 来适配各浏览器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!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>

然后,定义我们将要使用到的对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 本地流和远端流
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
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 函数及工具函数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

知乎:童欧巴

这是一个终身学习的男人,他在坚持自己热爱的事情,欢迎加入前端食堂,和这个男人一起开心的变胖~

推荐阅读:

海贼王 One Piece,一起康康Vue版本号中的彩蛋

Vue3 DOM Diff 核心算法解析

在看和转发是莫大鼓励❤

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-10-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端食堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
贪吃蛇小游戏程序(C语言)
最近暑假无事开始用C语言写一些小游戏小游戏程序,这个贪吃蛇当然是经典中的经典,所以先开始写。
跋扈洋
2021/06/15
2.8K0
贪吃蛇小游戏程序(C语言)
贪吃蛇代码来咯
#include<stdio.h> #include<time.h> #include<windows.h> #include<stdlib.h> #define U 1 #define D 2 #define L 3 #define R 4 //蛇的状态,U:上 ;D:下;L:左 R:右 typedef struct SNAKE //蛇身的一个节点 { int x; int y; struct SNAKE *next; }snake; //全局变量// int score=0,add=10;//总得分与
神无月
2018/06/25
1.5K0
四个c语言小游戏
1、我使用的是编译软件是vc6.0 2、如果代码无法运行,你可以尝试吧文件xxx.c改为xxx.cpp 3、四个小游戏我都运行过,确保是可以运行的。虽然可玩性、操作性。。。
全栈程序员站长
2022/08/30
3.3K0
C语言——贪吃蛇
Windows可以给我们提供各种服务(每一个服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的。这些函数的服务对象是应用程序(Application),所以便称之为ApplicationProgrammingInterface,简称API函数。。
HZzzzzLu
2024/11/26
1500
C语言——贪吃蛇
简单贪吃蛇C++语言编程(如何用c语言写贪吃蛇)
(有一些函数kbhit,getch,在这表示为_kbhit与_getch)//不同编译器原因 注意在Dev等集成开发软件下可能会CE
全栈程序员站长
2022/07/25
2K0
简单贪吃蛇C++语言编程(如何用c语言写贪吃蛇)
c语言实现贪吃蛇小游戏
如果运行之后出现的是上面的界面,就需要更改一下,鼠标右键点击控制台顶端,再点击设置
2的n次方
2024/10/15
1000
c语言实现贪吃蛇小游戏
【C语言】打造你的专属贪吃蛇
我们如果要在控制台上打印宽字符’●’或者’★’,汉字也是宽字符,就需要修改本地适配环境,在main函数中://修改适配本地的中文环境 setlocale(LC_ALL, "");,使用该函数要包含#include<locale.h> 头文件
大耳朵土土垚
2024/05/30
1260
【C语言】打造你的专属贪吃蛇
C语言实现贪吃蛇【完整版】
实现过程使用了WIN32的一些API,这里简单介绍一下这些API的功能。
用户11029129
2024/06/04
3450
C语言实现贪吃蛇【完整版】
【C语言】【数据结构】项目实践——贪吃蛇游戏(超详细)
贪吃蛇是一款休闲益智类游戏,有PC和手机等多平台版本。既简单又耐玩。该游戏通过控制蛇头方向吃食物,从而使得蛇变得越来越长,贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
用户11290673
2024/09/25
6180
【C语言】【数据结构】项目实践——贪吃蛇游戏(超详细)
贪吃蛇小游戏
秦jh
2024/01/19
1840
大二必做项目贪吃蛇超详解之下篇游戏核心逻辑实现
现在我们解释一下Snake结构体中的_SleepTime是怎么控制速度的。 首先我们要明确:程序的运行速度是非常快的,对于贪吃蛇这样的小项目来说,所有的代码都可以看作是瞬间完成的,如果直接执行,那贪吃蛇一定会在我们反应过来之前直接死亡,所以说我们需要使用Sleep函数让函数停下来一会儿来控制速度。
fhvyxyci
2024/09/24
1750
大二必做项目贪吃蛇超详解之下篇游戏核心逻辑实现
【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是⼀个很大的服务中心,这个服务中心提供了多种服务(每⼀种服务就是⼀个函数),调用这些服务可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
ZLRRLZ
2024/12/13
6790
【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)
C语言实现简单贪吃蛇代码
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/141348.html原文链接:https://javaforall.cn
全栈程序员站长
2022/09/01
2.4K0
C语言实现简单贪吃蛇代码
【C语言】贪吃蛇游戏的实现(一)[通俗易懂]
(注意:本代码是在VC++6.0环境下编译的,在其他环境如codeblocks下运行可能会产生意想不到的问题,请尽量使用VC。至于为什么要使用VC编译,哦,我亲爱的朋友,这只有上帝才知道)
全栈程序员站长
2022/09/05
1.4K0
【C语言】贪吃蛇游戏的实现(一)[通俗易懂]
【C语言】实践:贪吃蛇小游戏(附源码)
贪吃蛇小游戏想必大家都玩过吧,现在就要C语言代码来实现一下贪吃蛇小游戏
星辰与你
2024/10/17
6250
【C语言】实践:贪吃蛇小游戏(附源码)
c程序设计,贪吃蛇程序是什么_C语言编写贪吃蛇
思路:一行一行的输出,输出完一行就换行继续输出,直到输出完边框部分。 第一步:先输出第一行,输完第一行进行换行。(这里@代表◼)
全栈程序员站长
2022/11/17
2.1K0
花狗C语言彩色贪吃蛇(完整代码)
这是我在博客写的第一篇文章,如果哪里有问题还请多多指教!!以前我写的贪吃蛇是全图刷新的,导致在窗口运行时,眼睛都快闪瞎了!今天为大家带来了不闪的贪吃蛇!!!废话不多说,上图,上代码!!
花狗Fdog
2020/10/28
2.4K0
花狗C语言彩色贪吃蛇(完整代码)
C语言贪吃蛇完整代码
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
紫禁玄科
2022/03/24
4.1K0
C语言贪吃蛇完整代码
贪吃蛇小游戏
游戏截图: 源码如下: #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<Windows.h> #include<ctime> #i
大忽悠爱学习
2021/03/04
4800
推荐阅读
相关推荐
贪吃蛇小游戏程序(C语言)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档