socket实现视频通话-WebRTC

最近喜欢研究视频流,所以思考了双向通信socket,接下来我们就一起来看看本地如何实现双向视频通讯的功能吧~

客户端获取视频流

首先思考如何获取视频流呢?

其实跟录音的功能差不多,都是查询电脑上是否有媒体设备,如果有录音和录像的设备,首先就需要授权,然后将视频流通过socket传输给服务端。

获取媒体设备

js 复制代码
const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true
})

因为是打视频的功能,那A客户端本身也希望看到A的摄像头,所以我们直接将其赋值给一个video标签,就能看到图像了.

html 复制代码
<p>这是A页面</p>

<div class="local-stream-page">
  <video autoplay controls muted id="elA"></video>
  <button onclick="onStart()">打视频给B页面</button>
</div>

<script>

  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true
    })
    if (videoElA) {
      videoElA.srcObject = stream // 在 video 标签上播放媒体流
    }
    peerInit(stream) // 初始化连接
  } catch (error) {
    console.log('error:', error)
  }

</script>

然后就是重要部分了,我们需要用到WebRTC的API

RTCPeerConnection

RTCPeerConnection是WebRTC API中的一个对象,用于建立和管理两个或多个用户之间的实时通信。它允许通过互联网进行音频和视频通话,以及共享数据流。

RTCPeerConnection对象提供了一系列的方法和事件,用于配置、管理和控制媒体流的传输。它支持使用不同的技术,如ICE(Interactive Connectivity Establishment)和STUN(Session Traversal Utilities for NAT)来解决网络地址转换(NAT)问题,以便在防火墙后面的不同设备之间建立连接。

使用RTCPeerConnection对象,您可以创建媒体流并将其发送到其他设备,也可以接收来自其他设备的媒体流。它还支持使用SDP(Session Description Protocol)描述媒体会话的配置,以及通过ICE和STUN协议协商和转发媒体数据包的路由。

js 复制代码
const peerInit = stream => {
  // 1. 创建连接实例
  peerA = new RTCPeerConnection()
  // 2. 添加视频流轨道
  stream.getTracks().forEach(track => {
    peerA.addTrack(track, stream)
  })


  // peerA 端
  peerA.onicecandidate = event => {
    if (event.candidate) {
      socketA.send(JSON.stringify({ type: 'candid', data: event.candidate })) // socketA发送数据
    }
  }
  // 检测连接状态
  peerA.onconnectionstatechange = event => {
    if (peerA.connectionState === 'connected') {
      console.log('对等连接成功!')
    }
  }

  // 互换sdp认证
  transSDP()
}

到这里我们发送数据部分就是这样子啦,但是还不行,因为两者通视频,还需要SDP认证,什么是SDP认证呢?

SDP(Session Description Protocol)认证是指通过在SDP协议中添加特定的信息来验证身份或其他属性的方法。SDP协议是一种用于描述多媒体会话的信息协议,它包含了音频、视频等媒体的编码格式、分辨率、网络地址等信息,用于在通话双方之间建立和维护媒体连接。

在SDP认证中,通过在SDP协议中添加特定的信息,如用户名、会议ID等,双方可以互相验证身份。此外,还可以通过在SDP协议中包含数字签名或加密信息等技术来增强认证的安全性。

SDP认证通常用于多媒体通信、视频会议等应用场景中,以确保通信的安全性和可信度。在SDP认证中,需要使用相应的协议或算法来验证SDP信息的来源和完整性,以确认身份或其他属性的合法性。

互换SDP认证

js 复制代码
// peerA 端
const transSDP = async () => {
 
  let offer = await peerA.createOffer()
  // 向 peerB 传输 offer
  socketA.send(JSON.stringify({ type: 'offer', data: offer }))
  // 接收 peerB 传来的 answer
  socketA.onmessage = async evt => {
    
    let reader = new FileReader()
    reader.readAsText(evt.data, 'utf-8')
    reader.onload = async function() {
      let { type, data } = JSON.parse(reader.result)
      console.log(JSON.parse(reader.result), 111)
      if (type == 'answer') {
        await peerA.setLocalDescription(offer)
        await peerA.setRemoteDescription(data)
      }
    }
    
  }
}

这就是A客户端的全部代码啦~

放心,全部代码文章末尾会给到.

node服务端socket传输

接下来我们来看看服务端是如何处理的.对了,这里必须说一下,两个socket之间的通信,必须要靠服务端管理,所以这就是为什么一定要学node的原因😂

js 复制代码
const WebSocket = require('ws');

// 创建一个 WebSocket 服务器,监听 8080 端口
const wss = new WebSocket.Server({ port: 8000 });

// 当有客户端连接时,创建一个 WebSocket 并将其添加到客户端列表中
wss.on('connection', function connection(ws) {
  console.log('Client connected');

  // 当客户端发送消息时,将消息发送给所有客户端
  ws.on('message', function incoming(message) {
    console.log('Received message:', message.toString('utf8')); // 接受的对象,客户端发送的是字符串,Buffer

    // 将消息发送给所有客户端
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message); // 客户端接受的是blob格式数据
      }
    });
  });

  // 当客户端断开连接时,将其从客户端列表中删除
  ws.on('close', function close() {
    console.log('Client disconnected');
  });
});

服务端用到了ws依赖, 如何区分两个不同的socket客户端, 特别是在同一个服务器下,同一个端口,不同的页面下,我发现必须要给两个socket一个唯一的标识才能做到,所以这期就先出功能,后面再继续补一下ws的源码学习.

不过这里要区分清楚,这是将当前的client客户端发送给处理自己以外的,其他所以socket客户端,发送消息这里,就是一对多的关系哦.

客户端接受视频流

服务端处理完了,就进行下一个客户端如何接受视频流

刚刚的sdp认证,肯定不止止A页面的事情,都说了是认证,那肯定通信双方需要知晓.

这里有一个顺序问题

1.首先是A页面创建offer---createOffer

2.然后是B页面设置远程描述---setRemoteDescription

3.B页面生成发送到A页面的answer---createAnswer

4.B页面设置本地描述---setLocalDescription

5.A页面设置本地描述---setLocalDescription(传参是A页面的offer)

6.A页面设备远程描述--setRemoteDescription(传参是B页面的answer)

只要这上面6步都正常执行,B页面才能接收到A页面的视频流和音频流

js 复制代码
const transSDP = async () => {
  // 1. 创建 offer
  let offer = await peerA.createOffer()
  await peerB.setRemoteDescription(offer)
  // 2. 创建 answer
  let answer = await peerB.createAnswer()
  await peerB.setLocalDescription(answer)
  // 3. 发送端设置 SDP
  await peerA.setLocalDescription(offer)
  await peerA.setRemoteDescription(answer)
}

加上socket之后就是这样

不过既然是socket了,所以数据上要做转换处理,接收到的是blob数据

js 复制代码
// B接收A的消息
// peerB 端,接收 peerA 传来的 offer
socketB.onmessage = evt => {
  // console.log(evt.data)
  handleBlobToText(evt.data)
}

const handleBlobToText = (blob) => {
  let reader = new FileReader()
  reader.readAsText(blob, 'utf-8') // 接收到的是blob数据,先转成文本
  reader.onload = async function() {
    console.log(reader.result)
    let { type, data } = JSON.parse(reader.result) // 文本转对象
    console.log(JSON.parse(reader.result))
    if (type == 'offer') {
      await peerB.setRemoteDescription(data)
      console.log('2.然后是B页面设置远程描述', new Date().getTime())
      let answer = await peerB.createAnswer()
      console.log('3.B页面生成发送到A页面的answer', new Date().getTime())
      await peerB.setLocalDescription(answer)
      console.log('4.B页面设置本地描述', new Date().getTime())
      // 向 peerA 传输 answer
      socketB.send(JSON.stringify({ type: 'answer', data: answer }))
    }
    if (type == 'candid') {
      peerB.addIceCandidate(data)
    }
  }
}

socketB.onerror = function() {
  console.log('WebSocket error. Ready state:', socketB.readyState);
};

根据时间戳,就能发现这六步的顺序.

将接收到的视频流渲染到B页面的video标签中,这就能接受的A页面的视频流了.

js 复制代码
const socketB = new WebSocket('ws://localhost:8000');

const peerB = new RTCPeerConnection()
const videoElB = document.getElementById('elB')

// 监听数据传来
peerB.ontrack = async event => {
  const [remoteStream] = event.streams
  videoElB.srcObject = remoteStream
}

效果

这就是两个页面视频通讯的结果如下:

全部源码已经上传在GitHub上啦~

github.com/0522skylar/...

相关推荐
卸任12 小时前
Vibe coding工具使用心得
前端·ai编程
brien913 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance13 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M13 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写13 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李13 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易13 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding13 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马14 小时前
前沿视界
前端
kyriewen14 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员