Використовуючи HTML5 / Canvas / JavaScript, щоб зробити знімки екрана в браузері


923

"Повідомити про помилку" або "Зворотній зв'язок" від Google дозволяє вибрати область вікна веб-переглядача, щоб створити знімок екрана, який надсилається з вашими відгуками про помилку.

Знімок екрана інструменту зворотного зв’язку Google Скріншот Джейсона Малого, розміщений у двох примірниках .

Як вони це роблять? API JavaScript зворотного зв’язку Google завантажується звідси, і їх огляд модуля зворотного зв’язку демонструє можливість екрана.


2
Елліотт Шрехн написав у Твітті кілька днів тому:> @CatChen Цей пост stackoverflow не є точним. Скріншот Google Feedback зроблений повністю на стороні клієнта. :)
Горан Ракич

1
Це виглядає логічно, оскільки вони хочуть точно зрозуміти, як браузер користувача рендерує сторінку, а не як вони відображатимуть її на стороні сервера за допомогою свого двигуна. Якщо ви надішліть на сервер лише поточну сторінку DOM, вона пропустить будь-які невідповідності в тому, як браузер надає HTML. Це не означає, що відповідь Чена неправильна для зйомки екрана, вона просто схожа на те, що Google робить це по-іншому.
Горан Ракич

Елліотт згадав про Яна Кучу сьогодні, і я знайшов це посилання у твіті Яна: jankuca.tumblr.com/post/7391640769/…
Cat Chen

Я пізнаю це пізніше і побачу, як це можна зробити за допомогою системи рендерингу на стороні клієнта та перевіряю, чи дійсно Google це робить таким чином.
Кіт Чен

Я бачу використання порівнянняDocumentPosition, getBoxObjectFor, toDataURL, drawImage, прокладки для відстеження тощо. Це тисячі рядків прихованого коду для знешкодження та перегляду, хоча. Я хотів би побачити ліцензовану версію з відкритим кодом, я зв’язався з Елліоттом Шпреном!
Люк Стенлі

Відповіді:


1153

JavaScript може прочитати DOM і надати досить точне уявлення про це за допомогою canvas. Я працював над сценарієм, який перетворює HTML у зображення полотна. Сьогодні вирішив здійснити його для надсилання відгуків, як ви описали.

Сценарій дозволяє створювати форми зворотного зв'язку, які містять скріншот, створений у браузері клієнта, разом із формою. Скріншот заснований на DOM і, як такий, може бути не на 100% точним до реального зображення, оскільки він не робить фактичного скріншот, але будує скріншот на основі інформації, наявної на сторінці.

Він не вимагає рендеринга з сервера , оскільки все зображення створюється в браузері клієнта. Сам сценарій HTML2Canvas досі перебуває у дуже експериментальному стані, оскільки він не розбирає майже стільки атрибутів CSS3, як би я цього хотів, і не має підтримки для завантаження зображень CORS, навіть якщо проксі був доступний.

Ще досить обмежена сумісність веб-переглядача (не тому, що більше не вдалося підтримати, просто не встигли зробити його більш підтримуваним крос-браузером).

Для отримання додаткової інформації подивіться приклади тут:

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

редагувати сценарій html2canvas тепер доступний окремо тут, а деякі приклади тут .

редагувати 2 Ще одне підтвердження того, що Google використовує дуже подібний метод (насправді, виходячи з документації, єдиною головною відмінністю є їх асинхронний метод переходу / малювання) можна знайти в цій презентації Елліотт Шрехн з команди Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/


1
Дуже круто, Sikuli або Selenium можуть бути корисними для відвідування різних сайтів, порівнюючи знімок сайту від інструменту тестування до вашого html2canvas.js, що надає зображення з точки зору подібності пікселів! Цікаво, чи можна було автоматично переходити частини DOM за допомогою дуже простого рішення формули, щоб знайти, як розбирати альтернативні джерела даних для браузерів, де getBoundingClientRect недоступний. Я б, мабуть, користувався цим, якби він був з відкритим кодом, розглядав можливість самому з ним пограти. Приємна робота Ніклас!
Люк Стенлі

1
@Luke Stanley Я, швидше за все, підуть джерело на github у ці вихідні, все ще деякі незначні очищення та зміни, які я хочу зробити до цього часу, а також позбудусь зайвої залежності від jQuery, яку він має на даний момент.
Ніклас

43
Вихідний код тепер доступний на веб- сайті github.com/niklasvh/html2canvas , деякі приклади сценарію, що використовується html2canvas.hertzen.com там. Виправити ще багато помилок, тому я ще не рекомендував би використовувати сценарій у прямому середовищі.
Ніклас

2
будь-яке рішення, щоб змусити його працювати для SVG, буде чудовою підмогою. Це не працює з highcharts.com
Jagdeep

3
@Niklas Я бачу, що ваш приклад перетворився на справжній проект. Можливо, оновіть свій найбільш прихильний коментар щодо експериментального характеру проекту. Після майже 900 комісій я вважаю, що це трохи більше, ніж експеримент на даний момент ;-)
Jogai

70

Тепер ваш веб-додаток може зробити "рідний" скріншот цілого робочого столу клієнта, використовуючи getUserMedia():

Подивіться на цей приклад:

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

Клієнт повинен буде використовувати хром (поки що) і йому потрібно буде включити підтримку захоплення екрана під chrome: // прапорами.


2
я не можу знайти жодних демонстрацій просто зробити знімок екрана - все стосується спільного використання екрана. доведеться спробувати.
рив

8
@XMight, ви можете вибрати, чи дозволити це, включивши прапор підтримки захоплення екрана.
Метт Сінклер

19
@XMight Будь ласка, не думайте так. Веб-браузери повинні робити багато можливостей, але, на жаль, вони не відповідають їхнім реалізаціям. Цілком нормально, якщо браузер має таку функціональність, доки його запитують користувач. Ніхто не зможе зробити скріншот без вашої уваги. Але занадто великий страх призводить до поганих реалізацій, як API буфера обміну, який був повністю відключений, натомість створює діалогові вікна підтвердження, як для веб-камер, мікрофонів, можливостей екрана тощо.
StanE


7
@AgustinCautin Navigator.getUserMedia()застарілий, але трохи нижче написано "... Будь ласка, використовуйте новіший навігатор.mediaDevices.getUserMedia () ", тобто його щойно замінили на новий API.
левант

37

Як згадував Niklas, ви можете використовувати бібліотеку html2canvas, щоб зробити знімок екрана за допомогою JS у браузері. Я розширю його відповідь у цьому пункті, наводя приклад зйомки екрана за допомогою цієї бібліотеки:

У report()функціонуванні onrenderedпісля отримання зображення як URI даних ви можете показати його користувачеві і дозволити йому намалювати "область помилки" мишею, а потім надіслати скриншот і координати регіону на сервер.

У цьому прикладі async/await була зроблена версія: з гарною makeScreenshot()функцією .

ОНОВЛЕННЯ

Простий приклад, який дозволяє зробити знімок екрана, вибрати область, описати помилку та надіслати POST-запит ( тут jsfiddle ) (основна функція report()).


10
Якщо ви хочете дати мінус бал, залиште також коментар з поясненнями
Kamil Kiełczewski

Я думаю, що причина, по якій ви отримуєте недооцінку, - це, швидше за все, те, що html2canvas-бібліотека - це його бібліотека, а не інструмент, на який він просто вказав.
zfrisch

Це добре, якщо ви не хочете захоплювати ефекти після обробки (як фільтр розмиття).
vintproykt

Обмеження Усі зображення, які використовує сценарій, повинні містити те саме джерело, щоб мати змогу їх читати без допомоги проксі. Аналогічно, якщо у вас є інші елементи полотна на сторінці, які були помазані вмістом перехресного походження, вони стануть брудними та більше не читаються html2canvas.
aravind3

13

Отримайте знімок екрана як Canvas або Jpeg Blob / ArrayBuffer за допомогою 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)
    })
}

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

Цікаво, чому це було лише 1 підсумок, це виявилося дуже корисним!
Джей Даданія

Будь ласка, як це працює? Чи можете ви надати демонстрацію для новачків, як я? Thx
kabrice

@kabrice Я додав демонстраційну версію. Просто поставте код у консоль Chrome. Якщо вам потрібна підтримка старих браузерів, використовуйте: babeljs.io/en/repl
Микола

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 Capture Screen .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.