在Jitsi上用getDisplayMedia录制本地音频

事情的起因是这样的,我想往自己的Jitsi Meet实例中添加本地音频,但操作时发现结果并不是我想要的,所以我打算自己开发个简单的程序作为替代。在这过程中,我发现:

1. 用于屏幕截图的getDisplayMedia缺点多多;

2. 用于媒体录制的mediaRecorder也有诸多局限;

3. 在Jitsi Meet里添加HTML / JavaScript的操作非常简单;

下文是操作的具体细节和一些参考代码。点击这里可以查看我的成品。

注:如果你想查看Mozilla的Jan-Ivar给出的就此评论和注意事项,请移步评论区

问题凸显

几个月前,我构建了一个Jitsi Meet服务器。当时我想借其来更新我名为《两天内用WebRTC建立一家通信公司》的文章。当然,已经有很多讲解如何开发Jitsi Meet的文章或视频了,而且我也没有什么新鲜的或有趣的意见可分享了。但还有那么一项我迫切想改善的功能——录制。我经常做展示和录制会议,以供他人参考,为将来留存资料。表面上看,我的要求很简单:按需录制我的音频以及Jitsi Meet会话的音频和视频,将文件保存在本地。听起来操作很简单,实则不然。

Jitsi 云录制挑战

Jitsi中有一个用于记录的模块,叫Jibri。Jibri设置和配置的安装要比Jitsi Meet其他部件复杂。但如果你熟悉框架系统,几个小时甚至更少的时间就能把它安装好。Jibri加载了一个无头浏览器去充当呼叫的无声参与者,获取音频并将其保存到磁盘。这会占用大量资源,迫使我把服务器从5美元 / mo升级到了20美元 / mo。但它一次也只处理一条录音。如果你想一次性记录多条,可以设置启动多个Docker容器。但这样也越来越复杂了。除此之外,你还需要建立一种机制来在录制结束后,将文件传输到某个地方或设置Dropbox集成。我只想要一种能快速录制会话,之后进行共享的方法,但现行的功能把这个操作变得很复杂。是时候寻找一种更简单的方法了。

本地录音

另一种方法是仅在本地录制。其实本地记录会更安全,因为你不会把未加密的媒体留在服务器的某处。此外,因为你使用的是本地计算机来保存已经接收的媒体,而不是在云中添加新元素,所以也减少了资源消耗。 Jitsi实际上也有这么个选项,但它只能本地录制音频。而我需要把我看到的也记录下来,所以我开始尝试添加本地屏幕录像。

如何获得媒体

最傻瓜的操作是把一些音频放在一起进行Quicktime录制;只使用基于WebRTC的录制API或浏览器扩展程序之一也可以。对此我不做过多叙述,因为该操作并不对所有网络用户通用。我以前做过一个录音机,它可以重载createPeerConnection API,获取多个连接中的所有音轨,并将所有音频保存到文件中。实际上它可以和Jitsi Meet的多音频连接配合使用。我原本想设置一些可以同时录屏的canvas,并将其保存。但是我发现用getDisplayMedia操作会更简单。

getDisplayMedia

这篇文章中,我向大家介绍了用于共享桌面媒体的getDisplayMedia API。 其优点是所有主流浏览器都应用了getDisplayMedia。 缺点是应用方式各不相同,且可能会影响用户体验。

注:下文提到的Edge指的是基于Chromium的新Edge。

测试代码

我原以为这是一个容易评估的API,但我想错了。为收集数据,我写了代码来调用getDisplayMedia和测试参数。大家可以在GitHub或我的网站上找到这些代码

选择屏幕共享

录音器的选择也大有不同。Chrome和Edge允许在全显示窗口、应用程序窗口或浏览器标签中选择录音器。Firefox不允许用浏览器标签选择。而Safari对上述三种方式都不支持,只让你选择当前显示。

从下面这些图中你能看到各用户界面的差异。

Chrome

版本:84

可选项:全显示、窗口、标签

Edge

版本:84

可选项:全显示、窗口、标签

火狐(firefox)

版本:77

可选项:全显示、窗口

Safari

版本:13.1

可选项:当前显示

Chrome和Edge在窗口边缘内侧会显示一个蓝色的高亮框,以表明标签页正在被共享。

displaySurface选择约束无效

getDisplayMedia API包含一个displaySurface选项,用于在桌面显示、窗口、应用程序或浏览器标签之间进行选择。而对我来说,我只想记录我的Jitsi Meet标签,所以我就想是否可以通过限制部分选项来简化用户界面。采用约束应该就可以了。

然而规范中说:

“用户代理必须让终端用户每次都从所有可选项中来选择共享哪个页面,严禁使用约束来限制选择。”

事实上,不像getUserMedia,“约束在 getDisplayMedia 中的作用与在 getUserMedia 中不同。它们不帮助发现,用户选择后才能应用它。(原文)

这意味着在实践中这些约束毫无意义。想进一步了解的话大家可以去看规范,但基本上这些约束不能起到效果,所以使用它们也没有什么意义。你不能在选择页面启用enumerateDevices(),也不能寻找设备变化事件。

(说句题外话:或许这诸多限制是Google Hangouts仍使用自己的扩展机制,不用getDisplayMedia的原因。)

分帧器

有了getUserMedia,你可以应用视频分辨率和帧率约束。视频分辨率只在捕获后调整视频的大小。如果想减少周期,你也可以降低帧率。事实上,有很多屏幕共享的WebRTC视频会议服务会根据共享的内容,八帧率降到10或更小,以减少占用CPU。

用户手势要求

火狐和Safari需要一个用户手势(比如点击按钮)才能访问getUserMedia。Chrome和Edge不需要。

把下面的代码粘贴到JavaScript控制台中,亲自体验一下吧!

https://webrtchacks.com/wp-content/uploads/2020/06/pasted-image-0-4.png

iFrame权限

codepen中进行测试后,我发现iFrames也有限制。火狐和Safari如果没有特别允许权限就不能运行,他们的具体权限也不同。

火狐需要的权限代码是<iframe allow=”display-capture”> 。Safari的代码是<iframe allow=”display”> 。

Chrome和Edge没有任何特殊的iFrame要求。

追踪内容差异

这一点并不奇怪,当调用getSettings时,每个轨道返回的信息是有差异的。

除了Edge增加了一个videoKind的选项之外,Chrome和Edge的内容大致相同。我也不清楚他俩为何是一样的。火狐提供的视频信息最少。Safari只提供了一个值——frameRate。

MacOS Catalina权限

MacOS Catalina在OS引入了新的屏幕录制权限。也就是说你需要授予“屏幕录制”访问应用程序的权限。

但是此设置仅在重置应用程序后才会生效,如果你正在开会,需要马上用到电脑,这个操作就很不方便了。

如果你是第一次实现getDisplayMedia,那就需要提醒Mac用户该操作风险。

移动端支持

该支持并不存在。etDisplayMedia无法在Android或iOS安装的任何浏览器中使用。

具备音频功能的getDisplayMedia

虽然获取多个参与者的音频很难,重载peerConnection并拦截流是可以做到这一点的。然而这样就不能访问系统音频(捕获视频或共享应用程序音频)了。谢天谢地,getDisplayMedia规范允许捕获系统音频。目前我没有看到任何有关webrtc PSA的评论。但去年Chrome引进了此功能

音频参数

音频捕获参数和getUserMedia获得的参数形式一致。只要添加一个audio: true参数,我们就可以轻松捕获音频:

navigator.mediaDevices.getDisplayMedia({video:true,audio:true})

在MacOS上选择Chrome标签后,你会看到“共享音频”选项:

MacOS Chrome中带共享音频选项的getDisplayMedia相关界面

在Windows上选择“全屏”或选项卡时,会显示以下内容:

MacOS Edge带共享音频选项的getDisplayMedia相关界面

getDisplayMedia音频捕获的局限性

使用getDisplayMedia能捕获到的音频相当有限,且用户体验并不稳定。

操作系统决定行为

如上所示,MacOS只有共享选项卡时音频捕获才可用。Windows全屏或选项卡可用。Linux仅在标签页可用。音频捕获也不适用于OS中任何特定应用程序或窗口。

浏览器端支持

仅Chrome和Edge支持音频捕获,所以整体上浏览器对显示加音频捕获的支持是很差的。

不稳定的约束检查

navigator.mediaDevices.getDisplayMedia({audio:true}).then(console.log).catch(console.error);  results in a TypeError: Failed to execute 'getDisplayMedia' on 'MediaDevices': Audio only requests are not supported .

我们可以把参数改为{video:true,audio:true},但是如果用户未选择音频源,这里也不会发出警告或拒绝。如果你的应用需要音频,你需要通过计算音轨来进行检查:

navigator.mediaDevices.getDisplayMedia({video: true, audio:true}).then(s=>s.getAudioTracks().length>0).catch(console.error);

音频参数

在音轨输出上调用getSettings(),如下所示:

autoGainControl: true
channelCount: 1
deviceId: web-contents-media-stream://37:4
echoCancellation: true
latency: 0.01
noiseSuppression: true
sampleRate: 48000
sampleSize: 16

我无法调整参数来调整sampleSize。通过使用各种值进行快速测试,我可以把sampleRate更改为44100或48000。这恰好是PCM和Opus音频(WebRTC支持的强制音频编解码器)的默认速率。我也可以关闭autoGainControl、echoCancellation和noiseSuppression。但我并没有一个很好的方法来测试关掉他们是否真的有效。

注:关闭echoCancellation确实会返回2条音轨。实际上我们可以使用音频捕获设备进行更多测试,但并不属于我此次的评估范围。

使用MediaRecorder录制媒体

MediaRecorder API

mediaRecorder API使录制流变得超级容易。但是:

recorder = new MediaRecorder(recorderStream, {mimeType: 'video/webm'}); recorder.start();

然后我们可以只听新数据,再将其保存到某个位置的数组中:

recorder.ondataavailable = e => {
    if (e.data && e.data.size > 0) {
        recordingData.push(e.data);
    }
};

之后,我们可以重放,或将其保存到磁盘。详见MDN的mediaRecorder API指南。也可以参阅我有关播放和保存到磁盘的示例。

注:请谨慎录制时段参数(例如–recorder.start(100))!详情请见有关此问题WebRTC官方示例的评论。

各浏览器media Recorder的不同

我想录制多个流媒体,比如:

1. getUserMedia本地流,这样我就能知道我在说什么了;

2. 捕捉他人说话内容和我操作内容的系统音频;

3. 屏幕截图。

那我们是不是只需把几个音轨和一个视频轨添加到一个流中,然后将其发送到mediaRecorder呢?并没有这么简单。正如此Chromium说明所说,如今,Chrome浏览器中的mediaRecorder只能录制单一音轨。 Firefox 支持立体声录制; Safari完全不支持mediaRecorder使用。然而,有这么一段代码可以支持我们的录制操作。

WebAudio Mixing解决了Chrome mediaRecorder的录制问题

Chrome浏览器一次只能录制一条轨道。这个问题有两种解决方法:

1. 保存单个文件,然后使用ffmpeg,将其另存为一个多声道流;

2. 使用WebAudio将多个音频流混合成一个,再将这个音频流发送到recorder。

第二种方法比较简单,代码数少。输入2个流,混合音频轨,添加所有视频轨道即可(如果mediaRecorder中包含第二个视频流,添加各轨道时该视频流会被忽略)。

function mixer(stream1, stream2) {
   const ctx = new AudioContext();
   const dest = ctx.createMediaStreamDestination();

   if(stream1.getAudioTracks().length > 0)
       ctx.createMediaStreamSource(stream1).connect(dest);

   if(stream2.getAudioTracks().length > 0)
       ctx.createMediaStreamSource(stream2).connect(dest);

   let tracks = dest.stream.getTracks();
   tracks = tracks.concat(stream1.getVideoTracks()).concat(stream2.getVideoTracks());

   return new MediaStream(tracks)
}

若你需要能更稳定处理混合视频流的方法,可以参阅Muaz Khan的这篇文章

添加代码到Jitsi

下一步操作是将这段代码添加到Jitsi中。你可以在自己的页面中加载这段代码,然后用iFrame将其加载到Jitsi中。你也可以使用Jitsi iFrame API把iFrame自动加载到指定元素中。有关该操作的详细说明,大家可以参阅这篇文章。我就不赘述了。在不构建Jitsi源代码的情况下,我们如何完成这个操作呢?

使用Jitsi Meet静态文件

实际上,Jitsi是通过静态文件来加载各种网络元素的。在安装Debian时,这些文件位于/usr/share/jitsi-meet/ 。如果我们检查/usr/share/jitsi-meet/index.html,可以看到Jitsi Meet使用<!–#include virtual=”yourfile.html”>插入模块。

    <script><!--#include virtual="/config.js" --></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
    <!--#include virtual="connection_optimization/connection_optimization.html" -->
    <script src="libs/do_external_connect.min.js?v=1"></script>
    <script><!--#include virtual="/interface_config.js" --></script>
    <script><!--#include virtual="/logging_config.js" --></script>
    <script src="libs/lib-jitsi-meet.min.js?v=4025"></script>
    <script src="libs/app.bundle.min.js?v=4025"></script>
    <!--#include virtual="title.html" -->
    <!--#include virtual="plugin.head.html" -->
    <!--#include virtual="static/welcomePageAdditionalContent.html" -->
    <!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
  </head>

我们只需要在正文部分插入我们的内容,如下所示:

  <body>
    <!--#include virtual="body.html" -->
    <div id="react"></div>
    <!--#include virtual="static/recorder.html" -->
  </body>

代码

该静态目录下的文件会进行加载操作,你需要在此获取recorder文件。我把本地录音功能分成了2个文件:

1. 带button的HTML及CSS;

2. 上述提到所有逻辑的JavaScript文件,以及一些基本播放和保存控件。

文件里还包括一个bash脚本,它将复制这些文件并把recorder插入到Jitsi Meet的index.html中。

完整的文件源码是https://github.com/webrtcHacks/jitsiLocalRecorder

不局限于Jitsi

上述方法其实可以录制任何东西,只要加载它就可以了。我的网站上有关于该方法的一个演示,大家可以参考一下。

有缺憾的方法

这个方法对我来说是可行的,但确实不太完美。一方面因为我的CSS真的很烂,更因为这种方法有许多局限性。

屏幕录制UI差:带音频捕捉功能的getDisplayMedia能做的很少. 我可以通过录制选择来完成操作,但在实际应用中你不能通过询问用户来完成这样的操作。

没有音频设备选择:在没有指定媒体设备的情况下,我会调用getUserMedia,所以它可能和我在Jitsi Meet会话中使用的设备不一样。通过添加一个媒体设备选择的UI元素来,使用Jitsi的API或者重载getUserMedia能解决该问题。

没有录音提醒:很多情况下,你有义务告诉别人你正在录音。极简GUI只在本地发挥作用。与内置的Jitsi Meet录音工具不同,我的工具没有录音提醒。所以我试着用lib-jitsi-meet API添加一个新的录音方到通话中。但这有个缺点——它下线了点对点模式(p2p4121),这会消耗更多的资源。

总之,我完成了我的目标,但该方法可能并不适用于所有人。至少我在这个过程中有所收获!

文章地址:https://webrtchacks.com/jitsi-recording-getdisplaymedia-audio/

原文作者:Chad Hart

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营