Asynchronous Video Compression and Upload in React Native

My learnings on asynchronously compressing and uploading videos in react native

Mark Gituma
9 min readNov 16, 2020

One of the startups in my portfolio mbele.io utilizes video compression and upload quite extensively. However the process of building this into react native took some trial and error. This blog is meant to describe the process undertaken to implement this functionality and highlights the key ideas without too much complexity.

The main react-native packages used are:

In order to keep this blog as brief as necessary while conveying the key unique information, it won’t go into too much detail about how to configure the required packages. The standard documentation should be sufficient for this as well other blogs that will be highlighted.

The main steps to perform the compression and upload is as follows:

  1. Asynchronously select a video and save its variables to a database.
  2. Add the reference to a local queue in react native for synchronous processing.
  3. Compress if required and upload the result to firebase storage.
  4. Update the database entry with the firebase storage url and proceed with uploading the next video in the queue.

The repository for this blog can be found on https://github.com/gitumarkk/RNCompressUploadVideo/

Configuring React Native

In order to use video compression, a bare react native project is required as Native Modules are used e.g. react-native-image-picker & react-native-video-processing. This means if you are using expo, you need to eject it first because expo does not easily support adding Native Modules. Once react native have been set up, install the following packages:

yarn add redux react-redux redux-thunk react-native-video react-native-image-picker react-native-video-processing @react-native-firebase/app @react-native-firebase/storage @react-native-async-storage/async-storage

As react native is an ever evolving environment, parts of this blog might not work with newer packages. Therefore, it is a good idea to use the exact package versions found in the package.json in order to follow along. However, the overarching ideas should be similar with newer packages or even different packages.

Setting up the packages

To set up the redux state container with its associated asynchronous middleware, refer to the following files redux-store.js and index.js in the RNCompressUploadVideo repository. It is worth mentioning that in order to reduce the complexity of setting up and saving data to an external API, the AsyncStorage package is used to mock an API request. This functionality can be found in the mock-api.js file in the repo. This means a get, post, update or delete REST request is done against AsyncStorage. Unfortunately AsyncStorage has a default storage limit of 6 mb which becomes a limitation when mocking the storage of thumbnails. Therefore it is necessary to periodically clear the contents of the storage if you are following this blog.

The react-native-image-picker package is fairly easy to install. However during the writing of this blog, the author of the package is in the process of migrating it from version 2.x.x to 3.x.x which will introduce breaking changes. For this blog the more stable 2.x.x is used. To find out more about how to integrate react-native-image-picker with firebase storage as a backend please refer to the following blog integrate-firebase-storage-and-image-picker-in-react-native.

Installing react-native-video-processing mostly follows the documentation. However I had some issues on android due to changes made in the Android 10 core api regarding core binaries. In order to fix it, copy the repositories ./react-native-video-processing/android/src/main/jniLibs folder to your projects ./android/src/main/jniLibs folder (create it if it’s missing). This package uses ffmpeg to compress the video on android.

Setting up firebase storage is a bit more involved. Fortunately, there is a plethora of blogs that go into detail about how to configure it such as https://www.instamobile.io/mobile-development/react-native-firebase-storage/. Unfortunately, there are some issues I came across such as:

  • After setting up storage on the firebase developer console, for the purpose of this blog, update the firebase rules to the following:

Permissions

Once you set-up the packages, remember to update your permissions in order to be able to read the files. The permissions used for this project on android are:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/><uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Selecting, Compressing and Uploading

We will now look at the different steps associated with asynchronously processing and uploading the videos.

Selecting the video

The video is selected using react-native-image-picker. The following code found in the UploadButton.js file in the repo shows how this is done.

The code is quite straight forward. In order to select a video, the mediaType: 'video' option needs to be set. After the video is selected, the video location on the file system depending on the platform is retrieved and dispatched using thefileUpload asynchronous action which begins the upload process.

Compressing and Uploading the video

The code to compress and upload the video is the most complex part where the associated code is:

From the previous section, we call the fileUpload action which is an asynchronous redux action. This means that the user can continue using the app while the file uploads in the background.

  1. The first thing I like to do is store details of the file to be uploaded as well as associated metadata to a database. This is indicated by the const resp = await mockApi.post(data); function call. View the mock-api.js file to see the simulated api call to a database. When storing the initial information it can be useful to get the thumbnail and show it to the user while the video is uploading. The thumbnail in this case is a base64 encoded string. This incurs a small performance penalty which is limited by the device capability as opposed to the file size. Storing initial information to a database has a few benefits such as; it helps to have an audit trail in case the file fails to upload. If a user deletes a file mid upload, checking the database at the end of the compression step can indicate whether to proceed with the upload or not.
  2. The second step is to dispatch an action that appends the details of the file to be uploaded to a queue i.e. dispatch(addToQueue({ …resp })); . The reason I use a queue system is to try enforce synchronous compressing due to mobile device processing limitation i.e. the scenario where a less powerful device is compressing multiple files at once is not desirable and might lead to unexpected failure. Note, even though the overall system is asynchronous i.e. the user is not blocked from using the app, an attempt is made for the actual compression process to be synchronous. Next the uploadNext action is dispatched.
  3. The first thing the uploadNext function does is to check if there are files in the queue as well as check that there is no file currently uploading i.e. the current key in the redux store. The current key acts as a lock to try ensure only one file is compressing and uploading at a time. If both of these conditions are met, the first item in the queue is selected and the lock is set by calling the setCurrent action which updates the redux store. However, there is a special condition such that if uploadNext is called with the unlock argument, if there is a current file in the lock, then use that file instead of the next item in the queue. This is helpful if there is a need to restart an upload if the app crashes or if the user closes the app midway during an upload process and a lock exists.
  4. The asynchronous action to compress the video is then dispatched i.e. compressVideo. Note, even though this is an asynchronous action, it is called synchronously by using the await expression that waits for the promise to resolve. The compression step is straight forward where options are used to reduce the video height and width as well as to change the bitrate. Figuring out the correct bitrate and compression options is outside the scope of this blog and is an extensive topic in itself. Once the video compression is finished, a new scaled down thumbnail is generated and is used to replace the original thumbnail with the newer and more optimized thumbnail.
  5. The uploadVideo action is then dispatched which proceeds to upload the compressed file to firebase storage and updates the database entry with the new firebase URI of the file. This is also called with the await expression.
  6. After the upload, the removeFromQueue action is dispatched to remove the item from the queue as well as remove the lock.
  7. Once the video is uploaded, the uploadNext action is recursively dispatched to get the next video from the queue. This process continues until all the videos are processed.

This steps are highly simplified to highlight the general process, however, quite a bit more work needs to be done on it to make it suitable for a production application. Some potential considerations are:

  • You probably noticed the getVideosApi being dispatched after each step. This was done for simplicity sake in order to avoid the complexity of having to maintain the internal videoList state. Usually, intelligently updating the list would be necessary to synchronize the local and server state in an efficient and intuitive manner.
  • In this scenario, a simple lock is used. However, you need to consider scenarios where a video fails to upload and a lock remains, how do you remove the lock so that other videos can proceed uploading. In practice a try/catch statement might be useful in case of javascript errors to clear the lock (this might not work for Native errors that result in the app crashing). Perhaps in your use case, a lock is not necessary and you would want to allow for multiple uploads. In this scenario you need to consider how to prevent duplicated uploads.
  • How to handle errors and failures hasn’t been thoroughly investigated. However, the reason I prefer the queue system is that if an upload fails, or an app crashes or if the user closes the app during the upload step; calling the uploadNext action will continue processing uploads in the queue. However, there needs to be consideration about how many times to retry the upload process, otherwise the procedure might get stuck in an infinite loop in trying to upload a corrupted file which keeps causing the app to crash.
  • Something else to consider is if the video is small to begin with, it will not make sense to compress it further as it’s better to upload it straight to firebase. A check can be included to check the video file size before uploading. If above a specific threshold, then proceed with the compression process.

Displaying to the User

When the video is uploading, the status of the upload process can be displayed to the user. The following bit of code is a simplified way of doing this (found in the App.js file in the repository).

The main points to consider in the above file are:

  • When the app is initialized, the files that are stored in the database are fetched (this is a fairly standard step in applications). Then a check is performed to see if there are files in the upload queue that needs to be processed by dispatching the uploadNext action. Note, in this scenario the unlock variable is set in case there is a file that started the compression and upload process but didn’t complete.
  • The videos are rendered using the react native flat list. When rendering the list, a check is conducted to see if the video is in the uploading queue. If so, a message is displayed to the user to show that the video is uploading.
  • A modal is used to display a specific video when its thumbnail is clicked. The video itself is shown using the react-native-video package.

The image below shows the different states of the app during the upload process i.e. selecting the video (left), the video during the compression and upload process (middle) and viewing the video (right).

Conclusion

Uploading videos on mobile is a complex task as there are many variables to consider as well as many design decisions. This blog does not claim to be the best practices when uploading videos but is meant to be a guide in helping developers get to an optimal solution.

If you have any questions or anything needs clarification, you can book a time with me on https://mbele.io/mark

Resources

--

--