DEV Community

Keyur Paralkar
Keyur Paralkar

Posted on

How I build a YouTube Video Player with ReactJS: Add chapters to the video

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:

Youtube’s Video Frame Preview
Youtube’s progress bar gets split up into multiple mini sliders. Video source

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

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 


Enter fullscreen mode Exit fullscreen mode

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:

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 :

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.

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:
    • Update the render method of the slider component such that when chapters are enabled we map over them and create multiple sliders:
    • 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:
      
- It accepts a prop as `$width` that defines the width of this mini - slider component.
Enter fullscreen mode Exit fullscreen mode
  • Now pass the chapters to the slider component via the seekbar component as follows:

  • Once this is hooked the output will look like below

Chapters as mini sliders

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 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:

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:
  • Next, we get the previous and the next chapter elements:
  • Next, we fill all the previous chapters before the current chapter:
  • 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.

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

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.

  • 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:

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:

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:

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:

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:

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.

Thank you for reading!

Follow me on twittergithub, and linkedIn.

Enter fullscreen mode Exit fullscreen mode

Top comments (0)