使用HTML5/Canvas/JavaScript获取浏览器屏幕截图

使用HTML5/Canvas/JavaScript获取浏览器屏幕截图,javascript,html,canvas,screenshot,Javascript,Html,Canvas,Screenshot,谷歌的“报告一个Bug”或“反馈工具”允许你在浏览器窗口中选择一个区域来创建一个屏幕截图,该截图与你对Bug的反馈一起提交 杰森·斯莫尔截图,发布在 他们是怎么做到的?Google的JavaScript反馈API是从加载的,并将演示屏幕截图功能。JavaScript可以读取DOM,并使用画布呈现相当准确的DOM表示。我一直在编写一个脚本,将HTML转换为画布图像。今天决定将其实现为发送您所描述的反馈 该脚本允许您创建反馈表单,其中包括在客户端浏览器上创建的屏幕截图以及表单。屏幕截图基于DOM,

谷歌的“报告一个Bug”或“反馈工具”允许你在浏览器窗口中选择一个区域来创建一个屏幕截图,该截图与你对Bug的反馈一起提交

杰森·斯莫尔截图,发布在


他们是怎么做到的?Google的JavaScript反馈API是从加载的,并将演示屏幕截图功能。

JavaScript可以读取DOM,并使用
画布
呈现相当准确的DOM表示。我一直在编写一个脚本,将HTML转换为画布图像。今天决定将其实现为发送您所描述的反馈

该脚本允许您创建反馈表单,其中包括在客户端浏览器上创建的屏幕截图以及表单。屏幕截图基于DOM,因此可能不会100%精确到真实的表示,因为它不会生成实际的屏幕截图,而是基于页面上可用的信息构建屏幕截图

它不需要从服务器进行任何渲染,因为整个图像是在客户端浏览器上创建的。HTML2Canvas脚本本身仍然处于非常实验性的状态,因为它没有解析我希望它解析的CSS3属性,也没有任何支持来加载CORS图像,即使有代理可用

浏览器兼容性仍然相当有限(不是因为不能支持更多,只是没有时间让它更支持跨浏览器)

有关更多信息,请查看以下示例:

编辑 html2canvas脚本现在可以单独使用,有些还可以

编辑2 谷歌反馈团队Elliott Sprehn在本次演示中还证实了谷歌使用了一种非常类似的方法(事实上,根据文档,唯一的主要区别是他们的异步遍历/绘制方法):

您的web应用现在可以使用
getUserMedia()
获取客户端整个桌面的“本机”屏幕截图:

看看这个例子:

客户端将必须使用chrome(目前),并且需要在下启用屏幕捕获支持chrome://flags.

PoC 您可以使用该库在浏览器中使用JS截图。在这一点上,我将通过提供一个使用此库截图的示例(“概念验证”)来扩展他的答案:

函数报告(){
让region=document.querySelector(“body”);//整个屏幕
html2canvas(区域{
onrendered:函数(画布){
让pngUrl=canvas.toDataURL();//数据URL格式的png
设img=document.querySelector(“.screen”);
img.src=pngUrl;
//在这里,您可以允许用户设置错误区域
//并将其与“pngUrl”一起发送到服务器
},
});
}
.container{
边缘顶部:10px;
边框:实心1px黑色;
}

截图测试器
截图
以下是一个示例:

document.body.innerHTML='';
navigator.mediaDevices.getDisplayMedia()
。然后(mediaStream=>{
const video=document.querySelector('video');
video.srcObject=mediaStream;
video.onloadedmetadata=e=>{
video.play();
video.pause();
};
})
.catch(err=>console.log(`${err.name}:${err.message}`));

同样值得一看的是这些文档。

使用API以画布或Jpeg Blob/ArrayBuffer的形式获取屏幕截图:

修复1:仅对Electron.js使用带有chromeMediaSource的getUserMedia
修复2:抛出错误,而不是返回空对象
修复3:修复演示以防止错误:
必须从用户手势处理程序调用getDisplayMedia

// 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。如果您想更高效地使用内存,请探索


非常酷,Sikuli或Selenium可能适合访问不同的站点,将测试工具中的站点快照与html2canvas.js渲染图像的像素相似性进行比较!想知道您是否可以使用一个非常简单的公式求解器自动遍历DOM的各个部分,以找到如何为getBoundingClientRect不可用的浏览器解析备用数据源。如果它是开源的,我可能会使用它,我自己也在考虑玩弄它。干得好Niklas@Luke Stanley我很可能会在这个周末在github上发布源代码,在此之前我还想做一些小的清理和更改,并消除它目前不必要的jQuery依赖性。源代码现在可以在,一些正在使用的脚本示例中找到。仍然有很多bug需要修复,所以我不建议在实时环境中使用该脚本。任何使其适用于SVG的解决方案都会有很大帮助。它不适用于海图。com@Niklas我看到你的例子变成了一个真正的项目。也许可以更新你对该项目实验性质的最高估评论。近900次提交后,我认为这不仅仅是一次实验;-)Elliott Sprehn几天前:>@CatChen说stackoverflow帖子不准确。Google Feedback的屏幕截图完全是在客户端完成的。:)这是合乎逻辑的,因为他们希望准确地捕捉用户的浏览器呈现页面的方式,而不是在服务器端使用引擎呈现页面的方式。如果只将当前页面DOM发送到服务器,它将丢失浏览器呈现HTML的方式中的任何不一致。这并不意味着陈的回答在截图方面是错误的,只是看起来谷歌正在以不同的方式进行截图。Elliott今天提到了Jan Kuča,我在Jan的推文中找到了这个链接:我稍后会深入研究,看看如何与clien合作
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
// 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));