These days a large number of mobile apps allow users to upload videos and play those videos efficiently. While building these features in a React Native app we ran into some challenges. In this blog we will discuss those challenges and the solutions.
Where to host the videos
Since we are dealing with videos we need a service that will store, host, encode and be a CDN. After looking at various service providers we decided to go with Cloudinary. Cloudinary is a service provider with an end-to-end image/video-management solution for web and mobile applications, covering everything from uploads, storage, manipulations, optimizations to delivery with a fast content delivery network (CDN).
Setting up react-native-video player
We decided to use react-native-video v5.1.1 for playing videos in React Native application. Here is the guide to set up the video player.
1import Video from "react-native-video"; 2 3const Player = ({ uri }) => { 4 return ( 5 <SafeAreaView style={styles.container}> 6 <Video 7 style={styles.player} 8 source={{ uri }} 9 controls 10 resizeMode="contain" 11 /> 12 </SafeAreaView> 13 ); 14};
The above code snippet works perfectly on iOS but not on Android. On Android, we faced a known issue where the video doesn't play but the audio plays with significant delay. This issue can be resolved by setting exoplayer as the default player for Android in react-native.config.js in the root directory of the project.
1module.exports = { 2 dependencies: { 3 "react-native-video": { 4 platforms: { 5 android: { 6 sourceDir: "../node_modules/react-native-video/android-exoplayer", 7 }, 8 }, 9 }, 10 }, 11};
Setting up Cloudinary
A Cloudinary account is needed before proceeding further. Once the account is ready, following are the steps to enable unsigned upload for the account.
- Go to the Settings of Cloudinary.
- Select the Upload tab.
- Search for Upload presets section.
- Click on Enable unsigned uploading.
This generates an upload preset with a random name which will be required for the unsigned upload.
Setting up Client for Upload
Selecting Video from the gallery
We decided to use react-native-image-crop-picker v0.36.2 library to select the video from the gallery. Here is the guide for setting it up.
1import ImagePicker from "react-native-image-crop-picker"; 2 3const selectVideo = ({ setVideoToUpload }) => { 4 ImagePicker.openPicker({ mediaType: "video" }) 5 .then(setVideoToUpload) 6 .catch(console.error); 7};
Uploading Video
1// Cloud Name: Found on the Dashboard of Cloudinary. 2const URL = "https://api.cloudinary.com/v1_1/<CLOUD_NAME>/video/upload"; 3// Random Name: Generated After Enabling The Unsigned Uploading. 4const UPLOAD_PRESET = "<UPLOAD_PRESET_FOR_UNSIGNED_UPLOAD>"; 5 6const uploadVideo = (fileInfo, onSuccess, onError) => { 7 const { name, uri, type } = fileInfo; 8 let formData = new FormData(); 9 10 if (uri) { 11 formData.append("file", { name, uri, type }); 12 formData.append("upload_preset", UPLOAD_PRESET); 13 } 14 15 axios 16 .post(URL, formData, { 17 headers: { "Content-Type": "multipart/form-data" }, 18 }) 19 .then(res => onSuccess(res.data)) 20 .catch(error => onError(error)); 21}; 22 23export default { uploadVideo };
Fetching Videos on client
1import base64 from "base-64"; 2 3// API Key and Secret: Found on the Dashboard of Cloudinary. 4const API_KEY = "<API_KEY>"; 5const API_SECRET = "<API_SECRET>"; 6 7const URL = `https://api.cloudinary.com/v1_1/<CLOUD_NAME>/resources/video`; 8 9const getVideos = (onRes, onError) => { 10 axios 11 .get(URL, { 12 headers: { 13 Authorization: base64.encode(`${API_KEY}:${API_SECRET}`), 14 }, 15 }) 16 .then(res => onRes(res.data.resources)) 17 .catch(error => onError(error)); 18}; 19 20export default { getVideos };
Response:
1{ 2 "resources": [ 3 { 4 "asset_id": "475675ddd87cb3bb380415736ed1e3dc", 5 "public_id": "samples/elephants", 6 "format": "mp4", 7 "version": 1628233788, 8 "resource_type": "video", 9 "type": "upload", 10 "created_at": "2021-08-06T07:09:48Z", 11 "bytes": 38855178, 12 "width": 1920, 13 "height": 1080, 14 "access_mode": "public", 15 "url": "http://res.cloudinary.com/do77lourv/video/upload/v1628233788/samples/elephants.mp4", 16 "secure_url": "https://res.cloudinary.com/do77lourv/video/upload/v1628233788/samples/elephants.mp4" 17 } 18 //... 19 ] 20}
Transforming uploaded videos for faster delivery
One way to play a video is to first completely download the video on the client's device and then play it locally. The biggest drawback of this approach is that it could take a long time before the video is fully downloaded and till then the device is not playing anything at all. This strategy also requires a sophisticated algorithm to manage memory.
Another solution is to use Adaptive Bitrate Streaming(ABS) for playing videos. As the name suggests in this case the video is being streamed. It is one of the most efficient ways to deliver videos based on the client's internet bandwidth and device capabilities.
To generate ABS we add an eager transformation to the upload preset we created while setting up the Cloudinary account earlier.
An eager transformation runs automatically for all the videos uploaded using the upload preset which has the transformation added.
Setup Transformation
Here are the steps for adding an eager transformation to the upload preset.
- Go to the upload preset which was generated when the unsigned upload was enabled while setting up Cloudinary in the earlier section.
- Click on edit.
- Go to the Upload Manipulations section.
- Click on Add Eager Transformation to open the Edit Transaction window.
- Open the Format dropdown under the Format & shape section and select M3U8 Playlist (HLS).
- Select any streaming profile from the dropdown under the More options. Any profile matching your maximum required quality can be selected here.
Next upload video with the same preset trigger the transformation to generate the M3U8 format which is the one we need for playing the videos as streams.
Consuming video streams
To play streams, the type property must be provided to the react-native-video's source prop. In this case, type will be m3u8. Also, the URI needs to be updated with this same extension.
1- source={{ uri }} 2+ source={{ uri: uri.replace(`.${format}`, '.m3u8'), type: 'm3u8'}}
Conclusion
Once all the above mentioned steps were performed then we were able to upload and stream videos on both ios and android without any issues.