从0搭建一个WebRTC,实现多房间多对多通话,并实现屏幕录制

news2024/9/22 19:26:02

这篇文章开始会实现一个一对一WebRTC和多对多的WebRTC,以及基于屏幕共享的录制。本篇会实现信令和前端部分,信令使用fastity来搭建,前端部分使用Vue3来实现。 为什么要使用WebRTC WebRTC全称Web Real-Time Communication,是一种实时音视频的技术,它的优势是低延时。 本片文章食用者要求

  • 了解音视频基础

  • 能搭建简单的node服务,docker配置

  • vue框架的使用

环境搭建及要求 废话不多说,现在开始搭建环境,首先是需要开启socket服务,采用的是fastify来进行搭建。详情可以见文档地址,本例使用的是3.x来启动的。接下来安装fastify-socket.io3.0.0插件,详细配置可以见文档,此处不做详细解释。接下来是搭建Vue3,使用 vite 脚手架搭建简单的demo。 要求:前端服务运行在localhost或者https下。node需要redis进行数据缓存 获取音视频 要实现实时音视频第一步当然是要能获取到视频流,在这里我们使用浏览器提供的API,MediaDevices来进行摄像头流的捕获 enumerateDevices 第一个要介绍的API是enumerateDevices,是请求一个可用的媒体输入和输出设备的列表,例如麦克风,摄像机,耳机设备等。直接在控制台执行API,获取的设备如图

我们注意到里面返回的设备ID和label是空的,这是由于浏览器的安全策略限制,必须授权摄像头或麦克风才能允许返回设备ID和设备标签,接下来我们介绍如何请求摄像头和麦克风 getUserMedia 这个API顾名思义,就是去获取用户的Meida的,那我们直接执行这个API来看看效果 ps: 由于掘金的代码片段的iframe没有配置allow="display-capture *;microphone *; camera *"属性,需要手动打开详情查看效果

通过上述例子我们可以获取到本机的音视频画面,并且可以播放在video标签里,那么我们可以在获取了用户的流之后,重新再获取一次设备列表看看发生了什么变化

在获取了音视频之后,获取的设备列表的详细信息已经出现,我们就可以获取指定设备的音视频数据,详情可以见

这里介绍一下getUserMedia的参数constraints,

视频参数配置

interface MediaTrackConstraintSet {
    // 画面比例
    aspectRatio?: ConstrainDouble;
    // 设备ID,可以从enumerateDevices中获取
    deviceId?: ConstrainDOMString;
    // 摄像头前后置模式,一般适用于手机
    facingMode?: ConstrainDOMString;
    // 帧率,采集视频的目标帧率
    frameRate?: ConstrainDouble;
    // 组ID,用一个设备的输入输出的组ID是同一个
    groupId?: ConstrainDOMString;
    // 视频高度
    height?: ConstrainULong
    // 视频宽度
    width?: ConstrainULong;
}

音频参数配置

interface MediaTrackConstraintSet {
    // 是否开启AGC自动增益,可以在原有音量上增加额外的音量
    autoGainControl?: ConstrainBoolean;
    // 声道配置
    channelCount?: ConstrainULong;
    // 设备ID,可以从enumerateDevices中获取
    deviceId?: ConstrainDOMString;
    // 是否开启回声消除
    echoCancellation?: ConstrainBoolean;
    // 组ID,用一个设备的输入输出的组ID是同一个
    groupId?: ConstrainDOMString;
    // 延迟大小
    latency?: ConstrainDouble;
    // 是否开启降噪
    noiseSuppression?: ConstrainBoolean;
    // 采样率单位Hz
    sampleRate?: ConstrainULong;
    // 采样大小,单位位
    sampleSize?: ConstrainULong;
    // 本地音频在本地扬声器播放
    suppressLocalAudioPlayback?: ConstrainBoolean;
}

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

一对一连接 当我们采集到了音视频数据,接下来就是要建立链接,在开始之前需要科普一下WebRTC的工作方式,我们常见有三种WebRTC的网络结构

  1. Mesh

  2. MCU

  3. SFU 关于这三种模式的区别可以查看 文章来了解

在这里由于设备的限制,我们采用Mesh的方案来进行开发 一对一的流程 我们建立一对一的链接需要知道后流程是怎么流转的,接下来上一张图,便可以清晰的了解

这里是由ClientA发起B来接受A的视频数据。上图总结可以为A创建本地视频流,把视频流添加到PeerConnection里面 创建一个Offer给B,B收到Offer以后,保存这个offer,并响应这个Offer给A,A收到B的响应后保存A的远端响应,进行NAT穿透,完成链接建立。 话已经讲了这么多,我们该怎么建立呢,光说不做假把式,接下来,用我们的项目创建一个来试试 初始化 首先启动fastify服务,接下来在Vue项目安装socket.io-client@4然后连接服务端的socket

import { v4 as uuid } from 'uuid';
import { io, Socket } from 'socket.io-client';
const myUserId = ref(uuid());
let socket: Socket;
socket = io('http://127.0.0.1:7070', {
  query: {
    // 房间号,由输入框输入获得
    room: room.value,
    // userId通过uuid获取
    userId: myUserId.value,
    // 昵称,由输入框输入获得
    nick: nick.value
  }
});

可以查看chrome的控制台,检查ws的链接情况,如果出现跨域,请查看socket.io的server配置并开启cors配置。

创建offer

开始创建RTCPeerConnection,这里采用google的公共stun服务

const peerConnect = new RTCPeerConnection({
  iceServers: [
    {
      urls: "stun:stun.l.google.com:19302"
    }
  ]
})

根据上面的流程图我们下一步要做的事情是用上面的方式获取视频流,并将获取到的流添加到RTCPeerConnection中,并创建offer,把这个offer设置到这个rtcPeer中,并把offer发送给socket服务

let localStream: MediaStream;
​
stream.getTracks().forEach((track) => {
  peerConnect.addTrack(track, stream)
})
​
const offer = await peerConnect.createOffer();
await peerConnect.setLocalDescription(offer);
socket.emit('offer', { creatorUserId: myUserId.value, sdp: offer }, (res: any) => {
  console.log(res);
});

socket 服务收到了这份offer后需要给B发送A的offer

fastify.io.on('connection', async (socket) => {
    socket.on('offer', async (offer, callback) => {
      socket.emit('offer', offer);
      callback({
        status: "ok"
      })
    })
})

处理offer

B需要监听socket里面的offer事件并创建RTCPeerConnection,将这个offer设置到远端,接下来来创建响应。并且将这个响应设置到本地,发送answer事件回复给A

socket.on('offer', async (offer: { sdp: RTCSessionDescriptionInit, creatorUserId: string }) => {
    const peerConnect = new RTCPeerConnection({
      iceServers: [
        {
          urls: "stun:stun.l.google.com:19302"
        }
      ]
    })
​
    await peerConnect.setRemoteDescription(offer.sdp);
    const answer = await peerConnect.createAnswer();
    await peerConnect.setLocalDescription(answer);
    socket.emit('answer', { sdp: answer }, (res: any) => {
      console.log(res);
    }) 
})

处理answer

服务端广播answer

socket.on('offer', async (offer, callback) => {
      socket.emit('offer', offer);
      callback({
        status: "ok"
      })
    })

A监听到socket里面的answer事件,需要将刚才的自己的RTCpeer添加远端描述

socket.on('answer', async (data: { sdp: RTCSessionDescriptionInit }) => {
    await peerConnect.setRemoteDescription(data.sdp)
})

处理ICE-candidate

接下来A会获取到ICE候选信息,需要发送给B

peerConnect.onicecandidate = (candidateInfo: RTCPeerConnectionIceEvent) => {
  if (candidateInfo.candidate) {
    socket.emit('ICE-candidate', { sdp: candidateInfo.candidate }, (res: any) => {
      console.log(res);
    })
  }
}

广播消息是同理这里就不再赘述了,B获取到了A的ICE,需要设置候选

socket.on('ICE-candidate', async (data: { sdp: RTCIceCandidate }) => {
   await peerConnect.addIceCandidate(data.sdp)
})

接下来B也会获取到ICE候选信息,同理需要发送给A,待A设置完成之后便可以建立链接,代码同上,B接下来会收到流添加的事件,这个事件会有两次,分别是音频和视频的数据

处理音视频数据

peerConnect.ontrack = (track: RTCTrackEvent) => {
    if (track.track.kind === 'video') {
      const video = document.createElement('video');
      video.srcObject = track.streams[0];
      video.autoplay = true;
      video.style.setProperty('width', '400px');
      video.style.setProperty('aspect-ratio', '16 / 9');
      video.setAttribute('id', track.track.id)
      document.body.appendChild(video)
    }
    if (track.track.kind === 'audio') {
      const audio = document.createElement('audio');
      audio.srcObject = track.streams[0];
      audio.autoplay = true;
      audio.setAttribute('id', track.track.id)
      document.body.appendChild(audio)
    }
}

到这里你就可以见到两个视频建立的P2P链接了。到这里为止只是建立了视频的一对一链接,但是我们可以通过这些操作进行复制,就能进行多对多的连接了。 多对多连接 在开始我们需要知道,一个人和另一个人建立连接双方都需要创建自己的peerConnection。对于多人的情况,首先我们需要知道进入的房间里面当前的人数,给每个人都创建一个RtcPeer,同时收到的人也回复这个offer给发起的人。对于后进入的人,需要让已经创建音视频的人给后进入的人创建新的offer。 基于上面的流程,我们现在先实现一个成员列表的接口 成员列表的接口 在我们登录socket服务的时候我们在query参数里面有房间号,userId和昵称,我们可以通过redis记录对应的房间号的登录和登出,从而实现成员列表。 可以在某一个人登录的时候获取一下redis对应房间的成员列表,如果没有这个房间,就把这个人丢进新的房间,并且存储到redis中,方便其他人登录这个房间的时候知道现在有多少人。

fastify.io.on('connection', async (socket) => {
  const room = socket.handshake.query.room;
  const redis = fastify.redis;
  let userList;
  // 获取当前房间的数据
  await getUserList()
​
    async function getUserList() {
      const roomUser = await redis.get(room);
      if (roomUser) {
        userList = new Map(JSON.parse(roomUser))
      } else {
        userList = new Map();
      }
    }
    
    async function setRedisRoom() {
      await redis.set(room, JSON.stringify([...userList]))
    }
    
    function rmUser(userId) {
      userList.delete(userId);
    }
    
    
    if (room) {
      // 将这人加入到对应的socket房间
      socket.join(room);
      await setRedisRoom();
      // 广播有人加入了
      socket.to(room).emit('join', userId);
    }
    // 这个人断开了链接需要将这个人从redis中删除
    socket.on('disconnect', async (socket) => {
      await getUserList();
      rmUser(userId);
      await setRedisRoom();
    })
​
})

到上面为止,我们实现了成员的记录、广播和删除。接下来是需要实现一个成员列表的接口,提供给前端项目调用。

fastify.get('/userlist', async function (request, reply) {
  const redis = fastify.redis;
  return await redis.get(request.query.room);
})

多对多初始化

由于需要给每个人发送offer,需要对上面的初始化函数进行封装。

/**
 * 创建RTCPeerConnection
 * @param creatorUserId 创建者id,本人
 * @param recUserId 接收者id
 */
const initPeer = async (creatorUserId: string, recUserId: string) => {
  const peerConnect = new RTCPeerConnection({
    iceServers: [
      {
        urls: "stun:stun.l.google.com:19302"
      }
    ]
  })
  return peerConnect;
})

由于存在多份rtc的映射关系,我们这里可以用Map来实现映射的保存

const peerConnectList = new Map();
​
const initPeer = () => {
   // ice,track,new Peer等其他代码
   ......
   peerConnectList.set(`${creatorUserId}_${recUserId}`, peerConnect);
}

获取成员列表

上面实现了成员列表。接下来进入了对应的房间后需要轮询获取对应的成员列表

let userList = ref([]);
const intoRoom = () => {
    //其他代码
    ......
    
    setInterval(()=>{
      axios.get('/userlist', { params: { room: room.value }}).then((res)=>{
        userList.value = res.data
      })
    }, 1000)
}

创建多对多的Offer和Answer

在我们获取到视频流的时候,可以对在线列表里除了自己的人都创建一个RTCpeer,来进行一对一连接,从而达到多对多连接的效果。

// 过滤自己
const emitList = userList.value.filter((item) => item[0] !== myUserId.value);
for (const item of emitList) {
  // item[0]就是目标人的userId
  const peer = await initPeer(myUserId.value, item[0]);
  await createOffer(item[0], peer);
}
​
const createOffer = async (recUserId: string, peerConnect: RTCPeerConnection, stream: MediaStream = localStream) => {
  if (!localStream) return;
  stream.getTracks().forEach((track) => {
    peerConnect.addTrack(track, stream)
  })
  const offer = await peerConnect.createOffer();
  await peerConnect.setLocalDescription(offer);
  socket.emit('offer', { creatorUserId: myUserId.value, sdp: offer, recUserId }, (res: any) => {
    console.log(res);
  });
}

那么在socket服务中我们怎么只给对应的人进行事件广播,不对其他人进行广播,我们可以用找到这个人userId对应的socketId,进而只给这一个人广播事件。

// 首先获取IO对应的nameSpace
const IONameSpace = fastify.io.of('/');
​
// 发送Offer给对应的人
socket.on('offer', async (offer, callback) => {
  // 重新从reids获取用户列表
  await getUserList();
  // 找到目标的UserId的数据
  const user = userList.get(offer.recUserId);
  if (user) {
    // 找到对应的socketId
    const io = IONameSpace.sockets.get(user.sockId);
    if (!io) return;
    io.emit('offer', offer);
    callback({
      status: "ok"
    })
  }
})

其他人需要监听socket的事件,每个人都需要处理对应自己的offer。

socket.on('offer', handleOffer);
const handleOffer = async (offer: { sdp: RTCSessionDescriptionInit, creatorUserId: string, recUserId: string }) => {
  const peer = await initPeer(offer.creatorUserId, offer.recUserId);
  await peer.setRemoteDescription(offer.sdp);
  const answer = await peer.createAnswer();
  await peer.setLocalDescription(answer);
  socket.emit('answer', { recUserId: myUserId.value, sdp: answer, creatorUserId: offer.creatorUserId }, (res: any) => {
    console.log(res);
  })
}

接下来的步骤其实就是和一对一是一样的了,后面还需要发起offer的人处理对应peer的offer、以及ICE候选,还有流进行挂载播放。

socket.on('answer', handleAnswer)
// 应答方回复
const handleAnswer = async (data: { sdp: RTCSessionDescriptionInit, recUserId: string, creatorUserId: string }) => {
  const peer = peerConnectList.get(`${data.creatorUserId}_${data.recUserId}`);
  if (!peer) {
    console.warn('handleAnswer peer 获取失败')
    return;
  }
  await peer.setRemoteDescription(data.sdp)
}
......处理播放,处理ICE候选

到目前为止,就实现了一个基于mesh的WebRTC的多对多通信。在这里附上了一个完整的Demo可供参考 socketServer FontPage 基于WebRTC的屏幕录制 getDisplayMedia 这个API是在MediaDevices里面的一个方法,是用来获取屏幕共享的。 这个 MediaDevices 接口的 getDisplayMedia() 方法提示用户去选择和授权捕获展示的内容或部分内容(如一个窗口)在一个 MediaStream 里. 然后,这个媒体流可以通过使用 MediaStream Recording API 被记录或者作为WebRTC 会话的一部分被传输。 await navigator.mediaDevices.getDisplayMedia() 复制代码 MediaRecorder 获取到屏幕共享流后,需要使用 MediaRecorder这个api来对流进行录制,接下来我们先获取屏幕流,同时创建一个MeidaRecord类

let screenStream: MediaStream;
let mediaRecord: MediaRecorder;
let blobMedia: (Blob)[] = [];
const startLocalRecord = async  () => {
  blobMedia = [];
  try {
      screenStream = await navigator.mediaDevices.getDisplayMedia();
      screenStream.getVideoTracks()[0].addEventListener('ended', () => {
        console.log('用户中断了屏幕共享');
        endLocalRecord()
      })
​
      mediaRecord = new MediaRecorder(screenStream, { mimeType: 'video/webm' });
​
      mediaRecord.ondataavailable = (e) => {
        if (e.data && e.data.size > 0) {
          blobMedia.push(e.data);
        }
      };
​
      // 500是每隔500ms进行一个保存数据
      mediaRecord.start(500)
  } catch(e) {
      console.log(`屏幕共享失败->${e}`);
  }
}

获取到了之后可以使用Blob进行处理

const replayLocalRecord = async () => {
  if (blobMedia.length) {
    const scVideo = document.querySelector('#screenVideo') as HTMLVideoElement;
    const blob = new Blob(blobMedia, { type:'video/webm' })
    if(scVideo) {
       scVideo.src = URL.createObjectURL(blob);
    }
  } else {
    console.log('没有录制文件');
  }
}
​
const downloadLocalRecord = async () => {
  if (!blobMedia.length) {
    console.log('没有录制文件');
    return;
  }
  const blob = new Blob(blobMedia, { type: 'video/webm' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `录屏_${Date.now()}.webm`;
  a.click();
}

这里有一个基于Vue2的完整例子

ps: 由于掘金的代码片段的iframe没有配置allow="display-capture *;microphone *; camera *"属性,需要手动打开详情查看效果

后续将会更新,WebRTC的自动化测试,视频画中画,视频截图等功能

作者:sxuan

★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/138271.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安全狗重磅发布数据安全解决方案·数垒

一、 势在必行的数据安全 近年来,随着《网络安全法》、《数据安全法》、“数据二十条”等多部法律、意见法规等的相继颁布,数据安全管理与防护在国家政策上成为势在必行的行动之一。与此同时,伴随着数字经济时代的到来,数以万计…

Java基础语法-学习笔记

目录 01Java语言的发展 02Java的三大平台 03Java的主要特性 04JRE和JDK 1. 注释 使用的技巧 注意点 2. 关键字 2.1 概念 2.2 第一个关键字class 3. 字面量 区分技巧 4. 变量 4.1 什么是变量? 4.2 变量的定义格式 5. 数据类型 5.1 Java语言数据类型的…

STM32——TIM输出比较

文章目录一、TIM输出比较输出比较简介PWM简介输出比较通道(高级)输出比较通道(通用)输出比较模式控制器工作原理PWM基本结构参数计算三、PWM驱动LED呼吸灯电路设计关键代码关键函数与参数引脚重映射取消默认调试功能函数极性选择决定占空比,周期的三个函数四、PWM驱…

Design pattern-js的设计模式(一)

前言 什么是设计模式?(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间…

【youcans 的 OpenCV 例程 300篇】254.OpenCV 绘制图像标记

『youcans 的 OpenCV 例程300篇 - 总目录』 【youcans 的 OpenCV 例程 300篇】254. OpenCV 绘制标记 7.1 绘图函数基本参数 OpenCV提供了绘图功能,可以在图像上绘制直线、矩形、圆、椭圆等各种几何图形。 函数 cv.line()、cv.rectangle()、cv.circle()、cv.polyli…

深度学习:08 训练、测试和验证集的说明

目录 用于深度学习的数据集 训练集 验证集 测试集 总结 用于深度学习的数据集 接下来,我将在这篇里面讨论在训练和测试神经网络期间使用的不同数据集。 出于模型的训练和测试目的,我们应该将数据分解为三个不同的数据集。这些数据集将包含以下内容…

【小f的刷题笔记】(JS)阶乘 - 阶乘后的零 LeetCode172 阶乘函数后K个零 LeetCode793

【阶乘】 一、阶乘后的零: LeetCode172 链接: 172.阶乘后的零 题目: 思路: 0的产生是一定是因为2*5产生的,所以就是找因数 并且,可想而知,找的到因数5,必然找的到因数2与之搭配…

【MySQL】深入理解B+树索引

文章目录1. 前言2. 索引方案3. InnoDB的索引方案4. 索引的分类4.1 聚簇索引4.2 二级索引4.3 联合索引5. InnoDB中的B树索引的注意事项5.1 内节点中目录项记录的唯一主5.2 一个页至少容纳2条记录6. MyISAM中的索引⽅案简单介绍1. 前言 索引,是MySQL快速查询的秘籍。…

ARMv8/ARMv9:深入理解MPIDR_EL1寄存器中的affinity

快速链接: . 👉👉👉 个人博客笔记导读目录(全部) 👈👈👈 付费专栏-付费课程 【购买须知】:【精选】ARMv8/ARMv9架构入门到精通-[目录] 👈👈👈官方文档(ARM ARM文档)的介绍如下所示 翻译一下MPIDR_EL1相关的英文,如下所示: 作用: 在多处理器系统中,为调…

Xshell 连接虚拟机(Ubuntu、CentOS)

对于一些linux的初学者来说,在没有自己的服务器时可以选择使用虚拟机来代替(如ubuntu、centos等)进行相关的学习。下面介绍下如何使用xshell来远程连接虚拟机。 注意:下面我以Ubuntu来举例说明。 1、创建虚拟机 虚拟机的创建网络…

1、数据库安装修改root密码管理自启服务

MySQL的下载和安装 登录MySQL官网下载MySQL.zip包 MySQL :: Download MySQL Community Server 下载完毕可自行选择存储位置,进行解压 解压后配置环境变量 完成配置后在MySQL目录下新建一个my.ini配置文件 文件写入以下内容 [client] # 设置mysql客户端默认字符集…

面向对象2(static修饰变量和方法、Javabean类、测试类和工具类、对main方法的理解、继承、子类继承父类构造方法变量和方法)

1、static修饰变量和方法 JDK8以前,静态区在方法区里面,JDK8开始,静态区挪到了堆内存当中 理解: 因为静态方法没有this,而非静态方法是有一个隐含的参数this的,所以想在静态方法里面调用非静态变量或方法就…

电子招标采购系统源码之传统采购模式面临的挑战

采购类型多 采购制度:采购金额、部门、品类的差异导致管理标准不同。 采购流程:从供应商管理、寻源操作到合同签订、订单执行,业务流程长,审批节点多,传统管理透明度低,联动性差。 供应商管理难 寻源&#…

亚马逊云科技 Build On - Serverless助力企业降本增效

亚马逊云科技 Build On - Serverless开启零售新篇章梅开三度活动体验实验问题总结一点建议咖啡案例实验Serverless学习总结梅开三度 Hi,作为一名Builder,这也是第三次参加由AWS&CSDN共同举办的Build On活动,跟前几期一样,活动举办方也是…

Java集合ArrayList-学习笔记

目录 ArrayList 集合和数组的优势对比: 1.1 ArrayList类概述 1.2 ArrayList类常用方法 1.3 ArrayList存储字符串并遍历 1.4 ArrayList存储学生对象并遍历 1.5 查找用户的索引 1.6 判断用户的是否存在 ArrayList 集合和数组的优势对比: 集合长度可…

Ardupilot EKF3核心算法《状态量速度与位置预测方程》

目录 文章目录 目录摘要1. Ardupilot EKF3核心算法《状态量速度预测方程》2. Ardupilot EKF3核心算法《状态量位置预测方程》3. Ardupilot EKF3核心算法《状态量速度与位置预测方程》摘要 本节主要记录Ardupilot EKF3核心算法《状态量速度与位置预测方程》的过程,欢迎批评指正…

mysql生产数据库被误删

23年的头一天上班安装数据库,因为ssh工具来回切换失误,犯下不可饶恕的错误,居然将生产数据库全部删除,工作十几年头一次干这种蠢事,第一时间反应是一世英名毁于一旦,赶紧跑路。第二反应还是想办法看能否挽回…

aws eks 使用 cloudformation 创建并更新自管节点组

参考资料 更新现有自行管理的节点组 使用eks自管节点组能够最大程度控制节点的各项配置和参数,包括并不限于ami,节点类型等 但是使用自管节点也给用户带来了较大的维护和更新成本。对于节点组的更新操作,我们可以使用cloudformation的方式…

速览Visual Studio 2022 中的新增功能

目录 性能改进 Visual Studio 2022 为 64 位 在文件中更快地查找 Git 工具速度更快 生成新式应用 适用于 C、.NET 和热重载的更佳开发工具 Blazor & 的汇报ASP.NET 的 Razor 编辑器 热重载 创新触手可及 Git 多存储库支持和行暂存支持 IntelliCode 改进 为每个…

魔方(15)二、三、四、五阶棋盘魔方

棋盘魔方 这价格实在可恶,于是我拿现成的魔方自己贴贴纸。 二阶棋盘魔方A 有4个角块是3面黑,4个角块是3面白,而6个面都是棋盘色。 只需要2步就可以转化成: 所以任意状态只要转化成这个状态再加2步就能复原了。 二阶棋盘魔方B&a…