谷歌的“报告错误”或“反馈工具”让你选择浏览器窗口的一个区域来创建一个截图,并提交你对错误的反馈。

截图来自Jason Small,发布在一个重复的问题中。

他们是怎么做到的?谷歌的JavaScript反馈API从这里加载,反馈模块的概述将演示截图功能。


当前回答

使用getDisplayMedia API获取截图作为画布或Jpeg Blob / ArrayBuffer:

修复1:使用getUserMedia与chromeMediaSource仅为电子。js 修复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

其他回答

PoC

正如Niklas所提到的,你可以使用html2canvas库在浏览器中使用JS进行截图。我将通过提供一个使用这个库截屏的例子(“概念证明”)来扩展他的回答:

function report() { let region = document.querySelector("body"); // whole screen html2canvas(region, { onrendered: function(canvas) { let pngUrl = canvas.toDataURL(); // png in dataURL format let img = document.querySelector(".screen"); img.src = pngUrl; // here you can allow user to set bug-region // and send it with 'pngUrl' to server }, }); } .container { margin-top: 10px; border: solid 1px black; } <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script> <div>Screenshot tester</div> <button onclick="report()">Take screenshot</button> <div class="container"> <img width="75%" class="screen"> </div>

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

在这个例子中,async/await版本使用了很好的makeScreenshot()函数。

更新

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

async function report() { let screenshot = await makeScreenshot(); // png dataUrl let img = q(".screen"); img.src = screenshot; let c = q(".bug-container"); c.classList.remove('hide') let box = await getBox(); c.classList.add('hide'); send(screenshot,box); // sed post request with bug image, region and description alert('To see POST requset with image go to: chrome console > network tab'); } // ----- Helper functions let q = s => document.querySelector(s); // query selector helper window.report = report; // bind report be visible in fiddle html async function makeScreenshot(selector="body") { return new Promise((resolve, reject) => { let node = document.querySelector(selector); html2canvas(node, { onrendered: (canvas) => { let 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"); let send = q(".send"); let start=0; let sx,sy,ex,ey=-1; r.style.width=0; r.style.height=0; let 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 //zoom out 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(); let req = new XMLHttpRequest(); formData.append("box", JSON.stringify(box)); formData.append("screenshot", image); req.open("POST", '/upload/screenshot'); req.send(formData); } .bug-container { background: rgb(255,0,0,0.1); margin-top:20px; text-align: center; } .send { border-radius:5px; padding:10px; background: green; cursor: pointer; } .region { position: absolute; background: rgba(255,0,0,0.4); } .example { height: 100px; background: yellow; } .bug { margin-top: 10px; cursor: crosshair; } .hide { display: none; } .screen { pointer-events: none } <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.min.js"></script> <body> <div>Screenshot tester</div> <button onclick="report()">Report bug</button> <div class="example">Lorem ipsum</div> <div class="bug-container hide"> <div>Select bug region: click once - move mouse - click again</div> <div class="bug"> <img width="75%" class="screen" > <div class="region"></div> </div> <div> <textarea class="bug-desc">Describe bug here...</textarea> </div> <div class="send">SEND BUG</div> </div> </body>

这是一个完整的截图示例,可以在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));

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

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

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

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

想了解更多信息,请看下面的例子:

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

编辑 html2canvas脚本现在可以分别在这里和一些例子。

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

使用getDisplayMedia API获取截图作为画布或Jpeg Blob / ArrayBuffer:

修复1:使用getUserMedia与chromeMediaSource仅为电子。js 修复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

这里有一个使用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文档。