Sử dụng HTML5 / Canvas / JavaScript để chụp ảnh màn hình trong trình duyệt


924

"Báo cáo lỗi" hoặc "Công cụ phản hồi" của Google cho phép bạn chọn một khu vực trong cửa sổ trình duyệt của mình để tạo ảnh chụp màn hình được gửi cùng với phản hồi của bạn về lỗi.

Ảnh chụp màn hình công cụ phản hồi của Google Ảnh chụp màn hình của Jason Small, được đăng trong một câu hỏi trùng lặp .

Làm thế nào họ làm điều này? API phản hồi JavaScript của Google được tải từ đâytổng quan về mô-đun phản hồi của họ sẽ thể hiện khả năng chụp màn hình.


2
Elliott Sprehn đã viết trong một Tweet vài ngày trước:> @CatChen Bài đăng stackoverflow đó không chính xác. Ảnh chụp màn hình của Google Feedback được thực hiện hoàn toàn phía khách hàng. :)
Goran Rakic

1
Điều này hợp lý khi họ muốn nắm bắt chính xác cách trình duyệt của người dùng đang hiển thị một trang, chứ không phải cách họ kết xuất nó ở phía máy chủ bằng cách sử dụng công cụ của họ. Nếu bạn chỉ gửi DOM trang hiện tại đến máy chủ, nó sẽ bỏ lỡ mọi sự không nhất quán trong cách trình duyệt hiển thị HTML. Điều này không có nghĩa là câu trả lời của Chen là sai khi chụp ảnh màn hình, có vẻ như Google đang làm điều đó theo một cách khác.
Goran Rakic

Elliott đã đề cập đến Jan Kuča ngày hôm nay và tôi đã tìm thấy liên kết này trong tweet của Jan: jankuca.tumblr.com/post/7391640769/ Kẻ
Cat Chen

Tôi sẽ nghiên cứu vấn đề này sau và xem cách nó có thể được thực hiện với công cụ kết xuất phía máy khách và kiểm tra xem Google có thực sự làm theo cách đó không.
Mèo Chen

Tôi thấy việc sử dụng so sánhDocumentP vị trí, getBoxObjectFor, toDataURL, drawImage, theo dõi phần đệm và những thứ tương tự. Đó là hàng ngàn dòng mã bị xáo trộn để khử nhiễu và xem qua mặc dù. Tôi rất muốn thấy một phiên bản được cấp phép nguồn mở của nó, tôi đã liên hệ với Elliott Sprehn!
Luke Stanley

Câu trả lời:


1154

JavaScript có thể đọc DOM và hiển thị một cách thể hiện khá chính xác bằng cách sử dụng canvas. Tôi đã làm việc với một kịch bản chuyển đổi HTML thành một hình ảnh canvas. Hôm nay quyết định thực hiện nó để gửi phản hồi như bạn mô tả.

Tập lệnh cho phép bạn tạo các biểu mẫu phản hồi bao gồm ảnh chụp màn hình, được tạo trên trình duyệt của khách hàng, cùng với biểu mẫu. Ảnh chụp màn hình dựa trên DOM và như vậy có thể không chính xác 100% với đại diện thực vì nó không tạo ảnh chụp màn hình thực tế, nhưng xây dựng ảnh chụp màn hình dựa trên thông tin có sẵn trên trang.

không yêu cầu bất kỳ kết xuất nào từ máy chủ , vì toàn bộ hình ảnh được tạo trên trình duyệt của máy khách. Bản thân tập lệnh HTML2Canvas vẫn ở trạng thái thử nghiệm, vì nó không phân tích gần như nhiều thuộc tính CSS3 mà tôi muốn, cũng như không có bất kỳ hỗ trợ nào để tải hình ảnh CORS ngay cả khi có proxy.

Khả năng tương thích trình duyệt vẫn còn khá hạn chế (không phải vì không thể hỗ trợ nhiều hơn, chỉ là không có thời gian để làm cho trình duyệt chéo được hỗ trợ nhiều hơn).

Để biết thêm thông tin, hãy xem các ví dụ ở đây:

http://hertzen.com/experiment/jsfeedback/

chỉnh sửa Tập lệnh html2canvas hiện có sẵn riêng ở đây và một số ví dụ ở đây .

chỉnh sửa 2 Một xác nhận khác rằng Google sử dụng một phương pháp rất giống nhau (trên thực tế, dựa trên tài liệu, sự khác biệt lớn duy nhất là phương pháp di chuyển / vẽ không đồng bộ của họ) có thể được tìm thấy trong bản trình bày này bởi Elliott Sprehn từ nhóm Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/


1
Rất tuyệt, Sikuli hoặc Selenium có thể tốt để truy cập các trang web khác nhau, so sánh ảnh chụp của trang web từ công cụ kiểm tra với hình ảnh hiển thị html2canvas.js của bạn về độ tương tự pixel! Tự hỏi nếu bạn có thể tự động duyệt qua các phần của DOM bằng trình giải công thức rất đơn giản để tìm cách phân tích các nguồn dữ liệu thay thế cho các trình duyệt nơi getBoundingClientRect không khả dụng. Có lẽ tôi sẽ sử dụng cái này nếu nó là nguồn mở, đang xem xét việc tự chơi với nó. Niklas làm tốt lắm!
Luke Stanley

1
@Luke Stanley Tôi rất có thể sẽ tung nguồn lên github vào cuối tuần này, vẫn còn một số thay đổi nhỏ và thay đổi tôi muốn làm trước đó, cũng như loại bỏ sự phụ thuộc jQuery không cần thiết hiện có.
Niklas

43
Mã nguồn hiện có sẵn tại github.com/niklasvh/html2canvas , một số ví dụ về tập lệnh sử dụng html2canvas.hertzen.com ở đó. Vẫn còn nhiều lỗi cần khắc phục, vì vậy tôi không khuyên bạn nên sử dụng tập lệnh trong môi trường trực tiếp.
Niklas

2
bất kỳ giải pháp nào để làm cho nó hoạt động cho SVG sẽ là một trợ giúp tuyệt vời. Nó không hoạt động với highcharts.com
Jagdeep

3
@Niklas Tôi thấy ví dụ của bạn phát triển thành một dự án thực sự. Có thể cập nhật nhận xét được đánh giá cao nhất của bạn về bản chất thử nghiệm của dự án. Sau gần 900 lần cam kết, tôi sẽ nghĩ nó nhiều hơn một chút so với một thử nghiệm vào thời điểm này ;-)
Jogai

70

Ứng dụng web của bạn giờ đây có thể chụp ảnh màn hình 'gốc' của toàn bộ máy tính để bàn của khách hàng bằng cách sử dụng getUserMedia():

Hãy xem ví dụ này:

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

Máy khách sẽ phải sử dụng chrome (hiện tại) và sẽ cần bật hỗ trợ chụp màn hình dưới chrome: // flags.


2
Tôi không thể tìm thấy bất kỳ bản demo nào chỉ bằng cách chụp ảnh màn hình - tất cả mọi thứ là về chia sẻ màn hình. Sẽ phải thử nó.
JWL

8
@XMight, bạn có thể chọn có cho phép điều này hay không bằng cách bật cờ hỗ trợ chụp màn hình.
Matt Sinclair

19
@XMight Xin đừng nghĩ như thế này. Các trình duyệt web sẽ có thể làm rất nhiều thứ, nhưng thật không may, chúng không phù hợp với việc triển khai của chúng. Hoàn toàn ổn, nếu một trình duyệt có chức năng như vậy, miễn là người dùng được hỏi. Không ai có thể tạo một ảnh chụp màn hình mà không có sự chú ý của bạn. Nhưng quá nhiều nỗi sợ dẫn đến việc triển khai kém, như API clipboard, đã bị vô hiệu hóa hoàn toàn, thay vào đó tạo các hộp thoại xác nhận, như cho webcam, mics, khả năng chụp màn hình, v.v.
StanE

3
Điều này không được chấp nhận và sẽ bị xóa khỏi tiêu chuẩn theo developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin 16/2/18

7
@AgustinCautin Navigator.getUserMedia()không được dùng nữa, nhưng ngay bên dưới dòng chữ "... Vui lòng sử dụng navigator.mediaDevices.getUserMedia () ", tức là nó chỉ được thay thế bằng API mới hơn.
levant đâm vào

37

Như Niklas đã đề cập, bạn có thể sử dụng thư viện html2canvas để chụp ảnh màn hình bằng cách sử dụng JS trong trình duyệt. Tôi sẽ mở rộng câu trả lời của anh ấy trong điểm này bằng cách cung cấp một ví dụ về chụp ảnh màn hình bằng thư viện này:

Trong report()chức năng onrenderedsau khi lấy hình ảnh dưới dạng URI dữ liệu, bạn có thể hiển thị nó cho người dùng và cho phép anh ta vẽ "vùng lỗi" bằng chuột và sau đó gửi ảnh chụp màn hình và tọa độ vùng tới máy chủ.

Trong ví dụ này async/await phiên bản đã được thực hiện: với makeScreenshot()chức năng tốt đẹp .

CẬP NHẬT

Ví dụ đơn giản cho phép bạn chụp ảnh màn hình, chọn vùng, mô tả lỗi và gửi yêu cầu POST ( ở đây là jsfiddle ) (chức năng chính là report()).


10
Nếu bạn muốn cho điểm trừ, hãy để lại nhận xét với lời giải thích
Kamil Kiełczewski

Tôi nghĩ lý do tại sao bạn bị hạ cấp rất có thể là thư viện html2canvas là thư viện của anh ấy, không phải là một công cụ mà anh ấy chỉ ra.
zfrisch

Sẽ tốt thôi nếu bạn không muốn chụp các hiệu ứng xử lý hậu kỳ (dưới dạng bộ lọc mờ).
vintproykt

Hạn chế Tất cả các hình ảnh mà tập lệnh sử dụng cần nằm trong cùng một nguồn gốc để có thể đọc chúng mà không cần sự hỗ trợ của proxy. Tương tự, nếu bạn có các thành phần canvas khác trên trang đã bị làm mờ với nội dung có nguồn gốc chéo, chúng sẽ trở nên bẩn và không thể đọc được bởi html2canvas.
aravind3

13

Nhận ảnh chụp màn hình dưới dạng Canvas hoặc Jpeg Blob / ArrayBuffer bằng API 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)
    }

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

BẢN GIỚI THIỆU:

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

Tự hỏi tại sao điều này chỉ có 1 upvote, điều này tỏ ra thực sự hữu ích!
Jay Dadhania

Xin vui lòng làm thế nào nó hoạt động? Bạn có thể cung cấp một bản demo cho những người mới như tôi không? Thx
kabrice

@kabrice Mình thêm bản demo. Chỉ cần đặt mã trong bảng điều khiển Chrome. Nếu bạn cần hỗ trợ trình duyệt cũ, hãy sử dụng: babeljs.io/en/repl
Nikolay Makhonin

8

Đây là một ví dụ sử dụng: 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}`));

Cũng đáng kiểm tra là các tài liệu API chụp màn hình .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.