How I build a YouTube Video Player with ReactJS: Add chapters to the video
Tags: webdev, react, javascript, opensource
Published: 2024-02-21 11:08:15

## Introduction

If you are into online learning/education or an unboxing video of a gadget on youtube, etc. Then I am pretty sure you might have come across this thing here:

<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4gmtoavgn09uqcp9gjru.gif" alt="Youtube’s Video Frame Preview" />
  <figcaption>Youtube’s progress bar gets split up into multiple mini sliders. <a href="https://www.youtube.com/watch?v=TwDJhUJL-5o&t=356s" target="_blank">Video source</figcaption>
</figure>
</p>

The progress bar is broken into sections and each section describes what it represents. This is mostly done by the video's author so that viewers can quickly get a gist of what they will be learning in this section or the next few minutes. 

These quick gist of each section are called chapters. In this article, we will be looking at how to implement them.

So without further ado, let us get started.

## Prerequisites

- What are VTTs? *[My previous Blog post](https://dev.to/keyurparalkar/how-i-build-a-youtube-video-player-with-reactjs-building-frametooltip-component-3o6p)*, [*MDN*](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API)
- Project Architecture: *Previous Post: [In depth](https://dev.to/keyurparalkar/how-i-build-a-youtube-video-player-with-reactjs-building-frametooltip-component-3o6p) or you can read the [summary](https://dev.to/keyurparalkar/building-with-react-js-create-your-own-youtube-video-player-starting-with-basics-1lpp)*
- Slider component: Previous post: *[How I build a YouTube Video Player with ReactJS: Build the Seekbar control](https://dev.to/keyurparalkar/how-i-build-a-youtube-video-player-with-reactjs-build-the-seekbar-control-3kg)*
- Understanding of *[track element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)*.
- Understanding of Javascript’s map function, editing element attributes.
- React: [`useImperativeHandle` hook](https://react.dev/reference/react/useImperativeHandle)

## The What?

So this broken time bar/seek bar/ progress bar that you see on the video is nothing but chapters. This is a very common feature that you get to see on YouTube.  

We already have built the seek bar in our previous blog posts and with some tweaks to the existing Slider component we can achieve this functionality.

## The Why?

We are building this component because it:

- Tells the user what is happening in the given time frame.
- Gives a quick reference to the content the user is looking for with the help of chapter text below the frame snapshot.
- Provides a better User experience.

## The How?

Before we get started with the implementation let us have a quick overview of what the steps are:

- Generate VTT for chapters
- Load the VTT into the video with the help of `track` element.
- Extract chapters from the `track` element.
- Update the Slider component for the chapter logic
- Display the chapters in the tooltip

There isn’t much jargon here if you have gone through the previous article. If not, I would highly recommend going through it from the prerequisites section.

> _💡 **NOTE:** The implementation of chapter fill css-variable was inspired from the https://www.vidstack.io/_

## Generating VTT for chapters

Web video text track is a file format that has time-based data. Meaning, that it contains text that needs to be shown on the video along with the information of when this text needs to be shown(nth second). 

Since we want the users to know from which second to which second the chapter starts and ends we will write a VTT file like the one below:

```
WEBVTT

00:00:00.000 --> 00:00:15.000
Chapter 1

00:00:15.000 --> 00:00:42.000
Chapter 2

00:00:42.000 --> 00:00:48.000
Chapter 3

00:00:48.000 --> 00:01:01.000
Chapter 4

00:01:01.000 --> 00:01:09.000
Chapter 5 
```

This tells us that, from the 0th to the 15th second there would be text that corresponds to Chapter 1 and so on.

## Load the VTT into the video

Next, to load the VTT into the video we make use of the `[track` element.](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track) We update the `Video.tsx`  like below:

{% embed https://gist.github.com/keyurparalkar/a7d5349ba981c00cd7a1e176f41fa34d %}

Here we added a track element with kind attribute of chapters. In the src attribute make sure that you place the location/url of the chapter’s `vtt` file that we generated above.

## Extracting chapters from the `track` element

Now that we have `track` element in place let us make use of it to extract the chapters into our code. What we want here is that we need these chapters when the video is loaded completely. 

To do that, we already have a `useEffect` in the `Video.tsx` file that hydrates the store with some parameters. We make use of this same effect to capture the chapters. Copy and paste below `useEffect` :

{% embed https://gist.github.com/keyurparalkar/a105a36f76aa24935e202e114d1f8fe3 %}

Here we make use of the video element’s `loadeddata` event to hydrate the store. The code is pretty self-explanatory but I would like to explain the part of the chapter here:

- In line 12, we make use of `trackChaptersRef` which is being passed to the track element. From this, we access the `track` object.
- In line 13, we get all the cues in the track.
- On the next line, we make use of the `map` function to return an array of objects that consists of:
    - index
    - name of the chapter, start and end time
    - `percentageTime`: This represents what percentage of time this chapter contributes to out of the total video duration.

I know this hydration of chapter object with additional fields doesn’t make any sense right now but it will when we get into the changes of the slider component. We will discuss this in the next section.

## Update the Slider component for the chapter logic

Folks, take a cup of coffee now since this will be a long section. I will try my best to make it interesting 🙂.

Before proceeding further, I would highly recommend going through this [Slider component implement article.](https://dev.to/keyurparalkar/how-i-build-a-youtube-video-player-with-reactjs-build-the-seekbar-control-3kg)

Here is the outline of the steps that we will be doing to achieve the chapter functionality:

- Split the Slider component into multiple mini-slider components
- Handling Slider fill across the multiple mini-sliders
- Handling mouse moves on hover, click and drag.
- Updating the overall slider on playback
- Styling the slider

### Split the Slider component into multiple mini-slider components

Ok so hear me out:

*Chapters on Slider is nothing but a bunch of mini sliders.*

From this what I mean is, in this functionality, we won’t have a single slider of width 100%. But instead, we will have mini sliders of length equal to the width of `percentageTime`. This state is the derived state that we computed when we loaded the video component in the above section.

We got the jest of what we are doing, now let us start with the implementation. 

- The first thing that we need to do is, extend our Slider component such that it accepts chapters as a prop:
    - Let us update the `SliderProps` interface:
      {% embed https://gist.github.com/keyurparalkar/81b481f65ec510da29e0eb60291a46d8 %}
    - Update the render method of the slider component such that when chapters are enabled we map over them and create multiple sliders:
      {% embed https://gist.github.com/keyurparalkar/36817fb0fb1a2a42300ee24b2644c58b %}
    - We also maintain references to all these mini sliders with the help of chapterRefs. It is an array ref that holds the reference to these elements. We do this in this line:
    ```
 ref={(el: HTMLDivElement) => el && (chapterRefs.current[index] = el)}
    ```
    - Quick thing to note here, We have created a StyleChapterContainer. It may look like this:
      {% embed https://gist.github.com/keyurparalkar/bb1debdeefd701620b99c822ba593126 %}
    - It accepts a prop as `$width` that defines the width of this mini - slider component.

- Now pass the chapters to the slider component via the seekbar component as follows:
{% embed https://gist.github.com/keyurparalkar/31d99d3dc42f1e5dc022ac8d86e6b50b %}

- Once this is hooked the output will look like below

![Chapters as mini sliders](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5b19o40eoow9fahpqt2.png)

### Handling filling logic on mouse move and click events

The above logic needs to be handled for the following scenarios:

- While clicking and,
- While dragging the slider

Here is the pictorial representation of these scenarios:

- While Clicking:

<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/erymo9rbzpjls48q4cw5.gif" alt="Click interaction on sliders with chapters" />
  <figcaption>Click interaction on sliders with chapters</figcaption>
</figure>
</p>

- While dragging:

<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8ryc9ia89pw6gc3bvo6.gif" alt="Drag interaction on sliders with chapters" />
  <figcaption>Drag interaction on sliders with chapters</figcaption>
</figure>
</p>

<aside>
_💡 **NOTE: I would like to remind you guys that to go through this blogpost: *[How I build a YouTube Video Player with ReactJS: Build the Seekbar control](https://dev.to/keyurparalkar/how-i-build-a-youtube-video-player-with-reactjs-build-the-seekbar-control-3kg)* before proceeding further. Because it contains a lot of jargon which is better explained there.**_
</aside>

So here we go, below is the `handleClick` event handler that takes care of fill the chapters on click:

{% embed https://gist.github.com/keyurparalkar/7b4923fbe5b3a9ee422b0fa966f7bf85 %}

Below is the explanation for the same:

- This part right here calculates all the chapter's width and with the help of the current `--slider-fill` value we get the current chapter index:

{% embed https://gist.github.com/keyurparalkar/6526458f517de9aa818e3415023b707a %}

- Next, we get the previous and the next chapter elements:

{% embed https://gist.github.com/keyurparalkar/a8e72d26fc9a33a2b6a5315f22380a30 %}

- Next, we fill all the previous chapters before the current chapter:

{% embed https://gist.github.com/keyurparalkar/d9cc8fa53b62fc186606391cbe494dff %}

  - A thing to note here, we track the fill of each slider element with the help of the CSS variable: `--chapter-fill`.

  - We also need to update our `StyledSliderFill` component such that whenever chapters exist we make use of the `—chapter-fill` CSS variable.

{% embed https://gist.github.com/keyurparalkar/63f3557ced732bee2be8a33319f0c5f9 %}

- Next, we summon all the chapter fill values below:

{% embed https://gist.github.com/keyurparalkar/c687ee5fde6863cc3cdb728201ca1f7b %}

Then, to fill the current chapter we make use of the `computeCurrentWidthFromPointerPos` utility function and then finally set its value to the current chapter’s `--chapter-fill`.

{% embed https://gist.github.com/keyurparalkar/ddc7ccd0a980627d2b78b62d149153d8 %}

- For scenarios of clicking from right to left direction i.e. going back in video, we fill the next chapter to 0 from the current chapter:

{% embed https://gist.github.com/keyurparalkar/5db60a1d56c079c4c517da6ad6f73383 %}

Similarly, we make use of the `handleMouseMove` function that handles the filling of chapters during the `mousemove` event i.e. when we are dragging the slider knob:

{% embed https://gist.github.com/keyurparalkar/19f907b14f2b8544ab137d9e37578dbb %}

This is the same function we used to update the `--slider-pointer` CSS variable in the previous blog posts. The purpose of this function from the chapters point of view is as follows:

- Update the chapter fill of the respective elements
- Add a `data-chapter-dragging` attribute

I would like you guys to go through the comments in the above code block to understand the functionality. 

Just a quick note: The purpose of adding `data-chapter-dragging` attribute is to use it in styling which we will look into in the later section.

### Updating the slider position on playback

There is one more scenario left that we need to cover which is handling the playback mechanism across multiple sliders. Now to do this we need a mechanism that will help us to update the `--chapter-fill` CSS variable of each chapter whenever the current duration of the video changes.

This is a simple task. We just need to expose a function that updates this chapter fill for the given chapter with the help of `[useImperativeHandle` hook](https://react.dev/reference/react/useImperativeHandle).

The first thing is to update the `useImperativeHandle` hook in the `Slider` component:

{% embed https://gist.github.com/keyurparalkar/b469c0177ad0aa247ab2934b7f4f9870 %}

We introduced a new function here: `updateChapterFill` which accepts the current chapter’s index and the percentage completion of that chapter. 

Next, we consume this function via refs in the `Seekbar` component like below:

{% embed https://gist.github.com/keyurparalkar/bb8a7ee86006c4ce7e99d3893a98f6e9 %}

Inside the `Seekbar` component, we access the `currentTime` and the `chapters` from the player context. 

Then we make use of the `useEffect` that updates on `currentTime` and `isSeeking`. Along with updating the `slider-fill` it also updates the chapter fill with the help of `updateChapterFill`.

We do some simple calculations and get the `currentChapterFillWidth` by dividing the `currentTime` by the total chapter duration. 

lastly, we pass this to our update function.

This is how our functionality will look like when during playback:

<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whuawmq3nm8u8ucmy6nd.gif" alt="Chapter fill on video playback" />
  <figcaption>Chapter fill on video playback</figcaption>
</figure>
</p>

### Styling the chapter sliders

One last thing that is pending is adding a small styling effect that will give better UX feedback. The style that I am talking about is increasing the width of the slider whenever we are dragging the slider like below:


<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyb08j2kbzjpj4tt56qy.gif" alt="Emphasising chapters on drag" />
  <figcaption>Emphasising chapters on drag</figcaption>
</figure>
</p>

To do this we make use of the `data-chapter-dragging` attribute that we add in the `handleMouseMove` function. We use this attribute in `StyledContainer` of the `Slider` component:

{% embed https://gist.github.com/keyurparalkar/73fb9c8960658dc2668416106118a24a %}

We make sure that whenever we are dragging i.e. whenever the container has the `data-dragging` attribute only then check for any child div that has `data-chapter-dragging` attribute. If it has then we update the height of the slider/container by `8px`.

## Summary

So to summarize we achieved the following things in this blogpost:

- We understand the reason why are we doing this feature.
- We saw how to get the chapter's data into the video via the track element.
- And lastly, we saw the changes that we need to make to the slider component to achieve the desired chapter UX.

The entire code for this tutorial can be found [here](https://github.com/keyurparalkar/react-youtube-player-clone).

Thank you for reading!

Follow me on [twitter](https://twitter.com/keurplkar), [github](http://github.com/keyurparalkar), and [linkedIn](https://www.linkedin.com/in/keyur-paralkar-494415107/).