8000 @uppy/screen-capture: add screenshot button by qxprakash · Pull Request #5737 · transloadit/uppy · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

@uppy/screen-capture: add screenshot button #5737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 26, 2025

Conversation

qxprakash
Copy link
Collaborator
@qxprakash qxprakash commented May 5, 2025

Screenshot Support for Screen Capture Plugin

Closes #3310

Overview

Added screenshot capture functionality to @uppy/screen-capture plugin, allowing users to take screenshots alongside screen recordings.

API Changes

Added new configuration options:

  • enableScreenshots: Toggle screenshot functionality (default: false)
  • screenshotQuality: Control image compression (0.0-1.0, default: 0.9)
  • preferredImageMimeType: Set output format ('image/jpeg'|'image/png'|'image/webp', default: 'image/png')

Implementation Details

  • Reused existing screen capture stream for drawing screenshot and converting them to Blob and storing the image
  • proper state cleanup , in case of error and component unmount
  • added missing component prop types and return types in RecordButton and SubmitButton Components
  • Added validations and Fallbacks for the configuration args

Hi @Murderlon let me know what do you think about this, also how do I update the docs for this plugin ? and the stackblitz live example.

Screenshots

image

Copy link
Contributor
github-actions bot commented May 5, 2025
Diff output files
diff --git a/packages/@uppy/screen-capture/lib/RecordButton.js b/packages/@uppy/screen-capture/lib/RecordButton.js
index 1ed69fc..655a089 100644
--- a/packages/@uppy/screen-capture/lib/RecordButton.js
+++ b/packages/@uppy/screen-capture/lib/RecordButton.js
@@ -53,14 +53,13 @@ export default function RecordButton(_ref) {
         "aria-hidden": "true",
         focusable: "false",
         className: "uppy-c-icon",
-        width: "100",
-        height: "100",
-        viewBox: "0 0 100 100",
+        width: "24",
+        height: "24",
+        viewBox: "0 0 24 24",
+        fill: "currentColor",
       },
-      h("circle", {
-        cx: "50",
-        cy: "50",
-        r: "40",
+      h("path", {
+        d: "M4.5 4.5a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h8.25a3 3 0 0 0 3-3v-9a3 3 0 0 0-3-3H4.5ZM19.94 18.75l-2.69-2.69V7.94l2.69-2.69c.944-.945 2.56-.276 2.56 1.06v11.38c0 1.336-1.616 2.005-2.56 1.06Z",
       }),
     ),
   );
diff --git a/packages/@uppy/screen-capture/lib/RecorderScreen.js b/packages/@uppy/screen-capture/lib/RecorderScreen.js
index ea999a6..d975c9d 100644
--- a/packages/@uppy/screen-capture/lib/RecorderScreen.js
+++ b/packages/@uppy/screen-capture/lib/RecorderScreen.js
@@ -8,8 +8,10 @@ function _extends() {
   },
     _extends.apply(null, arguments);
 }
-import { Component, h } from "preact";
+import { Component, Fragment, h } from "preact";
+import DiscardButton from "./DiscardButton.js";
 import RecordButton from "./RecordButton.js";
+import ScreenshotButton from "./ScreenshotButton.js";
 import StopWatch from "./StopWatch.js";
 import StreamStatus from "./StreamStatus.js";
 import SubmitButton from "./SubmitButton.js";
@@ -29,6 +31,8 @@ class RecorderScreen extends Component {
       recording,
       stream: videoStream,
       recordedVideo,
+      enableScreenshots,
+      capturedScreenshotUrl,
     } = this.props;
     const videoProps = {
       playsinline: true,
@@ -54,27 +58,45 @@ class RecorderScreen extends Component {
       h(
         "div",
         {
-          className: "uppy-ScreenCapture-videoContainer",
+          className: "uppy-ScreenCapture-mediaContainer",
         },
         h(StreamStatus, this.props),
-        h(
-          "video",
-          _extends({
-            ref: videoElement => {
-              this.videoElement = videoElement;
+        capturedScreenshotUrl && !recording && !recordedVideo
+          ? h(
+            "div",
+            {
+              className: "uppy-ScreenCapture-imageContainer",
             },
-            className: "uppy-ScreenCapture-video",
-          }, videoProps),
-        ),
-        h(StopWatch, this.props),
+            h("img", {
+              src: capturedScreenshotUrl,
+              className: "uppy-ScreenCapture-media",
+              alt: "screenshotPreview",
+            }),
+          )
+          : h(
+            "video",
+            _extends({
+              ref: videoElement => {
+                this.videoElement = videoElement;
+              },
+              className: "uppy-ScreenCapture-media",
+            }, videoProps),
+          ),
+        h("div", null, h(StopWatch, this.props)),
       ),
       h(
         "div",
         {
           className: "uppy-ScreenCapture-buttonContainer",
         },
-        h(RecordButton, this.props),
-        h(SubmitButton, this.props),
+        recordedVideo || capturedScreenshotUrl
+          ? h(Fragment, null, h(SubmitButton, this.props), h(DiscardButton, this.props))
+          : h(
+            Fragment,
+            null,
+            enableScreenshots && !recording && h(ScreenshotButton, this.props),
+            h(RecordButton, this.props),
+          ),
       ),
     );
   }
diff --git a/packages/@uppy/screen-capture/lib/ScreenCapture.js b/packages/@uppy/screen-capture/lib/ScreenCapture.js
index 1306ed8..6d7ac06 100644
--- a/packages/@uppy/screen-capture/lib/ScreenCapture.js
+++ b/packages/@uppy/screen-capture/lib/ScreenCapture.js
@@ -25,6 +25,7 @@ function isScreenRecordingSupported() {
 function getMediaDevices() {
   return window.MediaRecorder && navigator.mediaDevices;
 }
+const SUPPORTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp"];
 const defaultOptions = {
   displayMediaConstraints: {
     video: {
@@ -42,6 +43,8 @@ const defaultOptions = {
     audio: true,
   },
   preferredVideoMimeType: "video/webm",
+  preferredImageMimeType: "image/png",
+  enableScreenshots: true,
 };
 export default class ScreenCapture extends UIPlugin {
   constructor(uppy, opts) {
@@ -72,6 +75,8 @@ export default class ScreenCapture extends UIPlugin {
     this.stopRecording = this.stopRecording.bind(this);
     this.submit = this.submit.bind(this);
     this.streamInterrupted = this.streamInactivated.bind(this);
+    this.captureScreenshot = this.captureScreenshot.bind(this);
+    this.discardRecordedMedia = this.discardRecordedMedia.bind(this);
     this.captureActive = false;
     this.capturedMediaFile = null;
   }
@@ -237,6 +242,23 @@ export default class ScreenCapture extends UIPlugin {
       throw error;
     });
   }
+  discardRecordedMedia() {
+    const {
+      capturedScreenshotUrl,
+      recordedVideo,
+    } = this.getPluginState();
+    if (capturedScreenshotUrl) {
+      URL.revokeObjectURL(capturedScreenshotUrl);
+    }
+    if (recordedVideo) {
+      URL.revokeObjectURL(recordedVideo);
+    }
+    this.capturedMediaFile = null;
+    this.setPluginState({
+      recordedVideo: null,
+      capturedScreenshotUrl: null,
+    });
+  }
   submit() {
     try {
       if (this.capturedMediaFile) {
@@ -276,8 +298,22 @@ export default class ScreenCapture extends UIPlugin {
       });
       this.outputStream = null;
     }
+    const {
+      capturedScreenshotUrl,
+      recordedVideo,
+    } = this.getPluginState();
+    if (capturedScreenshotUrl) {
+      URL.revokeObjectURL(capturedScreenshotUrl);
+    }
+    if (recordedVideo) {
+      URL.revokeObjectURL(recordedVideo);
+    }
     this.setPluginState({
+      recording: false,
+      streamActive: false,
+      audioStreamActive: false,
       recordedVideo: null,
+      capturedScreenshotUrl: null,
     });
     this.captureActive = false;
   }
@@ -301,6 +337,88 @@ export default class ScreenCapture extends UIPlugin {
     };
     return Promise.resolve(file);
   }
+  async captureScreenshot() {
+    var _this$mediaDevices;
+    if (!((_this$mediaDevices = this.mediaDevices) != null && _this$mediaDevices.getDisplayMedia)) {
+      throw new Error("Screen capture is not supported");
+    }
+    try {
+      let stream = this.videoStream;
+      if (!stream) {
+        const newStream = await this.selectVideoStreamSource();
+        if (!newStream) {
+          throw new Error("Failed to get screen capture stream");
+        }
+        stream = newStream;
+      }
+      const video = document.createElement("video");
+      video.srcObject = stream;
+      await new Promise(resolve => {
+        video. => {
+          video.play();
+          resolve(null);
+        };
+      });
+      const canvas = document.createElement("canvas");
+      canvas.width = video.videoWidth;
+      canvas.height = video.videoHeight;
+      const ctx = canvas.getContext("2d");
+      if (!ctx) {
+        throw new Error("Failed to get canvas context");
+      }
+      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+      let mimeType = this.opts.preferredImageMimeType;
+      if (!mimeType || !SUPPORTED_IMAGE_TYPES.includes(mimeType)) {
+        this.uppy.log(`Unsupported image type "${mimeType}", falling back to image/png`, "warning");
+        mimeType = "image/png";
+      }
+      const quality = 1;
+      return new Promise((resolve, reject) => {
+        canvas.toBlob(
+          blob => {
+            if (!blob) {
+              reject(new Error("Failed to create screenshot blob"));
+              return;
+            }
+            const fileExtension = getFileTypeExtension(mimeType) || "png";
+            const file = {
+              source: this.id,
+              name: `Screenshot ${new Date().toISOString()}.${fileExtension}`,
+              type: mimeType,
+              data: blob,
+            };
+            try {
+              this.capturedMediaFile = file;
+              const screenshotUrl = URL.createObjectURL(blob);
+              this.setPluginState({
+                capturedScreenshotUrl: screenshotUrl,
+              });
+              resolve();
+            } catch (err) {
+              if (this.getPluginState().capturedScreenshotUrl) {
+                this.setPluginState({
+                  capturedScreenshotUrl: null,
+                });
+              }
+              if (!err.isRestriction) {
+                this.uppy.log(err, "error");
+              }
+              reject(err);
+            } finally {
+              video.srcObject = null;
+              canvas.remove();
+              video.remove();
+            }
+          },
+          mimeType,
+          quality,
+        );
+      });
+    } catch (err) {
+      this.uppy.log(err, "error");
+      throw err;
+    }
+  }
   render() {
     const recorderState = this.getPluginState();
     if (!recorderState.streamActive && !this.captureActive && !this.userDenied) {
@@ -311,10 +429,13 @@ export default class ScreenCapture extends UIPlugin {
       _extends({}, recorderState, {
         onStartRecording: this.startRecording,
         onStopRecording: this.stopRecording,
+        enableScreenshots: this.opts.enableScreenshots,
+        onScreenshot: this.captureScreenshot,
         onStop: this.stop,
         onSubmit: this.submit,
         i18n: this.i18n,
         stream: this.videoStream,
+        onDiscard: this.discardRecordedMedia,
       }),
     );
   }
diff --git a/packages/@uppy/screen-capture/lib/SubmitButton.js b/packages/@uppy/screen-capture/lib/SubmitButton.js
index 514969e..8df861b 100644
--- a/packages/@uppy/screen-capture/lib/SubmitButton.js
+++ b/packages/@uppy/screen-capture/lib/SubmitButton.js
@@ -4,9 +4,10 @@ export default function SubmitButton(_ref) {
     recording,
     recordedVideo,
     onSubmit,
+    capturedScreenshotUrl,
     i18n,
   } = _ref;
-  if (recordedVideo && !recording) {
+  if ((recordedVideo || capturedScreenshotUrl) && !recording) {
     return h(
       "button",
       {
diff --git a/packages/@uppy/screen-capture/lib/locale.js b/packages/@uppy/screen-capture/lib/locale.js
index 3d42e1e..10beca5 100644
--- a/packages/@uppy/screen-capture/lib/locale.js
+++ b/packages/@uppy/screen-capture/lib/locale.js
@@ -8,5 +8,7 @@ export default {
     streamPassive: "Stream passive",
     micDisabled: "Microphone access denied by user",
     recording: "Recording",
+    takeScreenshot: "Take Screenshot",
+    discardMediaFile: "Discard Media",
   },
 };
diff --git a/packages/@uppy/utils/lib/getFileTypeExtension.js b/packages/@uppy/utils/lib/getFileTypeExtension.js
index 6e5b7d4..61faa22 100644
--- a/packages/@uppy/utils/lib/getFileTypeExtension.js
+++ b/packages/@uppy/utils/lib/getFileTypeExtension.js
@@ -8,6 +8,7 @@ const mimeToExtensions = {
   "image/heic": "heic",
   "image/heif": "heif",
   "image/jpeg": "jpg",
+  "image/webp": "webp",
   "image/png": "png",
   "image/svg+xml": "svg",
   "video/mp4": "mp4",

@qxprakash qxprakash marked this pull request as draft May 5, 2025 10:58
@qxprakash qxprakash marked this pull request as ready for review May 5, 2025 12:13
@qxprakash qxprakash changed the title Add screenshot Feat: Add screenshot to @uppy/screen-capture plugin May 5, 2025
@qxprakash
Copy link
Collaborator Author
qxprakash commented May 6, 2025

@Murderlon , here are few of my questions relating to this PR

  1. In order to handle validations for params take for example preferredImageMimeType when the user provides an invalid value (let's suppose a random string instead of a correct mime type) , I was confused between , throwing an error and letting user know that they've entered a wrong value vs just using a fallback value ,

Both approaches have their own sets of pros and cons:

  • In case of an error , the onus is on the user to read the docs carefully and use correct values.

  • In case of a fallback, the user might get a false sense of things working as they want, but in reality, it's not as we're using a fallback value.

    I went with the second approach.

  1. I couldn't find any existing unit tests for this plugin ?
  2. Do you think it makes sense for adding a preview functionality similar to the screencast video ?

Copy link
Member
@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good 👌

Some comments:

  • I think we should replace both icons in the button with camera and video-camera from https://heroicons.com/solid
  • I'm not sure if the screenshotQuality option makes sense to add, can't we always do max quality? If people want compression you can add @uppy/compressor. Bit of a conflicting feature with that plugin.
  • Should we make enableScreenshots default to true? I'm open to both.
  • I think fallback makes sense when an incorrect mime type is given to align with how video does it.

I couldn't find any existing unit tests for this plugin ?

Quite a few plugins aren't tested so that's fine. I don't think we need it for this feature.

Do you think it makes sense for adding a preview functionality similar to the screencast video ?

That would be nice, yes!

@qxprakash
Copy link
Collaborator Author
qxprakash commented May 15, 2025

Hi @Murderlon , Thanks for your comments !

  • Done
  • I'm not sure if the screenshotQuality option makes sense to add, can't we always do max quality? If people want compression you can add @uppy/compressor. Bit of a conflicting feature with that plugin.
  • Yeah, that makes sense! I originally added it as a parallel to the similar option in video, I’ve removed it now quality defaults to max.
  • Should we make enableScreenshots default to true? I'm open to both.
  • My reasoning for keeping it false by default was that it was requested by a specific subset of users. That said, enabling it by default probably wouldn’t hurt either. My only concern is that existing users will get this feature automatically when they update Uppy, even if they didn’t ask for it.

Do you think it makes sense for adding a preview functionality similar to the screencast video ?

That would be nice, yes!

  • Added

@qxprakash
Copy link
Collaborator Author

A couple more things I thought about:

  • Once the user has taken a screenshot and it’s in preview, there’s no point in showing the screenshot button again — there's really no reason to take an identical screenshot. So, I removed it.

  • If the user has started recording, the screenshot button should be hidden. Keeping it visible could lead to some edge cases, such as:

    • The user taking a screenshot while the recording is still live.
    • User taking a screenshot when video is in preview mode

So, I’ve disabled the screenshot button in the above cases.


TL;DR

  • When recording starts, the screenshot button is hidden until the user either uploads or discards the recording.
  • Once a screenshot has been taken and is in preview, the screenshot button also disappears.

Copy link
Member
@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM

  • Once the user has taken a screenshot and it’s in preview, there’s no point in showing the screenshot button again — there's really no reason to take an identical screenshot. So, I removed it.

I think it's currently a bit confusing, after a screenshot the button that is auto focussed is to start a recording. This seems counter intuitive after just taking a screenshot. I'm wondering if we should shared submit/discard button combination after taking a screenshot or taking a video. Submit closes the plugin, discard returns to the button combo record/screenshot.

What do you think?

@Murderlon Murderlon changed the title Feat: Add screenshot to @uppy/screen-capture plugin @uppy/screen-capture: add screenshot button May 20, 2025
@qxprakash
Copy link
Collaborator Author
qxprakash commented May 23, 2025

Thanks for the review !

  • Once the user has taken a screenshot and it’s in preview, there’s no point in showing the screenshot button again — there's really no reason to take an identical screenshot. So, I removed it.

I think it's currently a bit confusing, after a screenshot the button that is auto focussed is to start a recording. This seems counter intuitive after just taking a screenshot. I'm wondering if we should shared submit/discard button combination after taking a screenshot or taking a video. Submit closes the plugin, discard returns to the button combo record/screenshot.

What do you think?

Yup, agreed!
I’ve added a cancel button to discard the media and return to the previous state, where the user is presented with both the screenshot and record buttons. This is much more consistent.
Also, after discarding, the getDisplayMedia stream is not stopped, so the initially selected screen or window remains available.

image

Copy link
Member
@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good now!

One last suggestion: swap the order of discard and submit. My gut feeling is that having the autofocus and visual order on submit is more intuitive.

Then ready to merge

@qxprakash
Copy link
Collaborator Author

All good now!

One last suggestion: swap the order of discard and submit. My gut feeling is that having the autofocus and visual order on submit is more intuitive.

Then ready to merge

alright sure @Murderlon , there was a small name conflict in the discard button which was failing yarn build , hence I've changed it , will swap the button order now.

@Murderlon Murderlon merged commit 839c8b3 into transloadit:main May 26, 2025
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add screenshot option to @uppy/screen-capture
2 participants
0