Vastiny

Mar 06, 2020

Electron 截屏方案分析

实现一个在 Electron 内截屏的功能,大概有六种实现方式,讨论一下选择哪个方案。最后再说一下高分屏的截图如何变得清晰。

可选方案

第一种,使用 Canvas 截图

这种技术有一个非常有名的库:html2canvas,可以支持相对比较简单的 DOM 截图

目前一直是处于实验状态(very experimental state),如果理解这个库的限制,那么是一个还不错的选择

原理是使用 BoundCurves 对 DOM 进行 path 绘制。例如下面一段选自 html2canvas 库中的代码:

1
2
3
4
5
6
7
8
9
10
this.topLeftPaddingBox =
tlh > 0 || tlv > 0
? getCurvePoints(
bounds.left + borderLeftWidth,
bounds.top + borderTopWidth,
Math.max(0, tlh - borderLeftWidth),
Math.max(0, tlv - borderTopWidth),
CORNER.TOP_LEFT
)
: new Vector(bounds.left + borderLeftWidth, bounds.top + borderTopWidth);

计算每一个 DOM 的形状,再把这些数据,比如 topLeftPaddingBox 放到 Stack 中,最后放在队列中,一个一个的绘制。

优势

  • 当然是兼容性非常好, 支持 canvas 的就行

劣势

  • 因为是要把现存的 DOM 搬到 canvas 中,所以只能识别库支持的 css 属性,比如 transform 不支持。
  • 获取资源要同源,如果有其它跨域资源,布局会混乱
  • 大量计算工作,性能损耗较大

第二种,使用 SVG 构图

如下简单的例子,可以把 DOM 通过 svg 的 foreignObject 放到 canvas 中,就可以截图了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这段代码出自它处

const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
'<foreignObject width="100%" height="100%">' +
'<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
'<em>I</em> like' +
'<span style="color:white; text-shadow:0 0 2px blue;">' +
'cheese</span>' +
'</div>' +
'</foreignObject>' +
'</svg>'
const DOMURL = window.URL || window.webkitURL || window
const img = new Image()
const svg = new Blob([data], {type: 'image/svg+xml;charset=utf-8'})
const url = DOMURL.createObjectURL(svg)
img.onload = function () {
ctx.drawImage(img, 0, 0)
DOMURL.revokeObjectURL(url)
}
img.src = url

做得好的是 rasterizeHTML.js,同时可以支持直接填写url,然后获取后自己截图。

rasterizeHTML.js 和 html2canvas 的区别
之前是 rasterizeHTML.js 使用 foreignObject,而 html2canvas 自己从最基础的开始。
推荐直接使用 foreignObject 更加安全,目前 html2canvas 和 rasterizeHTML 都兼容使用 foreignObject (html2canvas 看源代码里面已经支持了)。

第三种,使用 Chrome 的 getDisplayMedia

1
2
3
4
5
6
7
const captureStream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: {
width: { max: 1280 },
height: { max: 720 }
}
})

比如在 Chrome 中执行,会弹出下面的窗口,然后开启屏幕分享:
![](_image/Screen Shot 2020-03-07 at 00.10.21.png)

这种可以实现录制整个屏幕,或者某个 Tab,从而分享画面,不过录音这个功能会碰到一些阻碍。

优势

  • 实现非网页内的内容也可以截取

劣势

  • 需要用户手动允许系统的屏幕录像权限
  • 获得的视频需要截取

第四种,使用 Electron 的 webContents.capturePage

Electron 内置了一个截取网页内的接口 webContents.capturePage ,如下:

1
2
3
4
5
6
7
8
9
10
const remote = require('electron').remote
const webContents = remote.getCurrentWebContents()

const nativeImage = await webContents.capturePage({
x: 0,
y: 0,
width: 1000,
height: 2000
})
remote.require('fs').writeFile(TEMP_URL, nativeImage.toPng())

优势

  • 简单,快捷
  • 支持截取完整的页面

劣势

  • 只能截取当前页面的内容

第五种,使用 Electron DesktopCapturer module

五的第一种:

DesktopCapturer 模块里面可以通过设置录屏的 video,然后通过 canvas 获取录制屏幕数据。前面的 getDisplayMedia 也可以通过这个方式获取到截屏图片。

例如下面的代码:

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
const { desktopCapturer } = require('electron')

const sources = await desktopCapturer.getSources({
types: ['window', 'screen']
})

for (let i = 0; i < sources.length; ++i) {
if (sources[i].name === 'Electron') {
navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sources[i].id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720
}
}
})
.then((stream) => handleStream(stream))
.catch((e) => handleError(e))
return
}
}

function handleStream (stream) {
const video = document.querySelector('video')
video.srcObject = stream
video.onloadedmetadata = (e) => video.play()
}

function handleError (e) {
console.log(e)
}

五的第二种:

之前看到有个地方,用 Thumbnail 拿来做屏幕截图 :P

1
2
3
4
5
6
7
8
9
10
11
const { desktopCapturer } = require('electron')

const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
}, thumbnailSizeSize: {
width: 300,
height: 300,
})

const source = sources.find(e => e.name === 'Electron Window Title')
const thumbnailNativeImage = source.thumbnail

优势

  • 可以搜索特定窗口进行录屏
  • electron 封装了一层,使用更加简便

劣势

  • 如同前面的 getDisplayMedia,也需要用户手动开启屏幕录像权限
  • 如果用 desktopCapturer 的 thumbnail 来截屏,耗性能

第六种,使用系统原生的屏幕录制接口

这种就不细讲了,每端都适配复杂的原生接口,这样就已经迷失方向了。

截屏的图片不是 2x 图片

如果用上面的任何一种方法,在高分屏里面截图(window.devicePixelRatio === 2),最后得到的图片还是图片宽高放大一倍,但像素还是低一倍。

后来发现其实是图片头中有记录 meta 信息。里面就有关于 dpi 的信息。找到包 changedpi 可以修改 canvas 和 DataUrl 拿到的图片 dpi。

1
2
3
4
5
6
7
8
9
10
11
// 创建一个标准的 72 dpi
const IMAGE_QUALITY = 0.92
const dataUrl = canvas.toDataURL('image/jpeg', IMAGE_QUALITY);

function maxDPIRatio() {
const displayScale = remote.screen.getAllDisplays().map(display => display.scaleFactor)
return Math.max.apply(null, displayScale) // 高分屏里面一般是 2
}

const STANDARD_DPI = 72
const daurl150dpi = changedpi.changeDpiDataUrl(dataUrl, maxDPIRatio() * STANDARD_DPI);

canvas 一般情况下,最高只能获得 72 dpi 分辨率的图片。

总结

每个点都没有太详细的讲,但已经可以通过这些选择最终的方案了。最后当然推荐使用的是 webContents.capturePage 的方法。简单易用,在功能上能做到最好。

参考

引用

OLDER > < NEWER