利用WebGL和WebRTC实现大区在线

2020-06-26 22:43:01

作者声明:[Ed.。注:此帖子包含促销!这基本上是对我目前的在线互动节目“破碎的空间”的一次推广。]。

[但我也有很多很酷的技术东西要告诉你,所以请留下来,如果这听起来很有趣,请随意在这里买票!]。

可以肯定地说,过去几个月的情况与我们许多人的预期有很大不同。3月初,当隔离开始,我所有的投影设计演唱会都被取消时,我联系了几个亲密的朋友,谈到完全在网上建立非线性影院体验的事情。很多人一直在尝试在Zoom上表演戏剧(并找到了许多有趣和聪明的方式来使用这种媒体),但我觉得如果我们建立自己的定制视频会议应用程序,我们就可以做出一些真正独特的东西。

疯狂?那肯定是10年前的事了。但网络平台的进步(以及移动设备中摄像头和硬件的质量)将这一点带入了“可能-如果有点堂吉诃德式的话”的境地。

三个月后,我们已经做到了:“破碎的空间”是一部互动剧场,讲述了一个双星系统的双星(“母亲”)已经消失的故事。你和你的观众同伴是明星骑师,他们被征召乘坐宇宙飞船四处飞来飞去,以了解Matra系统的居民,看看你们能做些什么来帮助你们。

如果你想更好地了解正在发生的事情,这里有个预告片。

如果你看了那个预告片,你可能已经知道这个节目涉及3D图形(WebGL)、视频会议(WebRTC),也许还包括一些音乐和音频处理(Web Audio)。这些都是我过去在某种程度上使用过的技术,但这次展览要求我以完全不同的方式来思考它们。这一切都是因为以下中心问题:

这个基于WebRTC的监控系统允许后台乐队实时看到舞台,并从演员那里获得线索。前台门户使用WebGL进行渲染。(欢迎来到2019年巴尔的摩摇滚歌剧协会莎士维尔)

在过去的几年里,我已经使用WebGL和WebRTC为许多节目构建了投影/视频效果,但在每一种情况下,我都在编写只能在我的计算机上运行的代码。如果我遇到影响我的电脑(或我的iPhone,或我的Raspberry PI团队)的设备特定错误,我有足够的时间和访问权限来调试它或想出解决办法。这次不行。

如果有人支付我们15美元,通过电子邮件收到登录链接,试图在一台便宜的2014年笔记本电脑上打开它,但它无法在不崩溃的情况下加载,那就是退款。我们不仅仅是在摆弄先进的网络功能,我们还需要以最经久耐用、最具弹性的方式部署它们,同时还需要捕获刚刚足够的数据和日志,以查明谁有问题,并在节目开始前理想地解决这些问题。

这意味着超越您在入门教程中看到的代码种类,真正深入研究这些API如何处理内存管理、它们如何响应连接问题、它们如何失败,以及如何在运行时进行调整以确保帧率保持在较高水平并且丢弃的数据包很少。我学到了很多关于制作这些特性的知识,我想尽可能多地与大家分享,但首先让我们谈谈基础知识。

“破碎空间”的参观者被分成6组,每组5人。每组被分配到一艘围绕系统飞行的“船”上,他们与节目中的角色作为一个组进行互动。与会者可以选择将麦克风静音,如果他们愿意的话,可以关闭摄像头(边上有一个短信聊天,他们可以用来交流),尽管我们鼓励人们在感觉舒服的情况下让他们一直开着。

7台Janus WebRTC媒体服务器。每艘船一张,还有一张额外的,用来播放结尾场景的实况转播。

Admin App是一款Reaction/Redux Web应用程序,它处理日程安排表演,跟踪哪个演员正在与哪艘船交谈,并具有实时仪表板,其中包含客户端错误和连接问题的日志。

演员应用程序(Actor App),一个由演员使用的Reaction/Redux网络应用程序,用于通过音频、视频、文本和从船舶库存中赠送/取走物品与观众互动。

Attendee App是一款Reaction/Redux网络应用程序,与会者可以在其中观看演出,并通过音频、视频和文本与演员和其他人互动。

当我说Show Service“跟踪节目的整体状态”时,我指的是所有状态。大多数Redux操作(在所有3个应用程序中)不会改变Reducer的状态,而是命中Show Service上的端点,该端点更改数据库中保存的状态,然后从数据库获取所有状态,并将其作为WebSocket消息发送给所有用户,然后该消息将覆盖Reducer的大部分状态。这样,任何客户端在API调用返回之前乐观地更新其减少器都不会导致“孤立状态”,并且可以通过单个数据库调用来获取所有用户的更新状态(这个针对每个人的刷新所有状态的方法被取消并实现了排队机制,以确保我们不会同时有太多的数据库调用)。

我基本上采用了丹·阿布拉莫夫的简单而优雅的系统,并对其进行了这样的处理。抱歉,丹。

(我要指出的是,这种体系结构在某种程度上是有机地出现的,而且可能有更干净或更优化的方式来实现这种“Redux-但-the-server-is-the-Reducer”体系结构。我对像Relay或Meteor这样的产品是否更适合V2的反馈意见持开放态度。)。

通过Admin应用程序,我们可以查看谁已登录以及节目中发生了什么。

在3个Reaction/Redux应用程序中,Admin应用程序使用的Web技术最少,几乎只是一个显示数据库内容的仪表盘。作为管理员,我们可以看到哪些演员和与会者已经登录,哪些船当前正在访问哪些演员,如果需要,我们可以在船上之间移动与会者,我们还可以看到演员和与会者遇到的错误列表(稍后将详细介绍)。

演员应用程序允许表演者与观众互动,并查看与他们交谈的船只的信息。

Actor App更为复杂,因为它需要处理WebRTC和媒体设备问题。它还有3种模式:正常、移动(如果窗口的尺寸小于500px,则自动使用)和Headless(如果querystring变量Headless设置为true,则使用)。正常模式由大约一半的演员使用,能够让他们看到自己,配置音频/视频设备,更改有关用户、角色或行星的任何信息,以及查看有关与他们交谈的船只的信息(他们的聊天、库存、以前地方的历史等)。

演员应用程序的移动和无头模式使我们的演员可以灵活地使用他们可用的任何设备

Mobile模式使用基于选项卡的布局来尝试并实现所有相同的目标,但是由于一次只能在屏幕上显示一个选项卡,单独使用它有点麻烦。我们有Headless模式,它不执行任何音频/视频流,但显示关于当前船的所有信息,并且可以在任何笔记本电脑上运行。许多演员选择使用移动模式(因为他们的手机有很好的摄像头),但在旧笔记本电脑上保持无头模式打开,以便更容易使用其他功能。

与会者应用程序和演员应用程序一样复杂,但没有Headless模式(这实际上只是演员的高级用户功能)。这款应用程序有多个屏幕,对应于节目的不同阶段:

Presshow-大堂屏幕,与会者可以在这里会见将与他们一起飞行的船友。

自由播放-节目的主要部分,参观者在与角色互动和从导航屏幕选择下一个目的地之间交替。

结束-这只是将参与者从应用程序重定向到一个静态页面,其中包含我们演员的个人资料。

在节目的最后,我表演了一个场景,扮演星际骑师的人工智能指挥官恐慌上校。在这个场景中,我走遍了每一艘船,回顾了他们设法收集的物品。然后,一个“决心”发生了,我不会破坏这里。

这场表演是在我的台式PC(英特尔8700K+RTX 2080TI)上呈现的,并带有我专门为此构建的Actor应用程序的特殊版本。这里的Three.js场景包括至少7个带有MeshPhysicalMaterial和12个Moving PointLight的大型模型,所以我没有在与会者的电脑上渲染,而是在我的电脑上渲染,并使用Elgato Camlink 4K、ffmpeg和Janus(下面解释)流媒体插件将其流式传输给他们。

动作捕捉工作是通过从ARKit文档中提取苹果TrackingAndVisualizingFaces示例的修改版本,并让它通过WebSocket将每一帧mocap数据发送到服务器,然后服务器将其转发到Host应用程序来完成的。令人惊讶的是,它可以在低于100ms的延迟下工作,但它确实如此。

我计划在展览结束后将所有这些都开源,但我想清除git的历史,以防有任何秘密存在,我要警告任何感兴趣的洞穴探险者,它将更像是一件“有趣的艺术品”,而不是“一件你可以很容易在当地跑起来的东西”。

在撰写本文时,在浏览器中进行实时视频会议的唯一免插件方式是通过WebRTC,这是一种实时通信的Web标准,可以处理流式音频、视频,甚至是任意数据包。在教程中,WebRTC经常被演示为一种点对点通信工具,它当然可以做到这一点。但是如果您在一个房间中有5个与会者,那么每个人必须广播4个音频/视频流(每个与会者一个),并接收另外4个流。对于有连接问题或正在使用移动网络的人来说,这可能很困难。

通常,如果您想构建一个基于Web的视频会议产品,您会希望云中的服务器能够为您多路复用这些流。这样,每个与会者只需要发送他们的流的一个副本(他们仍然必须接收另外4个,这是无能为力的)。这两种方法的行业术语是“网状”与。“选择性转发单元”(通常缩写为SFU)。

有很多开源服务器可以做到这一点,我们用于粉碎空间的服务器是MeetEcho团队的Janus。我是在其他项目中熟悉它的,它的维护人员已经投入了多年的持续工作(认真看看这张贡献者图表),而且由于它是用C语言编写的,所以它的速度非常快,几乎从未在EC2上的t3a.nan上达到两位数的CPU使用率。

Janus不仅为您多路复用数据流,它还拥有一些API,可以帮助您完成启动WebRTC会话所需的大量麻烦的信令和NAT攻击。所有这些都封装在一个客户端JS库中,其中包含许多很好的、有用的示例,可以作为项目的起点(如果您还不了解它,我真的很喜欢它)。

我遇到的两个棘手问题围绕着接下来的两个部分的主题:

但除此之外,贾纳斯很棒。我从来不需要因为内存泄漏而随机重启服务器,插件系统为你提供了大量的功能选项,你可以在几分钟内创建一个Docker镜像并部署它。说真的,看看这玩意儿。

创建和维护WebRTC PeerConnection是您在前端可以做的最具副作用的事情之一。您需要:

多种类型的错误,每种错误都有不同的错误处理程序(其错误需要根据其原因以不同的方式处理)。

创建更有状态副作用的多种其他事件(如用户加入房间、离开房间或切换静音开关)。

需要“清理”或处理大量未解决的问题,比如来自本地媒体设备的连接对象或流(我们稍后将介绍这些内容)。

我没有任何尊重我的时间的观念,所以很自然地,我选择用Redux来构建这个应用程序。

撇开玩笑不谈,我真的很喜欢Redux和它让我组织应用程序的方式。我已经使用它很多年了(通常是通过生成器-反应-webpack-REDUX),并且发现它非常适用于许多项目。尽管如此,它最大的弱点之一是在处理副作用方面。我通常使用redux-thunk包来处理异步或副作用流程,但对于这一次,我决定尝试redux-saga。这是个不错的选择。

Redux Saga需要一些时间才能让你头脑清醒(与JS世界的其他人不同的是,它跳下了异步/等待列车,登上了发电机一号)。但是一旦这样做了,您就会发现它能够创建抽象,承诺和异步/等待只能梦想着像事件通道一样,可取消的异步任务,甚至是单线程事件循环中的“分叉”进程。这对于像这样的时候是很棒的,在这样的时候,你需要旋转一些东西来响应一个动作,吸入它发出的任何动作,并在你完成它之后清理干净。我打算写一篇关于在WebRTC应用程序中使用Redux Saga的更详细的文章,因为它们是为彼此而生的配对。

尽管这是许多WebRTC入门教程的主题,但这款应用程序最难的部分是首先获得用户的摄像头和麦克风。当然,80%的时间都很简单,只需等待Navigator.mediaDevices.getUserMedia({audio:true,video:true}),然后如果用户认为您的应用程序值得,您就会收到一个您可以在<;video>;元素中查看的MediaStream,然后发送一个RTCPeerConnection等等。虽然这基本上是正确的,但如果您正在构建一个真正的生产应用程序,您还需要考虑其他一些问题。

首先,如果用户拒绝您的摄像头和/或麦克风请求,您需要一种在不减少太多功能的情况下优雅地后退的方法。在我们的案例中,我们礼貌地要求用户允许我们使用他们的麦克风和摄像头,并让他们知道他们可以在节目中随时打开或关闭它们。但如果他们不信任我们,想要通过短信聊天(同时观看和听到他们的船友和演员)完成整部剧,我们也要确保让他们这样做。这使得事情变得复杂(部分原因是我们试图检测用户是否允许摄像头但不允许麦克风,反之亦然),但我们只是触及了皮毛。

如果你的用户的设备设置像我的一样一团糟,你需要给他们工具让他们自己找出来。

getUserMedia可用的默认视频和音频源可能不是用户打算使用的。例如,用户可能具有:

一个好的USB麦克风,一个带有坏麦克风的蓝牙耳机(但它是最近安装的,是浏览器的默认麦克风)。

在他们的监视器上安装了一个良好的USB摄像头,在他们的笔记本电脑上安装了一个以“翻盖模式”运行的内置摄像头(但浏览器选择了这种模式作为默认模式)。

一个好的usb网络摄像头,当你试图访问它时,它会“挂起”…。因为它正被像SplitCam这样的网络摄像头过滤器应用程序使用,SplitCam提供了它自己的伪视频设备,您应该改用它。

一种采集卡或类似的设备,以网络摄像头的形式呈现,但实际上只显示用户监视器上的内容。

因此,您自然会想要制作一个选择器,允许用户选择要使用的设备(或者根本不使用设备)。为此,您需要Navigator.mediaDevices.EnumererateDevices(),它返回用户可用设备的完整列表、它们的用户可读标签,以及您可以用来请求其提要的deviceID,所有这些都不会提示用户许可。

如果用户没有授予权限(或者没有被询问),您将得到一个列表,其中只包含默认的音频和视频设备、它们的groupID(而不是deviceID),没有标签。这是为了防止指纹识别,而且一切都很好,因为您可以通过调用Navigator.mediaDevices来确定是否提示用户访问麦克风/摄像头……嗯,没有办法做到这一点。

因此,当我们的应用程序启动时,获取可用设备列表的逻辑如下所示:

尝试{//在此之后,我们将确定是否请求了权限let stream=await Navigator。媒体设备。getUserMedia({video:true,audio:true});//但这些可能不是我们真正想要的设备,//因此转储此流,等待用户选择设备流。getTracks()。地图(Track=>;{流。移动轨道(轨道);轨道。stop();});//如果我们已经走到这一步,我们就知道我们会得到好的名单,让设备=等待导航器。媒体设备。枚举设备();}catch(E){控制台。日志(`获取输入设备时出错:${e.。消息}`);}。

没错:我们获取一个流(并且希望默认的媒体设备可用,并且不会挂起),然后如果我们获得了一个流(这意味着我们已经获得了许可,可以获得真正的设备列表),我们立即转储该流,然后获得设备的列表。当然,这要复杂得多--如果我们无法获取我们试图分别获取摄像头和麦克风的流,然后使用这些结果(在转储流之后)来计算出用户授予了我们什么权限--但是就像Saga/WebRTC集成一样,这个主题可能也值得写一篇自己的文章。

但我还会讲得更久一些:您需要非常熟悉每个浏览器+操作系统组合关于设备访问和偶尔的“坏习惯”的不成文规则。诸如此类的事情:

在iOS上:一次只能有一个应用程序可以访问摄像头,所以如果用户刚刚结束了缩放呼叫,但忘记关闭它,即使用户之前已经授予了您的应用程序权限,尝试拉取媒体流时也会出现错误。

在iOS上:Safari App、Safari独立PWAS和SafariViewController都可以使用WebRTC。UIWebViews和WKWebViews不能,Navigator.mediaDevices实际上是未定义的。这意味着Chrome for iOS不支持WebRTC,但如果你从Slake或现代电子邮件应用程序打开一个链接,WebView实际上可以使用WebRTC。这是出于安全原因,与过去的“Safari App or Bust”相比,这是一种改进,但我真的希望苹果和谷歌能想出一种方法,在iOS版的Chrome上实现这一点。

一般来说:你经常会遇到一些无用的错误,比如“找不到设备”或“视频源启动失败”。这些可能意味着几乎任何事情,但我经常发现,切换到“无输入”几秒钟,然后再次切换到设备可以修复它,如果设备通过USB连接,拔下插头并重新插入设备也可以。这是一个很难远程诊断的问题,而且是一个真正令人讨厌的问题。

关于一般的网络摄像头:请记住,网络摄像头通常以特定的分辨率和帧率开始录制,如果另一个应用程序或页面请求使用不同分辨率或帧率的相同摄像头,就会像是“我已经在录制了,你会得到你想要的”。网络摄像头通常不能接受传感器输入,并将其转换为不同应用程序的不同形式。我建议从不同的厂商买2个便宜的摄像头,这样测试更容易,覆盖面更好。

最后,我还要注意一些使用媒体设备的开发人员需要注意的事项:

如果你要实现这种设备切换(对于消费者视频应用来说,这是非常强制性的用户体验),这意味着你不能依赖像Janus.js这样的库来为你处理这些事情。您需要自己获取MediaStream并将其交给您的WebRTC库,而不是仅仅告诉库“使用视频而不是音频”。

当您切换设备时,不像将您的MediaStream中的MediaTrack与新的MediaStream中的MediaTrack交换那么简单,因为这样做会导致旧的MediaTrack触发终止事件,从而终止您的RTCPeerConnect。

..