Menggunakan HTML5 / Canvas / JavaScript untuk mengambil tangkapan layar di dalam browser

924

"Laporkan Bug" atau "Alat Umpan Balik" Google memungkinkan Anda memilih area jendela browser Anda untuk membuat tangkapan layar yang dikirimkan bersama umpan balik Anda tentang bug.

Screenshot Alat Umpan Balik Google Cuplikan layar oleh Jason Small, diposting dalam pertanyaan rangkap .

Bagaimana mereka melakukan ini? API umpan balik Google JavaScript dimuat dari sini dan ikhtisar mereka tentang modul umpan balik akan menunjukkan kemampuan tangkapan layar.

joelvh
sumber
2
Elliott Sprehn menulis dalam Tweet beberapa hari yang lalu:> @CatChen Post stackoverflow itu tidak akurat. Tangkapan layar Google Umpan Balik dilakukan sepenuhnya di sisi klien. :)
Goran Rakic
1
Ini terlihat logis karena mereka ingin mengetahui dengan tepat bagaimana browser pengguna merender halaman, bukan bagaimana mereka akan merendernya di sisi server menggunakan mesin mereka. Jika Anda hanya mengirim DOM halaman saat ini ke server, maka akan kehilangan semua inkonsistensi dalam cara browser merender HTML. Ini tidak berarti jawaban Chen salah untuk mengambil tangkapan layar, sepertinya Google melakukannya dengan cara yang berbeda.
Goran Rakic
Elliott menyebut Jan Kuča hari ini, dan aku menemukan tautan ini di tweet Jan: jankuca.tumblr.com/post/7391640769/...
Cat Chen
Saya akan menggali ini nanti dan melihat bagaimana hal itu dapat dilakukan dengan mesin rendering sisi klien dan memeriksa apakah Google benar-benar melakukannya dengan cara itu.
Cat Chen
Saya melihat penggunaan compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, tracking padding dan hal-hal seperti itu. Ini ribuan baris kode yang dikaburkan untuk menghilangkan kekaburan dan melihat melalui sekalipun. Saya ingin melihat versi berlisensi open source untuk itu, saya telah menghubungi Elliott Sprehn!
Luke Stanley

Jawaban:

1155

JavaScript dapat membaca DOM dan menyajikan representasi yang cukup akurat dari penggunaan itu canvas. Saya telah mengerjakan skrip yang mengubah HTML menjadi gambar kanvas. Memutuskan hari ini untuk mengimplementasikannya dalam mengirimkan umpan balik seperti yang Anda jelaskan.

Skrip memungkinkan Anda untuk membuat formulir umpan balik yang mencakup tangkapan layar, dibuat di browser klien, bersama dengan formulir. Tangkapan layar didasarkan pada DOM dan karena itu mungkin tidak 100% akurat untuk representasi nyata karena tidak membuat tangkapan layar yang sebenarnya, tetapi membuat tangkapan layar berdasarkan informasi yang tersedia di halaman.

Itu tidak memerlukan rendering apa pun dari server , karena keseluruhan gambar dibuat di browser klien. Skrip HTML2Canvas sendiri masih dalam kondisi sangat eksperimental, karena tidak menguraikan hampir semua atribut CSS3 yang saya inginkan, juga tidak memiliki dukungan untuk memuat gambar CORS bahkan jika proxy tersedia.

Kompatibilitas browser masih sangat terbatas (bukan karena lebih banyak tidak dapat didukung, hanya saja belum punya waktu untuk membuatnya lebih didukung browser lintas).

Untuk informasi lebih lanjut, lihat contoh di sini:

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

sunting Skrip html2canvas sekarang tersedia secara terpisah di sini dan beberapa contoh di sini .

sunting 2 Konfirmasi lain bahwa Google menggunakan metode yang sangat mirip (pada kenyataannya, berdasarkan pada dokumentasi, satu-satunya perbedaan utama adalah metode traffing / gambar async mereka) dapat ditemukan dalam presentasi ini oleh Elliott Sprehn dari tim Google Umpan Balik: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
sumber
1
Sangat keren, Sikuli atau Selenium mungkin baik untuk pergi ke situs yang berbeda, membandingkan bidikan situs dari alat pengujian dengan gambar yang dihasilkan html2canvas.js Anda dalam hal kesamaan piksel! Bertanya-tanya apakah Anda dapat secara otomatis melintasi bagian DOM dengan pemecah rumus yang sangat sederhana untuk menemukan cara mem-parsing sumber data alternatif untuk peramban di mana getBoundingClientRect tidak tersedia. Saya mungkin akan menggunakan ini jika itu open source, sedang mempertimbangkan mempermainkannya sendiri. Niklas yang bagus!
Luke Stanley
1
@ Lukas Stanley Saya kemungkinan besar akan membuang sumber di github akhir pekan ini, masih beberapa pembersihan kecil dan perubahan yang ingin saya lakukan sebelum itu, serta menyingkirkan ketergantungan jQuery yang tidak perlu yang saat ini dimilikinya.
Niklas
43
Kode sumber sekarang tersedia di github.com/niklasvh/html2canvas , beberapa contoh skrip yang digunakan html2canvas.hertzen.com di sana. Masih banyak bug untuk diperbaiki, jadi saya tidak akan merekomendasikan menggunakan skrip di lingkungan langsung.
Niklas
2
solusi apa pun untuk membuatnya berfungsi untuk SVG akan sangat membantu. Ini tidak berfungsi dengan highcharts.com
Jagdeep
3
@ Niklas Saya melihat contoh Anda tumbuh menjadi proyek nyata. Mungkin perbarui komentar Anda yang paling banyak dipilih tentang sifat eksperimental dari proyek ini. Setelah hampir 900 komitmen, saya akan berpikir ini lebih dari sekadar eksperimen pada titik ini ;-)
Jogai
70

Aplikasi web Anda sekarang dapat mengambil tangkapan layar 'asli' dari seluruh desktop klien menggunakan getUserMedia():

Lihat contoh ini:

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

Klien harus menggunakan chrome (untuk saat ini) dan harus mengaktifkan dukungan tangkapan layar di bawah chrome: // flags.

Matt Sinclair
sumber
2
saya tidak dapat menemukan demo hanya dengan mengambil tangkapan layar - semuanya tentang berbagi layar. harus mencobanya.
jwl
8
@XMight, Anda dapat memilih apakah akan mengizinkan ini dengan mengibarkan bendera dukungan tangkapan layar.
Matt Sinclair
19
@XMight Tolong jangan berpikir seperti ini. Browser web harus dapat melakukan banyak hal, tetapi sayangnya mereka tidak konsisten dengan implementasinya. Benar-benar ok, jika browser memiliki fungsi seperti itu, selama pengguna ditanyai. Tidak seorang pun akan dapat membuat tangkapan layar tanpa perhatian Anda. Tetapi terlalu banyak rasa takut mengakibatkan implementasi yang buruk, seperti clipboard API, yang telah dinonaktifkan sama sekali, alih-alih membuat dialog konfirmasi, seperti untuk webcam, mikrofon, kemampuan tangkapan layar, dll.
StanE
3
Ini sudah tidak digunakan lagi dan akan dihapus dari standar menurut developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
7
@AgustinCautin Navigator.getUserMedia()sudah usang, tetapi tepat di bawahnya tertulis "... Silakan gunakan navigator yang lebih baru.mediaDevices.getUserMedia () ", yaitu baru saja diganti dengan API yang lebih baru.
levant mengadu
37

Seperti yang disebutkan Niklas, Anda dapat menggunakan pustaka html2canvas untuk mengambil tangkapan layar menggunakan JS di peramban. Saya akan memperluas jawabannya pada poin ini dengan memberikan contoh pengambilan tangkapan layar menggunakan perpustakaan ini:

Dalam report()fungsi dalam onrenderedsetelah mendapatkan citra sebagai data URI Anda dapat menunjukkan kepada pengguna dan memungkinkan dia untuk menarik "bug wilayah" oleh mouse dan kemudian mengirim screenshot dan wilayah koordinat ke server.

Dalam contoh async/await ini dibuat versi: dengan makeScreenshot()fungsi yang bagus .

MEMPERBARUI

Contoh sederhana yang memungkinkan Anda mengambil tangkapan layar, memilih wilayah, menjelaskan bug, dan mengirim permintaan POST (di sini jsfiddle ) (fungsi utamanya adalah report()).

Kamil Kiełczewski
sumber
10
Jika Anda ingin memberikan poin minus, tinggalkan juga komentar dengan penjelasan
Kamil Kiełczewski
Saya pikir alasan mengapa Anda downvoted kemungkinan besar perpustakaan html2canvas adalah perpustakaannya, bukan alat yang ia tunjukkan.
zfrisch
Tidak apa-apa jika Anda tidak ingin menangkap efek post-processing (seperti filter blur).
vintproykt
Keterbatasan Semua gambar yang digunakan skrip harus berada di bawah asal yang sama agar dapat membacanya tanpa bantuan proxy. Demikian pula, jika Anda memiliki elemen kanvas lainnya pada halaman, yang telah dinodai dengan konten lintas-asal, mereka akan menjadi kotor dan tidak lagi dapat dibaca oleh html2canvas.
aravind3
13

Dapatkan tangkapan layar sebagai Canvas atau Jpeg Blob / ArrayBuffer menggunakan getDisplayMedia API:

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

DEMO:

// 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",
})
Nikolay Makhonin
sumber
Bertanya-tanya mengapa ini hanya memiliki 1 upvote, ini terbukti sangat membantu!
Jay Dadhania
Tolong, bagaimana cara kerjanya? Bisakah Anda memberikan demo untuk pemula seperti saya? Thx
kabrice
@kabrice saya menambahkan demo. Cukup masukkan kode di konsol Chrome. Jika Anda memerlukan dukungan browser lama, gunakan: babeljs.io/en/repl
Nikolay Makhonin
8

Inilah contohnya menggunakan: 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}`));

Juga patut dicoba adalah dokumentasi Screen Capture API .

JSON C11
sumber