Screen recorder with JS

With jQuery File Uploader and some javascript, you can create a screen recorder that can record any browser tab or the entire screen. This guide will explain how to add screen recording to Uploadcare jQuery File Uploader widget via a custom tab. As a result, your users can take screen recordings with a widget added to your site. Video files will be uploaded directly to your Uploadcare project.

The widget offers an API that allows you to add new tabs and implement custom logic, e.g., add custom upload sources. Learn more about custom tabs from Uploadcare jQuery File Uploader docs.

We'll use plain HTML, CSS, and JS, so no tricky setup is required. You need to create three files in your project folder:

  • index.html
  • style.css
  • app.js

index.html

Set up the index.html file first:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>My web app</title>
    {/* Link our styles.css file */}
    <link rel="stylesheet" href="style.css" />
    {/* Add the uploadcare.js library to install [the widget](/docs/uploads/file-uploader/#install) */}
    <script
      src="https://ucarecdn.com/libs/widget/3.x/uploadcare.full.min.js"
      {/* add the uploadcare.js library to install [the widget](/docs/uploads/file-uploader/#install) */}
      charset="utf-8"
    ></script>
    {/* Add a script tag with global widget [settings](/docs/uploads/file-uploader/#config-globals) */}
    {/* Replace the YOUR_PUBLIC_KEY with [yours API keys](/docs/start/settings/#keys-public) */}
    <script>
      UPLOADCARE_PUBLIC_KEY = "YOUR_PUBLIC_KEY";
      UPLOADCARE_TABS = "recorder file camera";
      UPLOADCARE_PREVIEW_STEP = true;
    </script>
</head>
  <body>
    {/* Add an SVG icon that we’ll be using as the custom tab icon in the widget */}
    <svg width="0" height="0" style="position: absolute;">
      <symbol id="uploadcare--icon-recorder" viewBox="0 0 32 32">
        <path
          d="M 16 22.928 L 23.416 27.4 L 21.454 18.965 L 28 13.292 L 19.37 12.552 L 16 4.6 L 12.629 12.552 L 4 13.292 L 10.546 18.965 L 8.584 27.4 Z"
        />
      </symbol>
    </svg>
    <p>
      {/* Add a hidden input element with role="uploadcare-uploader" */}
      {/* Uploadcare.js library will turn it into the uploader button automatically */}
      <input
        type="hidden"
        role="uploadcare-uploader"
        name="file_upload"
      />
    </p>
    {/* Link our app.js file */}
    <script src="app.js"></script>
  </body>
</html>

style.css

Set up the style.css file:

.start-btn,
.start-btn:hover {
  min-width: 200px;
  max-width: 300px;
  font-size: 1rem;
}

.uploadcare--tab_name_recorder {
  flex-direction: column;
  align-items: center;
}

.uploadcare--widget__button {
  padding: 0.75em;
}

We'll be adding some additional UI elements to our custom tab, and the CSS above is all about positioning and styling them.

app.js

Finally, set up the app.js file to make things work. We'll be adding code step by step and explaining each step in detail.

Let's add the getSupportedMime function. It must detect the most suitable video file type and codec for the user's browser. We'll not go into detail about how it works, but If you'd like to learn more about this, check out this thread on Stackoverflow.

function getSupportedMime() {
  const types = ["webm", "ogg", "mp4", "x-matroska"];
  const codecs = [
    "vp9",
    "vp9.0",
    "vp8",
    "vp8.0",
    "avc1",
    "av1",
    "h265",
    "h.265",
    "h264",
    "h.264",
    "opus",
    "pcm",
    "aac",
    "mpeg",
    "mp4a"
  ];
  const isSupported = MediaRecorder.isTypeSupported;
  const supported = [];
  types.forEach((type) => {
    const mimeType = `video/${type}`;
    codecs.forEach((codec) =>
      [
        `${mimeType};codecs=${codec}`,
        `${mimeType};codecs:${codec.toUpperCase()}`,
        `${mimeType}`
      ].forEach((variation) => {
        if (isSupported(variation)) supported.push(variation);
      })
    );
    if (isSupported(mimeType)) supported.push(mimeType);
  });
  return supported[0];
}

Now let's register a new tab:

uploadcare.registerTab("recorder", screenRecorderTab);

screenRecorderTab is a function invoked when a user opens the widget. We need to define it and implement the tab logic inside:

function screenRecorderTab(container, button, dialog, settings) {

  // Add UI elements to the tab
  button[0].title = "Screen Recorder";
  const header = document.createElement("h1");
  header.innerText = "Screen recorder";
  header.style.textAlign = "center";
  const startBtn = document.createElement("button");
  startBtn.innerText = "Record screen";
  startBtn.classList.add(
    "start-btn",
    "uploadcare--widget__button",
    "uploadcare--widget__button_type_open"
  );
  container[0].appendChild(header);
  container[0].appendChild(startBtn);

  // Check if browser supports screen recording
  if (!navigator.mediaDevices.getDisplayMedia) {
    startBtn.innerText = "Screen recording is not supported";
    startBtn.disabled = true;
    return;
  } else {
    startBtn.addEventListener("click", recordScreen);
  }

  // Screen recording implementation
  async function recordScreen() {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: true
    });

    const mime = getSupportedMime();
    let extension = mime.split("/").pop().split(";")[0];
    // Change extension to MKV if mime-type x-matroska
    if (extension === "x-matroska") {
      extension = "mkv";
    }

    // Initiate MediaRecorder
    const mediaRecorder = new MediaRecorder(stream, {
      mimeType: mime
    });

    // Disable record button and change its text when recording in progress
    startBtn.innerText = "Recording...";
    startBtn.disabled = true;

    const chunks = [];
    mediaRecorder.addEventListener("dataavailable", (e) => {
      chunks.push(e.data);
    });

    mediaRecorder.addEventListener("stop", () => {
      let blob = new Blob(chunks, {
        type: chunks[0].type
      });
      let file = new File([blob], `recording.${extension}`);
      let upload = uploadcare.fileFrom("object", file);
      startBtn.innerText = "Record Screen";
      startBtn.disabled = false;
      dialog.addFiles([upload]);
    });

    //Start the recorder
    mediaRecorder.start();
  }
}

The callback function screenRecorderTab allows us to access the tab container and button via its container and button arguments, so we don't have to use extra selectors. Also, we check if the browser supports the required API (getDisplayMedia):

if (!navigator.mediaDevices.getDisplayMedia) {
    startBtn.innerText = "Screen recording is not supported";
    startBtn.disabled = true;
    return;
  } else {
    startBtn.addEventListener("click", recordScreen);
  }

If so, we add a click event listener to the Record Screen button that calls the recordScreen function. We'll take a look at the function soon. So far, our custom tab should look like this:

Now let's explore the recordScreen function. We'll be using the following browser APIs to get and record the video stream from the screen:

As a first step, we need to get a video stream from the screen:

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: true
});

Then, using our utility function getSupportedMime, we pick the most suitable mime type and specify the extension for the output video file:

const mime = getSupportedMime();
let extension = mime.split("/").pop().split(";")[0];

// Change extension to MKV if mime-type x-matroska
if (extension === "x-matroska") {
  extension = "mkv";
}

Now we can initiate MediaRecorder and pass the stream to it:

const mediaRecorder = new MediaRecorder(stream, {
  mimeType: mime
});

Further, once the recording process has started, we disable the Record Screen button and change its text to “Recording…”:

startBtn.innerText = "Recording...";
startBtn.disabled = true;

Data from the video stream comes in chunks, and we need a variable to store it. Also, we add an event listener that will push every new chunk of video data to our variable:

const chunks = [];
mediaRecorder.addEventListener("dataavailable", (e) => {
  chunks.push(e.data);
});

A user can stop the recording at any time, and when this happens, we need to build a file object from the video data chunks:

mediaRecorder.addEventListener("stop", () => {
  let blob = new Blob(chunks, {
    type: chunks[0].type
  });
  let file = new File([blob], `recording.${extension}`);
  let upload = uploadcare.fileFrom("object", file);
  startBtn.innerText = "Record Screen";
  startBtn.disabled = false;
  dialog.addFiles([upload]);
});

Let's see what this piece of code does: once the user stops recording, we create a new Blob from our chunks and set its type as the type of the first element in the chunks array. Then we create a new File from the blob and put its name to “recording” and its extension to the extension we specified before based on the mime type. At this point, we can upload the file to Uploadcare by calling jQuery File Uploader API docs:

let upload = uploadcare.fileFrom("object", file);

Then we change the state of the Record Screen button and add the file to the widget's dialog with:

dialog.addFiles([upload]);

The remaining line looks like this:

mediaRecorder.start();

It starts the MediaRecorder when the Record Screen button is clicked.

Recording continues until the Stop button is pressed. After the recording is stopped, the video file is added to the Uploadсare project and is available on the Files tab.