范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

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

  这篇文章开始会实现一个一对一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进行数据缓存
  C++音视频开发 WebRTC学习资料 :点击领取 →音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  获取音视频
  要实现实时音视频第一步当然是要能获取到视频流,在这里我们使用浏览器提供的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; }
  C++音视频开发WebRTC学习资料 :点击领取 →音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  一对一连接
  当我们采集到了音视频数据,接下来就是要建立链接,在开始之前需要科普一下WebRTC的工作方式,我们常见有三种WebRTC的网络结构MeshMCUSFU 关于这三种模式的区别可以查看 文章来了解
  在这里由于设备的限制,我们采用Mesh的方案来进行开发一对一的流程
  我们建立一对一的链接需要知道后流程是怎么流转的,接下来上一张图,便可以清晰的了解
  这里是由ClientA发起B来接受A的视频数据。上图总结可以为A创建本地视频流,把视频流添加到PeerConnection里面 创建一个Offer给B,B收到Offer以后,保存这个offer,并响应这个Offer给A,A收到B的响应后保存A的远端响应,进行NAT穿透,完成链接建立。
  话已经讲了这么多,我们该怎么建立呢,光说不做假把式,接下来,用我们的项目创建一个来试试初始化
  首先启动fastify服务,接下来在Vue项目安装socket.io-client@4然后连接服务端的socketimport { 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的offerfastify.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事件回复给Asocket.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
  服务端广播answersocket.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候选信息,需要发送给BpeerConnect.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接下来会收到流添加的事件,这个事件会有两次,分别是音频和视频的数据
  C++音视频开发 WebRTC学习资料 :点击领取 →音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  处理音视频数据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的多对多通信
  C++音视频开发 WebRTC学习资料 :点击领取 →音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  基于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的自动化测试,视频画中画,视频截图等功能

2022年度北京积分落户公示数据解读01郊区职住加分2022年度上岸人数为6006人,包括取得郊区职住加分的总计5400人,其中8分2378人,9分537人,10分265人,11分306人,12分1402人。812分不摘除这三颗毒瘤,房地产很难稳健发展受国际局势日益复杂化新冠疫情的影响等绪多因素影响,房地产市场躺赚的时代一去不复返了。人们逐渐从狂热买房的噩梦中惊醒,向理性买房转变,买房的多种负担逐渐为人们所认识,其中,房地产三大2022年10月6日星期四币圈行情过去几个月为加密货币的价格带来了过山车般的体验。由于宏观因素的影响,加密货币市场一直在徘徊。但过去的24小时在市场上创造了新的力量。几乎所有资产都采取了上涨,推动市场进入绿色。随着房贷越短越好还是越长越好?银行员工不少人都在白送钱大多数人买房都是需要贷款的,那么房贷怎么申请就有很大的讲究了。比如我一个朋友首付30多万买了一套房子,每个月需要还房贷6000多元,其中利息占到4000,每个月还的本金很少,所以他李逸轩10。6获利了结短区间,金银回踩还需多,黄金白银走势分析昨日黄金市场出现获利了结过程,早盘开盘在1725。4的位置后行情小幅拉升给出1728的位置后行情回落,日线最低给到了1700。3的位置后尾盘拉升,日线最终收线在了1716。2的位置再创记录!马斯克的SpaceX成功将首位俄罗斯宇航员送入太空钛快讯刚发布完刷爆社交平台的特斯拉机器人擎天柱五天后,世界首富埃隆马斯克(ElonMusk)创立的另一家商业航天公司SpaceX(美国太空探索技术公司)今天再创新纪录。当地时间10月5日宁可面临受冻风险也要与俄罗斯天然气划清界限,欧洲在想什么?央视网消息(记者阚纯裕剪辑王卓婕)1966年,为了压低荷兰天然气价格,意大利和奥地利开始和苏联商谈进口计划。这一年,勃列日涅夫成为苏联的最高领导人,他认为,向欧洲出售石油和天然气一俄罗斯恢复向意大利供应天然气新华社北京10月6日电俄罗斯天然气工业股份公司(俄气)5日在社交媒体发布消息说,经由奥地利向意大利的天然气供应已经恢复。俄气说,在奥地利监管制度变化后,该公司已经与意大利客户找到解初识便相爱雷咖泽KW75客制化机械键盘体验这是我第一次接触客制化键盘,以前虽然有所听闻,但总觉得太烧钱,真的玩儿进去,恐怕跟单反的烧钱速度没区别。客制化键盘之所以那么热门,主要是跟它可以对外壳PCB板定位板背光轴体键帽等套2023年养老金还会继续增调吗?4个信号释放利好,快来看看养老金是多数退休人员维持基本生活的唯一经济来源,人们到手的养老金越多,晚年生活就越有保障,所以大家都十分关注养老金待遇调整的消息,这不,2022年职工基本养老金刚刚实现18连涨,就11个投稿途径,让你轻松过稿,写作也能月入过万我是紫薇郎,点击关注,分享读书与写作干货!我知道的,都交给你。1教育类跟领亲子伴读中心征稿内容亲子类原创故事儿童绘本插画儿童剧本杀稿费500012000元新东方家庭教育征稿内容亲子
女子为美去纹花臂,痛到落泪也不放弃,网友给人一种清纯的感觉有人喜欢喝酒,有人喜欢唱歌,不管是谁,都会有一个爱好存在,或是小众或是大众,或是被理解或是被误会,但不管如何,这我喜欢些独属于自己的爱好,始终让自己十分着迷。只不过并不是所有的爱好宣布造车18个月后,小米似乎陷入了困境2021年3月30日,小米集团发布公告宣布造车。公告显示,本公司董事会正式批准智能电动汽车业务立项。本公司拟成立一家全资子公司,负责智能电动汽车业务。首期投资为100亿元人民币,预中金中金ESG评级房地产行业摘要本篇要点本篇是中金ESG手册评级篇之房地产行业,从国内房地产行业基本特征着手,梳理房地产行业ESG趋势政策和实践,并在中金ESG评级体系下,重点分析房地产行业ESG重要性议题和千年古墓内出现大量蓝色人骨,考古队惊慌,是外星人遗体?一直以来,人们对外星生物的好奇从未停止过。然而,直到现在我们连外星人的一根头发也没有见过,对于外星人的样貌,我们一直都停留在影视剧中,虽然也有人说自己见到过,但始终没有得到科学印证python抓取斗图网站的表情包本文介绍抓取斗图啦这一网址的表情包,内容比较简单,数据没有加密。一。思路介绍受害者网址httpswww。pkdoutu。com抓包工具找到表情包数据类型与所在位置,即数据为静态数据十二道金牌召回精忠报国的岳飞能不能不听令?精忠报国岳飞的命运让人唏嘘,好不容易打了胜仗,却被皇帝用十二道金牌召回,打入天牢,最终被处死。很多人肯定在为他惋惜时感慨过,要是岳飞别那么听话就好了,是不是?对于岳飞听令被召回,我爱因斯坦人性的善良今天想讲述的这个人想必很多人都耳熟能详,但是他背后的一些高光可能并不是人尽皆知,确切地说应该是他们,所以接下来的主人翁是爱因斯坦和他的伙伴们。他们所做的一切他们的担忧在第二次世界大纣殿皆腐骨,兴亡定环回商汤依靠仁义,创建了商朝。商朝是中国历史上第二个奴隶制国家和第一个有文字记载的朝代。闻名世界的甲骨文,就产生在商朝。商朝经历了17代31个王,终于商纣王,对比鲜明的是商汤仁德收天下赵一曼(国1950)小校观影笔记本赵一曼,1950年拍摄的老电影,还能让人浮想联翩。记得年少,在接受革命英雄主义教育时,是观看的电影之一,那时已是70年代,是我记忆中的好电影之一。那时,年少记不住电影黄帝之后第一伟人梁启超称他为黄帝之后第一伟人,因为他是中国古代第一个敢于同北方游牧民族作战并取得胜利的君王,在这一点上,他的地位足可以与秦皇汉武相媲美,甚至于他在位期间,其军事力量得到了空前的发展名将王翦的政治智慧如果用个人的幸福指数来衡量,名将并不是一个理想的职业。不过王翦好像是个例外,这位辅佐秦始皇统一六国的大功臣就其战绩来说并不能超越前辈白起,甚至比不上自己的对手李牧。可是要说幸福指数