Usando HTML5 / Canvas / JavaScript para tomar capturas de pantalla en el navegador

924

Google "Informar un error" o "Herramienta de comentarios" le permite seleccionar un área de la ventana de su navegador para crear una captura de pantalla que se envía con sus comentarios sobre un error.

Captura de pantalla de Google Feedback Tool Captura de pantalla de Jason Small, publicada en una pregunta duplicada .

¿Cómo estan haciendo esto? La API de comentarios de JavaScript de Google se carga desde aquí y su descripción general del módulo de comentarios demostrará la capacidad de captura de pantalla.

joelvh
fuente
2
Elliott Sprehn escribió en un Tweet hace unos días:> @CatChen Esa publicación de stackoverflow no es precisa. La captura de pantalla de Google Feedback se realiza completamente del lado del cliente. :)
Goran Rakic
1
Esto parece lógico, ya que quieren captar exactamente cómo el navegador del usuario está representando una página, no cómo lo harían en el lado del servidor utilizando su motor. Si solo envía la página actual DOM al servidor, perderá cualquier inconsistencia en la forma en que el navegador procesa el HTML. Esto no significa que la respuesta de Chen sea incorrecta para tomar capturas de pantalla, solo parece que Google lo está haciendo de una manera diferente.
Goran Rakic
Elliott mencionó a Jan Kuča hoy, y encontré este enlace en el tuit de Jan: jankuca.tumblr.com/post/7391640769/…
Cat Chen
Profundizaré en esto más adelante y veré cómo se puede hacer con el motor de renderizado del lado del cliente y comprobaré si Google realmente lo hace de esa manera.
Cat Chen
Veo el uso de compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, seguimiento de relleno y cosas por el estilo. Sin embargo, son miles de líneas de código ofuscado las que se deben desuscar y mirar. Me encantaría ver una versión con licencia de código abierto, ¡me puse en contacto con Elliott Sprehn!
Luke Stanley

Respuestas:

1155

JavaScript puede leer el DOM y representar una representación bastante precisa de eso usando canvas. He estado trabajando en un script que convierte HTML en una imagen de lienzo. Decidió hoy implementarlo para enviar comentarios como usted describió.

El script le permite crear formularios de comentarios que incluyen una captura de pantalla, creada en el navegador del cliente, junto con el formulario. La captura de pantalla se basa en el DOM y, como tal, puede no ser 100% precisa para la representación real, ya que no hace una captura de pantalla real, pero construye la captura de pantalla en función de la información disponible en la página.

No requiere ninguna representación del servidor , ya que la imagen completa se crea en el navegador del cliente. El script HTML2Canvas en sí todavía se encuentra en un estado muy experimental, ya que no analiza casi todos los atributos CSS3 que quisiera, ni tiene soporte para cargar imágenes CORS incluso si hubiera un proxy disponible.

Todavía es bastante limitada la compatibilidad del navegador (no porque no se pueda soportar más, simplemente no he tenido tiempo de hacerlo más compatible con el navegador cruzado).

Para obtener más información, eche un vistazo a los ejemplos aquí:

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

editar El script html2canvas ahora está disponible por separado aquí y algunos ejemplos aquí .

edit 2 Otra confirmación de que Google usa un método muy similar (de hecho, según la documentación, la única diferencia importante es su método asíncrono de desplazamiento / dibujo) se puede encontrar en esta presentación de Elliott Sprehn del equipo de comentarios de Google: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
fuente
1
Muy bueno, Sikuli o Selenium podrían ser buenos para ir a diferentes sitios, comparando una foto del sitio desde la herramienta de prueba con su imagen renderizada html2canvas.js en términos de similitud de píxeles. Me pregunto si podría atravesar automáticamente partes del DOM con un solucionador de fórmulas muy simple para encontrar cómo analizar fuentes de datos alternativas para navegadores donde getBoundingClientRect no está disponible. Probablemente usaría esto si fuera de código abierto, si estuviera considerando jugar conmigo mismo. Buen trabajo Niklas!
Luke Stanley
1
@Luke Stanley Probablemente lanzaré la fuente en github este fin de semana, todavía algunas pequeñas limpiezas y cambios que quiero hacer antes de eso, así como deshacerme de la dependencia innecesaria de jQuery que tiene actualmente.
Niklas
43
El código fuente ahora está disponible en github.com/niklasvh/html2canvas , algunos ejemplos del script en uso html2canvas.hertzen.com allí. Todavía hay muchos errores que corregir, por lo que no recomendaría usar el script en un entorno en vivo todavía.
Niklas
2
Cualquier solución para que funcione para SVG será de gran ayuda. No funciona con highcharts.com
Jagdeep
3
@Niklas veo que tu ejemplo se convirtió en un proyecto real. Tal vez actualice su comentario más votado sobre la naturaleza experimental del proyecto. Después de casi 900 confirmaciones, creo que es un poco más que un experimento en este punto ;-)
Jogai
70

Su aplicación web ahora puede tomar una captura de pantalla 'nativa' de todo el escritorio del cliente usando getUserMedia():

Echa un vistazo a este ejemplo:

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

El cliente tendrá que usar Chrome (por ahora) y deberá habilitar el soporte de captura de pantalla en Chrome: // flags.

Matt Sinclair
fuente
2
No puedo encontrar demostraciones de solo tomar una captura de pantalla: todo se trata de compartir pantalla. Tendré que probarlo.
jwl
8
@XMight, puede elegir si desea permitir esto alternando la marca de soporte de captura de pantalla.
Matt Sinclair
19
@ XMight Por favor, no pienses así. Los navegadores web deberían poder hacer muchas cosas, pero desafortunadamente no son consistentes con sus implementaciones. Está absolutamente bien, si un navegador tiene esa funcionalidad, siempre que se le pregunte al usuario. Nadie podrá hacer una captura de pantalla sin su atención. Pero demasiado miedo da como resultado implementaciones malas, como la API del portapapeles, que se ha deshabilitado por completo, en lugar de crear cuadros de diálogo de confirmación, como cámaras web, micrófonos, capacidad de captura de pantalla, etc.
StanE
3
Esto quedó en desuso y se eliminará del estándar de acuerdo con developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
77
@AgustinCautin Navigator.getUserMedia()está en desuso, pero justo debajo dice "... Utilice el navegador más reciente.mediaDevices.getUserMedia () ", es decir, se reemplazó por una API más nueva.
Levant Pied
37

Como Niklas mencionó , puede usar la biblioteca html2canvas para tomar una captura de pantalla usando JS en el navegador. Extenderé su respuesta en este punto proporcionando un ejemplo de tomar una captura de pantalla usando esta biblioteca:

En report()función onrendereddespués de obtener la imagen como URI de datos, puede mostrárselo al usuario y permitirle que dibuje "región de error" con el mouse y luego envíe una captura de pantalla y coordenadas de región al servidor.

En este ejemplo async/await se hizo la versión: con buena makeScreenshot()función .

ACTUALIZAR

Ejemplo simple que le permite tomar una captura de pantalla, seleccionar una región, describir un error y enviar una solicitud POST ( aquí jsfiddle ) (la función principal es report()).

Kamil Kiełczewski
fuente
10
Si quieres dar un punto negativo, deja también un comentario con explicación
Kamil Kiełczewski
Creo que la razón por la que te están rechazando es muy probable que la biblioteca html2canvas sea su biblioteca, no una herramienta que simplemente señaló.
zfrisch
Está bien si no desea capturar efectos de postprocesamiento (como filtro de desenfoque).
vintproykt
Limitaciones Todas las imágenes que utiliza el script deben residir bajo el mismo origen para que pueda leerlas sin la ayuda de un proxy. Del mismo modo, si tiene otros elementos de lienzo en la página, que se han contaminado con contenido de origen cruzado, se volverán sucios y html2canvas ya no podrán leerlos.
aravind3
13

Obtenga una captura de pantalla como Canvas o Jpeg Blob / ArrayBuffer utilizando la 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)
    })
}

MANIFESTACIÓN:

// 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
fuente
¡Me pregunto por qué esto tuvo solo 1 voto a favor, esto resultó ser realmente útil!
Jay Dadhania
Por favor, ¿cómo funciona? ¿Puedes proporcionar una demostración para novatos como yo? Thx
kabrice
@kabrice Agregué una demostración. Simplemente ponga el código en la consola de Chrome. Si necesita compatibilidad con navegadores antiguos, use: babeljs.io/en/repl
Nikolay Makhonin
8

Aquí hay un ejemplo usando: 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}`));

También vale la pena echarle un vistazo a los documentos de la API de captura de pantalla .

JSON C11
fuente