Usando HTML5 / Canvas / JavaScript para capturar capturas de tela no navegador

924

"Relatar um bug" ou "Ferramenta de feedback" do Google permite selecionar uma área da janela do navegador para criar uma captura de tela que é enviada com seus comentários sobre um bug.

Captura de tela da Ferramenta de feedback do Google Captura de tela de Jason Small, postada em uma pergunta duplicada .

Como eles estão fazendo isso? A API de feedback JavaScript do Google é carregada a partir daqui e sua visão geral do módulo de feedback demonstrará o recurso de captura de tela.

joelvh
fonte
2
Elliott Sprehn escreveu em um Tweet alguns dias atrás:> @CatChen Essa postagem de stackoverflow não é precisa. A captura de tela do Google Feedback é feita inteiramente do lado do cliente. :)
Goran Rakic
1
Isso parece lógico, pois eles querem entender exatamente como o navegador do usuário está processando uma página, não como eles a renderizariam no lado do servidor usando seu mecanismo. Se você enviar apenas o DOM da página atual para o servidor, ocorrerá uma inconsistência na forma como o navegador está processando o HTML. Isso não significa que a resposta de Chen esteja errada para tirar capturas de tela, apenas parece que o Google está fazendo isso de uma maneira diferente.
Goran Rakic
Elliott mencionado Jan Kuca hoje, e eu encontrei este link no tweet de Jan: jankuca.tumblr.com/post/7391640769/...
Cat Chen
Vou abordar isso mais tarde e ver como isso pode ser feito com o mecanismo de renderização do lado do cliente e verificar se o Google realmente faz dessa maneira.
Cat Chen
Eu vejo o uso de compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, rastreamento de preenchimento e coisas assim. São milhares de linhas de código ofuscado para ofuscar e analisar o problema. Adoraria ver uma versão licenciada de código aberto, entrei em contato com Elliott Sprehn!
Luke Stanley

Respostas:

1154

O JavaScript pode ler o DOM e renderizar uma representação bastante precisa desse uso canvas. Eu tenho trabalhado em um script que converte HTML em uma imagem de tela. Decidiu hoje fazer uma implementação para enviar feedbacks como você descreveu.

O script permite criar formulários de feedback que incluem uma captura de tela, criada no navegador do cliente, junto com o formulário. A captura de tela é baseada no DOM e, como tal, pode não ser 100% precisa da representação real, pois não faz uma captura de tela real, mas cria a captura de tela com base nas informações disponíveis na página.

Não requer nenhuma renderização do servidor , pois toda a imagem é criada no navegador do cliente. O script HTML2Canvas em si ainda está em um estado muito experimental, pois não analisa quase os atributos CSS3 que eu gostaria que ele tivesse, nem tem suporte para carregar imagens CORS, mesmo que um proxy estivesse disponível.

Compatibilidade de navegador ainda bastante limitada (não porque mais não possa ser suportado, apenas não teve tempo para torná-lo mais compatível com vários navegadores).

Para mais informações, dê uma olhada nos exemplos aqui:

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

editar O script html2canvas agora está disponível separadamente aqui e em alguns exemplos aqui .

editar 2 Outra confirmação de que o Google usa um método muito semelhante (de fato, com base na documentação, a única grande diferença é o método assíncrono de deslocamento / desenho) pode ser encontrada nesta apresentação por Elliott Sprehn, da equipe do Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
fonte
1
Muito legal, Sikuli ou Selenium podem ser bons para ir a sites diferentes, comparando uma imagem do site da ferramenta de teste à sua imagem renderizada html2canvas.js em termos de similaridade de pixels! Gostaria de saber se você poderia percorrer automaticamente partes do DOM com um solucionador de fórmula muito simples para descobrir como analisar fontes de dados alternativas para navegadores onde getBoundingClientRect não está disponível. Eu provavelmente usaria isso se fosse de código aberto, estivesse pensando em brincar comigo mesmo. Bom trabalho Niklas!
101111 Luke Stanley
1
@Luke Stanley Provavelmente, lançarei a fonte no github neste fim de semana, ainda com algumas pequenas limpezas e alterações que eu quero fazer antes disso, além de me livrar da dependência desnecessária do jQuery atualmente.
Niklas
43
O código fonte está agora disponível em github.com/niklasvh/html2canvas , alguns exemplos do script em uso html2canvas.hertzen.com lá. Ainda há muitos bugs para corrigir, então ainda não recomendo o uso do script em um ambiente ativo.
Niklas
2
qualquer solução para fazê-lo funcionar para o SVG será uma grande ajuda. Ele não funciona com highcharts.com
Jagdeep
3
@ Niklas vejo seu exemplo se transformar em um projeto real. Talvez atualize seu comentário mais votado sobre a natureza experimental do projeto. Depois de quase 900 commits eu acho que é um pouco mais do que uma experiência neste momento ;-)
Jogai
70

Seu aplicativo da Web agora pode fazer uma captura de tela 'nativa' de toda a área de trabalho do cliente usando getUserMedia():

Veja este exemplo:

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

O cliente precisará usar o chrome (por enquanto) e precisará ativar o suporte à captura de tela sob os sinalizadores chrome: //.

Matt Sinclair
fonte
2
Não consigo encontrar nenhuma demonstração de apenas tirar uma captura de tela - tudo se resume ao compartilhamento de tela. terá que tentar.
jwl
8
@XMight, você pode optar por permitir isso alternando o sinalizador de suporte de captura de tela.
Matt Sinclair
19
@ XMight Por favor, não pense assim. Os navegadores da Web devem ser capazes de fazer muitas coisas, mas infelizmente não são consistentes com suas implementações. Não há problema se um navegador tiver essa funcionalidade, desde que o usuário esteja sendo solicitado. Ninguém poderá fazer uma captura de tela sem sua atenção. Mas muito medo resulta em implementações ruins, como a API área de transferência, que foi desactivada por completo, em vez criar diálogos de confirmação, como por webcams, microfones, capacidade de captura de tela, etc.
Stane
3
Isso foi descontinuado e será removido do padrão de acordo com developer.mozilla.org/pt-BR/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
7
O @AgustinCautin Navigator.getUserMedia()está obsoleto, mas logo abaixo diz "... Por favor, use o mais recente navigator.mediaDevices.getUserMedia () ", ou seja, ele foi substituído por uma API mais recente.
levant pied
37

Como Niklas mencionou, você pode usar a biblioteca html2canvas para fazer uma captura de tela usando JS no navegador. Vou estender sua resposta neste ponto, fornecendo um exemplo de captura de tela usando esta biblioteca:

Em report()função onrendereddepois de receber imagem como dados URI você pode mostrá-lo para o usuário e permitir-lhe para chamar a "região bug" com o mouse e, em seguida, enviar um screenshot e região coordenadas para o servidor.

Em este exemplo async/await versão foi feita: com boa makeScreenshot()função .

ATUALIZAR

Exemplo simples que permite tirar capturas de tela, selecionar região, descrever erros e enviar solicitação POST ( aqui jsfiddle ) (a função principal é report()).

Kamil Kiełczewski
fonte
10
Se você quer dar ponto negativo, deixe também o comentário com uma explicação
Kamil Kiełczewski
Eu acho que a razão pela qual você está recebendo votos negativos é mais provável que a biblioteca html2canvas seja a biblioteca dele, não uma ferramenta que ele simplesmente apontou.
Zfrisch
Tudo bem se você não deseja capturar efeitos de pós-processamento (como filtro de desfoque).
vintproykt 21/01/19
Limitações Todas as imagens que o script usa precisam residir na mesma origem para poder lê-las sem a ajuda de um proxy. Da mesma forma, se você tiver outros elementos da tela na página, que foram contaminados pelo conteúdo de origem cruzada, eles ficarão sujos e não serão mais legíveis pelo html2canvas.
Aravind3
13

Obtenha a captura de tela como Canvas ou Jpeg Blob / ArrayBuffer usando a 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",
})
Nikolay Makhonin
fonte
Pergunto-me por que isso teve apenas 1 voto positivo, isso provou ser realmente útil!
Jay Dadhania 28/02
Por favor, como isso funciona? Você pode fornecer uma demonstração para iniciantes como eu? Thx
kabrice 18/04
@kabrice Adicionei uma demonstração. Basta colocar o código no console do Chrome. Se você precisar de suporte para navegadores antigos, use: babeljs.io/en/repl
Nikolay Makhonin
8

Aqui está um exemplo 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}`));

Também vale a pena conferir os documentos da API do Screen Capture .

JSON C11
fonte