@xata.io/screenshot
Version:
A zero-dependency browser-native way to take screenshots powered by the native web MediaDevices API.
137 lines (121 loc) • 6.72 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.XataScreenshot = {}));
})(this, (function (exports) { 'use strict';
/**
* Checks if the current browser supports the MediaDevices API.
*/
const checkIfBrowserSupported = () => { var _a; return Boolean((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getDisplayMedia); };
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
const createVideoElementToCaptureFrames = (mediaStream) => {
const video = document.createElement("video");
video.autoplay = true;
video.muted = true;
video.playsInline = true;
video.srcObject = mediaStream;
video.setAttribute("style", "position:fixed;top:0;left:0;pointer-events:none;visibility:hidden;");
return video;
};
const paintVideoFrameOnCanvas = (video) => {
var _a, _b, _c;
// Get the video settings
// @ts-ignore because getTracks is very much valid in modern browsers
const videoTrackSettings = (_a = video.srcObject) === null || _a === void 0 ? void 0 : _a.getTracks()[0].getSettings();
// Create a canvas with the video's size and draw the video frame on it
const canvas = document.createElement("canvas");
canvas.width = (_b = videoTrackSettings === null || videoTrackSettings === void 0 ? void 0 : videoTrackSettings.width) !== null && _b !== void 0 ? _b : 0;
canvas.height = (_c = videoTrackSettings === null || videoTrackSettings === void 0 ? void 0 : videoTrackSettings.height) !== null && _c !== void 0 ? _c : 0;
const ctx = canvas.getContext("2d");
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0);
return canvas;
};
const playCameraClickSound = (url) => {
const audio = document.createElement("audio");
audio.loop = false;
audio.src = url;
audio.play();
audio.remove();
};
const sleep = (timeoutInMs = 300) => new Promise((r) => {
setTimeout(r, timeoutInMs);
});
const stopCapture = (video) => {
var _a;
// @ts-ignore because getTracks is very much valid in modern browsers
const tracks = (_a = video.srcObject) === null || _a === void 0 ? void 0 : _a.getTracks();
tracks === null || tracks === void 0 ? void 0 : tracks.forEach((track) => track.stop());
// This is the only way to clean up a video stream in the browser so...
// eslint-disable-next-line no-param-reassign
video.srcObject = null;
video.remove();
};
const waitForFocus = (result) => __awaiter(void 0, void 0, void 0, function* () {
yield sleep();
if (document.hasFocus()) {
return result;
}
return waitForFocus(result);
});
/**
* Takes a screenshot of the current page using a the native browser [`MediaDevices`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
*/
const takeScreenshot = ({ onCaptureEnd, onCaptureStart, quality = 0.7, type = "image/jpeg", soundEffectUrl, } = {}) => __awaiter(void 0, void 0, void 0, function* () {
yield (onCaptureStart === null || onCaptureStart === void 0 ? void 0 : onCaptureStart());
return navigator.mediaDevices
.getDisplayMedia({
// This is actually supported, but only in Chrome so not yet part of the TS typedefs, so
// @ts-ignore
preferCurrentTab: true,
video: { frameRate: 30 },
})
.then(waitForFocus) // We can only proceed if our tab is in focus.
.then((result) => __awaiter(void 0, void 0, void 0, function* () {
// So we mount the screen capture to a video element...
const video = createVideoElementToCaptureFrames(result);
// ...which needs to be in the DOM but invisible so we can capture it.
document.body.appendChild(video);
// Now, we need to wait a bit to capture the right moment...
// Hide this modal...
// Play camera click sound, because why not
if (soundEffectUrl) {
playCameraClickSound(soundEffectUrl);
}
// Wait for the video feed...
yield sleep();
// Paint the video frame on a canvas...
const canvas = paintVideoFrameOnCanvas(video);
// Set the data URL in state
const screenshot = canvas.toDataURL(type, quality);
// Stop sharing screen.
stopCapture(video);
// Clean up
canvas.remove();
yield (onCaptureEnd === null || onCaptureEnd === void 0 ? void 0 : onCaptureEnd());
return screenshot;
}));
});
exports.checkIfBrowserSupported = checkIfBrowserSupported;
exports.takeScreenshot = takeScreenshot;
Object.defineProperty(exports, '__esModule', { value: true });
}));