搜索文章

输入关键词开始搜索

Puppeteer学习笔记

程序设计#JavaScript#数据可视化#AI#Linux#Docker

调用方法

一些知识

页面transform.scale放大后截图

scale:2

大小:364KB

scale:5

大小:1225KB

截屏越大,速度越慢

比如手机页面,高宽分别放大5倍(实际截屏面积应该是放大了25倍),9秒的视频,截了近500秒,差不多是50倍的时间,太夸张了。

不过这可能也跟我笔记本没接电源有关系,还需要再测试一下。

录制

常用参数说明

--no-sandbox: 去沙箱运行

--disable-dev-shm-usage: 默认情况下,Docker运行一个/dev/shm共享内存空间为64MB 的容器。这通常对Chrome来说太小,并且会导致Chrome在渲染大页面时崩溃。要修复,必须运行容器 docker run --shm-size=1gb 以增加/dev/shm的容量。从Chrome 65开始,使用--disable-dev-shm-usage标志启动浏览器即可,这将会写入共享内存文件/tmp而不是/dev/shm.

常用操作

注入自定义JS

通过page.evaluate()实现,参考这个文章:

https://www.cnblogs.com/wuweiblogs/p/12918968.html

常见问题

选择jpeg还是png

https://www.zhihu.com/question/29758228

因为Chromium安装失败进而导致Puppeteer安装失败

https://blog.csdn.net/qq799028706/article/details/88602254

下载chromium是自动从官方过去下载的,我们在内网是无法安装Puppeteer的,需要跳过下载Chromium:

env PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install puppeteer

注意,新的puppeteer版本中,这个环境变量变成了PUPPETEER_SKIP_DOWNLOAD,具体使用哪个环境变量,请根据报错提示信息来选择。

如果不是内网环境,而只是单纯因为墙的问题,那么你可以选择科学上网,或者切换下npm源:

# 切换下载源为国内镜像
npm config set puppeteer_download_host=https://npm.taobao.org/mirrors

npm i puppeteer

Browser is not downloaded

如果全局安装Puppeteer后,仍然提示这个信息,那一般是因为你后续安装的某个包依赖了puppeteer,这个包的node_modules目录下下载了puppeteer,程序使用了这个puppeteer,而没有使用全局puppeteer的缘故。

比如我遇到的一次,就是后续安装了timesnap这个包,项目启动后使用了timesnap/node_modules/puppeteer下的程序,没有使用全局的puppeteer。

解决方案:删除timesnap/node_modules/puppeteer 即可。

离线安装Chromium

比如内网就可以用这个方式安装:

1、先安装puppeteer:npm install puppeteer

2、查看node_modules/puppeteer/.local-chromium下面的chromium版本号

3、去 https://npm.taobao.org/mirrors/chromium-browser-snapshots/ 下载对应系统对应版本的chromium包

4、解压到node_modules/puppeteer/.local-chromium下,路径类似这样:

node_modules/puppeteer/.local-chromium/win64-722234/chrome-win/

如需全局安装Puppeteer,也可以采用和类似的操作。

文字模糊

这个多出现在页面文字颜色和背景出现渗透的情况。

另外和字体也有关系,比如雷达图中,同样是文字,雷达图上的就很清晰,但是legend的文字就很模糊,即使放大了也很模糊。

我们尝试了将页面通过scale放大,发现效果很明显,比如柱状图(4)和折线图(9)上的文字就很清晰了。但是出现了新的问题,就是失败率很高。

(TODO)scale较大时,失败率非常高

比如scale设置为5,绝大部分视频都生成失败了。

似乎和进程数有关系?我设置了进程数为3个,都成功了。

chrome进程数远大于程序设置的多进程数量

比如我程序设置了多进程数量最大为16,但是查看windows的chrome.exe进程的数量,有多达83个,且每个进程的PID都不一样。

无法播放mp4文件

https://github.com/tungs/timecut/issues/19

有人建议安装个Chrome浏览器来解决

这里列出了Chromium支持的几种视频格式:

https://www.chromium.org/audio-video

  • VP8

  • VP9

  • AV1 [Only Chrome OS, Linux, macOS, and Windows at present]

  • Theora [Except on Android variants]

  • H.264 [Google Chrome only]

  • MPEG-4 [Google Chrome OS only]

因为HTML中的video标签只支持mp4格式,因此即使将视频转为上述格式也不行。

CentOS安装Chrome浏览器:

https://blog.csdn.net/yelllowcong/article/details/80159963

PS:我没安装glib2,等验证下是否可行

#安装glib2 yum update glib2 -y

(精)preparePage无法等待到后续添加的页面元素

场景描述

我们采用了视频作为背景,加入到网页中去。但是视频文件比较大,加载是需要一定时间的,因此我们想通过preparePage()这个配置项,等视频加载完成后,再执行截屏操作。

我们是在视频加载完成后,给页面写上一个新的元素,然后在preparePage中判断,如果出现了这个元素,就开始截屏;但是我们发现preparePage的回调中,永远获取不到这个元素。

然后我们发现,单纯的puppeteer播放视频是没问题的,之所以出现这个问题,是因为加入了anime.js的原因,在timeline的回调里面,生成了这个元素;而anime.js是通过requestAnimationFrame()来实现的,因此猜测和timecut的requestAnimationFrame阻塞机制有关系

然后我们不使用anime.js,直接通过timecut,在页面中的视频加载成功后,通过requestAnimationFrame追加元素,发现preparePage中也识别不了,因此确定肯定是和timecut的requestAnimationFrame阻塞机制有关系。

页面中追加元素的代码:

function append() {
    var myScript = document.createElement('div');
    myScript.id = 'canplay';
    document.body.appendChild(myScript);
}

const myVideo = document.getElementById('myvideo');
myVideo.onloadeddata = function () {
    console.log('视频已经加载好第一帧了');
    requestAnimationFrame(() => {
        console.log('准备添加元素');
        append();
    })
};

timecut中preparePage做的处理:

preparePage: (puppeteerPage) => new Promise((resolve, reject) => {
    puppeteerPage
        .waitForSelector('#canplay', {
        timeout: 5000,
    })
        .then(() => {
        console.log('成功获取到页面新添加的元素了,即将开始截屏')
        resolve();
    }, (err) => {
        console.log('获取页面新添加的元素失败了', err);
        reject();
    });
}),

preparePage的原理

想要解决这个问题,得弄清楚2个事情:

1、Puppeteer的preparePage的原理,错了,应该是waitForSelector()的原理

我猜测一下,waitForSelector应该是每一帧里面去尝试读一下该元素的属性。

2、timesnap阻塞requestAnimationFrame的原理

到底阻塞了啥?应该就是截屏操作吧?也就是上一个截屏没完成,不播放下一帧。但是没等到元素,我肯定不会截屏啊,这样就导致出现死循环了。

也就是timesnap的阻塞机制和Puppeteer的waitForSelector在流程逻辑上冲突了。

timesnap压根没考虑waitForSelector,即页面元素后续添加的情况。这属于timesnap的设计缺陷?

经测试,一帧都不播放了,没有任何一个requestAnimationFrame被触发。

仔细调试timesnap的源码,我们发现它这样改写了requestAnimationFrame的机制:

机制

我们通过requestAnimationFrame函数加入的每一帧的自定义逻辑,都会通过timesnap的_requestAnimationFrame,被加入_animationFrameBlocks这个队列中。但是注意只是能成功加入,执行是另外的逻辑触发的。

这些自定义逻辑被执行顺序大致是这样的:

preparePage->处理每一帧截屏的markers->goToTimeAndAnimateForCapture/goToTimeAndAnimate-> window._timesnap_runAnimationFrames->_runAnimationFrames

机制

具体实现可以看一下timesnap/index.js里面的逻辑:

if (marker.type === 'Capture') {
            p = timeHandler.goToTimeAndAnimateForCapture(browserFrames, marker.time);
 } else if (marker.type === 'Only Animate') {
            p = timeHandler.goToTimeAndAnimate(browserFrames, marker.time);
          }

而在这个timesnap/index.js中,需要先等preparePage()执行完毕,才能执行后续的处理markers;而改写后的requestAnimationFrame,只有在markers中才会被触发执行。所以这就形成了这个现象:

preparePage()等不到页面元素,因此一直卡住

preparePage()一直没有resolve,因此无法触发markers的初始化

markers未执行初始化逻辑,因此不会触发runAnimationFrames

不触发runAnimationFrames,之前通过requestAnimationFrame注册的给页面添加元素的操作,一直不成功

了解了这个机制,那么就很容易制定解决方案了。我们可以将preparePage中判断元素是否存在的逻辑,移动到preparePageForScreenshot中,在第一帧截屏之前,做这个元素是否存在的判断:

// isVideoLoaded是在前面定义的变量,用于标记视频是否已加载,避免每一帧都执行判断页面元素的逻辑,造成性能损耗
preparePageForScreenshot: (page, frameNumber, totalFrames) => new Promise((resolve, reject) => {
    if (isVideoLoaded) {
        resolve();
    } else {
        page
            .waitForSelector('#canplay', {
            timeout: 5000,
        })
            .then(() => {
            console.log('成功获取到新添加的元素了,即将开始截屏')
            isVideoLoaded = true;
            resolve();
        }, (err) => {
            console.log('获取页面新添加的元素失败了', err);
            reject();
        });
    }
}),

不过后面我们发现这种处理方式是有问题的:因为Vue中调用了nexttick,实际上生成video元素是在第二次requestAnimationFrame里,这种方案只执行了第一次requestAnimationFrame,进不去第二次requestAnimationFrame。

因此我们修改了解决方案:不在onBegin中生成#video标签,而是改为直接写在Vue的created方法里面:

机制

其他问题

我发现可以获取到新加的元素,但是视频不播放了,经排查,是因为我selector写错了。

资料

timesnap的阻塞机制

测试用的代码

testPreparePage.js:

/**
 * 排查preparePage()中无法获取后续添加的元素的问题
 */
const timecut = require('../lib/timecut');

const dir = 'D:/test/micro-video/';
const selector = 'body';
const url = 'http://127.0.0.1:8000/index.html';

// 视频是否已加载
let isVideoLoaded = false;
const option = {
  url,
  viewport: {
    width: 1080,
    height: 1920,
  },
  combine: true,
  selector, // 元素选择标志
  screenshotType: 'png',
  fps: 20, // 帧数
  duration: 5, // 截取时长
  quiet: false,
  keepFrames: true,
  frameCache: `${dir}images`,
  // TODO:手动指定存放截图的文件夹
  frameDir: '',
  output: `${dir}output/result.mp4`,
  // Puppeteer/Chromium参数,详见:https://peter.sh/experiments/chromium-command-line-switches/
  // TODO:添加禁用浏览器缓存的参数
  launchArguments: [
    '--no-sandbox',
    '–no-zygote',
    '--disable-setuid-sandbox',
    '--disable-gpu',
    '--disable-dev-shm-usage',
    '–single-process',
    '--no-first-run',
  ],
  headless: false,
  // 无头浏览器没法播放视频
  executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
  preparePage: (puppeteerPage) => new Promise((resolve, reject) => {
    puppeteerPage
      .waitForSelector('body', {
        timeout: 5000,
      })
      .then(() => {
        console.log('preparePage完成')
        resolve();
      }, (err) => {
        console.log('preparePage失败', err);
        reject();
      });
  }),
  preparePageForScreenshot: (page, frameNumber, totalFrames) => new Promise((resolve, reject) => {
    if (isVideoLoaded) {
      resolve();
    } else {
      page
        .waitForSelector('#canplay', {
          timeout: 5000,
        })
        .then(() => {
          console.log('成功获取到新添加的元素了,即将开始截屏')
          isVideoLoaded = true;
          resolve();
        }, (err) => {
          console.log('获取页面新添加的元素失败了', err);
          reject();
        });
    }
  }),
};

console.log('视频截屏的配置信息:', option);

(async () => {
  const start = new Date().getTime();
  await timecut(option).then(() => {
    const end = new Date().getTime();
    const cost = (end - start) / 1000;
    console.log(`Done, cost ${cost} seconds.`);
  });
})();

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      div {
        width: 400px;
        height: 400px;
        background-color: purple;
        position: fixed;
        top: 0;
        left: 100px;
      }
    </style>
  </head>
  <body>
    <div></div>
    <video src="./bg1.mp4" id="myvideo" autoplay muted></video>
    <script>
      function randomColor() {
        var color = 'rgb(';
        for (var i = 0; i < 3; i++) color += parseInt(Math.random() * 256) + ',';
        //去除最后一个逗号
        // color=color.slice(0,-1)
        color = color.substring(0, color.length - 1) + ')';
        return color;
      }
      setInterval(() => {
        document.querySelector('div').style.backgroundColor = randomColor();
      }, 500);

      requestAnimationFrame(printRequestAnimationFrame);

      const myVideo = document.getElementById('myvideo');
      myVideo.onloadeddata = function () {
        console.log('视频已经加载好第一帧了');
        // append();
        requestAnimationFrame(() => {
          console.log('准备添加元素');
          append();
        })
      };

      function printRequestAnimationFrame()
      {
        console.log('播放中....');
        requestAnimationFrame(printRequestAnimationFrame);
      }


      function append() {
        // 得先引入axios
        var myScript = document.createElement('div');
        myScript.id = 'canplay';
        document.body.appendChild(myScript);
      }
    </script>
  </body>
</html>

参考资料

【精】浏览器工作原理讲解:

https://zhuanlan.zhihu.com/p/102149546

https://www.jianshu.com/p/bb85f89375ce

中文文档:

https://zhaoqize.github.io/puppeteer-api-zh_CN/#?product=Puppeteer&version=v5.5.0&show=api-class-page