使用HTML5 / Canvas / JavaScript拍摄浏览器内屏幕截图


923

使用Google的“报告错误”或“反馈工具”,您可以选择浏览器窗口的区域来创建屏幕截图,并在屏幕上提交有关错误的反馈。

Google反馈工具截图 Jason Small的屏幕截图,张贴在一个重复的问题中

他们是如何做到的?Google的JavaScript反馈API从此处加载,其反馈模块概述将演示屏幕截图功能。


2
Elliott Sprehn几天前在推文中写道:> @CatChen该stackoverflow帖子不准确。Google反馈的屏幕截图完全是在客户端完成的。:)
Goran Rakic 2011年

这是合乎逻辑的,因为他们想准确捕获用户浏览器如何呈现页面,而不是他们如何使用引擎在服务器端呈现页面。如果仅将当前页面DOM发送到服务器,它将丢失浏览器呈现HTML方式的任何不一致之处。这并不意味着Chen的答案对于截取屏幕截图是错误的,只是看起来Google采取了不同的方式。
Goran Rakic 2011年

Elliott今天提到了JanKuča,我在Jan的推文中找到此链接:jankuca.tumblr.com/post/7391640769/…–
Cat Chen

稍后,我将对此进行深入研究,看看如何使用客户端渲染引擎来完成它,并检查Google是否真的以这种方式做到了。
Cat Chen

我看到使用compareDocumentPosition,getBoxObjectFor,toDataURL,drawImage,跟踪填充等。这是成千上万行经过混淆处理的代码,它们可以消除混淆并浏览。我希望看到它的开源许可版本,我已经联系了Elliott Sprehn!
卢克·斯坦利

Answers:


1152

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

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

不需要来自服务器的任何渲染,因为整个图像是在客户端的浏览器上创建的。HTML2Canvas脚本本身仍处于试验性状态,因为它无法解析我想要的几乎所有CSS3属性,即使有可用的代理,它也不支持加载CORS图像。

仍然与浏览器的兼容性非常有限(不是因为无法支持更多功能,只是没有时间使其更受跨浏览器支持)。

有关更多信息,请在此处查看示例:

http://hertzen.com/experiments/jsfeedback/

编辑 html2canvas脚本现在可以在此处单独使用,在此处可以使用一些示例

编辑2 Google反馈小组的Elliott Sprehn在此演示文稿中可以找到另一个证实Google使用了非常相似的方法(事实上,根据文档,唯一的不同是它们的遍历/绘制异步方法): http: //www.elliottsprehn.com/preso/fluentconf/


1
非常酷,Sikuli或Selenium可能适合去不同的站点,将站点从测试工具到html2canvas.js渲染图像的快照按像素相似度进行比较!想知道您是否可以使用非常简单的公式求解器自动遍历DOM的各个部分,以找到如何为getBoundingClientRect不可用的浏览器解析备用数据源。如果它是开源的,我可能会使用它,而我自己也在考虑使用它。尼克拉斯,工作不错!
卢克·斯坦利

1
@Luke Stanley我很可能会在本周末在github上发布源代码,但在此之前我还想做一些小小的清理和更改,并摆脱它目前具有的不必要的jQuery依赖关系。
尼克拉斯,

43
现在可以在github.com/niklasvh/html2canvas上找到源代码,那里有一些使用html2canvas.hertzen.com的脚本示例。仍然有很多错误需要修复,因此我不建议您在实时环境中使用该脚本。
尼古拉斯

2
任何使其适用于SVG的解决方案都会有很大的帮助。它不适用于highcharts.com
Jagdeep

3
@Niklas我看到您的示例已发展成为一个真正的项目。也许更新有关项目实验性质的最受好评的评论。在进行了将近900次提交之后,我认为这不仅仅只是一个实验;-)
Jogai

70

您的网络应用现在可以使用getUserMedia()以下命令获取客户端整个桌面的“本地”屏幕截图:

看一下这个例子:

https://www.webrtc-experiment.com/Pluginfree-Screen-Sharing/

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


2
我找不到仅拍摄屏幕截图的演示,所有内容都与屏幕共享有关。将不得不尝试。
jwl

8
@XMight,您可以通过切换屏幕捕获支持标志来选择是否允许此操作。
马特·辛克莱

19
@XMight请不要这样想。Web浏览器应该能够执行很多操作,但是不幸的是,它们与实现方式不一致。只要浏览器具有这样的功能,只要询问用户就可以。没有您的注意,没有人可以制作屏幕截图。但是过多的恐惧会导致不良的实现,例如剪贴板API(已被完全禁用),而是创建了确认对话框,例如网络摄像头,麦克风,屏幕截图功能等
。– StanE


7
@AgustinCautin Navigator.getUserMedia()已过时,但在其下方仅显示“ ...请使用较新的navigator.mediaDevices.getUserMedia() ”,即它已被较新的API取代。
黎凡特

37

Niklas所述,您可以使用html2canvas库在浏览器中使用JS截屏。在这一点上,我将通过使用该库拍摄屏幕截图的示例来扩展他的答案:

在获取图像作为数据URI后的report()功能中onrendered,您可以将其显示给用户,并允许他用鼠标绘制“错误区域”,然后将屏幕截图和区域坐标发送给服务器。

此示例中, async/await版本为:具有良好的makeScreenshot()功能

更新

一个简单的示例,它允许您截屏,选择区域,描述错误并发送POST请求(此处为jsfiddle)(主要功能是report())。


10
如果您想减去一点,请留下评论并
做出

我认为您之所以被低估的原因很可能是html2canvas库是他的库,而不是他简单指出的工具。
zfrisch

如果您不想捕获后处理效果(作为模糊滤镜),那就很好。
vintproykt

局限性脚本使用的所有图像都必须位于同一原点,以使其无需代理即可读取它们。同样,如果您的页面上还有其他画布元素,这些画布元素已被跨域内容污染,则它们将变脏并且无法被html2canvas读取。
aravind3

13

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

// 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)
    }

    try {
        // for electron js
        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)
    }

    return stream
}

async function takeScreenshotCanvas() {
    const stream = await takeScreenshotStream()

    if (!stream) {
        return null
    }

    // 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()
    })

    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()
    if (!canvas) {
        return null
    }
    return getJpegBlob(canvas)
}

async function takeScreenshotJpegBytes() {
    const canvas = await takeScreenshotCanvas()
    if (!canvas) {
        return null
    }
    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)
    })
}

演示:

// 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
let formdata = new FormData()
formdata.append("screenshot", screenshotJpegBlob)
await fetch('https://your-web-site.com/', {
    method: 'POST',
    body: formdata,
    'Content-Type' : "multipart/form-data",
})

不知道为什么只有1个投票,这真的很有帮助!
杰伊·达达尼亚

请如何运作?您可以为像我这样的新手提供演示吗?Thx
kabrice

@kabrice我添加了一个演示。只需将代码放入Chrome控制台即可。如果您需要旧版浏览器支持,请使用:babeljs.io/en/repl
Nikolay Makhonin

7

以下是使用示例: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文档。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.