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

IT技术 javascript html canvas screenshot html2canvas
2021-01-17 20:15:46

Google 的“报告错误”或“反馈工具”可让您选择浏览器窗口的一个区域来创建屏幕截图,并与您对错误的反馈一起提交。

谷歌反馈工具截图 Jason Small 的截图,发布在一个重复的问题中

他们是如何做到这一点的?Google 的 JavaScript 反馈 API 是从这里加载的他们对反馈module的概述将演示屏幕截图功能。

6个回答

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

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

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

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

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

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

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

编辑 2 谷歌反馈团队的 Elliott Sprehn 在这个演示文稿中可以找到谷歌使用非常相似的方法(实际上,根据文档,唯一的主要区别是它们的异步遍历/绘图方法)的另一个确认: http: //www.elliottsprehn.com/preso/fluentconf/

任何使其适用于 SVG 的解决方案都会有很大帮助。它不适用于 highcharts.com
2021-03-24 20:15:46
@Luke Stanley 我很可能会在本周末将源代码扔到 github 上,在那之前我还想做一些小的清理和更改,以及摆脱它目前拥有的不必要的 jQuery 依赖。
2021-03-25 20:15:46
非常酷,Sikuli 或 Selenium 可能适合访问不同的站点,将来自测试工具的站点截图与您的 html2canvas.js 渲染图像在像素相似度方面进行比较!想知道您是否可以使用非常简单的公式求解器自动遍历 DOM 的各个部分,以找到如何为 getBoundingClientRect 不可用的浏览器解析备用数据源。如果它是开源的,我可能会使用它,我正在考虑自己玩弄它。干得好,尼古拉斯!
2021-03-28 20:15:46
源代码现在可以在github.com/niklasvh/html2canvas 获得,在那里使用html2canvas.hertzen.com 的一些脚本示例还有很多错误需要修复,所以我不建议在实时环境中使用该脚本。
2021-04-05 20:15:46
@Niklas 我看到你的例子成长为一个真正的项目。也许更新您对项目的实验性质最受好评的评论。在将近 900 次提交之后,我认为这不仅仅是一次实验;-)
2021-04-07 20:15:46

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

看看这个例子:

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

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

@XMight,您可以通过切换屏幕捕获支持标志来选择是否允许这样做。
2021-03-10 20:15:46
@XMight 请不要这样想。Web 浏览器应该可以做很多事情,但不幸的是它们与它们的实现不一致。绝对没问题,如果浏览器有这样的功能,只要用户被问到。没有您的注意,没有人能够制作屏幕截图。但是太多的恐惧会导致糟糕的实现,比如剪贴板 API,它已经被完全禁用,而是创建确认对话框,比如网络摄像头、麦克风、屏幕截图功能等。
2021-03-30 20:15:46
@AgustinCautinNavigator.getUserMedia()已弃用,但在其下方显示“...请使用较新的navigator.mediaDevices.getUserMedia() ”,即它只是被较新的 API 取代。
2021-03-30 20:15:46
根据developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia,这已被弃用并将从标准中删除
2021-04-02 20:15:46
我找不到任何仅截取屏幕截图的演示——一切都与屏幕共享有关。将不得不尝试。
2021-04-05 20:15:46

概念验证

正如Niklas 提到的,您可以使用html2canvas库在浏览器中使用 JS 截取屏幕截图。在这一点上,我将通过提供一个使用此库(“概念证明”)截取屏幕截图的示例来扩展他的回答:

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

本示例中 async/await,制作了版本:具有很好的makeScreenshot()功能

更新

简单的例子,它允许你截屏、选择区域、描述错误和发送 POST 请求(这里是 jsfiddle)(主要功能是report())。

限制 脚本使用的所有图像都需要驻留在同一来源下,以便它能够在没有代理帮助的情况下读取它们。同样,如果页面上有其他画布元素,这些元素已被跨源内容污染,它们将变脏并且 html2canvas 不再可读。
2021-03-20 20:15:46
如果您不想捕捉后期处理效果(作为模糊滤镜),那也很好。
2021-03-24 20:15:46
我认为您被否决的原因很可能是 html2canvas 库是他的库,而不是他简单指出的工具。
2021-03-26 20:15:46

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

修复 1:使用 getUserMedia 和 chromeMediaSource 仅用于 Electron.js
修复 2:抛出错误而不是返回空对象
修复 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
我喜欢这个答案,但不幸的是,它在屏幕截图中包含“选择要共享的屏幕”对话框,并且覆盖了屏幕的很大一部分。至少对我来说使用 Chrome。
2021-03-14 20:15:46
这是在 Firefox 中拍下 mysef 的照片...只是不授予使用凸轮的权限:)
2021-03-30 20:15:46
请问它是如何工作的?你能为像我这样的新手提供一个演示吗?谢谢
2021-03-31 20:15:46
@kabrice 我添加了一个演示。只需将代码放在 Chrome 控制台中即可。如果您需要旧浏览器支持,请使用:babeljs.io/en/repl
2021-04-06 20:15:46
想知道为什么这只有 1 个赞成票,事实证明这真的很有帮助!
2021-04-07 20:15:46

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