「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
前言
之前做心电图桌面应用时实现了应用内截屏的功能,涉及到canvas,动画,文件下载下载等知识点,在此做一总结,本文章的方法只在 window 10 测试过。项目使用的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 v14.17.5,electron 版本 11.0.0。
※注:本文代码区域每行开头的“+”表示新增,“-”表示删除,“M”表示修改;代码中的“...”表示省略。
1 前置知识
1.1 Canvas API:CanvasRenderingContext2D.drawImage()
drawImage 可以将图像文件写入画布,做法是读取图片后,使用drawImage()
方法将这张图片放上画布。
CanvasRenderingContext2D.drawImage()
有三种使用格式。
ctx.drawImage(image, dx, dy);
ctx.drawImage(image, dx, dy, dWidth, dHeight);
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
复制代码
参数的含义如下:
- image:图像元素,具体类型参考文档:developer.mozilla.org/zh-CN/docs/…
- sx:图像内部的横坐标,用于映射到画布的放置点上。
- sy:图像内部的纵坐标,用于映射到画布的放置点上。
- sWidth:图像在画布上的宽度,会产生缩放效果。如果未指定,则图像不会缩放,按照实际大小占据画布的宽度。
- sHeight:图像在画布上的高度,会产生缩放效果。如果未指定,则图像不会缩放,按照实际大小占据画布的高度。
- dx:画布内部的横坐标,用于放置图像的左上角
- dy:画布内部的纵坐标,用于放置图像的右上角
- dWidth:图像在画布内部的宽度,会产生缩放效果。
- dHeight:图像在画布内部的高度,会产生缩放效果。
下面是最简单的使用场景,将图像放在画布上,两者左上角对齐。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var img = new Image();
img.src = 'image.png';
img.onload = function () {
ctx.drawImage(img, 0, 0);
};
复制代码
上面代码将一个 PNG 图像放入画布。这时,图像将是原始大小,如果画布小于图像,就会只显示出图像左上角,正好等于画布大小的那一块。
如果要显示完整的图片,可以用图像的宽和高,设置成画布的宽和高。
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var image = new Image(60, 45);
image.onload = drawImageActualSize;
image.src = 'https://example.com/image.jpg';
function drawImageActualSize() {
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0, 0, this.naturalWidth, this.naturalHeight);
}
复制代码
上面代码中,<canvas>
元素的大小设置成图像的本来大小,就能保证完整展示图像。由于图像的本来大小,只有图像加载成功以后才能拿到,因此调整画布的大小,必须放在image.onload
这个监听函数里面。
此节摘取自阮一峰大神的文档:wangdoc.com/webapi/canv…
1.2 其他
其余vue、electron的知识点请自行学习
2 如何实现
2.1 截屏效果展示
截屏完成后可以将 截屏的图片保存到本地。
2.2 实现思路:
- electron 主进程中的模块 desktopCapturer 配合
navigator.mediaDevices.getUserMedia
API ,可以访问那些用于从桌面上捕获音频和视频的媒体源信息。从而捕获桌面获得媒体流数据 stream
const { remote, desktopCapturer } = require('electron')
// id: 数字-与显示相关联的唯一的标志符
// size <Object>: 屏幕尺寸, 含width、height属性
const { id, size } = remote.screen.getPrimaryDisplay()
const dialog = remote.dialog
const fs = require('fs')
const captureScreen = cb => {
// darwin: 苹果; inux: linux; win32: windows
if (process.platform === 'win32') {
//老坑:desktopCapture => linux下无效
desktopCapturer
.getSources({
// types: 列出要捕获的桌面源类型的字符串数组, 可用类型为 screen 和 window
types: ['screen'],
// thumbnailSize: 媒体源缩略图应缩放到的尺寸大小。 默认是 150 x 150。 当您不需要缩略图时,设置宽度或高度为0。 这将节省用于获取每个窗口和屏幕内容时的处理时间
thumbnailSize: { width: 0, height: 0 },
})
// sources <DesktopCapturerSource[]>: 使用一个 DesktopCapturerSource 对象数组进行解析,每个 DesktopCapturerSource 表示可以捕获的一个屏幕或一个单独的窗口
// source.display_id <String> : 一个由 Screen API 返回的与 Display 的 id 对应匹配的唯一标识符。 在某些平台上,这相当于上面 id 字段中的 XX 部分,其他平台则有所不同。 它在不可用时将会是一个空字符串
.then(async sources => {
for (let source of sources) {
if (parseInt(source.display_id) === id) {
try {
const stream = await navigator.mediaDevices.getUserMedia(
{
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: source.id,
minWidth: size.width,
maxWidth: size.width,
minHeight: size.height,
maxHeight: size.height,
},
},
}
)
// console.log(stream);
handleStream(stream, cb)
} catch (error) {
console.log(error)
}
}
}
})
} else {
//linux
navigator.mediaDevices
.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
// chromeMediaSourceId: source.id, //出现NotReadableError,是因为getPrimaryDisplay()返回的id不一致,不做多屏幕直接去掉就可以了
minWidth: size.width,
maxWidth: size.width,
minHeight: size.height,
maxHeight: size.height,
},
},
})
.then(stream => handleStream(stream, cb))
}
}
...
复制代码
-
将媒体流数据 stream 赋值给 video 的 srcObject 属性从而可以播放媒体文件。
-
显示截图:通过 CanvasRenderingContext2D.drawImage() 方法将视频流图像文件写入到画布中以显示截取的屏幕。
- 下载截图:将canvas元素转化为base 64格式的数据,然后通过 nodejs 中的Buffer 模块转化为 buffer,再通过 electron 中的 dialog.showSaveDialog 及 nodejs 中的 fs.writeFile 下载即可。
...
let screenShootBlob
const handleStream = (stream, cb) => {
let video = document.getElementById('video')
// video.srcObject属性对应的媒体文件资源,可能是MediaStream、MediaSource、Blob或File对象。直接指定这个属性,就可以播放媒体文件
video.srcObject = stream
// 媒体文件元数据加载成功时触发
video.onloadedmetadata = () => {
video.play()
// createSaveImageCanvas(video)
let showScreenShootCanvas = document.getElementById('desktop_canvas')
showScreenShootCanvas.width = size.width
showScreenShootCanvas.height = size.height
showScreenShootCanvas.style.width = size.width + 'px'
showScreenShootCanvas.style.height = size.height + 'px'
const ctx = showScreenShootCanvas.getContext('2d')
// 用于擦除指定矩形区域的像素颜色,等同于把早先的绘制效果都去除
ctx.clearRect(0, 0, size.width, size.height)
//转为bitmap,可以提高性能,降低canvas渲染延迟
createImageBitmap(video).then(bmp => {
ctx.drawImage(
bmp,
0,
0,
size.width,
size.height,
0,
0,
size.width,
size.height
)
// 将 Canvas 数据转为 Data URI 格式的图像
let base64Data = showScreenShootCanvas.toDataURL('image/png')
let data = base64Data.split('base64,')[1]
// 创建包含 string 的新 Buffer。 encoding 参数即第二个参数标识将 string 转换为字节时要使用的字符编码,注意 new Buffer(data, 'base64') 已弃用
screenShootBlob = Buffer.from(data, 'base64')
// 1080,558 是截图后 对话框显示图片区域的宽高
ctx.drawImage(bmp, 0, 0, size.width, size.height, 0, 0, 1080, 558)
stream.getTracks()[0].stop() //关闭视频流,序号是反向的,此处只有一个所以是0
cb && cb()
})
}
}
const saveScreenShoot = () => {
dialog
.showSaveDialog({
title: '保存图片',
defaultPath: `${+new Date()}.png`,
filters: [ { name: 'Images', extensions: ['jpg', 'png'] },],
})
.then(res => {
// console.log(res, screenShootBlob)
if (res.filePath) {
fs.writeFile(res.filePath, screenShootBlob, 'binary', err => {
if (err) {
console.log(err)
} else {
console.log('保存成功')
}
})
}
})
}
export { captureScreen, saveScreenShoot }
复制代码
- xxx.vue 组件中的使用
...
<template>
<div >
<!-- 截屏弹框 -->
<div class="dialog_mask" v-show="isShowMask"></div>
<div class="dialog_mask_start" v-show="isShowMaskStart"></div>
<div class="dialog_screen_shoot" v-show="isShowScreenShoot">
<div class="title">快照预览</div>
<div class="screen_shoot_wrapper">
<!-- <canvas class="canvas" id="save_image_canvas">
您的浏览器不支持 Canvas
</canvas> -->
<canvas class="canvas" id="desktop_canvas">
您的浏览器不支持 Canvas
</canvas>
</div>
<div class="footer">
<el-button
class="custom_button"
type="primary"
@click="handleSaveClick"
>保存</el-button
>
<el-button
class="custom_button"
type="primary"
@click="cancelScreenshoot"
>取消</el-button
>
</div>
</div>
<video id="video"></video>
</div>
</template>
复制代码
- 组件中的 js 代码
...
import { captureScreen, saveScreenShoot } from '@/utils/captureScreen.js'
...
// 截取屏幕代码:
this.isShowMaskStart = true
// 会先捕获屏幕,再执行动画
captureScreen(() => {
this.isShowMaskStart = true
setTimeout(() => {
// console.log(mask);
let mask = document.querySelector('.dialog_mask_start')
mask.classList.add('screen_shoot_last')
// 动画结束后移除动画遮罩,显示截图后的 dialog
mask.addEventListener('transitionend', () => {
// console.log(this.isShowMaskStart);
this.isShowMaskStart = false
mask.classList.remove('screen_shoot_last')
this.isShowScreenShoot = true
})
}, 0)
})
// 保存图片代码:
调用 saveScreenShoot() 即可
复制代码
- css 代码
// 截屏遮罩层
.dialog_mask {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 777;
}
// 截屏动画开始的样式
.dialog_mask_start {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
z-index: 888;
background-color: rgba(255, 255, 255, 0.1);
transition: width 0.5s, height 0.5s;
transform: translate3d(0, 0, 0);
// 截屏动画结束的样式
&.screen_shoot_last {
border-radius: 10px;
width: 1080px;
height: 668px;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}
}
.dialog_screen_shoot {
z-index: 999;
position: absolute;
width: 1080px;
height: 668px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border-radius: 10px;
overflow: hidden;
.title {
width: 100%;
height: 40px;
background-color: $white;
display: flex;
align-items: center;
justify-content: center;
}
.screen_shoot_wrapper {
width: 100%;
height: 558px;
background-color: $black;
overflow: hidden;
position: relative;
#save_image_canvas {
position: absolute;
z-index: -999;
}
}
.footer {
height: 70px;
background: #eee;
padding: 0 30px;
display: flex;
align-items: center;
justify-content: flex-end;
}
}
#video {
position: absolute;
top: 100%;
}
复制代码