引言
在本文中,我们猜测我们将讨论Screen Capture API。 该API诞生于2014年,很难将其命名为新API,但浏览器支持仍然很薄弱。 但是,它可以用于个人项目或这种支持不是很重要的地方。
一些链接可帮助您入门:
如果与演示程序的链接断开(或者您懒得去那里)-这是完成的演示程序的外观:

让我们开始吧。
动机
最近,我想到了一个在其工作中使用QR码的Web应用程序的想法。 而且,尽管它们通常便于发送(例如,现实世界中的长链接),您可以在其中将手机对准它们,但在台式机上却有点复杂。 如果QR码在您需要阅读的同一设备的屏幕上,则您需要弄乱识别服务或从电话中识别它,然后将数据传输回PC。 不方便。
有些产品(例如1Password)针对这种情况提供了一种有趣的解决方案。 如果您需要通过QR码设置帐户,他们会打开一个半透明窗口,您可以将其中的代码拖到图像上,并自动识别该图像。 看起来是这样的:

如果我们可以为我们的应用程序实现类似的东西,那将是理想的。 但可能无法在浏览器中运行...
好吧,差不多。 屏幕捕获API及其唯一的getDisplayMedia
方法将为getDisplayMedia
。 getDisplayMedia
就像getUserMedia
一样,仅适用于设备屏幕,而不适用于其相机。 不幸的是,如上所述,浏览器支持远不及访问相机。 根据MDN的说法,它可以在Firefox,Chrome,Edge(尽管放在错误的位置-在navigator
,而不是在navigator.mediaDevices
)中使用+ Edge Mobile和Android版Opera。
预期中的两大移动浏览器旁边有很多很好奇的选择。
API本身非常简单。 它的工作原理与getUserMedia
相同,但是允许您从已定义的显示表面之一捕获视频流:
- 从监视器 (整个屏幕),
- 从某个应用程序的一个或多个窗口中,
- 从浏览器 ,或者从特定文档。 在Chrome中,此文档是一个单独的标签,但在FF中则没有此类选项。
浏览器API,它使您可以浏览浏览器之外的东西……听起来很熟悉,通常会带来一些麻烦,但是在这种情况下,它可能非常方便。 您可以从其他窗口捕获图片,例如,实时识别和翻译文本,例如Google Translate Camera。 好吧,可能还有更多有趣的用途。
我们收集
因此,我们弄清楚了API提供给我们的功能。 接下来是什么?
然后,我们需要将此视频流替换为可以处理的图像。 为此,我们使用<video>
, <canvas>
元素和其他一些JS。
该过程的特写看起来像这样:
- 直接流到
<video>
; - 以一定的频率在
<canvas>
绘制<video>
的内容; - 使用
getImageData
2D上下文方法从<canvas>
收集ImageData对象。
由于流水线这么长,整个过程听起来有些奇怪,但是这种方法非常流行,并且用于从getUserMedia
网络摄像头捕获数据。
忽略所有不相关的内容,以启动流并从中拉出帧,我们需要以下代码:
async function run() { const video = document.createElement('video'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const displayMediaOptions = { video: { cursor: "never" }, audio: false } video.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); const videoTrack = video.srcObject.getVideoTracks()[0]; const { height, width } = videoTrack.getSettings(); context.drawImage(video, 0, 0, width, height); return context.getImageData(0, 0, width, height); } await run();
如上所述,首先我们创建<video>
和<canvas>
元素,并向画布请求2D上下文( CanvasRenderingContext2D
)。
然后我们定义流量限制/条件 。 与相机中的流不同,它们很少。 我们说我们不想看到光标,并且我们不需要音频。 尽管在撰写本文时,任何人仍不支持音频捕获。
之后,我们将接收到的MediaStream
类型的流挂接到<video>
元素。 请注意, getDisplayMedia
返回一个Promise。
最后,从流上接收到的数据中,我们记得视频的分辨率,以便正确地将其绘制到画布上,绘制帧并从ImageData
拉出ImageData对象。
为了充分利用,您很可能希望循环处理帧而不是一次。 例如,当您等待所需的图像出现在框架中时。 这里需要说几句话。
当涉及到“以恒定循环处理DOM中的某些内容”时,想到的第一件事很可能是requestAnimationFrame
。 但是,在我们的情况下,将无法使用它。 事实是,当选项卡停止活动时,浏览器将暂停rAF循环处理。 在我们的情况下,这时我们将要处理图像。
在这方面,我们将使用旧的setInterval
代替rAF。 但是他的处境并不顺利。 在非活动标签中,回调操作之间的间隔至少为1秒 。 尽管如此,这对我们来说已经足够了。
最后,当我们到达框架时,我们可以根据需要对其进行处理。 出于本演示的目的,我们将使用jsQR库。 这非常简单:输入接受ImageData
,即图像的宽度和高度。 如果接收到的图像具有QR码,您将获得一个带有已识别数据的JS对象。
让我们用几行代码来补充前面的示例:
const imageData = await run(); const code = jsQR(imageData.data, streamWidth, streamHeight);
做完了!
NPM
我认为可以将本示例后面的主要代码打包到一个npm库中,并在最初使用时节省一些时间以备后用。 该库非常简单,在这个阶段它只接受将ImageData
发送到的回调,另外一个参数是发送数据的频率。 您需要自己进行所有处理。 我会考虑扩展功能是否有意义。
该库称为stream-display
: NPM | Github 。
它的使用减少为字面上的三行代码和一个回调:
const callback = imageData => {...}
演示可以在这里看到。 还有一个CodePen版本可用于快速实验。 这两个示例都使用上面的NPM包。
关于测试的一点
将这些代码打包到库中后,我不得不考虑如何对其进行测试。 我绝对不想拖动50MB的无头Chrome浏览器在其中运行一些小测试。 尽管为所有组件编写存根的想法似乎很痛苦,但最终我还是这样做了。
选择了tape
作为测试跑步者。 这是我最终不得不模拟的:
document
对象和DOM元素。 为此,我选择了jsdom ;- 一些缺少实现的jsdom方法:
HTMLMediaElement#play
, HTMLCanvasElement#getContext
和navigator.mediaDevices#getDisplayMedia
; - 时间。 为此,我使用了
useFakeTimers
库的useFakeTimers
库useFakeTimers
称为lolex
。 它将其替换设置为setInterval
, requestAnimationFrame
和许多其他随时间运行的函数,还允许您控制此假时间的流向。 但请注意:jsdom在其初始化过程中的某个位置会占用时间,如果您先打开sinon,一切都会冻结。
我还将sinon用于所有需要监视的功能存根。 其余部分由空的JS函数实现。
当然,您可以自由选择已经熟悉的工具。 但是,我希望这个清单可以让您提前准备,因为现在您知道您要处理的内容了。
最终结果可以在库存储库中看到。 看起来不太漂亮,但是可以用。
结论
事实证明,该解决方案并不像本文开头提到的透明窗口那样优雅,但是也许有一天网络会出现。 我们只能希望,当浏览器学习通过其窗口进行查看时,这些功能将由我们严格控制。 同时,请记住,当您在Chrome中浏览屏幕时-可以对其进行解析,记录等。 因此,不要随意翻唱!
我希望本文之后的人能够自己学到新的技巧。 如果您有其他用途的想法,请在评论中写下。 很快见。