Using RecordRTC with React, Express, & S3

RecordRTC is a library that allows for client-side recording, using WebRTC. It's a fantastic tool (created and well-maintained by Muaz Khan) that I've been integrating into a project at the Media Lab.

This post will walk through a simple example of using RecordRTC with React and Express. The app will record you for 4 seconds, then upload to S3 using a signed URL. I will highlight key components below; the entire git repository is here for further exploration.

App overview

The app view will be very simple, just a button to start recording and a view of you on the webcam. Once the file is successfully uploaded to S3, the page will notify you. For simplicity, we'll inject RecordPage.react.js into our app.js entry point. There is one other component, Webcam.react.js, which is a "dumb component"— it's stateless and receives props from its parent.

Webcam

As the app mounts, we check to see if the browser supports getUserMedia(). Currently, only Chrome, Firefox, and Opera support it. Here is the componentDidMount() function in RecordPage.react.js:

componentDidMount() {  
  if(!hasGetUserMedia()) {
    alert("Your browser cannot stream from your webcam. Please switch to Chrome or Firefox.");
    return;
  }
  this.requestUserMedia();
}

If the browser fits, we're good to go, and will initialize the webcam with requestUserMedia. We use the captureUserMedia function in AppUtils.js:

export function captureUserMedia(callback) {  
  var params = { audio: false, video: true };
  navigator.getUserMedia(params, callback, (error) => {
    alert(JSON.stringify(error));
  });
};

Then pass in the callback when we call it in the component:

requestUserMedia() {  
  captureUserMedia((stream) => {
    this.setState({ src: window.URL.createObjectURL(stream) });
  });
}

The stateless Webcam component is a simple video tag that sets its source as the props that are passed down from its parent:

render() {  
  return (
    <video src={this.props.src} />
  )
}

We'll inject the Webcam into our main page, RecordPage, along with a button to start recording. Time to get into the RecordRTC API!

Recorder Functions

When the "start recording" button is clicked, it triggers startRecord(), which begins a RecordRTC stream:

startRecord() {  
  captureUserMedia((stream) => {
    this.state.recordVideo = RecordRTC(stream, { type: 'video' });
    this.state.recordVideo.startRecording();
  });
  ...

The first parameter to the RecordRTC function is the stream itself, while the second is an object for configuring recorder settings (more details here). In this case, we'll specify a video recording and fall back to default settings.

In this example, we'll automatically stop recording after 4 seconds, using a setTimeout function, which calls stopRecord(). Here, we'll simply take the RecordRTC stream, this.state.recordVideo, and call stopRecording(), which takes a callback:

stopRecord() {  
  this.state.recordVideo.stopRecording(() => {
...

Sidenote: cross-browser recording

If the browser used is Firefox, RecordRTC can record audio and video together. If you're using Chrome, audio and video are recorded separately, which you could merge using a program like ffmpeg. If that's the case initRecorder() to maximize sync (details here) and check out the record to node example.

Working with AWS

Let's return to the stopRecord() function, and look at the callback parameter:

  stopRecord() {
    this.state.recordVideo.stopRecording(() => {
      let params = {
        type: 'video/webm',
        data: this.state.recordVideo.blob,
        id: Math.floor(Math.random()*90000) + 10000
      }
      this.setState({ uploading: true });
      S3Upload(params)
  ...

Once the video stops recording, we begin the process of uploading it to S3 with S3Upload(). The function takes 3 parameters: content type, a blob of data, and an id which we'll generate randomly.

We imported S3Utils at the top of RecordPage from AppUtils.js:

import { captureUserMedia, S3Upload } from './AppUtils';  

so let's head over to that file to handle S3 upload.

Get signed URL on Node server

S3Upload() is a promisified function that resolves when the S3 bucket upload succeeds (or fails). (More about the native promise here). The function first calls getSignedUrl, which tells our Node server to get a signed url from AWS:

function getSignedUrl(file) {  
  let queryString = '?objectName=' + file.id + '&contentType=' + encodeURIComponent(file.type);
  return fetch('/s3/sign' + queryString)
  .then((response) => {
    return response.json();
  })
  .catch((err) => {
    console.log('error: ', err)
  })
}

A signed url allows for secure uploading to AWS from the client. The server, which should be configured to have PUT permissions to the S3 bucket, generates a temporary url to which the client can then upload. More on signed urls here.

On the server side, we use an Express route handler module, which is imported in server.js, and takes an object parameter specifying bucket and ACL options:

app.use('/s3', require('./s3Router')({  
  bucket: config.bucket,
  ACL: 'public-read'
}))

In s3Router.js, the handler creates the signed url:

  router.get('/sign', function(req, res) {
    var filename = req.query.objectName;
    var mimeType = req.query.contentType;
    var ext = '.' + findType(mimeType);
    var fileKey = filename + ext;

    var s3 = new aws.S3();
  });

To upload to your bucket, simply make sure that your .aws/credentials file is correctly configured, and that you've specified your bucket name in server.js. Also, while the default ACL setting is private, here we set it as public so you can quickly access the uploaded video. You may need to configure your S3 bucket to allow for public-read ACL configuration.

After initializing the AWS SDK, we call s3.getSignedUrl() and pass in the parameters:

var params = {  
  Bucket: S3_BUCKET,
  Key: fileKey,
  Expires: 600,
  ContentType: mimeType,
  ACL: options.ACL || 'private'
};

s3.getSignedUrl('putObject', params, function(err, data) {  
  if (err) {
    console.log(err);
    return res.send(500, "Cannot create S3 signed URL");
  }
  res.json({
    signedUrl: data,
    publicUrl: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + fileKey,
    filename: filename
  });
});

'putObject' will specify that the signedUrl will be used in a PUT operation, and the third parameter is a callback that runs after a response is received. If the signedUrl retrieval was successful, then we send back the information to the client.

Upload content from client

Once the server sends back the signed url data, we can send the blob to S3 via xhr. xhr allows you to track the progress of the S3 upload using xhr.upload.onprogress.

export function S3Upload(fileInfo) {  
  return new Promise((resolve, reject) => {
    getSignedUrl(fileInfo)
    .then((s3Info) => {
      var xhr = createCORSRequest('PUT', s3Info.signedUrl);

      xhr.onload = function() {
        if (xhr.status === 200) {
          console.log(xhr.status)
          resolve(true);
        } else {
          console.log(xhr.status)   
          reject(xhr.status);
        }
      };

      xhr.setRequestHeader('Content-Type', fileInfo.type);
      xhr.setRequestHeader('x-amz-acl', 'public-read');

      return xhr.send(fileInfo.data);
    })
  })
}

The promise resolves when the xhr status returns 200. Once that happens, we update the state in RecordPage to tell the user the upload was successful:

.then((success) => {
  if(success) {
    this.setState({ uploadSuccess: true, uploading: false });
  }
}, (error) => {
  alert(error, 'error occurred. check your aws settings and try again.')
})

Bundling it all up

Finally, the entry point to our React app is src/app.js. Webpack will find all the modules, do its magic (e.g. converting ES6 to ES5) and produce one output, bundle.js. In case you're not familiar, Webpack is a tool that bundles and re-bundles your app modules very quickly, which makes for an great dev experience.

Here is how we'll tell Webpack to do that:

module.exports = {  
  entry: [
    'webpack-hot-middleware/client',
    './src/app.js'
  ],

  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, './build')
  },

We have one loader module with loaders for ES6 and React, as well as a few plugins, which I won't get into. More on Webpack here. The bundle.js is then served by src/index.html on our server.

Final thoughts

The MediaRecorder API, which would enable direct recording in the browser, is in development. You can check it out here, and play around with the experimental version. In the meantime, RecordRTC is a great and simple option for media recording.