Używanie HTML5 / Canvas / JavaScript do robienia zrzutów ekranu w przeglądarce

923

„Zgłoś błąd” lub „Narzędzie opinii” Google pozwala wybrać obszar okna przeglądarki, aby utworzyć zrzut ekranu z informacją o błędzie.

Zrzut ekranu narzędzia Google Feedback Zrzut ekranu autorstwa Jasona Smalla, opublikowany w zduplikowanym pytaniu .

Jak oni to robią? Interfejs API JavaScript do przesyłania opinii Google jest ładowany stąd, a ich przegląd modułu opinii pokaże możliwości zrzutów ekranu.

joelvh
źródło
2
Elliott Sprehn napisał w tweecie kilka dni temu:> @CatChen Ten post przepełnienia stosu nie jest dokładny. Zrzut ekranu Google Feedback jest wykonywany w całości po stronie klienta. :)
Goran Rakic
1
Wydaje się to logiczne, ponieważ chcą dokładnie uchwycić sposób, w jaki przeglądarka renderuje stronę, a nie sposób, w jaki renderują ją po stronie serwera za pomocą silnika. Jeśli wyślesz tylko bieżącą stronę DOM do serwera, przeoczy ona wszelkie niespójności w sposobie wyświetlania HTML przez przeglądarkę. Nie oznacza to, że odpowiedź Chena jest zła przy robieniu zrzutów ekranu, po prostu wygląda na to, że Google robi to w inny sposób.
Goran Rakic
Elliott wspomniał dziś o Jan Kuča, a ten link znalazłem w tweecie
Cat Chen
Zajmę się tym później i zobaczę, jak można to zrobić za pomocą mechanizmu renderowania po stronie klienta i sprawdzę, czy Google rzeczywiście robi to w ten sposób.
Cat Chen,
Widzę użycie funkcji CompareDocumentPosition, getBoxObjectFor, toDataURL, drawImage, padding śledzenia i podobnych rzeczy. Tysiące linii zaciemnionego kodu służą do usuwania zaciemnienia i przeglądania. Chciałbym zobaczyć wersję na licencji Open Source, skontaktowałem się z Elliott Sprehn!
Luke Stanley,

Odpowiedzi:

1153

JavaScript może odczytywać DOM i renderować dość dokładne przedstawienie tego za pomocą canvas. Pracowałem nad skryptem, który konwertuje HTML na obraz na płótnie. Zdecydowałem dzisiaj, aby wdrożyć to w wysyłanie informacji zwrotnych, jak opisano.

Skrypt umożliwia tworzenie formularzy opinii, które zawierają zrzut ekranu utworzony w przeglądarce klienta wraz z formularzem. Zrzut ekranu jest oparty na modelu DOM i jako taki może nie być w 100% dokładny do rzeczywistej reprezentacji, ponieważ nie tworzy rzeczywistego zrzutu ekranu, ale tworzy zrzut ekranu na podstawie informacji dostępnych na stronie.

Nie wymaga żadnego renderowania z serwera , ponieważ cały obraz jest tworzony w przeglądarce klienta. Sam skrypt HTML2Canvas jest nadal w bardzo eksperymentalnym stanie, ponieważ nie analizuje prawie tyle atrybutów CSS3, ile chciałbym, ani nie obsługuje ładowania obrazów CORS, nawet jeśli proxy było dostępne.

Nadal dość ograniczona kompatybilność z przeglądarkami (nie dlatego, że nie można było obsłużyć większej liczby, po prostu nie miałem czasu, aby uczynić ją bardziej obsługiwaną przez różne przeglądarki).

Aby uzyskać więcej informacji, spójrz na przykłady tutaj:

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

edycja Skrypt html2canvas jest teraz dostępny osobno tutaj i kilka przykładów tutaj .

edytuj 2 Kolejne potwierdzenie, że Google stosuje bardzo podobną metodę (w rzeczywistości, na podstawie dokumentacji, jedyną zasadniczą różnicą jest ich asynchroniczna metoda przejścia / rysowania) można znaleźć w tej prezentacji Elliott Sprehn z zespołu Google Feedback: http: //www.elliottsprehn.com/preso/fluentconf/

Niklas
źródło
1
Bardzo fajne, Sikuli lub Selenium mogą być dobre do przechodzenia do różnych stron, porównując ujęcie strony z narzędzia testowego do renderowanego obrazu html2canvas.js pod względem podobieństwa pikseli! Zastanawiam się, czy możesz automatycznie przechodzić przez części DOM za pomocą bardzo prostego narzędzia do rozwiązywania formuł, aby znaleźć sposób analizowania alternatywnych źródeł danych dla przeglądarek, w których getBoundingClientRect nie jest dostępny. Prawdopodobnie skorzystałbym z tego, gdyby był to program typu open source, sam rozważałem zabawę. Dobra robota, Niklas!
Luke Stanley,
1
@Luke Stanley Najprawdopodobniej wyrzucę źródło na github w ten weekend, wciąż kilka drobnych porządków i zmian, które chcę wprowadzić przedtem, a także pozbędę się niepotrzebnej zależności jQuery, którą obecnie ma.
Niklas
43
Kod źródłowy jest teraz dostępny na github.com/niklasvh/html2canvas , niektóre przykłady używanego skryptu html2canvas.hertzen.com tam. Wciąż wiele błędów do naprawienia, więc nie polecałbym jeszcze używania skryptu w środowisku na żywo.
Niklas
2
każde rozwiązanie, które sprawi, że będzie działać dla SVG, będzie bardzo pomocne. To nie działa z highcharts.com
Jagdeep
3
@Niklas Widzę, że twój przykład przerodził się w prawdziwy projekt. Może zaktualizuj swój najbardziej ceniony komentarz na temat eksperymentalnej natury projektu. Po prawie 900 zobowiązaniach pomyślałbym, że w tym momencie to trochę więcej niż eksperyment ;-)
Jogai
70

Twoja aplikacja internetowa może teraz wykonać „natywny” zrzut ekranu całego pulpitu klienta, używając getUserMedia():

Spójrz na ten przykład:

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

Klient będzie musiał używać chrome (na razie) i będzie musiał włączyć obsługę przechwytywania ekranu pod flagami chrome: //.

Matt Sinclair
źródło
2
nie mogę znaleźć żadnych demonstracji po prostu zrobienia zrzutu ekranu - wszystko dotyczy udostępniania ekranu. będę musiał spróbować.
jwl
8
@XMight, możesz zdecydować, czy na to zezwolić, przełączając flagę obsługi przechwytywania ekranu.
Matt Sinclair,
19
@XMight Proszę nie myśleć w ten sposób. Przeglądarki internetowe powinny być w stanie robić wiele rzeczy, ale niestety nie są spójne z ich implementacjami. Jest absolutnie w porządku, jeśli przeglądarka ma taką funkcjonalność, o ile użytkownik jest pytany. Nikt nie będzie w stanie wykonać zrzutu ekranu bez Twojej uwagi. Ale zbyt duży strach powoduje złe implementacje, takie jak całkowicie wyłączony interfejs API schowka, zamiast tego tworzą okna dialogowe potwierdzenia, takie jak w przypadku kamer internetowych, mikrofonów, możliwości zrzutów ekranu itp.
StanE
3
To było przestarzałe i zostanie usunięte ze standardu zgodnie z developer.mozilla.org/en-US/docs/Web/API/Navigator/getUserMedia
Agustin Cautin
7
@AgustinCautin Navigator.getUserMedia()jest przestarzałe, ale tuż pod nim jest napisane „... Proszę użyć nowszej navigator.mediaDevices.getUserMedia () ”, tj. Właśnie został zastąpiony nowszym API.
levant pied
37

Jak wspomniał Niklas , możesz użyć biblioteki html2canvas, aby zrobić zrzut ekranu za pomocą JS w przeglądarce. Rozszerzę jego odpowiedź w tym punkcie, podając przykład wykonania zrzutu ekranu przy użyciu tej biblioteki:

W report()funkcji onrenderedpo uzyskaniu obrazu jako URI danych możesz pokazać go użytkownikowi i pozwolić mu na narysowanie „regionu błędu” myszką, a następnie wysłać zrzut ekranu i współrzędne regionu na serwer.

W tym przykładzie async/await powstała wersja: z ładną makeScreenshot()funkcją .

AKTUALIZACJA

Prosty przykład, który pozwala zrobić zrzut ekranu, wybrać region, opisać błąd i wysłać żądanie POST ( tutaj jsfiddle ) (główną funkcją jest report()).

Kamil Kiełczewski
źródło
10
Jeśli chcesz dać punkt ujemny, zostaw również komentarz z wyjaśnieniem
Kamil Kiełczewski,
Myślę, że powodem, dla którego jesteś zaniedbywany, jest najprawdopodobniej, że biblioteka html2canvas jest jego biblioteką, a nie narzędziem, które po prostu wskazał.
zfrisch,
W porządku, jeśli nie chcesz przechwytywać efektów przetwarzania końcowego (jako filtr rozmycia).
vintproykt
Ograniczenia Wszystkie obrazy używane przez skrypt muszą znajdować się w tym samym źródle, aby można je było odczytać bez pomocy serwera proxy. Podobnie, jeśli na stronie znajdują się inne elementy płótna, które zostały skażone treściami pochodzącymi z różnych źródeł, zostaną one zabrudzone i nie będą już widoczne dla html2canvas.
aravind3
13

Pobierz zrzut ekranu jako Canvas lub Jpeg Blob / ArrayBuffer, używając interfejsu 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)
    })
}

PRÓBNY:

// 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
źródło
Zastanawiam się, dlaczego miał tylko 1 głos pozytywny, okazało się to naprawdę pomocne!
Jay Dadhania
Proszę, jak to działa? Czy możesz dostarczyć demo dla początkujących, takich jak ja? Thx
kabrice
@kabrice Dodałem demo. Wystarczy umieścić kod w konsoli Chrome. Jeśli potrzebujesz obsługi starych przeglądarek, użyj: babeljs.io/en/repl
Nikolay Makhonin
8

Oto przykład użycia: 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}`));

Warto również sprawdzić dokumentację API Screen Capture .

JSON C11
źródło