HTML5 / Canvas / JavaScriptを使用してブラウザー内のスクリーンショットを撮る


924

Googleの「バグの報告」または「フィードバックツール」を使用すると、ブラウザウィンドウの領域を選択して、バグに関するフィードバックとともに送信されるスクリーンショットを作成できます。

Googleフィードバックツールのスクリーンショット 重複質問に投稿されたJason Smallのスクリーンショット

彼らはこれをどのように行っていますか?GoogleのJavaScriptのフィードバックAPIからロードされ、ここフィードバックモジュールのその概要は、スクリーンショット機能のデモンストレーションを行います。


2
Elliott Sprehn 数日前にツイートに書き込みました。> @CatChenそのstackoverflowの投稿は正確ではありません。Googleフィードバックのスクリーンショットは完全にクライアント側で行われます。:)
Goran Rakic

1
これは、ユーザーのブラウザがページをレンダリングする方法を正確にキャッチしたいので、エンジンを使用してサーバー側でページをレンダリングする方法ではなく、論理的に継ぎ合わせます。現在のページのDOMのみをサーバーに送信すると、ブラウザーがHTMLをレンダリングする方法の不整合が失われます。これは、スクリーンショットを撮るのにチェンの答えが間違っていることを意味するのではなく、Googleが別の方法で行っているように見えるだけです。
Goran Rakic

エリオットは、今日の月Kučaを述べた、と私は月のつぶやきでこのリンクを見つけた:jankuca.tumblr.com/post/7391640769/...
猫陳

これについては後で詳しく説明し、クライアント側のレンダリングエンジンでどのように実行できるかを確認し、Googleが実際にそのように実行しているかどうかを確認します。
Cat Chen

compareDocumentPosition、getBoxObjectFor、toDataURL、drawImage、トラッキングパディングなどの使用を確認します。難読化を解除して調べるのは、何千行もの難読化されたコードです。私はそれのオープンソースライセンス版を見たいと思います。エリオットスプレンに連絡しました!
ルークスタンレー

回答:


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
ピクセルの類似性に関して、テストツールからのサイトのショットとhtml2canvas.jsでレンダリングされたイメージを比較して、非常にクールで、SikuliまたはSeleniumは別のサイトに行くのに適しているかもしれません!非常に単純な数式ソルバーを使用してDOMの一部を自動的にトラバースして、getBoundingClientRectが使用できないブラウザーの代替データソースを解析する方法を見つけることができるかどうか疑問に思います。もしそれがオープンソースで、自分でいじることを考えていたなら、私はおそらくこれを使うでしょう。いい仕事Niklas!
ルークスタンリー、

1
@Luke Stanley今週末、ソースをgithubに投げる可能性が高いです。それでも、それまでにちょっとしたクリーンアップと変更を行い、現在必要なjQueryの依存関係を取り除きます。
Niklas

43
ソースコードがgithub.com/niklasvh/html2canvasで利用可能になりました。そこで使用されているスクリプトの例として、html2canvas.hertzen.comがあります。まだ修正すべきバグがたくさんあるので、まだライブ環境でスクリプトを使用することはお勧めしません。
Niklas、2011

2
SVGで機能させるためのソリューションは、非常に役立ちます。highcharts.comでは機能しません
Jagdeep、

3
@Niklas私はあなたの例が実際のプロジェクトに成長したのを見ます。プロジェクトの実験的な性質についての最も賛成のコメントを更新してください。ほぼ900回のコミットの後、私はそれがこの時点での実験よりも少し多いと思うでしょう;-)
Jogai

70

Webアプリは、次を使用してクライアントのデスクトップ全体の「ネイティブ」スクリーンショットを取得できるようになりましたgetUserMedia()

この例を見てください:

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

クライアントは(今のところ)chromeを使用している必要があり、chrome:// flagsで画面キャプチャのサポートを有効にする必要があります。


2
スクリーンショットを撮っただけのデモは見つかりません-すべてはすべて画面共有に関するものです。それを試さなければならないでしょう。
jwl 2014年

8
@XMightでは、画面キャプチャサポートフラグを切り替えることで、これを許可するかどうかを選択できます。
Matt Sinclair、

19
@XMightこのように考えないでください。Webブラウザーは多くのことを実行できるはずですが、残念ながら、それらの実装は一貫していません。ユーザーが要求されている限り、ブラウザにそのような機能があれば、それはまったく問題ありません。誰もあなたの注意なしにスクリーンショットを作成することはできません。しかし、あまりにも多くの恐怖は、完全に無効にされたクリップボードAPIなどの悪い実装に
つながり

3
これは非推奨であり、developer.mozilla.org
en

7
@AgustinCautin Navigator.getUserMedia()は非推奨ですが、そのすぐ下に「...新しいnavigator.mediaDevices.getUserMedia()を使用してください」と表示されます。つまり、新しいAPIに置き換えられただけです。
レバントが

37

ニクラスは言及を使用できhtml2canvasのブラウザでJSを使用してスクリーンショットを取るためのライブラリを。この点について、このライブラリを使用してスクリーンショットを撮る例を提供することで、彼の答えを拡張します。

report()機能でonrenderedデータURIとして画像を取得した後、あなたはそれをユーザーに示し、彼はマウスで「バグ地域を」描くし、サーバーにスクリーンショットや地域の座標を送信できるようにすることができます。

、この例 async/awaitのバージョンが作られました。素敵でmakeScreenshot()機能

更新

スクリーンショットを撮り、地域を選択し、バグを説明し、POSTリクエストを送信する簡単な例(ここではjsfiddle)(主な機能はreport())。


10
マイナスポイントを与えたい場合は、説明も
含めて

あなたが反対票を投じている理由は、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",
})

なぜこれが賛成票を1つしか持たなかったのか不思議に思いましたが、これは本当に役に立ちました!
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}`));

また、スクリーンキャプチャAPIドキュメントも確認してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.