Puppeteer学习笔记

调用方法

一些知识

页面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:

1
env PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install puppeteer

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

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

1
2
3
4
# 切换下载源为国内镜像
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阻塞机制有关系。

页面中追加元素的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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做的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
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里面的逻辑:

1
2
if (marker.type === 'Capture') {
p = timeHandler.goToTimeAndAnimateForCapture(browserFrames, marker.time);
1
2
3
} 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中,在第一帧截屏之前,做这个元素是否存在的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* 排查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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<!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