How to Build a Video Player with Svelte

How to Build a Video Player with Svelte

In this article, we will build a custom video player with Svelte, a modern front-end JavaScript framework for developing dynamic and reactive web applications.

For styling, we will use Tailwind CSS, a utility-first CSS framework, and for the player controls, we will incorporate icons from the Svelte Icons Pack.

Install Required Packages

npx sv create svelte-video-player
cd svelte-video-player
npm install
npm run dev

Next let us install and setup Tailwind CSS and Svelte Icons Pack

Component Setup

Create a VideoPlayer.svelte file under src/lib directory and import it into src/routes/+page.svelte

<!--  src/routes/+page.svelte -->

<script>
    import VideoPlayer from '$lib/VideoPlayer.svelte';
</script>

<VideoPlayer />

Next we will create the UI and styling for the video player, with our custom control

<!--  src/lib/VideoPlayer.svelte -->

<script>
    import { Icon } from 'svelte-icons-pack';
    import { FaSolidPlay, FaSolidPause } from 'svelte-icons-pack/fa';
    import { VscMute, VscUnmute } from 'svelte-icons-pack/vsc';
    import { RiMediaFullscreenLine, RiMediaFullscreenExitFill } from 'svelte-icons-pack/ri';
</script>

<div class="bg-gray-200 h-screen flex items-center justify-center">
    <div
        class="relative w-3/4 h-3/4 bg-black flex items-center justify-center overflow-hidden player-container"
    >
        <video class="h-auto w-auto max-w-full">
            <track kind="captions" />
            <source src="/demo-video.mp4" type="video/mp4" />
        </video>

        <div class="absolute w-full bottom-0 left-0 bg-[#00000086]">
            <div class="w-full px-2">
                <div class="h-1 bg-gray-700 rounded-lg relative cursor-pointer" role="presentation">
                    <div
                        class="absolute h-full bg-white rounded-lg"
                        role="progressbar"
                        aria-label="played"
                    ></div>
                </div>
            </div>

            <div class="h-14 flex justify-between items-center px-4">
                <div class="flex items-center gap-4">
                    <button>
                        <Icon color="#ffffff" src={FaSolidPlay} size="22px" />
                    </button>

                    <button>
                        <Icon src={VscUnmute} color="#ffffff" size="22px" />
                    </button>

                    <div>
                        <p class="text-white">00:00 / 00:00</p>
                    </div>
                </div>

                <div>
                    <button>
                        <Icon color="#ffffff" src={RiMediaFullscreenLine} size="22px" />
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>

Play/Pause

The first function we will implement is the play/pause control. We will create a paused variable and bind it to the paused property of the video element using bind:paused. Then, we will create a function to toggle the play/pause state of the video by updating this variable.

<script>
    ...
    let paused;

    const togglePlay = () => {
        paused = !paused;
    };
</script>

...
<video class="h-auto w-auto max-w-full" bind:paused>
    <track kind="captions" />
    <source src="/demo-video.mp4" type="video/mp4" />
</video>
...

...
<button on:click={togglePlay}>
    <Icon color="#ffffff" src={paused ? FaSolidPlay : FaSolidPause} size="22px" />
</button>
...

Muted State

The next function is for the mute state of the video. Create a muted variable and bind it to the muted property of the video element, then create a function to toggle the state


<script>
...
    let muted;

    const toggleAudio = () => {
        muted = !muted;
    };
...
</script>

<video bind:paused bind:muted>
    <track kind="captions" />
    <source src="/demo-video.mp4" type="video/mp4" />
</video>

Time Display

Next, we will display the current time and total duration of the video next to the audio icon. Create a function to format time in hh:mm:ss format for better readability. Define duration and currentTime variables and bind them to the respective properties of the video element using bind:duration and bind:currentTime

<script>
...
    let currentTime = 0;
    let duration = 0;

    const formatVideoTime = (timeInSeconds) => {
        const hour = Math.floor(timeInSeconds / 3600)
            .toString()
            .padStart(2, '0');
        const minutes = Math.floor((timeInSeconds % 3600) / 60)
            .toString()
            .padStart(2, '0');
        const seconds = Math.floor(timeInSeconds % 60)
            .toString()
            .padStart(2, '0');

        return `${hour > 0 ? hour + ':' : ''}${minutes}:${seconds}`;
    };
</script>

<video
    class="h-auto w-auto max-w-full"
    bind:paused
    bind:muted
    bind:currentTime
    bind:duration
>
    <track kind="captions" />
    <source src="/demo-video.mp4" type="video/mp4" />
</video>

... 

<div>
    <p class="text-white">{formatVideoTime(currentTime)} / {formatVideoTime(duration)}</p>
</div>

Fullscreen Mode

We will use the native browser API to implement fullscreen mode. First, we will check if the document is currently in fullscreen mode. If it is, we will exit fullscreen mode. If it is not, we will request fullscreen mode for the target element. For compatibility, since Document.fullscreenElement is not fully supported in all browsers, we will account for vendor prefixes where necessary.

<script>
...
    let isFullScreen = false;
    let videoContainerElement;

    const toggleFullScreen = () => {
        if (
            document.fullscreenElement ||
            document.webkitFullscreenElement ||
            document.msFullscreenElement
        ) {
            // Exit fullscreen mode
            if (document.exitFullscreen) {
                document.exitFullscreen();
            } else if (document.webkitExitFullscreen) {
                document.webkitExitFullscreen();
            } else if (document.msExitFullscreen) {
                document.msExitFullscreen();
            }
            isFullScreen = false;
        } else {
            // Enter fullscreen mode
            if (videoContainerElement.requestFullscreen) {
                videoContainerElement.requestFullscreen();
            } else if (videoContainerElement.webkitRequestFullscreen) {
                videoContainerElement.webkitRequestFullscreen();
            } else if (videoContainerElement.msRequestFullscreen) {
                videoContainerElement.msRequestFullscreen();
            }
            isFullScreen = true;
        }
    };
</script>

<div class="bg-gray-200 h-screen flex items-center justify-center">
    <div
        class="relative w-3/4 h-3/4 bg-black flex items-center justify-center overflow-hidden player-container"
        bind:this={videoContainerElement}
    >
        <video class="h-auto w-auto max-w-full" bind:paused bind:muted>
            <track kind="captions" />
            <source src="/demo-video4.mp4" type="video/mp4" />
        </video>

    ...

        <button on:click={toggleFullScreen}>
            <Icon
                color="#ffffff"
                src={isFullScreen ? RiMediaFullscreenExitFill : RiMediaFullscreenLine}
                size="22px"
            />
        </button>
    </div>
</div>

Progress Bar

Now we are going to create a function for the custom progress bar, to display the video progress and handle manual scrubbing of the video.

<script>
...
    let progressbarElement;
    let progressPlayed = 0;
    let playerContainer = typeof window !== 'undefined' ? document.querySelector('.player-container') : null;

    const handleTimeUpdate = () => {
        const playedPercent = (time / duration) * 100;
        progressPlayed = playedPercent;
    };

    const scrubVideo = (event) => {
        const rect = progressbarElement.getBoundingClientRect();
        let relX = event.pageX - (rect.left + document.body.scrollLeft);

        if (relX < 0) {
            relX = 0;
        } else if (relX > progressbarElement.offsetWidth) {
            relX = progressbarElement.offsetWidth;
        }

        const relXPercent = (relX / progressbarElement.offsetWidth) * 100;
        const timeInSeconds = (duration * relXPercent) / 100;
        const playedPercent = (timeInSeconds / duration) * 100;

        progressPlayed = playedPercent;
        time = timeInSeconds;
    };

    const onProgressDragStop = () => {
        paused = false;
        playerContainer.removeEventListener('mousemove', scrubVideo);
        document.removeEventListener('mouseup', onProgressDragStop);
    };

    const handleProgressDrag = (event) => {
        paused = true;
        scrubVideo(event);
        playerContainer.addEventListener('mousemove', scrubVideo);
        document.addEventListener('mouseup', onProgressDragStop);
    };
</script>

<div class="bg-gray-200 h-screen flex items-center justify-center">
    <div
        class="relative w-3/4 h-3/4 bg-black flex items-center justify-center overflow-hidden player-container"
        bind:this={videoContainerElement}
    >
...

        <div class="w-full px-2">
            <div
                class="h-1 bg-gray-700 rounded-lg relative cursor-pointer"
                role="presentation"
                bind:this={progressbarElement}
                on:mousedown={handleProgressDrag}
            >
                <div
                    class="absolute h-full bg-white rounded-lg"
                    role="progressbar"
                    aria-label="played"
                    style="width: {progressPlayed}%"
                ></div>
            </div>
        </div>

...
    </div>
</div>

The handleProgressDrag function pauses the video and allows users to adjust the playback position by dragging the progress bar or clicking on it. It calculates the new playback time based on the mouse's position on the bar and updates the video using the scrubVideo function. While the user is dragging, it listens for mousemove events to continuously update the playback position. When the user releases the mouse -mouseup event, it cleans up the event listeners and resumes playback.

The scrubVideo function calculates the playback position based on the user's interaction with the progress bar. It determines the mouse's relative position relX on the progress bar, ensuring the value stays within its boundaries. This relative position is converted into a percentage relXPercent, which is used to calculate the corresponding playback time in seconds. Finally, it updates the progressPlayed percentage and sets the video's time to the new calculated position.

Keyboard Shortcuts

Let us handle keyboard shortcuts for toggling fullscreen and pause/play. We are going to use Svelte special elements to handle this <svelte:window> this will allow us to add event listeners to the window object. This should be placed at the top level of the component.

<script>
...
    const onKeyDown = (event) => {
        if (event.key === 'f') {
            toggleFullScreen();
        }

        if (event.key === ' ') {
            togglePlay();
        }
    };
</script>

<svelte:window on:keydown={onKeyDown} />

Using the keydown event, we will detect when the f key or spacebar is pressed and trigger the corresponding function.

Conclusion

In this tutorial, we covered the basic controls of a video player, including play/pause, mute, displaying the current time and duration, and toggling fullscreen mode. You can further extend this functionality by adding features such as playback speed controls, hiding the controls when the cursor is idle, and much more.

Here is a link to the code.

Thanks for reading!

Demo video by Alex Kuimov from Pixabay