ใช้ HTML5 / Canvas / JavaScript เพื่อถ่ายภาพหน้าจอในเบราว์เซอร์


924

"รายงานข้อบกพร่อง" ของ Google หรือ "เครื่องมือคำติชม" ของ Google ช่วยให้คุณสามารถเลือกพื้นที่ของหน้าต่างเบราว์เซอร์ของคุณเพื่อสร้างภาพหน้าจอที่ส่งมาพร้อมกับข้อเสนอแนะของคุณเกี่ยวกับข้อบกพร่อง

ภาพหน้าจอของเครื่องมือ Google ฟีดแบ็ก ภาพหน้าจอโดยเจสันขนาดเล็กที่โพสต์ในคำถามที่ซ้ำกัน

พวกเขาทำสิ่งนี้ได้อย่างไร API ข้อเสนอแนะ JavaScript ของ Google ถูกโหลดจากที่นี่และภาพรวมของโมดูลความคิดเห็นจะแสดงให้เห็นถึงความสามารถในการจับภาพหน้าจอ


2
Elliott Sprehn เขียนในทวีตเมื่อไม่กี่วันก่อน:> @CatChen โพสต์สแต็คโอเวอร์โฟลว์นั้นไม่ถูกต้อง ภาพหน้าจอของ Google ฟีดแบ็กทำโดยลูกค้าฝั่งทั้งหมด :)
Goran Rakic

1
วิธีนี้มีเหตุผลในขณะที่พวกเขาต้องการตรวจสอบว่าเบราว์เซอร์ของผู้ใช้แสดงผลหน้าเว็บอย่างไรไม่ใช่วิธีที่พวกเขาจะแสดงผลบนฝั่งเซิร์ฟเวอร์โดยใช้โปรแกรม หากคุณเพียงส่งหน้า DOM ปัจจุบันไปยังเซิร์ฟเวอร์ก็จะพลาดความไม่สอดคล้องใด ๆ ในวิธีที่เบราว์เซอร์แสดงผล HTML นี่ไม่ได้หมายความว่าคำตอบของเฉินนั้นผิดสำหรับการจับภาพหน้าจอ แต่ดูเหมือนว่า Google กำลังทำในลักษณะที่ต่างออกไป
Goran Rakic

Elliott พูดถึง Jan Kučaวันนี้และฉันพบลิงค์นี้ในทวีตของ Jan: jankuca.tumblr.com/post/7391640769/…
Cat Chen

ฉันจะขุดลงในนี้ในภายหลังและดูว่ามันสามารถทำได้ด้วยเครื่องมือการแสดงผลฝั่งไคลเอ็นต์และตรวจสอบว่าของ Google ทำจริง ๆ
Cat Chen

ฉันเห็นการใช้ CompareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, การติดตามการแพ็ดดิ้งและสิ่งต่าง ๆ เช่นนั้น มันเป็นโค้ดหลายพันบรรทัดที่ทำให้งงงวยที่จะทำให้งงงวยและมองผ่าน ฉันชอบที่จะดูโอเพนซอร์ซรุ่นลิขสิทธิ์ฉันได้ติดต่อ Elliott Sprehn!
ลุคสแตนลีย์

คำตอบ:


1154

JavaScript สามารถอ่าน DOM canvasและทำให้ตัวแทนที่ถูกต้องเป็นธรรมของว่าการใช้ ฉันทำงานกับสคริปต์ที่แปลง HTML เป็นภาพแคนวาส ตัดสินใจวันนี้เพื่อใช้งานมันเป็นการส่งข้อเสนอแนะตามที่คุณอธิบาย

สคริปต์ช่วยให้คุณสร้างแบบฟอร์มความคิดเห็นซึ่งรวมถึงภาพหน้าจอที่สร้างขึ้นบนเบราว์เซอร์ของลูกค้าพร้อมกับแบบฟอร์ม สกรีนช็อตใช้ DOM และอาจไม่ถูกต้อง 100% กับการแสดงจริงเนื่องจากไม่ได้สร้างสกรีนช็อตจริง แต่สร้างสกรีนช็อตตามข้อมูลที่มีอยู่ในหน้า

มันไม่จำเป็นต้องมีการแสดงผลใด ๆ จากเซิร์ฟเวอร์เป็นภาพทั้งหมดถูกสร้างขึ้นบนเบราว์เซอร์ของลูกค้า สคริปต์ HTML2Canvas นั้นยังอยู่ในสถานะทดลองใช้มากเนื่องจากไม่ได้แยกวิเคราะห์แอตทริบิวต์ CSS3 เกือบเท่าที่ฉันต้องการและไม่สนับสนุนการโหลดรูปภาพ CORS แม้ว่าจะมีพร็อกซีอยู่ก็ตาม

ความเข้ากันได้ของเบราว์เซอร์ค่อนข้าง จำกัด (ไม่ใช่เพราะไม่สามารถรองรับได้มากขึ้นเพียงแค่ไม่มีเวลาที่จะรองรับเบราว์เซอร์เพิ่มเติม)

สำหรับข้อมูลเพิ่มเติมดูตัวอย่างได้ที่นี่:

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

แก้ไข html2canvas สคริปต์อยู่ในขณะนี้แยกกันที่นี่และบางตัวอย่างที่นี่

แก้ไข 2 ยืนยันว่า Google ใช้วิธีการที่คล้ายกันมาก (ในความเป็นจริงขึ้นอยู่กับเอกสารที่แตกต่างที่สำคัญคือวิธีการ async ของพวกเขา traversing / การวาดภาพ) อีกสามารถพบได้ในงานนำเสนอนี้เอลเลียต Sprehn จาก Google ผลตอบรับทีม: http: //www.elliottsprehn.com/preso/fluentconf/


1
เจ๋งมาก, Sikuli หรือ Selenium อาจจะดีสำหรับการไปยังไซต์ต่าง ๆ , เปรียบเทียบภาพไซต์จากเครื่องมือทดสอบกับภาพ html2canvas.js ที่แสดงผลของคุณในแง่ของความคล้ายคลึงกันของพิกเซล! สงสัยว่าคุณสามารถสำรวจส่วนต่างๆของ DOM โดยอัตโนมัติด้วยตัวแก้สูตรที่ง่ายมาก ๆ เพื่อหาวิธีแยกวิเคราะห์แหล่งข้อมูลสำรองสำหรับเบราว์เซอร์ที่ไม่ได้รับ getBoundingClientRect ฉันอาจจะใช้มันถ้ามันเป็นโอเพนซอร์สกำลังพิจารณาด้วยตัวเอง Niklas ทำงานได้ดี!
ลุคสแตนลีย์

1
@ ลุคสแตนลีย์ฉันมักจะโยนแหล่งที่มากับ github สุดสัปดาห์นี้ยังมีบางส่วนที่สะอาดและการเปลี่ยนแปลงที่ฉันต้องการที่จะทำก่อนหน้านั้นเช่นเดียวกับกำจัด jQuery พึ่งพาที่ไม่จำเป็นมันมีอยู่ในปัจจุบัน
Niklas

43
ตอนนี้ซอร์สโค้ดมีให้ที่github.com/niklasvh/html2canvasตัวอย่างบางส่วนของสคริปต์ที่ใช้งานhtml2canvas.hertzen.com ที่นั่น ยังมีข้อบกพร่องมากมายที่ต้องแก้ไขดังนั้นฉันยังไม่แนะนำให้ใช้สคริปต์ในสภาพแวดล้อมจริง
Niklas

2
ทางออกใด ๆ ที่ทำให้มันทำงานกับ SVG จะเป็นความช่วยเหลือที่ดี มันไม่ทำงานกับ highcharts.com
Jagdeep

3
@ Niklas ฉันเห็นตัวอย่างของคุณเติบโตเป็นโครงการจริง อาจอัปเดตความคิดเห็นที่อัปเดตที่สุดของคุณเกี่ยวกับลักษณะการทดลองของโครงการ หลังจากเกือบ 900 คอมมิทฉันจะคิดว่ามันมากกว่าการทดสอบ ณ จุดนี้ ;-)
Jogai

70

ขณะนี้แอปพลิเคชันเว็บของคุณสามารถจับภาพหน้าจอ 'ดั้งเดิม' ของเดสก์ท็อปทั้งหมดของลูกค้าโดยใช้getUserMedia():

ดูตัวอย่างนี้:

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

ลูกค้าจะต้องใช้งาน Chrome (ตอนนี้) และจะต้องเปิดใช้งานการสนับสนุนการจับภาพหน้าจอภายใต้ chrome: //


2
ฉันไม่พบการสาธิตเพียงแค่จับภาพหน้าจอ - ทุกอย่างเกี่ยวกับการแชร์หน้าจอ จะต้องลอง
jwl

8
@XMight คุณสามารถเลือกว่าจะอนุญาตหรือไม่โดยสลับการตั้งค่าสถานะการสนับสนุนการจับภาพหน้าจอ
Matt Sinclair

19
@XMight โปรดอย่าคิดอย่างนี้ เว็บเบราว์เซอร์ควรสามารถทำสิ่งต่างๆมากมาย แต่น่าเสียดายที่พวกเขาไม่สอดคล้องกับการใช้งานของพวกเขา มันก็โอเคอย่างแน่นอนถ้าเบราว์เซอร์มีฟังก์ชั่นดังกล่าวตราบใดที่ผู้ใช้ถูกถาม จะไม่มีใครสามารถสร้างภาพหน้าจอได้หากคุณไม่สนใจ แต่มากเกินไปกลัวผลในการใช้งานที่ไม่ดีเช่น API คลิปบอร์ดซึ่งได้รับการปิดการใช้งานทั้งหมดแทนการสร้างการโต้ตอบการยืนยันเช่นเว็บแคม, ไมโครโฟน, ความสามารถในการจับภาพหน้าจอและอื่น ๆ
Stane

3
สิ่งนี้เลิกใช้แล้วและจะถูกลบออกจากมาตรฐานตามdeveloper.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin

7
@AgustinCautin Navigator.getUserMedia()เลิกใช้แล้ว แต่ด้านล่างระบุว่า "... โปรดใช้navigator.mediaDevices.getUserMedia () ที่ใหม่กว่านั่นคือเพิ่งถูกแทนที่ด้วย API ที่ใหม่กว่า
levant pied

37

ในฐานะที่เป็นคลาสที่กล่าวถึงคุณสามารถใช้html2canvasห้องสมุดที่จะใช้หน้าจอโดยใช้ JS ในเบราว์เซอร์ ฉันจะขยายคำตอบของเขาในจุดนี้โดยให้ตัวอย่างของการจับภาพหน้าจอโดยใช้ห้องสมุดนี้:

ในreport()ฟังก์ชั่นonrenderedหลังจากรับภาพเป็น data URI คุณสามารถแสดงให้ผู้ใช้เห็นและอนุญาตให้เขาวาด "bug bug" ด้วยเมาส์แล้วส่งภาพหน้าจอและพิกัดภูมิภาคไปยังเซิร์ฟเวอร์

ในตัวอย่างนี้ async/awaitรุ่นที่ถูกสร้างขึ้นมาด้วยดีฟังก์ชั่นmakeScreenshot()

UPDATE

ตัวอย่างง่ายๆที่ให้คุณถ่ายภาพหน้าจอเลือกภูมิภาคอธิบายบั๊กและส่งคำขอ POST ( ที่นี่ jsfiddle ) (ฟังก์ชั่นหลักคือreport())


10
หากคุณต้องการให้คะแนนลบแสดงความคิดเห็นด้วยคำอธิบาย
Kamil Kiełczewski

ฉันคิดว่าสาเหตุที่คุณถูกลดระดับลงน่าจะเป็นเพราะห้องสมุด html2canvas เป็นห้องสมุดของเขาไม่ใช่เครื่องมือที่เขาชี้ให้เห็น
zfrisch

มันก็โอเคถ้าคุณไม่ต้องการจับเอฟเฟกต์หลังการประมวลผล (เป็นตัวกรองเบลอ)
vintproykt

ข้อ จำกัด รูปภาพทั้งหมดที่สคริปต์ใช้จำเป็นต้องอยู่ภายใต้จุดเริ่มต้นเดียวกันเพื่อให้สามารถอ่านได้โดยไม่ต้องใช้พร็อกซี ในทำนองเดียวกันหากคุณมีองค์ประกอบภาพวาดอื่น ๆ บนหน้าซึ่งมีเนื้อหาที่มีจุดกำเนิดข้ามเนื้อหาจะสกปรกและไม่สามารถอ่านได้อีกต่อไปโดย html2canvas
aravind3

13

รับภาพหน้าจอเป็น Canvas หรือ Jpeg Blob / ArrayBuffer โดยใช้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)
    })
}

การสาธิต:

// 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 upvote นี่พิสูจน์แล้วว่าเป็นประโยชน์จริง ๆ !
Jay Dadhania

กรุณามันทำงานอย่างไร คุณสามารถให้ตัวอย่างสำหรับมือใหม่อย่างฉันได้ไหม ขอบคุณ
kabrice

@kabrice ฉันได้เพิ่มตัวอย่าง เพียงใส่รหัสในคอนโซล Chrome หากคุณต้องการการสนับสนุนเบราว์เซอร์เก่าให้ใช้: babeljs.io/en/repl
Nikolay Makhonin

8

นี่คือตัวอย่างการใช้: 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}`));

ที่ควรค่าแก่การเช็คเอาต์คือเอกสารAPI การดักจับหน้าจอ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.