Building a video player with a streamlined and innovative approach
Tags: react, typescript, reactvideoplayer, frontend
Published: 2024-01-24 15:00:59

Let’s develop a video player.

The video player will be implemented with a non-standard approach where control of the video element is done through a controller class, and its state is derived from the state of the DOM element. Instead of duplicating the state of the video element in the state of the React component, we use the existing state of the video element, making the code more efficient, stable, and reducing the amount of code. 

The approximate structure of the project that I will adhere to: 

```
├── src
│   ├── components
│   │   ├── player
|   |   |   ├── Player.styles.ts
|   |   |   └── Player.tsx
|   |   ├── progress 
|   |   |   ├── Progress.styles.ts
|   |   |   └── Player.tsx
|   |	├── timer
|   |	|   ├── Timer.styles.ts
|   |	|   ├── Times.tsx
|   |	|   └── utils.ts
|   |	├── volume
|   |	|   ├── Volume.styles.ts
|   |	|   └── Volume.tsx
|   |	├── controllers
|   |	|   ├── gesture-controller.ts
|   |	|   ├── rectanglre-controller.ts
|   |	|   └── video-controller.ts
|   |   ├── styles
|   |	|   └── common.styles.ts

```
###TECHNOLOGIES 
[ReactJS](https://react.dev/) - A library for web and native user interfaces. 
[TypeScript](https://typescriptlang.org/) - A strictly typed programming language based on JavaScript. 
[Styled Components](https://styled-components.com/) - A CSS-in-JS style library. 
[Screenfull](https://github.com/sindresorhus/screenfull) - A wrapper for cross-browser use of the JavaScript [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API). 
[Create React App](https://create-react-app.dev/) - A project template configured for ReactJS. 
You can see an example of the video player here: https://bymarsel.github.io/videoplayer-example/

##1. PLAY/PAUSE 
Let's start implementing our player with play/pause. For this, we'll start writing our controller. We describe the contract: play, pause methods, and add an element property where we will store a reference to the video DOM element. 

```ts
export class VideoController {
  private readonly element: HTMLVideoElement;

  constructor(el: HTMLVideoElement) {
    this.element = el;
  }

  play() {
  }

  pause() {
  }
}

```
Let's start describing the play method:

```ts
export class VideoController {
  private readonly element: HTMLVideoElement;
  //...
  play() {
    this.element.play();
  }
  //...
}

```
Seems simple enough? Actually, not quite. The play() method of the video element is asynchronous, which can lead to isome nteresting effects: 

1. Firstly, video.play() inside an asynchronous function can lead to the loss of the "user gesture token," necessary for playing the video. Without this token, the browser will think that the JS code is trying to do something bad, like showing ads without the user's consent, and just block the video playback. 

2. If you called play(), and then the user pressed pause, you will get a playback error in the browser console. Subsequent calls to play() will lead to a similar result. Why? Because play() is an asynchronous call, and the subsequent call to pause() interrupts the video loading, making subsequent playback impossible. The best way to prevent such a scenario is to block the pause call until the video starts playing. For example, you can wait until the play() call resolves.
 
Let's slightly change our play() and pause() methods: 

```ts
export class VideoController {
  private readonly element: HTMLVideoElement;
  private isPausingBlocked: boolean;

  constructor(el: HTMLVideoElement) {
    this.element = el;
  }

  play() {
    this.isPausingBlocked = true;

    this.element
      .play()
      .then(() => {
        this.isPausingBlocked = false;
      })
      .catch((e) => {
        console.log(e);
      });
  }

  pause() {
    if (!this.isPausingBlocked) {
      this.element.pause();
    }
  }
}

```
Here’s what's happening here: 

We set `this.isPausingBlocked = true` so that the pause call is ignored until the `this.element.play()` method finishes. 

Now we have a basic controller, let's start writing our player. So far, without adding any styles, we will add them later: 

```ts
import {
  FC,
  useState,
} from "react";

interface Props {
  src: string;
}

export const Player: FC<Props> = ({ src }) => {
  const [element, setElement] = useState<HTMLVideoElement | null>(null);

  return (
    <video src={src} ref={setElement} />
  )
}

```
It may seem strange or erroneous that we record a reference to the element in the React state. We use this approach so that we can subscribe to state changes. As soon as the reference to the DOM element is available, setElement will be called, which will lead to the component state update. 

Now let's create our controller: 

```ts
import {
  FC,
  useState,
	**useEffect**
} from "react";
**import { VideoController } from "../../controllers/video-controller";**

 // Player.tsx
export const Player: FC<Props> = ({ src }) => {
//...
const [controller, setController] = useState<VideoController | null>(null);
//...
useEffect(() => {
    if (element) {
      const newVideoController = new VideoController(element);
      setController(newVideoController);
    }
 }, [element, src]);
 //...
}

```
I think everything is clear here: as soon as the element appears in the state (see the code above), useEffect responds to it, and inside useEffect, we create a controller and also save it in the component state. 

Okay, now let's give the user the ability to start and pause the video. 

```ts
import {
  FC,
  useState,
	useEffect**,
	useCallback**
} from "react";

//...
export const Player: FC<Props> = ({ src }) => {
  // ...
  const [controller, setController] = useState<VideoController | null>(null);
  // ...
  const handlePlay = useCallback(() => {
    controller?.play();
  }, [controller]);

  const handlePause = useCallback(() => {
    controller?.pause();
  }, [controller]);

  return (
    <div>
      <video src={src} ref={setElement} />
      <button onClick={handlePlay}>play</button>
      <button onClick={handlePause}>pause</button>
    </div>
  )
}

```
Here we added pause and play buttons, which call the corresponding methods of the video controller. 

<aside>💡 In our implementation, we use separate buttons for different playback states. You can use one button and different icons.
</aside>

 Now our player supports two actions: play and pause. But the interface does not react to this, except for playing the video. What if we want to change the play icon to pause on the play event, or some other visual action? The first thing that comes to mind is to add a new property, `isPlaying` , to the component state and change it on click in handlePlay and handlePause: 

```ts
// do not copy, this is just an example
  const [isPlaying, setIsPlaying] = useState<boolean>(false);

  const handlePlay = useCallback(() => {
    controller?.play();
  }, [controller]);

  const handlePause = useCallback(() => {
    controller?.pause();
  }, [controller]);

```
This would work, if not for one thing: The video may be interrupted due to a connection break or slow internet because the video may play faster than it downloads, and our isPlaying won't know about it. 

Let's solve this problem. Let's return to our controller and add the getPlayingState method, which will return the current playback state. 

```ts
export class VideoController {
  // ...
  getPlayingState() {
    if (!this.element.paused) {
      return "playing";
    }

    return "paused";
  }
  // ...
}

```
With that sorted out, we need to somehow notify the React component that the playback state has changed. For this, we will make some changes to our VideoController: 

```ts
export class VideoController {
  //...
  private playingStateListeners: (() => void)[] = [];
  private boundEmitPlayingStateChangeListener: () => void;

  //...
  constructor(el: HTMLVideoElement) {
      //...
      this.element = el;
      //...
      this.boundEmitPlayingStateChangeListener = this.emitPlayingStateChangeListeners.bind(this);

      el.addEventListener(
        "play",
        this.boundEmitPlayingStateChangeListener
      );
      el.addEventListener(
        "pause",
        this.boundEmitPlayingStateChangeListener
      );
  }

  private emitPlayingStateChangeListeners() {
    try {
      this.playingStateListeners.map((s) => s());
    } catch (e) {
      console.error(e);
    }
  }

  subscribe(
    event: "playingState",
    callb: () => void
  ) {
    if (event === "playingState") {
      this.playingStateListeners.push(callb);
    }
  }
  //...
}

```
What's happening here? In very simple terms, we have made a mechanism for subscribing to video element events. In more detail: 

- `playingStateListeners` - an array of callbacks where we will store all subscribers to the playback state change. 
- `boundEmitPlayingStateChangeListener` - a link to the event listener, bound to the context of the VideoController instance through bind(). We store a separate link so that we can unsubscribe this listener from the event. 
- `emitPlayingStateChangeListeners` - a method that notifies subscriber components about the playback state change. 
- `subscribe` - a method for subscribing to events, in this case, to playingState. 

With that sorted out, let's teach our React component to react to these events. 

```ts
export const Player: FC<Props> = ({ src }) => {
  //...
  const [playingState, setPlayingState] = useState<
    "playing" | "paused"
  >("paused");

  useEffect(() => {
    if (controller) {
      controller.subscribe("playingState", () => {
        setPlayingState(controller.getPlayingState());
      });
    }
  }, [controller]);
  // ...

  return (
    <div>
      <video src={src} ref={setElement} />
      {playingState === "paused" && <button onClick={handlePlay}>play</button>}
      {playingState === "play" && <button onClick={handlePause}>pause</button>}
    </div>
  )

```
In `useEffect`, we subscribed to changes in `playingState`. For every change in `playingState`, we will update the component's state. We also made a small change: now, when a video is playing, a button with the text "pause" will be displayed, and conversely, when on "pause", a button with the text "play" will be displayed.

Wait, why go through such complexities when we could have simply implemented it as in the "isPlaying" example? To answer this question, let's return to our controller and add a couple more subscriptions: 

```ts
constructor(el: HTMLVideoElement) {
    //...
    el.addEventListener(
      "playing",
      this.boundEmitPlayingStateChangeListener
    );
    el.addEventListener(
      "waiting",
      this.boundEmitPlayingStateChangeListener
    );
    //...
  }

```
We subscribed to two events:

- The `playing` event will be thrown after video playback resumes, after being paused due to slow internet. The video is playing faster than it's loading.

- The `waiting` event will be thrown after video playback is paused due to insufficient data for video playback because the video has not yet finished loading. 

Unlike the `play` and `pause` events, the `playing` and `waiting` events will not be directly triggered by user actions.

Let's summarise these subscriptions: now, when the `waiting` event occurs, our player will switch to the pause state, and with `playing`, it will switch to the play state. 

We could stop here and say that we've figured out play/pause, but let's do something else, namely, subscribe to the "ended" event.

Why do we need it? To understand that the video has been fully played and to inform the user about it, or, for example, to offer the next video for viewing, as YouTube does. 

Let's make some changes to the controller: 

```ts
export class VideoController {
  //...
  constructor(el: HTMLVideoElement) {
      //...
      el.addEventListener(
        "ended",
        this.boundEmitPlayingStateChangeListener
      );
      //...
  }

  getPlayingState() {
    // ...
    if (this.element.ended) {
      return "ended";
    }
    // ...
  }
}

```
Here we’ve done the following:

- Subscribed to the "ended" event;
- Added a new `playingState` with the simple name `ended`.
 
But that's not all, let's teach our component to react to this event: 

```ts
export const Player: FC<Props> = ({ src }) => {
   const [playingState, setPlayingState] = useState<
     "playing" | "paused" | "ended"
   >("paused");

  //...
  const handlePlayAgain = useCallback(() => {
    controller?.replay();
  }, [controller]);

  return (
    <div>
       //...
      {playingState === "ended" && <button onClick={handlePlayAgain}>replay</button>}
    </div>
  )
}

```
Here we expanded the type of states in `playingState`, adding "ended", added a button with the text "ended" and added a click handler `handlePlayAgain`. 

A careful reader will notice that in `handlePlayAgain` we call the method `controller.replay()`, which we did not describe earlier, and this is indeed the case.

In fact, to restart the video, assuming that `videoElement.ended === true` (the video has played to the end), it's enough to call the `play()` method on the video element, or in our case, `controller.play()`, and the video will start again. 

But what if we wanted to interrupt the video and start it over again, for example, we need a "stop" button, then how to do it? Easy! Let's implement the `replay()` method in our VideoController: 

```ts
export class VideoController {
  //...
  private reset() {
    this.element.currentTime = 0;
  }

  replay() {
    if (this.getPlayingState() !== "ended") {
       this.reset();
    }

    this.play();
    }
  //...
}

```
Here we added the `reset` and `replay` methods: 

- To reset the video playback progress, you need to set the `currentTime` property to 0, then it will be played from the beginning. Which is what happens in the `reset()` method. 
- The `replay` method calls `reset` if the video has not ended, and calls the play method. 

We wrote a basic version of the player that can play and pause videos. Let’s keep moving.

##2. Timer

We've sorted out play/pause; now let's learn how to display the video playback progress time - a timer.

There are two ways to find out the current playback time of a video:

1. Subscribe to the `timeupdate` event and, when it's thrown, retrieve the current second of playback from the `currentTime` property of the video.
2. Retrieve the current second of playback from the `currentTime` property while the video is not paused.

We will use the second approach because the first one generates a `timeupdate` event every 250ms, but this can vary depending on the system. First, it loads our event loop with events, and secondly, our timer might "flip" oddly - sometimes faster by 250ms, sometimes slower, which will not look very good.

Let's describe our component: 

```ts
import { FC, useRef } from "react";
import { VideoController } from "../../controllers/video-controller";

interface Props {
  controller: VideoController | null;
}

export const Timer: FC<Props> = ({ controller }) => {
    const timerRef = useRef<HTMLSpanElement>(null);

    return <span ref={timerRef}>0:00</span>
}

```
Right now, the controller does not return the current playback time to us; let's add a getter `getPlayingState`. 

```ts
export class VideoController {
  //...
  getPlayingProgress() {
    return this.element.currentTime;
  }
  //...
}

```
Yes, it's a simple getter that returns `currentTime`. 

The basic structure is ready; let's get it to work. The first thing we'll do is add a function to our component that will update the current playback time. 

```ts
import { FC, useRef, **useCallback** } from "react";
import { formatCurrentVideoTime } from "./utils";
//...
export const Timer: FC<Props> = ({ controller }) => {
    //...
    const updateCurrentVideoTime = useCallback(() => {
      const timeEl = timerRef.current;

      if (timeEl?.textContent) {
        timeEl.textContent = formatCurrentVideoTime(
          controller?.getPlayingProgress() || 0
        );
      }
    }, [controller]);
    //...
}

```
Here we get a reference to the DOM element and set its current playback time, previously converting seconds to minutes using the `formatCurrentVideoTime` function. We need this function to convert `video.currentTime` from just seconds into human-readable minutes, hours, etc. 

<aside>💡 The code for the `formatCurrentVideoTime` function can be found [here](https://github.com/ByMarsel/videoplayer-example/blob/main/src/components/timer/utils.ts)
</aside>.

Let's describe a task that will set a task for updating the time: 

```ts
//...
import { formatCurrentVideoTime } from "./utils";

export const Timer: FC<Props> = ({ controller }) => {
    //...
    const runTask = useCallback(async () => {
       const task = () => {
         if (controller?.getPlayingState() === "playing") {
           updateCurrentVideoTime();
           window.requestAnimationFrame(task);
          }
      };

      window.requestAnimationFrame(task);
    }, [controller, updateCurrentVideoTime]);
    //...
}

```
Let's figure it out. Here we start the task through `requestAnimationFrame`, in which we call the `updateCurrentVideoTime` function, which updates the time and again calls `requestAnimationFrame`. Before this, we check that the video is still playing by requesting the current playback status through `controller?.getPlayingState() === "playing"`.

But that's not all. `RunTask` should be somehow called; let's add this functionality: 

```ts
import { FC, useRef, useCallback, **useEffect** } from "react";

export const Timer: FC<Props> = ({ controller }) => {
  //...
  useEffect(() => {
    if (controller) {
      controller.subscribe("playingState", runTask);
    }
  }, [controller, runTask]);
  //...
}

```
Here we subscribed to changes in `playingState` (play/pause/ended). For every change in `playingState`, we will call `runTask`, which will call the task to update the video timer.

Now let's add styles to our timer: 

```ts
//...
import { Time } from "./Timer.styles";

export const Timer: FC<Props> = ({ controller }) => {
		//... 
   return <Time ref={timerRef}>0:00</Time>;
}

```
Copy the Timer styles from [here](https://github.com/ByMarsel/videoplayer-example/blob/main/src/components/timer/Timer.styles.ts), and our Timer component is ready. 

<aside>💡 All component styles are described in the CSS-in-JS approach. The library used was [styled-components](https://styled-components.com/). If you're not familiar with this approach, no worries, you only need to copy the style file and place it in the appropriate folder according to the structure described at the beginning of the article.
</aside>

<aside>💡 Some styles, such as those for the sound control component, inherit common styles. Therefore, copy them from [here](https://github.com/ByMarsel/videoplayer-example/blob/main/src/styles/common.styles.ts) into the project before continuing.
</aside>

With the styles sorted out, let's slightly modify our Player component: 

```ts
//...
import { Timer } from "../timer/Timer";
//...

export const Player: FC<Props> = ({ src }) => {
  //...
  return (
    <StyledContainer ref={containerRef}>
      <StyledPlayer ref={setElement} src={src} />
      <StyledControls>
        <div>
          {playingState === "paused" && (
            <PlayPauseButton onClick={handlePlay}>play</PlayPauseButton>
          )}
          {playingState === "playing" && (
            <PlayPauseButton onClick={handlePause}>pause</PlayPauseButton>
          )}
          {playingState === "ended" && (
            <PlayPauseButton onClick={handlePlayAgain}>replay</PlayPauseButton>
          )}
        </div>
        <ProgressAndTimerContainer>
          **<Timer controller={controller} />**
        </ProgressAndTimerContainer>
      </StyledControls>
    </StyledContainer>
  );
};

```
Here we added styles to our player, and also, pay attention, we added the Timer component to our Player. In the block, all player control components will be located in the `StyledControlls` block.

##3. Playback status

As mentioned above, you can find out the current progress of video playback in seconds through the `video.currentTime `property. Next, we will work with this property of the video. 

<aside>💡 Take the styles for our progress from [here](https://github.com/ByMarsel/videoplayer-example/blob/main/src/components/progress/Progress.styles.ts) and [here](https://github.com/ByMarsel/videoplayer-example/blob/main/src/styles/common.styles.ts), just copy them into your project.
</aside>

Let's describe our Progress component: 

```ts
import { useRef } from 'react';
import { VideoController } from "../../controllers/video-controller";
import {
  StripValue,
  StripValuePlaceholder,
  StripValueWrapper,
} from "../../styles/common.styles";

interface Props {
	controller: VideoController | null;
}

export const Progress: FC<Props> = ({ controller }) => {
	const progressRef = useRef<HTMLDivElement>(null);
  const placeholderRef = useRef<HTMLDivElement>(null);
	
  return ( 
		<StripValueWrapper>
      <StripValue ref={progressRef} />
      <StripValuePlaceholder ref={placeholderRef} />
    </StripValueWrapper>
	)
}

```
The basic implementation of the component will be as follows: The component will consist of a progress bar (a wide strip) and a progress placeholder (a strip across the entire width of the player, showing 100%). This is the strip that will represent the entire progress of the video. It sounds complicated, but it's not in practice.

To calculate the percentage of the video played, let's teach our VideoController to return the duration of the video: 

```ts
export class VideoController {
	//...
  getDuration() {
    return this.element.duration;
  }
	//...
}

```
Here it's simple, we refer to the video element.duration property and get the duration of the video in seconds.

Let's add a method to our component that will change the width of the progress bar depending on `video.currentTime`: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
  //...
  const updateProgressBar = useCallback(() => {
    const progressEl = progressRef.current;

    if (progressEl) {
      const videoProgress = controller?.getPlayingProgress() || 0;
      const duration = controller?.getDuration();

      if (duration) {
        progressEl.style.width = `${(100 / duration) * videoProgress}%`;
      }
    }
  }, [controller]);
  //...
};

```
We take the current played second through `getPlayingProgress()`, then get the duration of the video through `getDuration()` and calculate the percentage of the video progress by the formula 100 / duration * videoProgress, after that, we write this value in percentages in the CSS property width of the element.

Now, like the timer, let's write code that will launch the task to update the progress bar in `requestAnimationFrame`: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
		//...
		const runTask = useCallback(async () => {
    const task = () => {
      if (controller?.getPlayingState() === "playing") {
				updateProgressBar();
        window.requestAnimationFrame(task);
      }
    };

    window.requestAnimationFrame(task);
  }, [controller, updateProgressBar]);

  useEffect(() => {
    if (controller) {
      controller.subscribe("playingState", runTask);
    }
  }, [controller, runTask]);
	//...
}

```
Here we did the same as in Timer: described a task that will be called in `requestAnimationFrame`, and subscribed to the `playingState` event, the handler of which will call `runTask`, which will cause our progress bar to be updated.

Let's display our Progress component in Player:

```ts
//...
import { Progress } from "../progress/Progress";

export const Player: FC<Props> = ({ src }) => {
  //...
  return (
    <StyledContainer ref={containerRef}>
      //...
      <StyledControls>
        //...
        <ProgressAndTimerContainer>
          **<Progress controller={controller} />**
          <Timer controller={controller} />
        </ProgressAndTimerContainer>
      </StyledControls>
    </StyledContainer>
  );
};

```

##4. Rewinding
In this chapter, we will implement video rewinding. As I mentioned earlier, video rewinding is achieved through the video's `currentTime` property. To rewind a video, you simply need to set the required second of the video in this property.

Let's implement this. To calculate the second to which we need to rewind the video, we will need to determine the cursor's position relative to the beginning of the progress block. To understand where our progress block is located, we will have to use `getBoundingClientRect`. 

<aside>💡 Be careful. When calling getBoundingClientRect, the browser recalculates the layout and returns the current values of the element's size and position. This can be a costly operation, so it is recommended to use this method cautiously and not to abuse its call within loops or frequently recurring events.
</aside>

Considering the above, let's implement a controller that will call `getBoundingClientRect` once and request it again on the resize:  

```ts
export class RectangleController {
	private readonly element: HTMLElement;
  private width: number = 0;
  private left: number = 0;
  private leftPadding: number = 0;

	getWidth() {
    return this.width;
  }

	getLeftOffset() {
    return this.left;
  }

	getLeftPadding() {
    return this.leftPadding;
  }

	private update() {
  }
}

```
Let's understand the structure of our controller. We will store three main variables in the controller: `width`, `left`, `leftPadding`, which will be accessible outside the controller. We will also store a reference to the DOM element, but the element will not be accessible externally.

Now let's describe the `update()` method: 

```ts
export class RectangleController {

	//...
	private update() {
    const domRect = this.element.getBoundingClientRect();
    
    this.leftPadding = this.element.offsetLeft;
    this.width = domRect.width;
    this.left = domRect.left;
	}
	//...

}

```
In this method, we will call `getBoundingClientRect` on our DOM element and fill `width`, `left`, `leftPadding`. `leftPadding` will be filled from `this.element.offsetLeft`, because `getBoundingClientRect` does not have this data. It's all straightforward at this point.

Now let's teach the controller to update the necessary values on `resize`.

```ts
export class RectangleController {
	//...
	private boundUpdate: () => void;

	constructor(element: HTMLElement) {
    this.element = element;
    this.boundUpdate = this.update.bind(this);
   
    window.addEventListener("resize", this.boundUpdate);

		this.boundUpdate();
	}
	//...
}

```
Here too, everything is quite simple. We subscribe to the resize event and pass the `update` method as a handler, bound to the context of our controller class using `.bind()`. At the end, we call `update`, or rather its bound version. 

We've figured out `RectangleController`, let’s keep moving.
To make it easier, let's implement helpers that will calculate the cursor position by the x-coordinate, taking into account the position of the block on the page: 

```ts
import { RectangleController } from "../../controllers/rectangle-controller";

export const calculateCursorPosition = (
  rectangleController: RectangleController,
  x: number
) => {
  const offsetLeft = rectangleController.getLeftOffset();
  return x - offsetLeft;
};

```
I think everything is clear here. We take the x-coordinate (or offset) of the element from the left edge and the x-coordinate of the mouse cursor and subtract one from the other x - `offsetLeft`, so we get the mouse cursor position relative to the element from 0px to the maximum width of the block.

Let's add another helper. It will immediately return the mouse cursor position in percentages relative to the element.

```ts
export const calculateCursorPositionInPercents = (
  rectangleController: RectangleController,
  x: number
) => {
  const widthInPercents = 100 / rectangleController.getWidth();

  return (
    widthInPercents * calculateCursorPosition(rectangleController, x)
  );
};

```
Here we get the number of percent per 1px width of the element and multiply it by the value obtained for the position of the cursor relative to the element. It sounds complicated, but let's try it this way: 

Suppose the width of the element is 200px, so when dividing 100% by 200px, we get a value of 0.5, meaning for each 1px there is 0.5% of the element's fill.

Now let's implement a helper that will calculate for us the second where the mouse cursor is located: 

```ts
export const calculateCurrentTimeByCursorPosition = (
  rectangleController: RectangleController,
  x: number,
  duration: number
) => {
  const normalizedPercent =
    calculateCursorPositionInPercents(rectangleController, x) / 100;

  return Math.min(duration * normalizedPercent, duration);
};

```
Here we convert our calculated percentage of the mouse cursor's position to a decimal number `normalizedPercent`. That is, 38% is converted to 0.38, etc. Then we take the duration of the video and multiply it by `normalizedPercent`, ultimately getting the number of seconds of video duration relative to the position of the mouse cursor on the element.

It seems we are done with the calculations for now, let's implement the video rewinding. The first thing we need to do is initialise our `RectangleController`: 

```ts
//...
import { RectangleController } from "../../controllers/rectangle-controller";
//...

export const Progress: FC<Props> = ({ controller }) => {

	//...
	const [rectangleController, setRectangleController] = useState<RectangleController | null>(null);

		  useEffect(() => {
		    if (placeholderRef.current) {
		      setRectangleController(new RectangleController(placeholderRef.current));
		    }
		  }, []);
	//...

}

```ts
Here everything is simple: we take `placeholderRef.current` and create a `RectangleController` with it.

But our `videoController` knows nothing about video rewinding, let's expand its capabilities: 

```ts
export class VideoController {
	//...
	seek(seconds: number) {
      this.element.currentTime = seconds;
  }
	//...
}

```
As mentioned above, to rewind a video, you just need to specify the necessary time in seconds. For this, we implemented the `seek` method, which takes time in seconds and sets it in `this.element.currentTime`.

Now let's implement video rewinding in the component: 

```ts
import {
  calculateCurrentTimeByCursorPosition
} from "./utils";

export const Progress: FC<Props> = ({ controller }) => {
		//...
		const seek = useCallback((x: number) => {
	    if (rectangleController && controller) {
	      const seekingValue = calculateCurrentTimeByCursorPosition(
	        rectangleController,
	        x,
	        controller.getDuration()
	      );

	      controller.seek(seekingValue);
			
	     if (controller.getPlayingState() !== "playing") {
	        window.requestAnimationFrame(updateProgressBar);
	     }
	    }
	  }, [controller, rectangleController, updateProgressBar])
		//...
}

```
Here we described the `seek` method in the component. Let's figure out what's happening here. 

We get the second to which we want to rewind, calculated by the position of the mouse cursor (by the x-coordinate) - the `calculateCurrentTimeByCursorPosition` method helps us with this - and we pass this value to the `seek` method. After calling `seek`, we update our playing progress bar by calling `window.requestAnimationFrame(updateProgressBar)`, provided the video is not playing.

Now, all we have left to do is to add a click handler, thanks to which the user will be able to rewind the video in our player by clicking on the progress bar: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
	//...
	const handleClick: React.MouseEventHandler<HTMLDivElement> = useCallback(
    (event) => {
        seek(event.clientX)
    },
    [seek]
  );
	
  return ( 
		<StripValueWrapper onClick={handleClick}>
      <StripValue ref={progressRef} />
      <StripValuePlaceholder ref={placeholderRef} />
    </StripValueWrapper>
	)
}

```
`handleClick` calls the `seek` method of the component we implemented above and passes the X-axis cursor coordinate taken from our click event.

<aside> 💡 An attentive reader would have noticed that we attach the clickHandler to the progress wrapper, not to the progress itself or the placeholder. We did this because the wrapper is wider, and clicking outside the bounds of the placeholder would result in a negative value, which would be interpreted as rewinding to 0 seconds or to the duration seconds if the click occurred beyond the progress. This is necessary because the user cannot and will not be precisely clicking to the pixel when rewinding the video to the beginning or end.

</aside>

After we supported the `seeking` event, it's time to make some adjustments to our Timer and Player. 

Now, when the video is playing, our timer will update. But what happens if the user pauses the video and rewinds it? Currently, our timer does not react to rewinding, so let's fix that. 

The first thing we will do is teach our controller to work with the `seeking` event. 

This is an event that is triggered when the video's `currentTime` property changes. 

```ts
export class VideoController {
	//...
	private seekingListeners: (() => Promise<void>)[] = [];
	//...
	private boundEmitSeekingListeners: () => void;
	
	constructor(el: HTMLVideoElement) {
		//...
		this.boundEmitSeekingListeners = this.emitSeekingListeners.bind(this);
		el.addEventListener("seeking", this.boundEmitSeekingListeners);
	}
	
	//...
	subscribe(
    event: "playingState" | **"seeking"**,
    callb: () => Promise<void>
  ) {
		//...
		if (event === "seeking") {
      this.seekingListeners.push(callb);
    }
	}
}

```
At this point, everything is fairly simple - in the constructor, we subscribed to the `seeking` event and added the ability to subscribe to this event for our components in the `subscribe` method. 

Now let's return to our Timer component and add support for this event: 

```ts
export const Timer: FC<Props> = ({ controller }) => {
	//...
  useEffect(() => {
    if (controller) {
      controller.subscribe("seeking", async () => {
        window.requestAnimationFrame(updateCurrentVideoTime);
      });
    }
  }, [controller, updateCurrentVideoTime]);
	//...
};

```
Here, it's just as simple: we subscribed to the `seeking` event. As soon as this event is thrown, `requestAnimationFrame` will be called with a function that updates our timer. That is, even if the video is on pause, and the user rewinds the video from 21 seconds to 1:30, the timer will update because the `seeking` event is called, and our handler will trigger the timer update. 

Now let's make changes to our Player: 

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
  useEffect(() => {
    if (controller) {
      //...
      controller.subscribe("seeking", async () =>
        setPlayingState(controller.getPlayingState())
      );
      //...
    }
  }, [controller]);
	//...
};

```
Here we subscribed to the `seeking` event. When processing this event, we record the current playback state of the video. 

Why was this done? For the sole case when the video has reached the end, and the user rewinds it, for example, to the middle. If we do not update the status, then the playback button will be for the action "start over" instead of the desired "play."

##5. Volume control 

```ts
//...
import { Volume } from "../volume/Volume";
**import { isIOS } from 'mobile-device-detect'**
//...

export const Player: FC<Props> = ({ src }) => {
  return (
    <StyledContainer ref={containerRef}>
      <StyledPlayer ref={setElement} src={src} />
      <StyledControls **isIOS={isIOS}**>
		      //...
        <ProgressAndTimerContainer>
	         //...
        </ProgressAndTimerContainer>
				**{!isIOS &&** <Volume controller={controller} />**}**
      </StyledControls>
    </StyledContainer>
  );
};

```
Now Volume will be hidden on an iOS device.

##6. Fullscreen mode

It's time to talk about fullscreen mode. The first thing we will do is install the [screenfull library](https://www.npmjs.com/package/screenfull). It is convenient for working with the [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API), and it is cross-browser compatible, well, almost. 

<aside>💡 The Fullscreen API is not supported on the iPhone, I will show you later how to work around this.
</aside>

So, let's get started. First, let's add a click handler that will request the browser to enter fullscreen mode: 

```ts

export const Player: FC<Props> = ({ src }) => {
//...
const handleToggleFullscreen: MouseEventHandler<HTMLButtonElement> =
    useCallback(() => {
      if (screenfull.isEnabled) {
        if (containerRef.current) {
          screenfull.toggle(containerRef.current);
        }
      } else {
       //...
      }
    }, [element]);
//...
}

```
 
Here everything is simple: first, we check if the Fullscreen API is available on the device through `screenfull.isEnabled`. If yes, then we try to request fullscreen mode if we are not in it, or exit from fullscreen mode if we are in it, using the `toggle` method of the screenfull library. 

<aside>💡 The attentive reader noticed that we request fullscreen mode for the container or wrapper of the player, not the video element itself. This is because if we request fullscreen mode for the video element, we will get the native video player in fullscreen mode without our styles and player buttons. For this reason, we request fullscreen mode for a div element, which simply shows us the element in full screen, as well as our custom video player in it.
</aside>

But simply calling fullscreen mode is not enough, we need to somehow react to it, for example, we want to change the icon on the button that opens/closes fullscreen mode. First, let's add the flag `isFullscreen` to the state: 

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
	const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
	//...
}

```
At this point, everything is fairly straightforward: we simply added the state of the `isFullscreen` flag, which we will change depending on whether we are in fullscreen mode or not.

Now let's subscribe to the transition/exit from fullscreen mode: 

```ts
export const Player: FC<Props> = ({ src }) => {
		//...
		useEffect(() => {
		    if (screenfull.isEnabled) {
			      screenfull.on("change", () => {
			        setIsFullscreen(screenfull.isFullscreen);
			      });
			    }
	  }, []);
		//...
}

```
Here we check that Fullscreen API is supported by the browser, and subscribe to the transition/exit from fullscreen mode and in the listener of this event, we record the value from `screenfull.isFullscreen` into the state.

Let's add a button that will call and close fullscreen: 

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
	return (
		<StyledContainer>
		//...
		</StyledControls>
				//...
   	    **<FullscreenButton onClick={handleToggleFullscreen}>**
          **{isFullscreen ? "]  [" : "[  ]"}**
        **</FullscreenButton>**
		</StyledControls>
		//...
	)
}

```
Here everything is simple: on click on the button, the handler we described above is called, and on the change of the `isFullscreen` variable, we change our “icon” of the button.

What about the iPhone? We have two ways:

- try to display the player in full width and height, darkening everything around;
- request fullscreen mode of the native iPhone player. 

The second option seems to me more reasonable and functional. Let's implement it: 

```ts
export const Player: FC<Props> = ({ src }) => {
  //...
  return (
    //...
    <StyledPlayer src={src} preload="metadata" ref={setElement} **playsInline** />
    //...
  );
};

```
At this point, once again, everything is simple: we set the `playsInline` attribute for the video element. `playsInline` is an attribute of the `<video>` element, which tells the browser to play the video inside the `<video>` element on the page, rather than in fullscreen mode. This is useful for mobile devices, especially on iOS devices, where video can automatically switch to fullscreen mode during playback. 

Setting the `playsInline` attribute to `true` ensures that the video will play inside the `<video>` element, rather than switching to fullscreen mode when play is pressed on iOS devices.

Next, let's improve our method: 

```ts
export const Player: FC<Props> = ({ src }) => {
//...
const handleToggleFullscreen: MouseEventHandler<HTMLButtonElement> =
    useCallback(() => {
      if (screenfull.isEnabled) {
        if (containerRef.current) {
          screenfull.toggle(containerRef.current);
        }
      } else {
       if (
          element &&
          (element as any)?.webkitSupportsPresentationMode("fullscreen")
        ) {
          (element as any)?.webkitSetPresentationMode("fullscreen");
        }
      }
    }, [element]);
//...
}

```

The `webkitSetPresentationMode` method is used to set the display mode of an element to fullscreen mode on devices supporting WebKit. In this case, we use it to request fullscreen mode for the video element on iPhone devices. 

We check for the support of the `webkitSetPresentationMode` method with the `fullscreen` type for the video element. If the device supports this method, we call it with the `fullscreen` parameter to request the display of the video in fullscreen mode. This allows the video to open in fullscreen mode, but in the native interface of the device.

This way, using the `webkitSetPresentationMode` method, we can request fullscreen mode for video on iPhone devices where fullscreen mode using the Fullscreen API is not supported. On some iOS devices, this works reliably only after the user has pressed play and the video has started playing. It would help us if we set the preload attribute of the video element to `auto`, then the browser would always load the video, even if the user does not plan to watch it. In our case, we have set the value to `metadata`.

<aside> 💡 When the `preload` attribute with the value `metadata` is set on a video element in HTML, the browser only loads the video's metadata. This includes information such as the video's duration, dimensions (width and height), and possibly the first frame for display as a thumbnail. However, the video itself (i.e., its content) will not be loaded until the user starts playback. This allows the page to load faster, as it avoids loading large amounts of video data if the user is not planning to watch it.
</aside>

But there is another way to solve this problem - ask the user to click on the player to start playback. This way, we hit two birds with one stone: achieve stable operation on iOS and optimise video loading.

Let's improve our Player component: 

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
	const [wasFirstPlayed, setWasFirstPlayed] = useState<boolean>(false);
	//...
}

```
Here we added a flag `wasFirstPlayed` to our player's state, which will indicate that the user clicked for the first time on the player to start the video and hide our improvised poster.

Let's add a handler that will change this flag: 

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
  const handleContainerClick = useCallback(() => {
    setWasFirstPlayed(true);
    controller?.play();
  }, [controller]);

	//...
  return (
    <StyledContainer **onClick={handleContainerClick}** ref={containerRef}>
			{!wasFirstPlayed && (
        <FirstPlayOverlay>
          <FirstPlayButton>Click to Play</FirstPlayButton>
        </FirstPlayOverlay>
      )}
      //...
    </StyledContainer>
  );
};

```
Now, on the first click on our entire player, the `wasFirstPlayed` flag will be set to true, the video will start playing, and our improvised poster will be hidden:

```ts
<FirstPlayOverlay>
    <FirstPlayButton>Click to Play</FirstPlayButton>
 </FirstPlayOverlay>

```

Since we've touched on the topic of playing the video by clicking on the entire player, and not just on the play/pause/replay buttons, let's improve our `handleContainerClick` so that it not only plays the video but also pauses it.

```ts
export const Player: FC<Props> = ({ src }) => {
	//...
  const handleContainerClick = useCallback(() => {
    if (controller?.getPlayingState() === "paused") {
      setWasFirstPlayed(true);
      controller?.play();
    }

    if (controller?.getPlayingState() === "playing") {
      controller?.pause();
    }
  }, [controller]);
};

```
Now our click event handler will play or pause the video depending on the current playback state.

But now we've created a bug. During rewinding and changing the volume, in general, with any click inside the player, the player will play/pause our video. Let's fix this: 

```ts
export const Player: FC<Props> = ({ src }) => {

const handleControllsClick: MouseEventHandler<HTMLDivElement> = useCallback(
    (event) => {
      event.stopPropagation();
    },
    []
  );

return (
    <StyledContainer **onClick={handleContainerClick}** ref={containerRef}>
			{!wasFirstPlayed && (
        <FirstPlayOverlay>
          <FirstPlayButton>Click to Play</FirstPlayButton>
        </FirstPlayOverlay>
      )}
			//...
			<StyledControls isIOS={isIOS} **onClick={handleControllsClick}**>
	    //... 
      </StyledControls>
    </StyledContainer>
  );)

}

```
The solution is very simple: catch the click event on bubbling and cancel its further propagation using the `stopPropagation` method of the event.

##7. Frame preview

In this chapter, we will implement a video frame preview feature. When the mouse cursor hovers over any point on the progress bar, we will display the frame corresponding to that position. However, this is just a demonstration version.

The first thing we will do is add our `FramePreview` component to the Progress component:  

```ts
//...
import { FramePreview } from "./Progress.styles";

export const Progress: FC<Props> = ({ controller }) => {
	//...
	const framePreviewRef = useRef<HTMLVideoElement>(null);
	
	return ( 
		<StripValueWrapper onClick={handleClick}>
      <StripValue ref={progressRef} />
      <StripValuePlaceholder ref={placeholderRef} />
			**<FramePreview
	        muted
	        preload="metadata"
	        src={controller?.getVideoSrc()}
	        ref={framePreviewRef}
      />**
    </StripValueWrapper>
	)
}

```

Here we add the `FramePreview` component, which is a child component of the `Progress` component. It represents a `<video>` element, which will be used to display the video frame preview.

In the `Progress` component, a reference `framePreviewRef` is created using `useRef`, which will be used to access the `<video>` element of the `FramePreview` component.

Inside the `Progress` component, the `FramePreview` component is utilised. It takes the following attributes: `muted` indicates that the sound should be turned off, `preload="metadata"` specifies that only video metadata should be loaded, and `src={controller?.getVideoSrc()}` sets the video source, which is obtained from the `controller `object.

Effectively, we will be showing a player within a player (like exzibit) and rewinding it to the second where the cursor is positioned in the main player. 

<aside>💡 It's best to implement video previews using a pre-prepared sprite (an image with video frame cutouts) on the server: this solution is more optimal as the sprite loads much faster than the video. To show the desired frame in our preview, in this case, the browser needs to load the video up to that second.
</aside>

First, let's implement a method that will draw the video frame: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
 //...
	const renderFramePreview = useCallback(
    (x: number) => {
      const framePreviewElement = framePreviewRef.current;

      const updatePosition = () => {
        if (rectangleController && framePreviewElement) {
          const cursorPosition = calculateCursorPosition(rectangleController, x);

          const currentTime = calculateCurrentTimeByCursorPosition(
            rectangleController,
            x,
            framePreviewElement.duration
          );

          const leftPadding = rectangleController.getLeftPadding();

          framePreviewElement.currentTime = currentTime;
          framePreviewElement.style.left = `${cursorPosition - 50 + leftPadding
            }px`;
        }
      }

      window.requestAnimationFrame(updatePosition)
    },
    [rectangleController]
  );
	//...
}

```
Let's understand what's happening here. First, we get `cursorPosition` (the position of the cursor) relative to the width of the progress block by the x-coordinate. Next, by the x-coordinate, we try to get `currentTime` (frame time) where our cursor is positioned.

Then we rewind the video in `FramePreview` to the obtained time and move our preview image to the cursor position on the mouse. 

<aside>💡 The position of `FramePreview` is calculated according to the formula: `cursorPosition - 50 + leftPadding`, where `cursorPosition` is the position of the mouse cursor on the progress block, 50 is half the width of FramePreview, and `leftPadding` is the padding inside the progress block.
</aside>

As expected, the task of changing the element's styles is placed in `requestAnimationFrame`.

We don't always need to show the frame preview, only when the mouse enters the progress block. 

```ts
export const Progress: FC<Props> = ({ controller }) => {
  //...
  const setFramePreviewVisible = useCallback(() => {
    const framePreviewElement = framePreviewRef.current;

    if (framePreviewElement) {
      framePreviewRef.current.style.visibility = "initial";
    }
  }, []);

  const hideFramePreview = useCallback(() => {
    const framePreviewElement = framePreviewRef.current;

    if (framePreviewElement) {
      framePreviewRef.current.style.visibility = "hidden";
    }
  }, []);
  //...

  return (
    <StripValueWrapper
      onClick={handleClick}
      onMouseEnter={setFramePreviewVisible}
      onMouseLeave={hideFramePreview}
    >
      <StripValue ref={progressRef} />
      <StripValuePlaceholder ref={placeholderRef} />
      <FramePreview
        muted
        preload="metadata"
        src={controller?.getVideoSrc()}
        ref={framePreviewRef}
      />
    </StripValueWrapper>
  );
};

```

Here everything is simple: on `mouseEnter` we change `visibility` to `initial`, and on `mouseLeave` we change it to `hidden`. And then we attach these event handlers to `StripValueWrapper`.

Now that the frame preview can appear and disappear, let's make it so that the frame preview moves with the mouse. 

```ts
export const Progress: FC<Props> = ({ controller }) => {
  //...
  const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = useCallback(
    (event) => {
        renderFramePreview(event.clientX);
    },
    [renderFramePreview]
  );
  //...

  return (
    <StripValueWrapper
      onClick={handleClick}
      onMouseEnter={setFramePreviewVisible}
      onMouseLeave={hideFramePreview}
			**onMouseMove={handleMouseMove}**
    >
      <StripValue ref={progressRef} />
      <StripValuePlaceholder ref={placeholderRef} />
      <FramePreview
        muted
        preload="metadata"
        src={controller?.getVideoSrc()}
        ref={framePreviewRef}
      />
    </StripValueWrapper>
  );
};

```
At this point, everything is pretty simple: we added the `handleMouseMove` event handler and attached it to the `mousemove` event of the `StripValueWrapper` component.

Now, when hovering the mouse, we will see the video frame preview at the second where the cursor is pointed.

##8. Touch device support

If the `click` event still works and features can be used, then when it comes to `mousemove`, it almost doesn't work as it should on touch devices. 

On touch devices, such as smartphones and tablets, the `mousemove` event does not function the same way as it does on a computer with a mouse. Instead, events like `touchstart`, `touchmove`, and `touchend` are used to track finger movements on the screen.

To support video rewinding on touch devices, you will need a `touchmove` event handler that will track the finger's coordinates on the screen and correspondingly change the position of the progress bar and the video time.

The first thing we do is implement a kind of GesturesController: 
 

```ts
export class GesturesController {
    private touchStarted = false;
    private lastFingerPositionX = 0;
    private timeoutId = 0; 

    touch() {
        window.clearTimeout(this.timeoutId)
        this.touchStarted = true;
    }

    touchEnd() {
        this.timeoutId = window.setTimeout(() => {
            this.touchStarted = false;
        }, 300)
    }

    getTouchStarted() {
        return this.touchStarted;
    }

    setLastFingerPosition(x: number) {
        this.lastFingerPositionX = x;
    }

    getLastFingerPosition() {
        return this.lastFingerPositionX;
    }
}

```
GesturesController is a class used for managing gestures on touch devices. It has the following functionalities:

- `touch()` - a method called when the screen is touched, sets the `touchStarted` flag to true.
- `touchEnd()` - a method called when the finger is lifted off the screen, sets the `touchStarted` flag to false after 300 milliseconds using `setTimeout`.
- `getTouchStarted()` - a method that returns the current value of the `touchStarted` flag.
- `setLastFingerPosition(x: number)` - a method that sets the last finger position on the X-axis.
- `getLastFingerPosition()` - a method that returns the last finger position on the X-axis.

This class allows tracking the start and end of a touch on the screen, as well as the last finger position. This is useful for implementing gestures and managing video rewinding on touch devices. 

<aside>💡 Pay attention to the `touchEnd` method. In it, we set the `touchStarted` flag with a delay of 300ms. This is done to ignore `mouseclick` events on touch devices. It's a small hack for the stability of our code.
</aside>

Let's support touch actions in our Progress component: 

```ts
//...
const gesturesController = new GesturesController();

interface Props {
  controller: VideoController | null;
}

export const Progress: FC<Props> = ({ controller }) => {
//...

```
Here we did something a bit unusual. `gesturesController` was created at the level of the JS module. Why? Because we don't need to store it in the state of our player, and one instance is enough for the entire life cycle of the module.

Now let's write a `touchstart` event handler: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
  //...
  const handleTouchStart: React.TouchEventHandler<HTMLDivElement> = useCallback(
    (event) => {
      gesturesController.touch();

      if (event.touches.length === 1) {
        const touch = event.touches[0];

        const x = touch.clientX;
        gesturesController.setLastFingerPosition(x);
      }

      setFramePreviewVisible();
    },
    [setFramePreviewVisible]
  );
  //...
};

```
Let's understand what's happening here. First, we will notify our `gesturesController` that a touch event has started. Then we check that the number of touches is one `event.touches.length === 1`, extract our touch and get its position on the x-axis. Next, we call `setFramePreviewVisible()` to make our video preview visible.

Now let's implement a `touchmove` event handler: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
	//...
	const handleTouchMove: React.TouchEventHandler<HTMLDivElement> = useCallback((event) => {
    if (event.touches.length === 1) {
      const touch = event.touches[0];

      const x = touch.clientX;
      gesturesController.setLastFingerPosition(x);
      renderFramePreview(x)
    }
  }, [renderFramePreview])
	//...
}

```
Here we also get the x-coordinate of the user's touch and record it in `gesturesController`, and also pass this coordinate to the `renderFramePreview` method to render the video frame preview.

Now let's implement a `touchend` event handler: 

```ts
export const Progress: FC<Props> = ({ controller }) => {
	//...
	const handleTouchMove: React.TouchEventHandler<HTMLDivElement> = useCallback(() => {
		gesturesController.touchEnd();

    seek(gesturesController.getLastFingerPosition())
    hideFramePreview()
  }, [renderFramePreview])
	//...
}

```
The simplest handler of all. Here we notify our `gesturesController` that the touch has ended. Then we call the `seek` method with the last touch coordinate for video rewinding and then hide our video frame preview.

This is not the only component that should support touch devices. Let's support touch devices in the Volume component. 

```ts
//...
const gesturesController = new GesturesController();

interface Props {
  controller: VideoController | null;
}

export const Volume: FC<Props> = ({ controller }) => {
//...

```

As in the Progress component, we initialise our `gesturesController`. 

Let's make changes to our click handler: 

```ts
const handleClick: React.MouseEventHandler<HTMLDivElement> = useCallback(
    (event) => {
      if (!gesturesController.getTouchStarted()) {
        updateVolume(event.clientX);
      }
    },
    [updateVolume]
  );

```
Here it's the same as in Progress, we don't process the click if a "touch" action is already in progress.

Let's implement touch device handlers: 

```ts
export const Volume: FC<Props> = ({ controller }) => {
  //...
  const handleTouchMove = useCallback(
    (event: React.TouchEvent) => {
      if (event.touches.length === 1) {
        const touch = event.touches[0];

        const x = touch.clientX;
        updateVolume(x);
      }
    },
    [updateVolume]
  );

  const handleTouchStart = useCallback(
    (event: React.TouchEvent) => {
      if (event.touches.length === 1) {
        const touch = event.touches[0];

        const x = touch.clientX;
        updateVolume(x);
      }

      gesturesController.touch();
    },
    [updateVolume]
  );

  const handleTouchEnd = useCallback(() => {
    gesturesController.touchEnd();
  }, []);

  return (
    <StyledStripValueWrapper
      onClick={handleClick}
      **onTouchMove={handleTouchMove}
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
      onTouchCancel={handleTouchEnd}**
    >
      <StyledStripeValue ref={volumeRef} />
      <StripValuePlaceholder ref={placeHolderRef} />
    </StyledStripValueWrapper>
  );
};

```
Here everything is analogous to what we did in the Progress component part.

##Final touches

The full implementation and all the code can be viewed here: https://github.com/ByMarsel/videoplayer-example.

Streaming Video: If you want features like switching video tracks with different qualities (480p, 720p), streaming video (downloading video in chunks) using HLS, subtitle support, and more, consider using the [hls.js](https://www.npmjs.com/package/hls.js) library. 

<aside>💡 HLS (HTTP Live Streaming) is a video format developed by Apple that allows streaming video via HTTP protocol. It is used for delivering video to various devices and platforms, including web pages, mobile apps, and smart TVs.

HLS divides video into small fragments, which are progressively downloaded and played by the client application. This allows delivering video of varying quality depending on the device and network bandwidth availability. For instance, if a device has a slow internet connection, HLS will automatically switch to lower quality video to ensure smooth playback without buffering.

HLS also supports the addition of subtitles, multi-channel audio, and other additional features. It has become a popular format for streaming services such as YouTube, Netflix, and Twitch.
</aside>

**Video Speed **

If you want to change the video speed, this can be done through the video property [playbackRate,](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate) with the maximum supported value being 4x.

**Autoplay **

Autoplay will only work if the video is muted, meaning the muted attribute of the video element is set to true. Otherwise, playback of such a video by the browser will be blocked.