HTML5 / Canvas / JavaScript를 사용하여 브라우저 내 스크린 샷 찍기


924

Google의 "버그 신고"또는 "피드백 도구"를 사용하면 브라우저 창의 영역을 선택하여 버그에 대한 피드백과 함께 제출 된 스크린 샷을 만들 수 있습니다.

Google 피드백 도구 스크린 샷 Jason Small의 스크린 샷은 중복 질문으로 게시되었습니다 .

그들은 이것을 어떻게하고 있습니까? 구글의 자바 스크립트 피드백 API가에서로드 여기피드백 모듈의 자신의 개요는 스크린 샷 기능을 시연 할 예정이다.


2
Elliott Sprehn 님이 며칠 전에 트윗을 작성 하셨습니다 :> @CatChen 그 스택 오버 플로우 게시물이 정확하지 않습니다. Google 피드백의 스크린 샷은 전적으로 클라이언트 측에서 수행됩니다. :)
Goran Rakic

1
엔진을 사용하여 서버 측에서 페이지를 렌더링하는 방식이 아니라 사용자의 브라우저가 페이지를 렌더링하는 방식을 정확하게 파악하려고하기 때문에 이음새가 논리적입니다. 현재 페이지 DOM 만 서버로 보내면 브라우저가 HTML을 렌더링하는 방식에 불일치가 없습니다. 첸의 대답이 스크린 샷을 찍는 데 잘못되었다는 의미는 아닙니다. 구글이 다른 방식으로하는 것처럼 보입니다.
Goran Rakic

Elliott는 오늘 Jan Kuča를 언급했으며 Jan의 트윗에서이 링크를 찾았습니다. jankuca.tumblr.com/post/7391640769/…
Cat Chen

나중에 자세히 살펴보고 클라이언트 측 렌더링 엔진으로 어떻게 수행 할 수 있는지 확인하고 Google이 실제로 그렇게하는지 확인합니다.
Cat Chen

compareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, 추적 패딩 등을 사용하는 것을 볼 수 있습니다. 난독 화하고 살펴보기 위해 수천 줄의 난독 화 된 코드입니다. 나는 그것의 오픈 소스 라이센스 버전을보고 싶습니다, 나는 Elliott Sprehn에게 연락했습니다!
Luke Stanley

답변:


1154

JavaScript는 DOM을 읽고를 사용하여 DOM을 상당히 정확하게 표현할 수 canvas있습니다. HTML을 캔버스 이미지로 변환하는 스크립트를 작성했습니다. 설명 된대로 피드백을 보내도록 구현하기로 결정했습니다.

이 스크립트를 사용하면 클라이언트 브라우저에서 만든 스크린 샷과 함께 피드백 양식을 양식과 함께 만들 수 있습니다. 스크린 샷은 DOM을 기반으로하므로 실제 스크린 샷을 만들지 않으므로 페이지에 제공되는 정보를 기반으로 스크린 샷을 작성하므로 실제 표현에 100 % 정확하지 않을 수 있습니다.

그것은 서버에서 모든 렌더링을 필요로하지 않습니다 전체 이미지가 클라이언트의 브라우저에 생성 될 때,. HTML2Canvas 스크립트 자체는 여전히 매우 실험적인 상태에 있습니다. 필요한 CSS3 속성을 거의 구문 분석하지 않으며 프록시를 사용할 수 있어도 CORS 이미지를로드 할 수 없습니다.

브라우저 호환성은 여전히 ​​제한적입니다 (더 많은 것을 지원할 수 없었기 때문에 더 많은 브라우저를 지원할 시간이 없었습니다).

자세한 내용은 다음 예제를 참조하십시오.

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

편집 html2canvas 스크립트를 별도로 사용할 수 있습니다 여기에 몇 가지 여기 예 .

편집 2 Google이 매우 유사한 방법을 사용한다는 또 다른 확인 (사실, 문서를 기반으로 한 유일한 차이점은 트래버스 / 그리기의 비동기 방법뿐 임)이 Google 피드백 팀의 Elliott Sprehn이 프레젠테이션에서 찾을 수 있습니다. http : //www.elliottsprehn.com/preso/fluentconf/


1
매우 멋진 Sikuli 또는 Selenium은 테스트 도구에서 html2canvas.js로 렌더링 된 이미지와 픽셀 유사성으로 사이트의 샷을 비교하여 다른 사이트로가는 데 좋습니다! getBoundingClientRect를 사용할 수없는 브라우저의 대체 데이터 소스를 구문 분석하는 방법을 찾기 위해 매우 간단한 공식 솔버로 DOM의 일부를 자동으로 탐색 할 수 있는지 궁금합니다. 나는 그것이 오픈 소스라면 아마도 그것을 사용하려고 생각했을 것입니다. 좋은 일 Niklas!
Luke Stanley

1
@Luke Stanley 나는 이번 주말에 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 : // flags에서 화면 캡처 지원을 활성화해야합니다.


2
스크린 샷을 찍는 데모를 찾을 수 없습니다. 모든 것이 화면 공유에 관한 것입니다. 시도해야합니다.
jwl

8
@XMight, 화면 캡처 지원 플래그를 토글하여이를 허용할지 여부를 선택할 수 있습니다.
매트 싱클레어

19
@XMight 제발 그렇게 생각하지 마십시오. 웹 브라우저는 많은 작업을 수행 할 수 있어야하지만 불행히도 구현과 일치하지 않습니다. 사용자에게 요청되는 한 브라우저에 이러한 기능이 있으면 괜찮습니다. 아무도주의를 기울이지 않고 스크린 샷을 만들 수 없습니다. 그러나 너무 많은 두려움으로 인해 클립 보드 API와 같이 구현이 완전히 불가능
해져

3
이 사용되지 않습니다 표준에있어서의에 제거됩니다 developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
아구스틴 Cautin에게

7
@AgustinCautin Navigator.getUserMedia()은 더 이상 사용되지 않지만 바로 아래에 "...라는 최신 navigator.mediaDevices.getUserMedia () "를 사용하십시오 . 즉, 새로운 API로 대체되었습니다.
levant 님이

37

으로 니클라스 언급 당신은 사용할 수 있습니다 html2canvas의 브라우저에서 JS를 사용하여 스크린 샷을 라이브러리를. 이 라이브러리를 사용하여 스크린 샷을 찍는 예를 제공 하여이 시점에서 그의 대답을 확장 할 것입니다.

에서 report()함수 onrendered데이터 URI로 이미지를 얻기 후에는 사용자에게 보여 그를 마우스로 "버그 영역을"그릴 다음 서버에 스크린 샷 및 지역 좌표를 보낼 수 있습니다.

이 예제 에서는 async/await버전 : with nice makeScreenshot()function 이 만들어졌습니다 .

최신 정보

스크린 샷을 찍고, 지역을 선택하고, 버그를 설명하고 POST 요청 ( 여기서는 jsfiddle )을 보낼 수있는 간단한 예입니다 (주 함수는 report()).


10
마이너스 포인트를
주려면

나는 당신이 downvoted되고있는 이유는 html2canvas 라이브러리가 그의 라이브러리 일뿐 만 아니라 그가 지적한 도구가 아닌 것 같습니다.
zfrisch

후 처리 효과 (블러 필터)를 캡처하지 않으려는 경우에는 문제가 없습니다.
vintproykt

제한 사항 프록시가 사용하지 않고 스크립트를 읽을 수 있으려면 스크립트가 사용하는 모든 이미지가 동일한 원점에 있어야합니다. 마찬가지로, 페이지에 다른 원본 요소로 오염 된 다른 캔버스 요소가 있으면 더러워지고 더 이상 html2canvas에서 읽을 수 없게됩니다.
aravind3 '

13

getDisplayMedia API를 사용하여 스크린 샷을 Canvas 또는 Jpeg Blob / ArrayBuffer로 가져옵니다 .

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

이것이 왜 단 하나의 공감대를 가지고 있었는지 궁금합니다. 이것이 정말 도움이되었습니다!
Jay Dadhania

어떻게 작동합니까? 나와 같은 초보자를위한 데모를 제공 할 수 있습니까? Thx
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}`));

또한 Screen Capture API 문서도 확인해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.