In this tutorial, we'll dive deep into how you can seamlessly integrate a video player in your React.js applications. Besides introducing the HTML5 <video> tag, exploring its capabilities and limitations, we'll also be talking about making use of the popular Video.js library to power some advanced recipes like adding Adaptive Bitrate Streaming (ABS) to our video playback.

Table of Contents

Setting up the dependencies

Before we start, we would need to setup our react project and its dependencies. For this tutorial, we would be using React 18.

Creating a new react app

The easiest and the most straight-forward way to create a new React.js app is using create-react-app. This simply exposes a single-line command which takes care of setting up the boilerplate.

npx create-react-app@latest react-video-player-ik-demo

When we run the above command, the utility starts setting up a fresh project boilerplate in the directory we specified i.e. react-video-player-ik-demo. This might take a few minutes, but once it is completed, you should see a success message on your terminal.

We can now open the project directory in an editor of our choice (for this tutorial, we would be using Visual Studio Code) and this is what it should look like.

This indicates that the project boilerplate is ready and we can run it using the npm start command from within the project directory. When we execute the command, it should just start up our application in the browser. At this stage, this is what we see in our browser (you can manually visit http://localhost:3000 in your browser if it doesn’t open by itself).

If this is what you see in your browser, congratulations - you have a fresh React.js app successfully running and are ready for the next step.

Basic video playback recipes with HTML5 <video> tag

We can make use of the HTML5 <video> tag to create a basic video player in our react app, without having to rely on third-party libraries or plugins.

Getting started with <video>

To do this, let’s go ahead and edit our App.js with the code below.

import React from 'react';

export default function App() {
    return (
        <video
            src='https://ik.imagekit.io/ikmedia/example_video.mp4'
            width='1440'
            height='680'
        />
    );
}

The src attribute accepts a local or a remote path to a video file that needs to be bound with the player.

On saving these changes and trying it out in the browser, you might see a video thumbnail but not the actual video.

This is the expected behaviour. But weren’t we supposed to add a video playback support? Correct, the reason for this is that we haven’t added any controls yet. Let’s go ahead and add a controls attribute to our <video> tag like:

<video
    src='https://ik.imagekit.io/ikmedia/example_video.mp4'
    width='1440'
    height='680'
    controls
/>

You should now see the basic controls like the play/pause toggle, the seek bar, full screen view etc. on your video.

ℹ️
The <video> tag provides us with a few attributes that we can use to control the behaviour of the video player. You can check out the MDN Web Docs for a comprehensive list of all attributes supported by the <video> tag.

And that’s it. This is how we build a basic video player in React.

Automatically playing videos

To automatically play the video as soon as the page loads, we can use the autoPlay attribute like:

<video
    src='https://ik.imagekit.io/ikmedia/example_video.mp4'
    width='1440'
    height='680'
    controls
    autoPlay
/>
⚠️
You may note that on some browsers (eg: Chrome 70), the videos don’t autoplay even after adding the autoPlay attribute. To fix this, we require another attribute muted to be added.

On some other devices / browsers (eg: Safari on iOS), you might need to add an attribute playsInline which basically indicates that the video is to be played “inline”, within the element’s playback area.

After the above-mentioned changes, our App.js should look like:

import React from 'react';

export default function App() {
    return (
        <video
            src='https://ik.imagekit.io/ikmedia/example_video.mp4'
            width='1440'
            height='680'
            controls
            autoPlay
            muted
            playsInline
        />
    );
}

To disable video autoplay, autoplay="false" will not work; the video will autoplay if the attribute is there in the <video> tag at all. To remove autoplay, the attribute needs to be removed altogether.

Adding a thumbnail poster

We can also display an image as a poster on the video until the video loads and a user action is registered on it. To do this, we can make use of the poster attribute like:

<video
    src='https://ik.imagekit.io/ikmedia/example_video.mp4'
    poster='https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=w-1200,h-680'
    width='1440'
    height='680'
    controls
/>

The poster attribute accepts a local or a remote path to an image that needs to be displayed as a poster.

If this attribute isn't specified, nothing is displayed until the first frame is available, then the first frame is shown as the poster frame.

⚠️
When the poster attribute is used along with autoPlay, the latter over-shadows the final behaviour and the video starts playing as soon as it is loaded without waiting for a user action.

Adjusting the width and height

We can adjust the width and height of the player by using the width and height attributes respectively. So to build a 600x300 player, our <video> element would look like:

<video
    src='https://ik.imagekit.io/ikmedia/example_video.mp4'
    poster='https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=w-1200,h-680'
    width='600'
    height='300'
    controls
/>
ℹ️
It is important to understand that this will only resize the player's HTML container in the browser. The actual video file which is being loaded over the network is still the same size.

In such cases, to optimise overall network bandwidth and improve user experience, we can modify the actual video file according to the container size. We'll talk about how ImageKit's Video API can do this for us in the further sections.

Looping the video

In some use-cases like marketing pages etc., we might want to play a video visual indefinitely. To loop over a video, we can use the loop attribute provided by the <video> tag like:

<video
    src='https://ik.imagekit.io/ikmedia/example_video.mp4'
    poster='https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=w-1200,h-680'
    width='1440'
    height='680'
    controls
    loop
/>

Advanced video playback recipes with Video.js

Now, while the native <video> tag might just work for the basic setups, more advanced use-cases around video playbacks such as multiple playback sources, multiple stream formats, support for ABS calls etc. for a mature framework. Fortunately, there are quite a few frameworks built to help us overcome limitations or just make our implementations streamlined. Let’s talk about one such library - Video.js.

Video.js is the most popular web video player library out there, being used by over 700,000 websites. It is built from the ground up for an HTML5 world and supports HTML5 video and Media Source Extensions, as well as other sources like YouTube and Vimeo (through plugins) on both desktops and mobile devices.

Getting started with Video.js

The easiest way to get started with Video.js in React is to install the library using npm.

npm install video.js@7.18.1

Once done, you should see it added to your dependencies list in package.json.

⚠️
Note how we specified a particular version of the library to be installed instead of defaulting to the latest release - this is because we’ll be using the library’s 7.18.1 version which is the last stable version which reliably supports some of the plugins we’d be using ahead in this tutorial. Make sure you stick to the same to avoid running into dependency issues in your implementations.

Once we have Video.js installed within our project, the next step is to start making changes in our project to create a basic video player.

Step 1: Make a VideoJS component

The first step is to import the videojs methods and classes from the library and bind them to the corresponding DOM element which will eventually be decorated as a fully-functioning video player.

Let’s create a new file called VideoJS.js inside our src/components directory. If the directory doesn’t exist, we create it ourselves. After this step, our project directory structure should look something like:

├── node_modules/
├── README
├── package.json
└── src/
    ├── App.css
    ├── App.js
    ├── App.test.js
    ├── components/
    │   └── VideoJS.js
    ├── index.css
    ├── index.js
    ├── logo.svg
    ├── reportWebVitals.js
    └── setupTests.js

In the VideoJS.js file, we’re going to setup our video player component.

import React, { useEffect } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';

export const VideoJS = (props) => {
  const videoRef = React.useRef(null);
  const playerRef = React.useRef(null);
  const { options, onReady } = props;

  useEffect(() => {

    // Make sure Video.js player is only initialized once
    if (!playerRef.current) {
      // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode. 
      const videoElement = document.createElement("video-js");

      videoElement.classList.add('vjs-big-play-centered');
      videoRef.current.appendChild(videoElement);

      const player = videojs(videoElement, options, () => {
        videojs.log('player is ready');
        onReady && onReady(player);
      });

      playerRef.current = player;

      // You could update an existing player in the `else` block here
      // on prop change, for example:
    } else {
      const player = playerRef.current;

      player.autoplay(options.autoplay);
      player.src(options.sources);
    }
  }, [onReady, options, videoRef]);

  // Dispose the Video.js player when the functional component unmounts
  React.useEffect(() => {
    const player = playerRef.current;

    return () => {
      if (player && !player.isDisposed()) {
        player.dispose();
        playerRef.current = null;
      }
    };
  }, [playerRef]);

  return (
    <div data-vjs-player>
      <div ref={videoRef} />
    </div>
  );
}

export default VideoJS;
ℹ️
Compatibility with React 18 StrictMode

Note that we are utilizing a placeholder element videoElement to append our Video.js element.

This approach is necessary because React 18 StrictMode introduces additional behaviours that aim to prepare for a reusable state in dev mode. Specifically, during a component's initial mounting, React simulates mounting, unmounting, and then mounting again.

When React simulates unmount, the video.js dispose removes the video element from the DOM, which invalidates the ref that React held, leading to an error. Hence, by wrapping it in the parent element, we can get rid of this error.

Step 2: Use the VideoJS component in our app

And now in our App.js, we import and use the component we built above like:

import React from 'react';
import videojs from 'video.js';
import VideoJS from './components/VideoJS';

export default function App() {
  const playerRef = React.useRef(null);

  const videoJsOptions = {
    controls: true,
    responsive: true,
    fluid: true,
    sources: [{
      src: 'https://ik.imagekit.io/ikmedia/example_video.mp4',
      type: 'video/mp4'
    }]
  };

  const handlePlayerReady = (player) => {
    playerRef.current = player;

    // You can handle player events here, for example:
    player.on('waiting', () => {
      videojs.log('player is waiting');
    });

    player.on('dispose', () => {
      videojs.log('player will dispose');
    });
  };

  return (
    <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
  );
}

Now, at this step, your project should successfully rebuild and on refreshing the browser, you should see a video player like shown below.

Autoplay, thumbnail posters, basic resizing and loops

Adding the basic autoplay, loops, posters etc. all can be done by modifying the configuration object like:

const videoJsOptions = {
  autoplay: true, // autoplay the videos
  muted: true,  // required by newest versions of browsers for autoplay to work
  controls: true, // show controls on the player
  responsive: true, // make the player responsive
  fluid: true,
  loop: true, // loop the video
  sources: [{
    src: 'https://ik.imagekit.io/ikmedia/example_video.mp4',
    type: 'video/mp4'
  }],
  // adding a thumbnail poster
  poster: 'https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=w-1200,h-680'
};

Now, unlike the <video> tag, to adjust the width and height in video.js, we can add a style attribute to the element. In this case, we’ll just create a new <div> block and apply the style to it.

<div style={{ width: '1440px', height: '900px' }}>
  <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
</div>

This will render a player container of size 1440x900.

Adaptive Bitrate Streaming (ABS)

Adaptive Bitrate Streaming (ABS) is an approach to video streaming that is an absolute game changer for content creators and viewers alike – adjusting the quality of the video stream in real-time, based on the viewer's internet connection. This means that regardless of the viewer's device or network speed, they can enjoy a seamless viewing experience with minimal buffering or lag.

Video.js player supports ABS out-of-the-box, which means when given a supported video file (for example, M3U8), it adjusts the stream source as per the bitrate. Besides, it also shows up controls for manual selections for users. Let’s see how to implement this.

Step 1: Setting up the dependencies

For enabling support for ABS, we need two more libraries to be installed to our project - videojs-http-source-selector and videojs-contrib-quality-levels. As the name suggests, the first library is used to enrich the player with user controls which help the user explicitly choose the quality of video playback, and the second library provides a framework of working with source quality levels.

Let’s go ahead and install them.

npm install videojs-http-source-selector@2.1.0 videojs-http-source-selector@1.1.6

After this step, you should see these two added as a dependency to your package.json.

Step 2: Configuring the imports in our VideoJS component

To make sure our player uses the libraries, we need to import them in our VideoJS component.

// Inside components/VideoJS.js

// For ABS Support
import "videojs-contrib-quality-levels";
import "videojs-http-source-selector";

Also, we would need to bind the source selector on our player instance for our player to be able to use the underlying methods. Let’s go ahead and do that.

// Make sure Video.js player is only initialized once
if (!playerRef.current) {
  ...
  ...
  playerRef.current = player;

  // Binding to the source selector for ABS
  player.httpSourceSelector();
  ...
  ...
} else {
  ...
  ...
  player.src(options.sources);
  
  // Binding to the source selector for ABS
  player.httpSourceSelector();
  
  ...
  ...
}

Step 3: Setting up a source video stream

Once we have the player set up for ABS, we need to update the source stream with a supported file. This can be done by using multiple raw video files embedded in a UTF8-encoded manifest file, eg. .m3u8.

For this demo, we are playing the video from the following manifest file - https://ik.imagekit.io/ikmedia/example_video.mp4/ik-master.m3u8?tr=sr-240_360_480_720_1080.

ℹ️
If you're wondering, the above URL to a manifest file contains pointers to multiple video streams each corresponding to a bitrate as described in the query parameter. So, in this case, we have a single source video file which is converted into multiple video files based on the required bitrates i.e. 240p, 360p, 480p, 720p and 1080p, and stored in an encoded manifest.

Later in this tutorial, we'll be talking about how we can use ImageKit's Video API to generate such .m3u8 or .mpd manifests.

Let’s head over to our App.js and update the source stream in the config.

const videoJsOptions = {
  ...
  ...
  sources: [{
    src: `https://ik.imagekit.io/ikmedia/example_video.mp4/ik-master.m3u8?tr=sr-240_360_480_720_1080`,
    type: 'application/x-mpegURL'
  }]
};

And that’s it. Let’s head over to our application in the browser to see it working.

Playback at 1080p on optimal network speeds 
Playback at 240p on reduced network speeds

If this is what you see in your browser - Congratulations! You’ve successfully implemented ABS on your stream using Video.js 🎉

ImageKit.io for Video Transformations and Optimisation

Video files, when transferred over a network to a user's device, can significantly impact web experience if not optimized. Implementing best practices for transformation and optimisation before delivery, hence, becomes vital. The techniques include, but are not limited to:

  • Resizing to proper dimensions.
  • Encoding to compatible formats.
  • Creating multiple bitrate versions for Adaptive Bitrate Streaming (ABS) for users on slower networks.

ImageKit.io is a powerful platform that offers real-time image and video optimisation tools. It serves as a comprehensive solution for end-to-end media management, optimisation, and delivery while supporting various transformations, resizing, cropping, format conversion, quality adjustment, etc. The platform also offers a generous free tier to get you started.

Let’s look at some use-cases where ImageKit’s Video API can not only help deliver better video playback experiences for your users but also optimise your video delivery and save on your network costs.

Resizing the video to adapt to the player’s dimensions

In the resizing approaches we’ve seen earlier in this tutorial, we have been resizing our player’s container dimensions. Although this works well to fit your player well within the UI, but is it optimised? The answer is no, and here’s why.

When you’re using a 4k raw video file which has the dimensions 3840x2160 and trying to load it over the network for the user only to fit the video inside a player of dimensions 640x480, there’s a huge unnecessary network overhead both for the user and for your systems. And a network overhead comes with a cost - in this case, both in terms of money and user experience.

So, how do you solve for this problem? A straightforward approach is creating multiple raw video files with different dimensions and call for the most compatible one based on the player’s container size. And here, you introduce overhead storage costs and a good amount of complexity in your code.

With ImageKit’s Video Transformations, you can transform your raw video file into an appropriate size on-the-go while loading it within your player’s container - with just a query parameter. Let’s see how.

The original video file is ~37.7MB

If we use the raw video file, we would be transferring ~37.7 MB over the network. To resize the video file for a 640x480 container, we need to add a query parameter tr with the value w-640.

Updated URL: https://ik.imagekit.io/ikmedia/example_video.mp4?tr=w-640

After resizing, we have reduced the network overhead to ~1.7 MB (a steep 95.49% reduction).

Let’s update our code to make use of this. In App.js,

// Add a constant
const playerWidth = 640;

// Updating the url in our sources configuration to use the constant
const videoJsOptions = {
  controls: true,
  responsive: true,
  fluid: true,
  sources: [{
    src: `https://ik.imagekit.io/ikmedia/example_video.mp4${playerWidth ? `?tr=w-${playerWidth}` : ''}`,
    type: 'video/mp4'
  }],
  poster: 'https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=w-1200,h-680'
};

// In the return statement, let's put a width to our element
return (
  <div style={{ width: `${playerWidth}px` }}>
    <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />
  </div>
);

To learn more about video resizing, you can check out the documentation here: Resize, crop, and other common video transformations | ImageKit.io Docs

Delivering the most suitable video format

With ImageKit’s Video API, you don’t need to worry about format selection or adding multiple sources as it will automatically identify formats compatible with your users' devices and deliver the most relevant and optimised format. Having said that, you can also deliver a particular format as per your need.

This can be achieved with the query parameter tr like tr=f-webm for WebM formats, tr=f-mp4 for MP4 formats and tr=f-auto for setting it to auto selection as mentioned above. You can learn more about this here: Automatic video format conversion | ImageKit.io Docs

Adaptive Bitrate Streaming (ABS)

Generating ABS manifests is a one-step process, thanks to ImageKit’s Video API which supports generating MPEG-DASH and HLS manifests on-the-fly with a single source video file.

If you're new to Adaptive Bitrate Streaming (ABS), you can check out this article which talks about ABS in-depth and compares both the protocols to help you pick one that suits you.

MPEG-DASH

To create an MPEG-DASH ABS playlist, you need to suffix the URL path with ik-master.mpd and use the tr query parameter with the sr value to specify the resolutions. Here’s an example:

https://ik.imagekit.io/ikmedia/example_video.mp4/ik-master.mpd?tr=sr-240_360_480_720_1080

HLS

To create an HLS ABS playlist, you need to suffix the URL path with ik-master.m3u8 and use the tr query parameter with the sr value to specify the resolutions. Here’s an example:

https://ik.imagekit.io/ikmedia/example_video.mp4/ik-master.m3u8?tr=sr-240_360_480_720_1080

Generating video thumbnail posters

In the sections above, we’ve seen how both <video> tag and Video.js library supports thumbnail posters which act as a “loading state” for our player. This poster can be a local image or a remote image which is referenced by its URL.

With ImageKit, you can generate posters from a video just by suffixing the URL path with ik-thumbnail.jpg. Adding to that, you can use a whole lot of transformation parameters to modify the poster as per your needs.

Let’s take an example to demonstrate this - we want the frame at the 10th second of the video to be our thumbnail and the poster size should be the same as the player size. Here’s how we’ll create a URL for the poster:

https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=so-10,w-640

Now, if we update this in our code like:

const playerWidth = 640;
...
...
const videoJsOptions = {
  controls: true,
  responsive: true,
  fluid: true,
  sources: [{
    src: `https://ik.imagekit.io/ikmedia/example_video.mp4${playerWidth ? `?tr=w-${playerWidth}` : ''}`,
    type: 'video/mp4'
  }],
  poster: `https://ik.imagekit.io/ikmedia/example_video.mp4/ik-thumbnail.jpg?tr=so-10${playerWidth ? `,w-${playerWidth}` : ''}`
};

We should be able to see the poster on our video player.

The poster is the frame from 10th second of the video with a width of 640px

After delving into the fundamental playback setup with the HTML5 <video> tag and exploring its various configuration options, including its limitations like the absence of ABS support, we turned to the Video.js library for more advanced functionalities. By experimenting with features like ABS, it's evident that React.js, coupled with the versatility of Video.js and the advanced transformation and optimization capabilities of ImageKit, can truly enhance your video playback experience. With these powerful tools at your disposal, you can elevate your video content delivery to unparalleled levels of quality and performance.

You can find the code implementation for this tutorial on Github.