@uppy/screen-capture
Version:
Uppy plugin that captures video from display or application.
380 lines (360 loc) • 11.9 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import { h } from 'preact';
import { UIPlugin } from '@uppy/core';
import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension';
import ScreenRecIcon from "./ScreenRecIcon.js";
import RecorderScreen from "./RecorderScreen.js";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore We don't want TS to generate types for the package.json
const packageJson = {
"version": "4.2.1"
};
import locale from './locale.js';
// Check if screen capturing is supported.
// mediaDevices is supprted on mobile Safari, getDisplayMedia is not
function isScreenRecordingSupported() {
var _navigator$mediaDevic;
return window.MediaRecorder && ((_navigator$mediaDevic = navigator.mediaDevices) == null ? void 0 : _navigator$mediaDevic.getDisplayMedia); // eslint-disable-line compat/compat
}
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
function getMediaDevices() {
return window.MediaRecorder && navigator.mediaDevices; // eslint-disable-line compat/compat
}
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
const defaultOptions = {
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#Properties_of_shared_screen_tracks
displayMediaConstraints: {
video: {
width: 1280,
height: 720,
frameRate: {
ideal: 3,
max: 5
},
cursor: 'motion',
displaySurface: 'monitor'
}
},
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/audio
userMediaConstraints: {
audio: true
},
preferredVideoMimeType: 'video/webm'
};
export default class ScreenCapture extends UIPlugin {
constructor(uppy, opts) {
super(uppy, {
...defaultOptions,
...opts
});
this.videoStream = null;
this.audioStream = null;
this.userDenied = false;
this.recorder = null;
this.outputStream = null;
this.recordingChunks = null;
this.mediaDevices = getMediaDevices();
// eslint-disable-next-line no-restricted-globals
this.protocol = location.protocol === 'https:' ? 'https' : 'http';
this.id = this.opts.id || 'ScreenCapture';
this.type = 'acquirer';
this.icon = ScreenRecIcon;
this.defaultLocale = locale;
this.i18nInit();
this.title = this.i18n('pluginNameScreenCapture');
// uppy plugin class related
this.install = this.install.bind(this);
this.setPluginState = this.setPluginState.bind(this);
this.render = this.render.bind(this);
// screen capturer related
this.start = this.start.bind(this);
this.stop = this.stop.bind(this);
this.startRecording = this.startRecording.bind(this);
this.stopRecording = this.stopRecording.bind(this);
this.submit = this.submit.bind(this);
this.streamInterrupted = this.streamInactivated.bind(this);
// initialize
this.captureActive = false;
this.capturedMediaFile = null;
}
install() {
if (!isScreenRecordingSupported()) {
this.uppy.log('Screen recorder access is not supported', 'warning');
return null;
}
this.setPluginState({
streamActive: false,
audioStreamActive: false
});
const {
target
} = this.opts;
if (target) {
this.mount(target, this);
}
return undefined;
}
uninstall() {
if (this.videoStream) {
this.stop();
}
this.unmount();
}
start() {
if (!this.mediaDevices) {
return Promise.reject(new Error('Screen recorder access not supported'));
}
this.captureActive = true;
this.selectAudioStreamSource();
return this.selectVideoStreamSource().then(res => {
// something happened in start -> return
if (res === false) {
// Close the Dashboard panel if plugin is installed
// into Dashboard (could be other parent UI plugin)
// @ts-expect-error we can't know Dashboard types here
if (this.parent && this.parent.hideAllPanels) {
// @ts-expect-error we can't know Dashboard types here
this.parent.hideAllPanels();
this.captureActive = false;
}
}
});
}
selectVideoStreamSource() {
// if active stream available, return it
if (this.videoStream) {
return new Promise(resolve => resolve(this.videoStream));
}
// ask user to select source to record and get mediastream from that
// eslint-disable-next-line compat/compat
return this.mediaDevices.getDisplayMedia(this.opts.displayMediaConstraints).then(videoStream => {
this.videoStream = videoStream;
// add event listener to stop recording if stream is interrupted
this.videoStream.addEventListener('inactive', () => {
this.streamInactivated();
});
this.setPluginState({
streamActive: true
});
return videoStream;
}).catch(err => {
this.setPluginState({
screenRecError: err
});
this.userDenied = true;
setTimeout(() => {
this.userDenied = false;
}, 1000);
return false;
});
}
selectAudioStreamSource() {
// if active stream available, return it
if (this.audioStream) {
return new Promise(resolve => resolve(this.audioStream));
}
// ask user to select source to record and get mediastream from that
// eslint-disable-next-line compat/compat
return this.mediaDevices.getUserMedia(this.opts.userMediaConstraints).then(audioStream => {
this.audioStream = audioStream;
this.setPluginState({
audioStreamActive: true
});
return audioStream;
}).catch(err => {
if (err.name === 'NotAllowedError') {
this.uppy.info(this.i18n('micDisabled'), 'error', 5000);
this.uppy.log(this.i18n('micDisabled'), 'warning');
}
return false;
});
}
startRecording() {
const options = {};
this.capturedMediaFile = null;
this.recordingChunks = [];
const {
preferredVideoMimeType
} = this.opts;
this.selectVideoStreamSource().then(videoStream => {
if (videoStream === false) {
throw new Error('No video stream available');
}
// Attempt to use the passed preferredVideoMimeType (if any) during recording.
// If the browser doesn't support it, we'll fall back to the browser default instead
if (preferredVideoMimeType && MediaRecorder.isTypeSupported(preferredVideoMimeType) && getFileTypeExtension(preferredVideoMimeType)) {
options.mimeType = preferredVideoMimeType;
}
// prepare tracks
const tracks = [videoStream.getVideoTracks()[0]];
// merge audio if exits
if (this.audioStream) {
tracks.push(this.audioStream.getAudioTracks()[0]);
}
// create new stream from video and audio
// eslint-disable-next-line compat/compat
this.outputStream = new MediaStream(tracks);
// initialize mediarecorder
// eslint-disable-next-line compat/compat
this.recorder = new MediaRecorder(this.outputStream, options);
// push data to buffer when data available
this.recorder.addEventListener('dataavailable', event => {
this.recordingChunks.push(event.data);
});
// start recording
this.recorder.start();
// set plugin state to recording
this.setPluginState({
recording: true
});
}).catch(err => {
this.uppy.log(err, 'error');
});
}
streamInactivated() {
// get screen recorder state
const {
recordedVideo,
recording
} = {
...this.getPluginState()
};
if (!recordedVideo && !recording) {
// Close the Dashboard panel if plugin is installed
// into Dashboard (could be other parent UI plugin)
// @ts-expect-error we can't know Dashboard types here
if (this.parent && this.parent.hideAllPanels) {
// @ts-expect-error we can't know Dashboard types here
this.parent.hideAllPanels();
}
} else if (recording) {
// stop recorder if it is active
this.uppy.log('Capture stream inactive — stop recording');
this.stopRecording();
}
this.videoStream = null;
this.audioStream = null;
this.setPluginState({
streamActive: false,
audioStreamActive: false
});
}
stopRecording() {
const stopped = new Promise(resolve => {
this.recorder.addEventListener('stop', () => {
resolve();
});
this.recorder.stop();
});
return stopped.then(() => {
// recording stopped
this.setPluginState({
recording: false
});
// get video file after recorder stopped
return this.getVideo();
}).then(file => {
// store media file
this.capturedMediaFile = file;
// create object url for capture result preview
this.setPluginState({
// eslint-disable-next-line compat/compat
recordedVideo: URL.createObjectURL(file.data)
});
}).then(() => {
this.recordingChunks = null;
this.recorder = null;
}, error => {
this.recordingChunks = null;
this.recorder = null;
throw error;
});
}
submit() {
try {
// add recorded file to uppy
if (this.capturedMediaFile) {
this.uppy.addFile(this.capturedMediaFile);
}
} catch (err) {
// Logging the error, exept restrictions, which is handled in Core
if (!err.isRestriction) {
this.uppy.log(err, 'warning');
}
}
}
stop() {
// flush video stream
if (this.videoStream) {
this.videoStream.getVideoTracks().forEach(track => {
track.stop();
});
this.videoStream.getAudioTracks().forEach(track => {
track.stop();
});
this.videoStream = null;
}
// flush audio stream
if (this.audioStream) {
this.audioStream.getAudioTracks().forEach(track => {
track.stop();
});
this.audioStream.getVideoTracks().forEach(track => {
track.stop();
});
this.audioStream = null;
}
// flush output stream
if (this.outputStream) {
this.outputStream.getAudioTracks().forEach(track => {
track.stop();
});
this.outputStream.getVideoTracks().forEach(track => {
track.stop();
});
this.outputStream = null;
}
// remove preview video
this.setPluginState({
recordedVideo: null
});
this.captureActive = false;
}
getVideo() {
const mimeType = this.recordingChunks[0].type;
const fileExtension = getFileTypeExtension(mimeType);
if (!fileExtension) {
return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`));
}
const name = `screencap-${Date.now()}.${fileExtension}`;
const blob = new Blob(this.recordingChunks, {
type: mimeType
});
const file = {
source: this.id,
name,
data: new Blob([blob], {
type: mimeType
}),
type: mimeType
};
return Promise.resolve(file);
}
render() {
// get screen recorder state
const recorderState = this.getPluginState();
if (!recorderState.streamActive && !this.captureActive && !this.userDenied) {
this.start();
}
return h(RecorderScreen, _extends({}, recorderState, {
// eslint-disable-line react/jsx-props-no-spreading
onStartRecording: this.startRecording,
onStopRecording: this.stopRecording,
onStop: this.stop,
onSubmit: this.submit,
i18n: this.i18n,
stream: this.videoStream
}));
}
}
ScreenCapture.VERSION = packageJson.version;