Google 的“报告错误”或“反馈工具”可让您选择浏览器窗口的一个区域来创建屏幕截图,并提交您对错误的反馈。
https://i.stack.imgur.com/CDhEi.png
他们是怎么做到的? Google 的 JavaScript 反馈 API 从 here 加载,their overview of the feedback module 将演示屏幕截图功能。
JavaScript 可以读取 DOM 并使用 canvas
呈现相当准确的表示。我一直在研究将 HTML 转换为画布图像的脚本。今天决定将其实施为发送您所描述的反馈。
该脚本允许您创建反馈表单,其中包括在客户端浏览器上创建的屏幕截图以及表单。屏幕截图基于 DOM,因此可能不是 100% 准确到真实表示,因为它不会制作实际的屏幕截图,而是根据页面上可用的信息构建屏幕截图。
它不需要来自服务器的任何渲染,因为整个图像是在客户端的浏览器上创建的。 HTML2Canvas 脚本本身仍处于非常实验性的状态,因为它几乎无法解析我希望它解析的 CSS3 属性,即使代理可用,它也不支持加载 CORS 图像。
仍然相当有限的浏览器兼容性(不是因为无法支持更多,只是没有时间让它更多地支持跨浏览器)。
有关更多信息,请查看此处的示例:
http://hertzen.com/experiments/jsfeedback/
edit html2canvas 脚本现在单独提供here 和一些examples here。
edit 2 另一个确认 Google 使用了非常相似的方法(事实上,根据文档,唯一的主要区别是它们的异步遍历/绘图方法)可以在 Elliott Sprehn 的演示文稿中找到Google 反馈小组:http://www.elliottsprehn.com/preso/fluentconf/
您的网络应用程序现在可以使用 getUserMedia()
对客户端的整个桌面进行“原生”屏幕截图:
看看这个例子:
https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/
客户端必须使用 chrome(目前),并且需要在 chrome://flags 下启用屏幕捕获支持。
Navigator.getUserMedia()
已被弃用,但在其下方显示“...请使用较新的 navigator.mediaDevices.getUserMedia()”,即它只是被较新的 API 取代。
概念验证
作为 Niklas mentioned,您可以使用 html2canvas 库在浏览器中使用 JS 截取屏幕截图。在这一点上,我将通过提供一个使用此库截取屏幕截图的示例(“概念证明”)来扩展他的答案:
函数报告(){让区域=文档.querySelector(“body”); // 全屏 html2canvas(region, { onrendered: function(canvas) { let pngUrl = canvas.toDataURL(); // dataURL 格式的 png let img = document.querySelector(".screen"); img.src = pngUrl; // 在这里你可以允许用户设置 bug-region // 并使用 'pngUrl' 发送到服务器 }, }); } .container { 边距顶部:10px;边框:纯色 1px 黑色; }
在 onrendered
中的 report()
函数中,将图像作为数据 URI 后,您可以将其显示给用户,并允许他通过鼠标绘制“bug 区域”,然后将屏幕截图和区域坐标发送到服务器。
在 this example 中制作了 async/await
版本:具有很好的 makeScreenshot()
功能.
更新
简单示例,可让您截屏、选择区域、描述错误和发送 POST 请求 (here jsfiddle)(主要功能是 report()
)。
异步函数报告(){让截图=等待makeScreenshot(); // png dataUrl let img = q(".screen"); img.src = 截图;让 c = q(".bug-container"); c.classList.remove('hide') let box = await getBox(); c.classList.add('隐藏');发送(截图,框); // 带有错误图像、区域和描述的 sed 发布请求 alert('要查看带有图像的 POST 请求,请转到:chrome 控制台 > 网络选项卡'); } // ----- 辅助函数 let q = s => document.querySelector(s); // 查询选择器助手 window.report = report; // 绑定报告在小提琴中可见 html async function makeScreenshot(selector="body") { return new Promise((resolve, reject) => { let node = document.querySelector(selector); html2canvas(node, { onrendered: ( canvas) => { 让 pngUrl = canvas.toDataURL(); resolve(pngUrl); }}); }); } async function getBox(box) { return new Promise((resolve, reject) => { let b = q(".bug"); let r = q(".region"); let scr = q(".screen "); 让 send = q(".send"); 让 start=0; 让 sx,sy,ex,ey=-1; r.style.width=0; r.style.height=0; 让 drawBox= () => { r.style.left = (ex > 0 ? sx : sx+ex ) +'px'; r.style.top = (ey > 0 ? sy : sy+ey) +'px'; r .style.width = Math.abs(ex) +'px'; r.style.height = Math.abs(ey) +'px'; } //console.log({b,r, scr}); b .addEventListener("click", e=>{ if(start==0) { sx=e.pageX; sy=e.pageY; ex=0; ey=0; drawBox(); } start=(start+1 )%3; }); b.addEventListener("mousemove", e=>{ //console.log(e) if(start==1) { ex=e.pageX-sx; ey=e.pageY-sy drawBox(); } }); send.addEventListener("click", e=>{ start=0; let a=100/75 //缩小img 75% resolve({ x:Math.floor(((ex > 0 ? sx : sx+ex )-scr.offsetLeft)*a), y:Math.floor(((ey > 0 ? sy : sy+ey )-b.offsetTop)*a), width:Math.floor( Math.abs(ex)*a), height:Math.floor(Math.abs(ex)*a), desc: q('.bug-desc').value }); }); }); } function send(image,box) { let formData = new FormData();让 req = new XMLHttpRequest(); formData.append("box", JSON.stringify(box)); formData.append("截图", image); req.open("POST", '/upload/screenshot'); req.send(formData); } .bug-container { 背景:rgb(255,0,0,0.1);边距顶部:20px;文本对齐:居中; } .send { 边框半径:5px;填充:10px;背景:绿色;光标:指针; } .region { 位置:绝对;背景:rgba(255,0,0,0.4); } .example { 高度:100px;背景:黄色; } .bug { 边距顶部:10px;光标:十字准线; } .hide { 显示:无; } .screen { 指针事件:无 }
< div>Screenshot tester
使用 getDisplayMedia API 以 Canvas 或 Jpeg Blob / ArrayBuffer 形式获取屏幕截图:
FIX 1:仅将 getUserMedia 与 chromeMediaSource 用于 Electron.js
FIX 2:抛出错误,而不是返回 null 对象
FIX 3:修复演示以防止错误:getDisplayMedia must be called from a user gesture handler
// docs: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
// see: https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/#20893521368186473
// see: https://github.com/muaz-khan/WebRTC-Experiment/blob/master/Pluginfree-Screen-Sharing/conference.js
function getDisplayMedia(options) {
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
return navigator.mediaDevices.getDisplayMedia(options)
}
if (navigator.getDisplayMedia) {
return navigator.getDisplayMedia(options)
}
if (navigator.webkitGetDisplayMedia) {
return navigator.webkitGetDisplayMedia(options)
}
if (navigator.mozGetDisplayMedia) {
return navigator.mozGetDisplayMedia(options)
}
throw new Error('getDisplayMedia is not defined')
}
function getUserMedia(options) {
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return navigator.mediaDevices.getUserMedia(options)
}
if (navigator.getUserMedia) {
return navigator.getUserMedia(options)
}
if (navigator.webkitGetUserMedia) {
return navigator.webkitGetUserMedia(options)
}
if (navigator.mozGetUserMedia) {
return navigator.mozGetUserMedia(options)
}
throw new Error('getUserMedia is not defined')
}
async function takeScreenshotStream() {
// see: https://developer.mozilla.org/en-US/docs/Web/API/Window/screen
const width = screen.width * (window.devicePixelRatio || 1)
const height = screen.height * (window.devicePixelRatio || 1)
const errors = []
let stream
try {
stream = await getDisplayMedia({
audio: false,
// see: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/video
video: {
width,
height,
frameRate: 1,
},
})
} catch (ex) {
errors.push(ex)
}
// for electron js
if (navigator.userAgent.indexOf('Electron') >= 0) {
try {
stream = await getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
// chromeMediaSourceId: source.id,
minWidth : width,
maxWidth : width,
minHeight : height,
maxHeight : height,
},
},
})
} catch (ex) {
errors.push(ex)
}
}
if (errors.length) {
console.debug(...errors)
if (!stream) {
throw errors[errors.length - 1]
}
}
return stream
}
async function takeScreenshotCanvas() {
const stream = await takeScreenshotStream()
// from: https://stackoverflow.com/a/57665309/5221762
const video = document.createElement('video')
const result = await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
video.play()
video.pause()
// from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const context = canvas.getContext('2d')
// see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
resolve(canvas)
}
video.srcObject = stream
})
stream.getTracks().forEach(function (track) {
track.stop()
})
if (result == null) {
throw new Error('Cannot take canvas screenshot')
}
return result
}
// from: https://stackoverflow.com/a/46182044/5221762
function getJpegBlob(canvas) {
return new Promise((resolve, reject) => {
// docs: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.95)
})
}
async function getJpegBytes(canvas) {
const blob = await getJpegBlob(canvas)
return new Promise((resolve, reject) => {
const fileReader = new FileReader()
fileReader.addEventListener('loadend', function () {
if (this.error) {
reject(this.error)
return
}
resolve(this.result)
})
fileReader.readAsArrayBuffer(blob)
})
}
async function takeScreenshotJpegBlob() {
const canvas = await takeScreenshotCanvas()
return getJpegBlob(canvas)
}
async function takeScreenshotJpegBytes() {
const canvas = await takeScreenshotCanvas()
return getJpegBytes(canvas)
}
function blobToCanvas(blob, maxWidth, maxHeight) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const scale = Math.min(
1,
maxWidth ? maxWidth / img.width : 1,
maxHeight ? maxHeight / img.height : 1,
)
canvas.width = img.width * scale
canvas.height = img.height * scale
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height)
resolve(canvas)
}
img.onerror = () => {
reject(new Error('Error load blob to Image'))
}
img.src = URL.createObjectURL(blob)
})
}
演示:
document.body.onclick = async () => {
// take the screenshot
var screenshotJpegBlob = await takeScreenshotJpegBlob()
// show preview with max size 300 x 300 px
var previewCanvas = await blobToCanvas(screenshotJpegBlob, 300, 300)
previewCanvas.style.position = 'fixed'
document.body.appendChild(previewCanvas)
// send it to the server
var formdata = new FormData()
formdata.append("screenshot", screenshotJpegBlob)
await fetch('https://your-web-site.com/', {
method: 'POST',
body: formdata,
'Content-Type' : "multipart/form-data",
})
}
// and click on the page
这是一个完整的屏幕截图示例,适用于 2021 年的 chrome。最终结果是一个可以传输的 blob。流程为:请求媒体>抓帧>画到画布>转移到blob。如果您想提高内存效率,请探索 OffscreenCanvas 或可能 ImageBitmapRenderingContext
https://jsfiddle.net/v24hyd3q/1/
// Request media
navigator.mediaDevices.getDisplayMedia().then(stream =>
{
// Grab frame from stream
let track = stream.getVideoTracks()[0];
let capture = new ImageCapture(track);
capture.grabFrame().then(bitmap =>
{
// Stop sharing
track.stop();
// Draw the bitmap to canvas
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
// Grab blob from canvas
canvas.toBlob(blob => {
// Do things with blob here
console.log('output blob:', blob);
});
});
})
.catch(e => console.log(e));
下面是一个使用示例:getDisplayMedia
document.body.innerHTML = '<video style="width: 100%; height: 100%; border: 1px black solid;"/>';
navigator.mediaDevices.getDisplayMedia()
.then( mediaStream => {
const video = document.querySelector('video');
video.srcObject = mediaStream;
video.onloadedmetadata = e => {
video.play();
video.pause();
};
})
.catch( err => console.log(`${err.name}: ${err.message}`));
Screen Capture API 文档也值得一试。
你可以试试我的新 JS 库:screenshot.js。
它可以拍摄真实的截图。
您加载脚本:
<script src="https://raw.githubusercontent.com/amiad/screenshot.js/master/screenshot.js"></script>
并截图:
new Screenshot({success: img => {
// callback function
myimage = img;
}});
您可以在项目页面中阅读更多选项。