震撼观众:用WebGL和WebRTC建造线上剧院

简介

毫无疑问,我们过去几个月的生活与预期中的大相径庭。3月初隔离开始,我所有的投影设计工作都被取消了。我和几个好朋友商议建立一个网络非线性剧院体验软件。许多人已经尝试在Zoom上演出了,而且他们发现了许多有趣巧妙的Zoom使用方法。我认为只要我们设计出自定义视频会议应用程序,就能大放异彩。

震撼观众:用WebGL和WebRTC建造线上剧院

这一切的起源——谷歌文档

天方夜谭?10年前或许是这样。但是现在Web平台快速发展,手机的摄像头和硬件的质量都有很大提高,我们很有希望能做到这件事,只不过听起来有点奇怪罢了。

三个月后的今天,我们做到了!“碎片空间”(Shatter Space)是一种双星系统的互动剧场。该系统的双星(“母系星球”)无故消失,你和其他观众都是“星际骑师”。你们应征入伍,在太空飞船中飞行以了解Matra系统的居民,以及你们能帮助他们做什么。

如果你想了解具体细节,请观看下面的剧场预告片。

https://youtu.be/iifZPEHJM6c

观看预告片,我们已经发现该剧场用到了3D图形(WebGL)、视频会议(WebRTC)以及一些音乐和音频处理(Web Audio)。这些都是我过去多少用过的技术。但是构建该剧场使我对它们进行了完全不同的思考。主要包括以下问题。

其他人的电脑

https://youtu.be/nGt8w9RbhvY

这个基于WebRTC的监视系统使后台的乐队能实时查看舞台情况以及演员的提示。舞台上的门户网站是使用WebGL呈现的。

过去几年中,我使用WebGL和WebRTC来为许多节目制作投影/视频效果,但是每次我编写的都是仅在我自己计算机上运行的代码。如果出现影响我计算机(或iPhone或Raspberry Pis团队)的单机错误,我随时都有方法调试它,或者想出一种解决方法。但是这次,这种方法就行不通了。

如果有人给我们15美元,然后他通过邮件收到登录链接,想在一台廉价的2014年笔记本电脑上打开该链接,而该笔记本电脑一加载链接就崩溃,那我们是要退款的。我们不仅要掌握高级网络功能,还需要以最坚挺、最灵活的方式部署它们,同时还要捕获足够的数据和日志以查明问题在哪里,并在演出开始之前完美解决它们。

这就意味着不仅要使用那些入门教程中的各种代码,而是真正深入研究这些API如何进行内存管理,如何应对连接问题,何种情况下会崩溃以及如何在运行时进行调整以确保帧率的高水平和少量丢包。我学到很多实现这些功能知识,想尽可能多地与大家分享。首先我们来谈谈基础知识。

平台基本概述

震撼观众:用WebGL和WebRTC建造线上剧院

一些参与者与节目中的角色互动

“碎片空间”的参与者分为6组,每组5人。每个组分配有一艘绕着系统飞行的“船”,参与者以团队形式与节目中的角色互动。他们可以随自己的意愿静音,或关闭摄像头(屏幕右面的聊天框供他们进行交流)。但如果可以的话,我们还是希望大家保持音视频联系。

碎片空间”平台由以下部分组成:

ExpressJS服务,用于跟踪节目的整体状态;

7个Janus WebRTC媒体服务器。每艘船一个,剩下一个用于播放直播结束画面;

Admin App是一款React / Redux Web应用程序,用于调度节目,跟踪哪名参与者正在与哪艘船交谈。该程序配备实时仪表板,客户端错误和连接性问题的日志储存在这里;

Actor应用程序,这是一个React / Redux网络应用程序。演员使用该程序通过音频、视频、文本来与观众互动,也可以根据船舶清单来分发物品;

与会者应用程序,这是一个React / Redux网络应用程序。与会者可以使用它来观看表演,并通过音频、视频和文本与演员互动;

主机应用程序,我用它来执行运动捕捉的结束场景。

我说Show Service “一直都在跟踪表演的整体状态”,指的是所有状态。大多数Redux操作(在3个应用程序中)都不会改变reducer的状态,而是击中Show Service上的一个端点,该端点会更改数据库中保存的状态,然后从数据库中获取所有状态,将其作为WebSocket消息发送给所有用户,然后覆盖大多数reducers的状态。这样,任何客户端都不会因为在API调用返回之前乐观地更新了自己的reducer而以“孤立状态”告终。并且我们通过单个数据库调用获取所有用户的更新状态(这个refresh-all-state -for-everyone方法会被反跳,然后实现一种排队机制,以确保我们同时刻不会有太多数据库调用)。

获取媒体设备

我们的app真的很好用!

尽管此应用出现在众多WebRTC入门教程的主题中,但它要解决的一大难题是首先获取用户的摄像头和麦克风许可。当然,大多数时候该操作都很简单——只需await navigator.mediaDevices.getUserMedia({ audio:true, video: true }),然后若用户认可你的应用程序,你会收到一个可在< video>元素中查看,或通过RTCPeerConnection发送的MediaStream。虽然该操作看起来可行,但如果你要构建一个实际的生产应用程序,则需要考虑许多其他问题。

首先,如果用户拒绝你摄像头、麦克风或同时开启两者的请求,你需要一种不减少过多功能还能正常回退的方法。就我们而言,我们礼貌地请用户允许我们使用他们的麦克风和摄像头,并让他们知道自己可以在演出中的任何时间开关麦克风和摄像头。但如果他们不信任我们,更愿意通过文字聊天的方式(同时他们还可以看到和听到其他玩家和演员)进行整个演出,那么我们也尊重他们的想法。但这使情况变得复杂了(有一部分原因是我们想借此检测用户是否允许使用摄像头,或者只允许使用麦克风),我们还没能解决这个问题。

震撼观众:用WebGL和WebRTC建造线上剧院

如果你的用户设备设置像我的一样混乱,你可以给他们工具,让他们自己解决问题。

用户可能没有用那些可用于getUserMedia的默认视频和音频源。比如用户用的可能是:

一个好的USB麦克风,一个麦克风坏了(但这是最近设置的浏览器默认麦克风)的蓝牙耳机;

显示器上安装的好的USB网络摄像头,以“翻盖模式”运行的笔记本电脑的内置网络摄像头(是浏览器默认的模式);

一个好的USB网络摄像头,但当你访问它时会出现“挂起”的情况,因为SplitCam等网络摄像头filter app(即代替用户而使用了自己伪视频的设备)正在占用它;

捕获卡或类似可以显示当网络摄像头用的设备,但实际只能展示用户显示屏上的内容。

这样的话你自然希望创建一个选择器,它能使用户选择要使用的设备(或完全不使用任何设备)。为此,你需要navigator.mediaDevices.enumerateDevices(),该函数会返回一个用户可用设备、其用户可读标签以及可用于收集反馈的deviceId的完整列表。所有这些都不会提示用户许可。

你可能要问为什么不提醒许可。如果未授予用户许可(或未询问用户),你会返回一个仅包含默认音频和视频设备,其组ID(不包括deviceId),且不包含标签的列表。这是为了不用进行指纹识别,这样操作的效果很好。因为你可以调用navigator.mediaDevices…uhoh there’s no way to do this来确定是否已提示用户进行麦克风/摄像机访问。

那么,启动我们的应用程序后,获取可用设备列表的逻辑如下所示:

  try {
    // After this we'll know for SURE whether we've asked permission
    let stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    // But these might not be the devices we actually want,
    // so dump this stream and wait for the user to select a device
    stream.getTracks().map(track => {
      stream.removeTrack(track);
      track.stop();
    });
    // If we've made it this far, we know we'll get "the good list"
    let devices = await navigator.mediaDevices.enumerateDevices();
  } catch(e) {
    console.log(`Error getting input devices: ${e.message}`);
  }

是的。我们获取了一个流(希望默认媒体设备可用并且不会挂起,如果确实获取了之后(意味着我们已经获得许可并可以获取真实设备列表),我们会立即转储该流,然后获取设备列表。当然,该操作会更复杂。如果我们无法获取流,我们会尝试单独获取摄像机和麦克风许可,再使用这些(转储流之后的)结果来找出用户已赋予我们的权限。像Saga / WebRTC集成一样,我们可能会再开一篇文章详述此操作。

但需要再强调一点:大家需要熟悉每个浏览器+操作系统组合中关于设备访问和突发状况的不成文规则。比如:

iOS上:一次只能有一个应用程序访问摄像头。所以如果用户刚结束Zoom通话但忘了退出程序,那么即使用户之前已经允许你的应用程序访问摄像头,你在拉取媒体流的时候也会出错。


iOS上: Safari应用程序、Safari独立PWA和SafariViewController都可以使用WebRTC, UIWebViews和WKWebViews不能。且navigator.mediaDevices实际上是未定义的,也就是说Chrome for iOS不支持WebRTC。


但是如果你从Slack或现代电子邮件应用程序打开链接,WebView实际上是可以使用WebRTC的。这样做出于安全考虑,也是对过去“Safari应用一家独大”情况的一种改进。但我真的希望Apple和Google能够有办法让WebRTC在iOS版Chrome浏览器中工作。


一般情况下: 你经常会碰到一些毫无价值的报错,比如“无法找到设备”或“视频源无法启动”。这些报错什么也说明不了,但我发现切换到“no input”几秒钟,然后再次返回设备;或拔出USB,再重新插入设备也可以解决问题。光靠远程诊断很难解决这个烫手山芋。


一般情况下的网络摄像头:大家一定要记住,网络摄像头会以特定的分辨率和帧率进行录制,如果另一个应用程序或页面请求使用不同分辨率或帧率访问摄像头,因为摄像头已经在录制了,所以只能按当前规格进行操作。网络摄像头通常无法接收其传感器输入,也不能将其转换为不同应用程序的不同形式。建议大家从不同的制造商处买2台便宜的网络摄像头,以使测试更容易,覆盖范围更好。

最后,我想说一些用媒体设备的开发人员需要注意的其他事项:

如果你要进行上述的设备切换(购物类视频应用必备功能),你就不能依赖Janus.js之类的库来为你处理此事。你不能仅仅向库下达“使用视频而不是音频”的指令,而是要自己获取媒体流,放到自己的WebRTC库里。


切换设备并不像你以前媒体流中的MediaTrack与你新媒体流中的MediaTrack交换那样简单,因为这样做会导致旧的MediaTrack触发结束事件,从而终止你的RTCPeerConnection。


因为没有(截止2020年6月跨平台协作的数据)MediaStream.replaceTrack()方法,所以你必须拆除所有有关WebRTC的内容,媒体设备一旦变化就要重新连接。因为要执行很多遍此操作,使其易于执行和防泄漏是很重要的。


上述问题的一种解决方法是通过Web Audio API传输所有音频,通过canvas元素传输所有视频,并使用.captureStream()创建一个永不“结束”的视频轨道。但是你要有一台不在Safari中运行的,功能强大的计算机,不然也无法正常进行该操作。

three.js和OffscreenCanvas

https://chrisuehlinger.com/images/shattered/planet-selection.mp4

观看简短的介绍/教程视频后,玩家可以在导航屏幕上找到自己。在这里,他们可以选择要前往的行星、太空站或其他目的地。小组中的任何成员都可以选择一个目的地以供所有人查看,然后挑一个成员(每次都会随机选择成员)选择要去哪个星球。此操作需要两部分时间完成:

用于进行选择的列表UI和用于确认选择的按钮。

用于显示目的地的Three.js渲染视图的canvas。

像上文提到的那样,我以前用过three.js,但那时页面通常是由3D构成的。对于这次的项目,我准备把3D场景构建到有许多其他功能的应用中。假设用户的设备快没电了,但这时还在运行Chrome,页面正以1 FPS或更低的速度渲染场景。我们想要确保用户仍然可以单击UI中的按钮,及时做出回应。

那么,因为用了高级Web技术产生了问题要怎么办呢?当然是要用更高级的web技术解决它。

OffscreenCanvas是一款Web API,可让你在Web Worker中执行独立于主UI线程的大量渲染工作。但是它并不能完全解决问题(如果你的渲染tank达到25FPS,它会大大降低主线程的FPS),但是它可以避免两个大问题:

1. Web Worker中的错误得到控制,且不会影响主线程(停掉3D动画即可)。

2. 有时,Three.js场景会完全锁定其线程(在编译着色器或加载复杂模型时),且在那个时候,UI是保持响应状态的。

但这也带来了新的问题,比如:

1. Safari不支持OffscreenCanvas。所以你需要一个polyfill,以便在主线程上运行所有代码。建议大家用offscreen-canvas来练手,但我把它设置为仅自己可用,以更好地支持拥使用多个canvas的工作人员,她们可以根据需要发送新的canvas。

如果你使用的是webpack,你要给工作人员代码创建一个新的入口点,以及一个标签,以便你的polyfill可以找到工作人员脚本的名称,并将其加载到合适的线程上。参考preload-webpack-plugin。

如果你用three.js,需要确保它在WebWorker中运行时不会尝试创建/修改DOM元素。你要给renderer.setSize()设置一个false标志,这样它就不会尝试修改canvas的样式。而且你需要在后台使用ImageBitmapLoader,而不是ImageLoader来制作自己的加载器版本,例如TextureLoader和FBXLoader。这基本上是直接替代了,但你也要清楚(在.flipY等处中)ImageBitmapLoader与ImageLoader的不同之处,并进行相应的调整。

three.js和内存管理

另一个新问题是内存管理——仅仅让three.js对象落在范围之外还不算真正的回收清理。因为three.js在后台保留了数据注册表以提高性能(考虑到WebGL的工作原理,这点是可以理解的)。

需要清理的类别都有一.dispose()方法。我的代码中基本都有。我用一个加载了所有模型的构造函数,为场景中的每个“事物”创建了一个类、(在运行时把它们同时添加到this.disposables数组中)一个适用于任何动画的.update()方法,以及一个.dispose()方法,该方法可在所有一次性对象上调用.dispose(),处理任何其他必要的清除工作。

export default class TLW {
  constructor(globals, options) {
    this.globals = globals;
    this.disposables = [];
    (new FBXLoader(globals.loadingManager))
      .setPath(`${config.ASSET_PATH}/planets/TLW/`)
      .load( `fighter_low.fbx`, ( object ) => {
        object.traverse(( child ) => {
          if ( child.isMesh ) {
            // Add each loaded geometry to the disposables list
            this.disposables.push(child.geometry);
            // Don't forget their materials
            this.disposables.push(child.material);
            // Or their textures
            this.disposables.push(child.material.map);
          }
        } );
        this.group.add( object );
      });
    this.group = new THREE.Group();
    this.group.position.set(...options.position);
    this.options = options;
  }
  dispose() {
    this.globals = null;
    this.disposables.map(asset => {
      // Wrap this in a try/catch and announce in the console if we
      // try to dispose of something that doesn't need to be
      try {
        asset.dispose();
      } catch (e){
        console.error('DISPOSAL ERROR', asset);
        console.error(e);
      }
    });
  }
}

用来装载ȾⱠẘ(一艘困在太空中的船)代码的简化版本

据我估计,此版本大概是用完整内存管理复杂场景所需工作量的60-70%,会有5-10%的漏洞(不包括我没有统计到的数据)。剩下的一个最大漏洞是,如果我实例化我的一个类,它会开始加载一些模型/纹理,加载没完成就会被销毁了。也就是加载-堵塞-泄漏。实践中,这种情况很少发生,并且由于内存占用而导致崩溃的情况很少见,因此,我很满意此项目方法。

之后我减免了大部分这样的需求。方法是坚持使用three.js场景(即使它不在屏幕上),此外,只要React装载/卸载了NavigationScreen组件,我就重建/销毁渲染器。这方法让场景加载(初始加载后)几乎能瞬间完成,这意味着内存泄漏的位置不多。

最后,我要坦白一个不足之处——因为列表用户界面占据着整个屏幕,我把three.js完全从移动设备(大致就是尺寸小于500px的设备)上剪切掉了。

远程检测和解决问题

震撼观众:用WebGL和WebRTC建造线上剧院

整个表演过程中,我都会监测实时错误日志。

通常在预定开始表演前的5至10分钟,我们已经能看到与会者登录的情况了。也就是说如果他们遇到问题,我们有5-10分钟的时间来解决。

这就需要多管齐下,在演出中的许多点进行干预:

接受宣传材料和登录的链接邮件,建议你使用Google Chrome(并不是硬性要求)。若你是iOS用户,就只能使用Safari了(其他iOS浏览器不兼容WebRTC)。

我们的技术支持团队会监测我们的公司邮件,该电子邮件用于向用户发送其登录链接。用户遇到问题时基本都通过邮件与我们联系,部分原因是:

应用程序中任何无法恢复的错误都会被React错误边界捕获,然后该边界会显示一个错误页面,其中包含错误消息以及导向我们公司电子邮件的mailto链接,以便他们可以直接与我们联系。

演出时,如果有与会者未登录,我们的技术支持团队会通过电子邮件与其联系,看看他们是否遇到了登录问题。

假设用户尚未关闭遥测功能,我们会立即收到演员和参与者应用产生的任何错误(无论是否可恢复)的通知。技术支持人员可以用这些通知,找出故障排除方法,向用户发送解决步骤的邮件(通常很简单,例如“刷新”、“更新浏览器”或“关闭麦克风然后再打开”)。这些错误日志不会一直保存在数据库中(它们会被直接发送到Admin应用,在刷新后消失)。

展开阅读全文

页面更新:2024-05-28

标签:参与者   麦克风   剧院   应用程序   摄像头   场景   加载   观众   状态   错误   操作   方法   媒体   用户   设备   科技   网络   视频

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top