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:
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, MDN
- Project Architecture: Previous Post: In depth or you can read the summary
- Slider component: Previous post: How I build a YouTube Video Player with ReactJS: Build the Seekbar control
- Understanding of track element.
- Understanding of Javascript’s map function, editing element attributes.
- React:
useImperativeHandle
hook
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:
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 thetrack
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: ```
- Let us update the
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.
-
Now pass the chapters to the slider component via the seekbar component as follows:
Once this is hooked the output will look like below
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!
Top comments (0)