是否可以使用 puppeteer 将 Javascript 对象传递给 nodejs?

Is it possible to use puppeteer to pass a Javascript object to nodejs?

背景

我正在使用 Posenet (see the in browser demo here) 进行关键点检测。我已经在 WebRTC MediaStream 上将其设置为 运行,s.t.:

客户端: 运行s 在计算机 A 上的 chrome 选项卡中。初始化 WebRTC 连接并将 MediaStream 发送到 Server。通过 WebRTC 的 DataChannel 从 Server 接收实时关键点数据。

服务器: 运行s 在机器 chrome 选项卡中 B,接收 WebRTC 流并将相应的 MediaStream 传递给 Posenet . Posenet 做它的事情并计算关键点。这个关键点数据然后通过 WebRTC 的 DataChannel 发送回 客户端 (如果你有更好的主意,我洗耳恭听)。

问题: 我想让服务器接收来自不同客户端的多个流和 运行 每个流上的 Posenet,向所有客户端发送实时关键点数据。虽然我对使用 Chrome 的服务器并不感到兴奋,但我现在可以使用 puppeteer 和 Chrome 的无头模式,主要是为了抽象出 WebRTC 的复杂性。

方法

我尝试了两种方法,非常支持#2:

方法 #1

运行 @tensorflow/tfjspuppeteer 上下文中(即在无头 chrome 选项卡中)。但是,我似乎无法获得 PoseNet Browser Demo working in headless mode, due to some WebGL error (it does work in non-headless mode though). I tried the following (passing args to puppeteer.launch() to enable WebGL, though I haven't had any luck - see here and here 作为参考):

const puppeteer = require('puppeteer');

async function main() {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--enable-webgl-draft-extensions', '--enable-webgl-image-chromium', '--enable-webgl-swap-chain', '--enable-webgl2-compute-context']
  });
  const page = await browser.newPage();
  await page.goto('https://storage.googleapis.com/tfjs-models/demos/posenet/camera.html', {
    waitUntil: 'networkidle2'
  });
  // Make chromium console calls available to nodejs console
  page.on('console', msg => {
    for (let i = 0; i < msg.args().length; ++i)
      console.log(`${i}: ${msg.args()[i]}`);
  });
}

main();

在无头模式下,我收到此错误消息。

0: JSHandle:Initialization of backend webgl failed
0: JSHandle:Error: WebGL is not supported on this device

这给我留下了 question #1:如何在 puppeteer 中启用 WebGL?

方法 #2

最好,我想 运行 posenet 使用 @tensorflow/tfjs-node 后端,以加速计算。因此,我会 link puppeteer@tensorflow/tfjs-node, s.t.:

问题

问题是我似乎无法在 node 中访问 puppeteer 的 MediaStream 对象 ,无法将此对象传递给 posenet.我只能访问 JSHandles and ElementHandles。是否可以将与句柄关联的 javascript 对象 传递给 node?

具体是抛出这个错误:

UnhandledPromiseRejectionWarning: Error: When running in node, pixels must be an HTMLCanvasElement like the one returned by the `canvas` npm package
    at NodeJSKernelBackend.fromPixels (/home/work/code/node_modules/@tensorflow/tfjs-node/dist/nodejs_kernel_backend.js:1464:19)
    at Engine.fromPixels (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/engine.js:749:29)
    at fromPixels_ (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/ops/browser.js:85:28)
    at Object.fromPixels (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/ops/operation.js:46:29)
    at toInputTensor (/home/work/code/node_modules/@tensorflow-models/posenet/dist/util.js:164:60)
    at /home/work/code/node_modules/@tensorflow-models/posenet/dist/util.js:198:27
    at /home/work/code/node_modules/@tensorflow/tfjs-core/dist/engine.js:349:22
    at Engine.scopedRun (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/engine.js:359:23)
    at Engine.tidy (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/engine.js:348:21)
    at Object.tidy (/home/work/code/node_modules/@tensorflow/tfjs-core/dist/globals.js:164:28)

记录传递给 NodeJSKernelBackend.prototype.fromPixels = function (pixels, numChannels) {..}pixels 参数,它的计算结果为 ElementHandle. I am aware that I can access serializable properties of a Javascript object, using puppeteer's page.evaluate。但是,如果我要传递 CanvasRenderingContext2DimageData(使用方法 getImageData() 通过调用 puppeteer.evaluate(..) 传递给 node,这意味着将整个原始图像字符串化然后在 node 的上下文中重建它。

这给我留下了 question #2:有什么方法可以直接在 node 中使来自 puppeteer 上下文的对象可访问(只读),而不必去通过例如puppeteer.evaluate(..)?

我推荐另一种方法是放弃在服务器端使用 puppeteer 的想法,而是在 Node.js 中实现一个实际的 WebRTC 客户端,然后通过 @tensorflow/tfjs-node.[= 直接使用 PoseNet 28=]

为什么不在服务器端使用 puppeteer

在服务器端使用 puppeteer 会带来很多复杂性。除了与多个客户端的活动 WebRTC 连接之外,您现在还必须为每个连接管理一个浏览器(或至少一个选项卡)。因此,您不仅要考虑与客户端的连接失败时会发生什么,还必须为其他情况做好准备,例如浏览器崩溃、页面崩溃、WebGL 支持(每页)、浏览器中的文档未加载、 memory/CPU 浏览器实例的使用,...

话虽如此,让我们回顾一下您的方法。

方法 1:运行Tensorflow.js inside puppeteer

您应该能够通过仅使用 cpu backend 获得此 运行。在使用任何其他代码之前,您可以像这样设置后端:

tf.setBackend('cpu');

您也可以获取 WebGL 运行(因为您 在使用 WebGL 和 puppeteer 时遇到问题)。但是,即使你得到它 运行,你现在也是 运行 一个 Node.js 脚本来启动一个 Chrome 浏览器,该浏览器启动一个 WebRTC 会话和 Tensorflow.js 在一个内部训练网站。复杂性方面,如果出现任何问题,这将非常难以调试...

方法 2:在 puppeteer 和 Node.js

之间传输数据

如果不大幅降低速度(关于帧的发送和接收),这种方法几乎是不可能的。 puppeteer 需要序列化任何交换的数据。 Node.js 和浏览器环境之间没有共享内存或共享数据对象这样的东西。这意味着您必须序列化每个帧(所有像素...)以将它们从浏览器环境传输到 Node.js。就性能而言,这对于小图片可能没问题,但当你的图片越大时效果会越差。


总而言之,如果您想采用两种方法中的一种,就会引入很多复杂性。因此,让我们看看替代方案。

替代方法:将您的视频流直接发送到您的服务器

您可以直接实现 WebRTC 对等点,而不是使用 puppeteer 建立 WebRTC 连接。我从你的问题中读到你害怕复杂性,但这可能是值得的。

要实现 WebRTC 服务器,您可以使用库 node-webrtc, which allows to implement a WebRTC peer on the server-side. There are multiple examples, of which one is very interesting for your use case. This is the video-compositing 示例,它在客户端(浏览器)和服务器 (Node.js) 之间建立连接以流式传输视频。然后服务器会修改发送的帧,并在它们上面放一个"watermark"。

代码示例

以下代码显示了来自 video-compositing example. The code reads a frame from the input stream and creates a node-canvas 对象的最相关行。

const lastFrameCanvas = createCanvas(lastFrame.width,  lastFrame.height);
const lastFrameContext = lastFrameCanvas.getContext('2d', { pixelFormat: 'RGBA24' });

const rgba = new Uint8ClampedArray(lastFrame.width *  lastFrame.height * 4);
const rgbaFrame = createImageData(rgba, lastFrame.width, lastFrame.height);
i420ToRgba(lastFrame, rgbaFrame);

lastFrameContext.putImageData(rgbaFrame, 0, 0);
context.drawImage(lastFrameCanvas, 0, 0);

您现在有一个 canvas 对象,您可以像这样将其用于 PoseNet:

const net = await posenet.load();

// ...
const input = tf.browser.fromPixels(lastFrameCanvas);
const pose = await net.estimateSinglePose(input, /* ... */);

现在需要将生成的数据传输回客户端,这可以通过使用数据通道来完成。存储库中还有一个示例 (ping-pong) 与此相关,它比视频示例简单得多。

虽然您可能担心使用 node-webrtc 的复杂性,但我建议尝试一下这种方法和 node-webrtc-examples。您可以先签出存储库。所有示例都已准备好尝试和使用。