|
@@ -1,31 +1,103 @@
|
|
|
-import { h } from 'preact'
|
|
|
-import { UIPlugin } from '@uppy/core'
|
|
|
+import { h, type ComponentChild } from 'preact'
|
|
|
+import { UIPlugin, Uppy, type UIPluginOptions } from '@uppy/core'
|
|
|
import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension'
|
|
|
-import ScreenRecIcon from './ScreenRecIcon.jsx'
|
|
|
-import RecorderScreen from './RecorderScreen.jsx'
|
|
|
+import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts'
|
|
|
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
|
|
|
+import ScreenRecIcon from './ScreenRecIcon.tsx'
|
|
|
+import RecorderScreen from './RecorderScreen.tsx'
|
|
|
|
|
|
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
|
+// @ts-ignore We don't want TS to generate types for the package.json
|
|
|
import packageJson from '../package.json'
|
|
|
-import locale from './locale.js'
|
|
|
+import locale from './locale.ts'
|
|
|
|
|
|
// Check if screen capturing is supported.
|
|
|
// mediaDevices is supprted on mobile Safari, getDisplayMedia is not
|
|
|
-function isScreenRecordingSupported () {
|
|
|
+function isScreenRecordingSupported() {
|
|
|
return window.MediaRecorder && navigator.mediaDevices?.getDisplayMedia // eslint-disable-line compat/compat
|
|
|
}
|
|
|
|
|
|
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
|
-function getMediaDevices () {
|
|
|
+function getMediaDevices() {
|
|
|
return window.MediaRecorder && navigator.mediaDevices // eslint-disable-line compat/compat
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Screen capture
|
|
|
- */
|
|
|
-export default class ScreenCapture extends UIPlugin {
|
|
|
+export interface ScreenCaptureOptions extends UIPluginOptions {
|
|
|
+ title?: string
|
|
|
+ displayMediaConstraints?: MediaStreamConstraints
|
|
|
+ userMediaConstraints?: MediaStreamConstraints
|
|
|
+ preferredVideoMimeType?: string
|
|
|
+}
|
|
|
+
|
|
|
+// 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',
|
|
|
+}
|
|
|
+
|
|
|
+type Opts = DefinePluginOpts<ScreenCaptureOptions, keyof typeof defaultOptions>
|
|
|
+
|
|
|
+export type ScreenCaptureState = {
|
|
|
+ streamActive: boolean
|
|
|
+ audioStreamActive: boolean
|
|
|
+ recording: boolean
|
|
|
+ recordedVideo: string | null
|
|
|
+ screenRecError: string | null
|
|
|
+}
|
|
|
+
|
|
|
+export default class ScreenCapture<
|
|
|
+ M extends Meta,
|
|
|
+ B extends Body,
|
|
|
+> extends UIPlugin<Opts, M, B, ScreenCaptureState> {
|
|
|
static VERSION = packageJson.version
|
|
|
|
|
|
- constructor (uppy, opts) {
|
|
|
- super(uppy, opts)
|
|
|
+ mediaDevices: MediaDevices
|
|
|
+
|
|
|
+ protocol: string
|
|
|
+
|
|
|
+ icon: ComponentChild
|
|
|
+
|
|
|
+ streamInterrupted: () => void
|
|
|
+
|
|
|
+ captureActive: boolean
|
|
|
+
|
|
|
+ capturedMediaFile: null | {
|
|
|
+ source: string
|
|
|
+ name: string
|
|
|
+ data: Blob
|
|
|
+ type: string
|
|
|
+ }
|
|
|
+
|
|
|
+ videoStream: null | MediaStream
|
|
|
+
|
|
|
+ audioStream: null | MediaStream
|
|
|
+
|
|
|
+ userDenied: boolean
|
|
|
+
|
|
|
+ recorder: null | MediaRecorder
|
|
|
+
|
|
|
+ outputStream: null | MediaStream
|
|
|
+
|
|
|
+ recordingChunks: Blob[] | null
|
|
|
+
|
|
|
+ constructor(uppy: Uppy<M, B>, opts?: ScreenCaptureOptions) {
|
|
|
+ super(uppy, { ...defaultOptions, ...opts })
|
|
|
this.mediaDevices = getMediaDevices()
|
|
|
// eslint-disable-next-line no-restricted-globals
|
|
|
this.protocol = location.protocol === 'https:' ? 'https' : 'http'
|
|
@@ -36,32 +108,6 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
|
|
|
this.defaultLocale = locale
|
|
|
|
|
|
- // set default options
|
|
|
- // 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',
|
|
|
- }
|
|
|
-
|
|
|
- // merge default options with the ones set by user
|
|
|
- this.opts = { ...defaultOptions, ...opts }
|
|
|
-
|
|
|
// i18n
|
|
|
this.i18nInit()
|
|
|
|
|
@@ -83,7 +129,7 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
this.capturedMediaFile = null
|
|
|
}
|
|
|
|
|
|
- install () {
|
|
|
+ install(): null | undefined {
|
|
|
if (!isScreenRecordingSupported()) {
|
|
|
this.uppy.log('Screen recorder access is not supported', 'warning')
|
|
|
return null
|
|
@@ -102,7 +148,7 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
return undefined
|
|
|
}
|
|
|
|
|
|
- uninstall () {
|
|
|
+ uninstall(): void {
|
|
|
if (this.videoStream) {
|
|
|
this.stop()
|
|
|
}
|
|
@@ -110,7 +156,7 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
this.unmount()
|
|
|
}
|
|
|
|
|
|
- start () {
|
|
|
+ start(): Promise<void> {
|
|
|
if (!this.mediaDevices) {
|
|
|
return Promise.reject(new Error('Screen recorder access not supported'))
|
|
|
}
|
|
@@ -119,29 +165,31 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
|
|
|
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)
|
|
|
- if (this.parent && this.parent.hideAllPanels) {
|
|
|
- this.parent.hideAllPanels()
|
|
|
- this.captureActive = false
|
|
|
- }
|
|
|
+ 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 () {
|
|
|
+ selectVideoStreamSource(): Promise<MediaStream | false> {
|
|
|
// if active stream available, return it
|
|
|
if (this.videoStream) {
|
|
|
- return new Promise(resolve => resolve(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)
|
|
|
+ return this.mediaDevices
|
|
|
+ .getDisplayMedia(this.opts.displayMediaConstraints)
|
|
|
.then((videoStream) => {
|
|
|
this.videoStream = videoStream
|
|
|
|
|
@@ -171,15 +219,16 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- selectAudioStreamSource () {
|
|
|
+ selectAudioStreamSource(): Promise<MediaStream | false> {
|
|
|
// if active stream available, return it
|
|
|
if (this.audioStream) {
|
|
|
- return new Promise(resolve => resolve(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)
|
|
|
+ return this.mediaDevices
|
|
|
+ .getUserMedia(this.opts.userMediaConstraints)
|
|
|
.then((audioStream) => {
|
|
|
this.audioStream = audioStream
|
|
|
|
|
@@ -198,19 +247,24 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- startRecording () {
|
|
|
- const options = {}
|
|
|
+ startRecording(): void {
|
|
|
+ const options: { mimeType?: string } = {}
|
|
|
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)) {
|
|
|
+ if (
|
|
|
+ preferredVideoMimeType &&
|
|
|
+ MediaRecorder.isTypeSupported(preferredVideoMimeType) &&
|
|
|
+ getFileTypeExtension(preferredVideoMimeType)
|
|
|
+ ) {
|
|
|
options.mimeType = preferredVideoMimeType
|
|
|
}
|
|
|
|
|
@@ -232,7 +286,7 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
|
|
|
// push data to buffer when data available
|
|
|
this.recorder.addEventListener('dataavailable', (event) => {
|
|
|
- this.recordingChunks.push(event.data)
|
|
|
+ this.recordingChunks!.push(event.data)
|
|
|
})
|
|
|
|
|
|
// start recording
|
|
@@ -248,14 +302,16 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- streamInactivated () {
|
|
|
+ streamInactivated(): void {
|
|
|
// 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) {
|
|
@@ -268,46 +324,53 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
this.audioStream = null
|
|
|
|
|
|
this.setPluginState({
|
|
|
- streamActive: false, audioStreamActive: false,
|
|
|
+ streamActive: false,
|
|
|
+ audioStreamActive: false,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- stopRecording () {
|
|
|
- const stopped = new Promise((resolve) => {
|
|
|
- this.recorder.addEventListener('stop', () => {
|
|
|
+ stopRecording(): Promise<void> {
|
|
|
+ const stopped = new Promise<void>((resolve) => {
|
|
|
+ this.recorder!.addEventListener('stop', () => {
|
|
|
resolve()
|
|
|
})
|
|
|
|
|
|
- this.recorder.stop()
|
|
|
+ this.recorder!.stop()
|
|
|
})
|
|
|
|
|
|
- return stopped.then(() => {
|
|
|
- // recording stopped
|
|
|
- this.setPluginState({
|
|
|
- recording: false,
|
|
|
+ return stopped
|
|
|
+ .then(() => {
|
|
|
+ // recording stopped
|
|
|
+ this.setPluginState({
|
|
|
+ recording: false,
|
|
|
+ })
|
|
|
+ // get video file after recorder stopped
|
|
|
+ return this.getVideo()
|
|
|
})
|
|
|
- // 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((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
|
|
|
- })
|
|
|
+ .then(
|
|
|
+ () => {
|
|
|
+ this.recordingChunks = null
|
|
|
+ this.recorder = null
|
|
|
+ },
|
|
|
+ (error) => {
|
|
|
+ this.recordingChunks = null
|
|
|
+ this.recorder = null
|
|
|
+ throw error
|
|
|
+ },
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- submit () {
|
|
|
+ submit(): void {
|
|
|
try {
|
|
|
// add recorded file to uppy
|
|
|
if (this.capturedMediaFile) {
|
|
@@ -321,7 +384,7 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- stop () {
|
|
|
+ stop(): void {
|
|
|
// flush video stream
|
|
|
if (this.videoStream) {
|
|
|
this.videoStream.getVideoTracks().forEach((track) => {
|
|
@@ -363,16 +426,25 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
this.captureActive = false
|
|
|
}
|
|
|
|
|
|
- getVideo () {
|
|
|
- const mimeType = this.recordingChunks[0].type
|
|
|
+ getVideo(): Promise<{
|
|
|
+ source: string
|
|
|
+ name: string
|
|
|
+ data: Blob
|
|
|
+ type: string
|
|
|
+ }> {
|
|
|
+ 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}"`))
|
|
|
+ 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 blob = new Blob(this.recordingChunks!, { type: mimeType })
|
|
|
const file = {
|
|
|
source: this.id,
|
|
|
name,
|
|
@@ -383,16 +455,20 @@ export default class ScreenCapture extends UIPlugin {
|
|
|
return Promise.resolve(file)
|
|
|
}
|
|
|
|
|
|
- render () {
|
|
|
+ render(): ComponentChild {
|
|
|
// get screen recorder state
|
|
|
const recorderState = this.getPluginState()
|
|
|
|
|
|
- if (!recorderState.streamActive && !this.captureActive && !this.userDenied) {
|
|
|
+ if (
|
|
|
+ !recorderState.streamActive &&
|
|
|
+ !this.captureActive &&
|
|
|
+ !this.userDenied
|
|
|
+ ) {
|
|
|
this.start()
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <RecorderScreen
|
|
|
+ <RecorderScreen<M, B>
|
|
|
{...recorderState} // eslint-disable-line react/jsx-props-no-spreading
|
|
|
onStartRecording={this.startRecording}
|
|
|
onStopRecording={this.stopRecording}
|