实现一个视频通话-WebRTC

8,933 阅读12分钟

写在开头

哈喽,各位好吖!😀 又是美好的一天呢。

七月了,也就是2024年下半年已经开始了,时间流逝飞快呢。🏃🏃🏃

各位上半年是否已经有所收获了呢?😁 反正小编是没有😴,还是平平淡淡,按部就班,过着自己的小日子,如下图:

c60c55796c39c8c1a0236ca82b3abb7.png

此时唯一的愿望就是开开心心,健健康康过好当下就好,哈哈。👻

Em....差不多半个月没来写文章了,没办法,近来工作繁忙😔,而今天在写文章时,发现文章发的贴图有大小限制了❗❗❗

image.png

文章封面图也放不了 .gif 格式的图了,唉...有点难受,俺还是喜欢贴一个动图在封面,每次找文章回顾就知道这篇文章大概是写了啥,这不挺好?

不过,算了,无所谓啦,这都不重要。😂

好,收,又扯远了,回到正题,这次要分享一个与 WebRTC 技术相关的内容,具体效果如下,请诸君按需食用。

202406241.gif

gif压缩之后就有点糊了。。。由于两个用户都是在小编本地,所以摄像头看到的都是同一个画面)

什么是WebRTC?

一种基于浏览器的多媒体即时通信技术,🌐能实现在浏览器之间交换任意数据而无需中间件的技术。

(听起来就很厉害的样子😁)

MDN解释:传送门

诞生背景:

随着互联网技术的发展,用户对于实时通信的需求不断增长,特别是在社交网络、在线教育和远程工作等领域。在WebRTC出现之前,若要在网页浏览器中进行实时音视频通信,通常需要依赖于插件,如 Flash,但这并非一个高效或者安全的解决方案。

2011年,Google开源了WebRTC项目,旨在为浏览器提供无需额外插件或安装程序的实时音视频通信能力。

WebRTC能做些什么?

WebRTC 因其开放性、跨平台性和低延迟特性,被广泛应用于各种实时通信场景。

  • 音视频通信:微信-视频通话、语音通话等。
  • 多人视频会议:腾讯会议、钉钉会议等。
  • 直播服务:实时直播,如游戏直播,观众可以通过WebRTC与直播者进行实时互动。
  • 即时消息:用户在网页上发送和接收即时消息。
  • 数据共享:用户可以在通话过程中实时共享文件,或者共享屏幕。
  • 视频监控:实时监控某区域目标安全等等。

getUserMedia()

先来介绍一个很关键、基础的API,它也是咱们实现视频通话的基础。

猪脚👉:navigator.mediaDevices.getUserMedia(constraints, successCallback, errorCallback);

navigator.mediaDevices :只读属性,会返回一个 MediaDevices 对象,该对象可提供对摄像头、麦克风或相机等媒体输入设备以及屏幕共享的连接访问。

(说白了就是这个对象能让你操作电脑的一些硬件设备😗)

如:

navigator.mediaDevices.getUserMedia(constraints) :调用该方法会先向用户获取使用硬件的许可,用户同意后,会产生一个 MediaStream 流对象,这个对象可能是视频、音频或者其他类型的流对象,这取决 constraints 的配置。

不懂的,再瞧瞧MDN的解释:传送门

说那么多不如写个示例感受感受😗:

<!DOCTYPE html>
<html>
<head>
  <style>
    #video {
      width: 300px;
      height: 300px;
      background-color: #f5f5f5;
      object-fit: cover;
    }
  </style>
</head>
<body>
  <video id="video"></video><br />
  <button id="startVideo">打开视频</button>
  <button id="stopVideo">暂停播放</button>
  <button id="continueVideo">继续播放</button>
  <button id="closeVideo">关闭播放</button>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const video = document.getElementById('video');
      const startVideo = document.getElementById('startVideo');
      const stopVideo = document.getElementById('stopVideo');
      const continueVideo = document.getElementById('continueVideo');
      const closeVideo = document.getElementById('closeVideo');

      startVideo.addEventListener('click', async () => {
        // 获取一个音视频流(MediaStream)
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true,
        });
        // 将音视频流赋值video
        video.srcObject = stream;
        // 播放音视频流内容
        video.play();
      });
      stopVideo.addEventListener('click', () => {
        video.pause();
      });
      continueVideo.addEventListener('click', () => {
        video.play();
      });
      closeVideo.addEventListener('click', () => {
        video.srcObject = null;
      });
    });
  </script>
</body>
</html>

效果:

2024062.gif

可以看到调用该方法后,首先会向用户获取硬件使用许可(拒绝就没得玩了😶),这里是获取了摄像头与麦克风的使用许可,当用户同意后,浏览器就能通过 video 标签实时播放电脑摄像头的画面;并且麦克风也会进行使用,你可以尝试说话,然后听听看有没有声音传出。😉

上面示例代码中,咱们将获取到的 MediaStream 流对象赋值给了 videosrcObject 属性,这个属性可能大家接触比较少,反正呢,它能帮我们处理 MediaStreamMediaSourceBlob 或者一个 File 类型(该类型继承自 Blob)的对象,先就这么硬记吧。👻

(结论:navigator.mediaDevices.getUserMedia() 方法能帮我们调用摄像头或麦克风?😮)

拍照功能

经过上面一个小案例,相信你对 getUserMedia() 方法有了一定的感受,接下来,我们再拿这个方法来实现一个常见功能-拍照。

<!DOCTYPE html>
<html>
<head>
  <style>
    #video, #canvas  {
      width: 300px;
      height: 300px;
      background-color: #f5f5f5;
      object-fit: cover;
    }
  </style>
</head>
<body>
  <!-- ... -->
  <button id="takePhoto">拍照</button><br />
  <canvas id="canvas"></canvas>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      ...
      const takePhoto = document.getElementById('takePhoto');
      
      // ...
      takePhoto.addEventListener('click', () => {
        const canvas = document.getElementById('canvas');
        const context = this.canvas.getContext('2d');
        context.drawImage(video, 0, 0, 300, 150);
      });
    });
  </script>
</body>
</html>

效果:

2024066.gif

当然,咱们也可以将图片下载下来:

takePhoto.addEventListener('click', () => {
  const canvas = document.getElementById('canvas');
  const context = this.canvas.getContext('2d');
  context.drawImage(video, 0, 0, 300, 150);
  // 下载图片
  const url = canvas.toDataURL('images/png')
  const a = document.createElement('a');
  const event = new MouseEvent('click');
  a.download = 'default.png';
  a.href = url;
  a.dispatchEvent(event);
});

其实,拍照这个功能网上有很多文章介绍了,大多也是使用 getUserMedia() 方法进行实现的,需要更详细的介绍,可以再去搜索搜索看哈,咱们加深一下感受就可以啦😉。

Socket.IO

接下来,再来介绍一个实时通信的库,这也是咱们最终案例会使用到的一个东东😶,我们单独先来看看。

Socket.IO 是一个库,可以在客户端和服务器之间实现低延迟双向基于事件的通信。

Diagram of a communication between a server and a client

Socket.IO官方文档:传送门

这个库主要有两个包:socket.iosocket.io-client,对应服务端与客户端的场景。

基础使用

咱们使用它们来写个小案例耍耍吧😀。

先随便找个空文件夹,npm init -y 初始化一下 package.json 文件。

然后,安装依赖:

npm install socket.io socket.io-client nodemon

为了方便,小编安装了 nodemon 包,它能帮我们在每次修改服务端代码保存后自动重启服务,省了手动重启的麻烦。

开始搭建服务,创建一个 server.js 文件:

const http = require('http');
const socket = require('socket.io');

// 创建服务
const server = http.createServer();

// 初始化socket.io
const io = socket(server, {
  cors: {
    origin: '*', // 允许跨域
  }
});

// 启动监听
io.on('connection', socket => {
  // 给客户端发送connectionSuccess事件
  socket.emit('connectionSuccess');
});

server.listen(3000, () => {
  console.log('服务启动:http://localhost:3000/');
});

上述,咱们创建了一个 http 服务,然后初始化了 socket.io 的监听,当监听到有新连接过来的时候,则会发出一个 connectionSuccess 事件。

connection 事件也可以改成它的别名:connect

配置启动命令并执行:

{
  "scripts": {
    "server": "nodemon server.js"
  }
}

image.png

好,服务端就这样简单搭建完了,接下来搞一下客户端。

创建一个 index.html 文件:

<!DOCTYPE html>
<html>
<body>
  <h1>橙某人</h1>
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      const socket = io('http://localhost:3000/');
      socket.on('connectionSuccess', () => {
        console.log('连接成功')
      });
    });
  </script>
</body>
</html>
image.png

打开页面,如果能看到浏览器控制台与服务端有响应的话,就说明服务端与客户端已经连接上了。🙏

上面的代码中,服务端从中给客户端主动发送消息了(connectionSuccess),客户端能监听并接收到。

咱们再来尝试从客户端给服务端主动发送消息,如果服务端也能监听并接收到,就能完整说明两端已经通了,咱们能随时在两端进行实时通信。

<!DOCTYPE html>
<html>
<body>
  <input id="input" placeholder="请输入内容" />
  <button id="btn">向服务端发送消息</button>
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      // ...
      
      const btn = document.getElementById('btn');
      const input = document.getElementById('input');
      btn.addEventListener('click', () => {
        socket.emit('hello', `我是客户端发来的消息:${input.value}`);
      });
    });
  </script>
</body>
</html>

服务端:

// ...

// 启动监听
io.on('connection', socket => {
  socket.emit('connectionSuccess');
  // 监听hello事件
  socket.on('hello', content => {
    console.log(content);
  });
});

// ...
2024067.gif

好,真通了🙏。

是不是挺简单😁,记住监听用 on ,发送就用 emit ,完事。

房间

上面示例中,当我们创建多个客户端,如浏览器创建多个Tab页面,这时只要服务端发送消息过来,所有的Tab页面都会接收到,这显然在某些场景下不是我们所期盼的❌,此时呢,就会引出一个"房间"的概念。

接下来,咱们来瞅瞅这个房间要如何使用,先看效果:

image.png

从图中可以看到,有三个客户端,其中上下两个客户端都是加入 123 这个房间,中间客户端加入了 123456 房间;当我们在 123 房间内发送消息,中间的客户端是接收不到的;同理,如果中间的客户端也发送消息,上下的客户端也是接收不到的;只有加入对应房间的客户端才能接收到,这就是房间的作用,相信都能理解哈😂。

来看看如何实现的,客户端:

<!DOCTYPE html>
<html>
<body>
  <input id="inputRoom" placeholder="请输入房间号" />
  <button id="joinBtn">加入房间</button>
  <div id="tip"></div>
  <input id="inputContent" placeholder="请输入内容" />
  <button id="sendBtn">发送消息</button>
  
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      const socket = io('http://localhost:3000/');
      socket.on('connectionSuccess', () => {
        console.log('连接成功')
      });
      
      const inputRoom = document.getElementById('inputRoom');
      const joinBtn = document.getElementById('joinBtn');
      const tip = document.getElementById('tip');
      const inputContent = document.getElementById('inputContent');
      const sendBtn = document.getElementById('sendBtn');
      
      joinBtn.addEventListener('click', () => {
        const roomId = inputRoom.value;
        socket.emit('joinRoom', roomId);
      });
      socket.on('roomInfo', roomId => {
        tip.innerText = `成功加入${roomId}房间`;
      });
      sendBtn.addEventListener('click', () => {
        const roomId = inputRoom.value;
        const content = inputContent.value;
        socket.emit('content', {roomId, content});
      });
      socket.on('message', result => {
        console.log(`服务端发来的消息:${result}`);
      });
    });
  </script>
</body>
</html>

服务端:

const http = require('http');
const socket = require('socket.io');
const server = http.createServer();
const io = socket(server, {
  cors: {
    origin: '*'
  }
});

io.on('connect', socket => {
  socket.emit('connectionSuccess');
  socket.on('hello', message => {
    console.log('客户端发来的消息:', message);
  });
  socket.on('joinRoom', roomId => {
    // 加入房间,没有就会创建房间
    socket.join(roomId);
    // 往房间内的客户端发送roomInfo事件
    io.to(roomId).emit('roomInfo', roomId)
  });
  socket.on('content', ({ roomId, content }) => {
    // 往房间内的客户端发送message事件
    io.to(roomId).emit('message', content)
  });
});

server.listen(3000, () => {
  console.log('服务启动:http://localhost:3000/');
});

主要就是认识了一下 socket.join()io.to() 方法的使用,暂时也就足够了,更多"房间"的内容:传送门

视频通话的实现

好了,有上述这些内容作为基础,接下来我们就可以开始探索如何实现视频通话的功能。

先来看看服务端的代码实现:

const socket = require('socket.io');
const http = require('http');

const server = http.createServer()

const io = socket(server, {
  cors: {
    origin: '*'
  }
});

io.on('connection', socket => {
  // 向客户端发送连接成功的消息
  socket.emit('connectionSuccess');
  // 加入房间
  socket.on('joinRoomEvent', roomId => {
    socket.join(roomId);
  })
  // 申请通话
  socket.on('callEvent', roomId => {
    io.to(roomId).emit('callEvent');
  });
  // 同意通话
  socket.on('acceptCallEvent', roomId => {
    io.to(roomId).emit('acceptCallEvent');
  });
  // 传送offer
  socket.on('offerEvent', ({ offer, roomId }) => {
    io.to(roomId).emit('offerEvent', offer);
  });
  // 传送Answer
  socket.on('answerEvent', ({ answer, roomId }) => {
    io.to(roomId).emit('answerEvent', answer);
  });
  // 传送candidate
  socket.on('candidateEvent', ({ candidate, roomId }) => {
    io.to(roomId).emit('candidateEvent', candidate)
  });
  // 挂断通话
  socket.on('end', roomId => {
    io.to(roomId).emit('end');
  });
})

server.listen(3000, () => {
  console.log('服务器启动成功');
});

服务端很简单,通过 socket.io 包咱们一共创建八个监听事件,用于服务客户端的消息交流。额外需要关注的是 offeranswercandidate 事件😯,它们是通话的关键。

启动服务与上面的一样,这里就不过多累述了,咱再来看看客户端的实现过程。

用户A:

<!DOCTYPE html>
<html>
<head>
  <title>用户A-语音通话</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="box">
    <h1>用户A</h1>
    <div class="container">
      <video id="videoA"></video>
      <div class="container-small">
        <video id="videoASmall"></video>
      </div>
      <div id="layerA" class="layer">
        <div class="layer__text">正在等待对方接受邀请...</div>
        <div class="layer__btn">
          <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6400a568de2e4a57b04e684be4004d52~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=48&h=48&s=870&e=png&a=1&b=f53c36" />
          <span>取消</span>
        </div>
      </div>
    </div>
    <div>
      <button id="videoBtn">视频通话</button>
      <button>语音通话</button>
    </div>
  </div>
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      // 初始化socket
      const socket = io('http://localhost:3000/');
      const roomId = '123456';
      socket.on('connectionSuccess', () => {
        console.log('连接成功');
        socket.emit('joinRoomEvent', roomId);
      });

      const videoBtn = document.getElementById('videoBtn');
      const layerA = document.getElementById('layerA');
      // video
      const videoA = document.getElementById('videoA');
      const videoASmall = document.getElementById('videoASmall');
      // stream
      let streamA = null;
      // 呼叫中
      let calling = false;
      // 通话中
      let communicating = false;

      // 视频通话
      videoBtn.addEventListener('click', async () => {
        calling = true;
        layerA.style.display = 'flex';
        const stream = await getLocalStream();
        videoA.srcObject = stream;
        videoA.play()
        streamA = stream;
        // 申请通话
        socket.emit('callEvent', roomId);
      });

      let peer = null;
      // 监听同意接听事件
      socket.on('acceptCallEvent', async () => {
        peer = new RTCPeerConnection();
        // 添加本地"音视频流"
        for (const track of streamA.getTracks()) {
          peer.addTrack(track, streamA);
        }
        peer.onicecandidate = event => {
          if (event.candidate) {
            // 将candidate发送给对方
            socket.emit('candidateEvent', { roomId, candidate: event.candidate });
          }
        }
        peer.ontrack = event => {
          calling = false;
          layerA.style.display = 'none';
          communicating = true;
          videoASmall.srcObject = event.streams[0];
          // 让video缓一下,再播放,否则可能存在流还没添加上,就播放视频而报错
          setTimeout(() => {
            videoASmall.play();
          })
        }
        // 生成offer
        const offer = await peer.createOffer({
          offerToReceiveAudio: 1,
          offerToReceiveVideo: 1
        });
        // 设置本地描述的offer
        await peer.setLocalDescription(offer);
        // 将offer发送给对方
        socket.emit('offerEvent', { roomId, offer });
      });
      socket.on('answerEvent', answer => {
        // 设置"远端"描述的answer
        peer.setRemoteDescription(answer);
      })
      socket.on('candidateEvent', async candidate => {
        await peer.addIceCandidate(candidate);
      });

      /** @name 获取本地音视频流 **/
      async function getLocalStream() {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true
        });
        return stream;
      }
    });
  </script>
</body>
</html>

网上看到一些关于 WebRTC 文章,有些在将本地"音视频流"添加到轨道集合中时,使用的是addStream这个方法,MDN上描述已经是废弃的方法:

image.png

试了一下,虽然还能用。但是,MDN推荐还是使用 addTrack 方法,当然,如果使用 addTrack 方法,那么在 ontrack 方法中获取的时候,也要改成使用 event.streams[0] 形式了,这点需要注意一下。

用户B:

<!DOCTYPE html>
<html>
<head>
  <title>用户B-语音通话</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="box">
    <h1>用户B</h1>
    <div class="container">
      <video id="videoB"></video>
      <div class="container-small">
        <video id="videoBSmall"></video>
      </div>
      <div id="layerB" class="layer">
        <div class="layer__text">用户A邀请你视频通话...</div>
        <div class="layer__btns">
          <div id="rejectBtn" class="layer__btn">
            <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6400a568de2e4a57b04e684be4004d52~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=48&h=48&s=870&e=png&a=1&b=f53c36" />
            <span>挂断</span>
          </div>
          <div id="acceptBtn" class="layer__btn">
            <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11f90e68b4074fd899d3d512ef9bba5f~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=48&h=48&s=881&e=png&a=1&b=47de66" />
            <span>接听</span>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    document.addEventListener('DOMContentLoaded', () => {
      const socket = io('http://localhost:3000/');
      const roomId = '123456';
      socket.on('connectionSuccess', () => {
        console.log('连接成功');
        socket.emit('joinRoomEvent', roomId);
      });
      const layerB = document.getElementById('layerB');
      const videoB = document.getElementById('videoB');
      const videoBSmall = document.getElementById('videoBSmall');
      let streamB = null;
      let calling = false;
      let communicating = false;

      // 监听申请通话的事件
      socket.on('callEvent', async () => {
        calling = true;
        layerB.style.display = 'flex';
        const stream = await getLocalStream();
        videoB.srcObject = stream;
        videoB.play()
        streamB = stream;
      });

      const acceptBtn = document.getElementById('acceptBtn');
      // 同意接听
      acceptBtn.addEventListener('click', () => {
        socket.emit('acceptCallEvent', roomId);
      });

      let peer = null;
      // 监听offer事件
      socket.on('offerEvent', async offer => {
        peer = new RTCPeerConnection();
        for (const track of streamB.getTracks()) {
          peer.addTrack(track, streamB);
        }
        peer.onicecandidate = event => {
          if (event.candidate) {
            socket.emit('candidateEvent', { roomId, candidate: event.candidate });
          }
        }
        peer.ontrack = event => {
          calling = false;
          layerB.style.display = 'none';
          communicating = true;
          videoBSmall.srcObject = event.streams[0];
          setTimeout(() => {
            videoBSmall.play();
          })
        }
        
        // 设置"远端"描述的offer
        await peer.setRemoteDescription(offer);
        // 生成answer
        const answer = await peer.createAnswer();
        // 设置本地描述的answer
        await peer.setLocalDescription(answer);
        // 将answer发送给对方
        socket.emit('answerEvent', { roomId, answer })
      });
      socket.on('candidateEvent', async candidate => {
        await peer.addIceCandidate(candidate);
      });
      async function getLocalStream() {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true
        });
        return stream;
      }
    });
  </script>
</body>
</html>

样式:

body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100vh;
    display: flex;
    justify-content: center;
}
.box {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.box:first-child {
    margin-right: 20px;
}
.container {
    width: 370px;
    height: 580px;
    background-color: #f5f5f5;
    margin-bottom: 20px;
    position: relative;
}
button {
    width: 100px;
    height: 40px;
    cursor: pointer;
    background-color: #fff;
    border: 1px solid #ccc;
    color: #555;
    font-size: 15px;
}
button:first-child {
    margin-right: 20px;
}
video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.container-small {
    width: 120px;
    height: 160px;
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 1;
}
.layer {
    width: 100%;
    height: 100%;
    position: absolute;
    z-index: 2;
    top: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 0.8);
    color: #fff;
    display: flex;
    flex-direction: column;
    align-items: center;
    display: none;
}
.layer__text {
    margin-top: 150px;
    margin-bottom: 260px;
}
.layer__btns {
    display: flex;
}
.layer__btns .layer__btn:last-child {
    margin-left: 100px;
}
.layer__btn {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    font-size: 12px;
    cursor: pointer;
}
.layer__btn img {
    border-radius: 50%;
    background-color: #fff;
    margin-bottom: 8px;
}

整个功能最关键API:

RTCPeerConnection:本地端和远程对等端之间的 WebRTC 连接。它提供了创建远程对等端连接、维护和监视连接,以及在连接不再需要时关闭连接的方法。

呃...也没什么好介绍这个API,咱们用就完事。😁

而从代码里面可以看到,用户A与用户B的代码都差不多,主要过程大概是:

  1. 用户A生成了一个 offer 的东东,通过 setLocalDescription 方法修改了自己本地端的描述,然后将它发给了用户B。
  2. 用户B也生成了一个 answer 的东东,也一样通过 setLocalDescription 方法修改了自己本地端的描述,然后将它发给了用户A。
  3. 最后,用户A与用户B通过使用 setRemoteDescription 方法,将对方发过来的 offeranswer 修改到自己远程端的描述。

这个过程呢,一般就是固定的,目的就是为了要打通两端之间的 WebRTC 连接,实现一个即时通信能力。

贴一个网上流传比较广,完整绘制了整个建立过程的图:

image.png

还有还有,咱们也需要关注 onicecandidateontrack 这两个事件的触发时机:

  • 调用 setLocalDescription 方法修改本地端描述时,会触发 onicecandidate 事件。
  • 调用 setRemoteDescription 方法修改远程端描述时,则会触发 ontrack 事件。

原理过程

此处省略一万字。。。

本来寻思着讲讲 WebRTC 的原理过程,但是,了解完一圈下来后,感觉太复杂了,好像不是自己能轻易讲得清楚的,写完可能还没网上现成的写得好呢,算啦算啦。🤡

找了两篇写得不错的文章,感兴趣的UU们可以自行观摩观摩:

从0到1打造一个 WebRTC 应用

有了WebRTC,直播可以这样玩!





至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。