I Made my Own Picture-in-Picture player

I guess I'm not the only one getting distracted easily when watching a video, right? That is exactly why I created my custom picture in picture player for my course videos.

Code on Github
🧑‍💻 🎥
... Almost ready!

I guess I'm not the only one getting distracted easily when watching a video, right? 😉 That is exactly why I created my custom picture in picture player for my course videos.

The player makes itself sticky in the corner of the page, while you browse the text version of the lesson or play around with any of the playgrounds on the page.

The Picture-in-Picture web API

Before we dive into how I created my own custom picture-in-picture player, it is worth mentioning there is also a native web API for picture in picture.

This API works great and is in a lot of cases the way to go. However, because browsers don't want websites to randomly spam video windows all over the place, the API has some limitations.

The biggest limitation is that there's a user interaction (for example a click) required before this API can be triggered. That means that you can't just trigger the picture-in-picture mode if you scroll down on the page like you see in the example above.

So we couldn't use this API for our use case. We had to roll our own.

How to make our own Picture-in-Picture player

The first step is to add a video player to the page. In the example above I used the Media Chrome video player because I like the way it looks, but it works just as well with the regular HTML video player.

Wrapping the player in a div

Once we've added the player on the page, it is very important to wrap the player in a div. This div shouldn't shrink in size if it's children are removed. This way we can make the player position fixed later, without the full contents of the page jumping up.

<div class="aspect-video">
<video class="aspect-video" src="video.mp4" controls></video>
</div>

Here I decided to use the aspect-video tailwind class. This class uses the CSS aspect-ratio property to give the element a fixed aspect ratio.

Because the div is a block element, it will by default be 100% the width of its parent, and because of the aspect ratio it will now also have an automatic height. Even if it doesn't have any children!

When should the player be sticky?

The easiest way to make the player sticky to the side of the page, is by using CSS position fixed. But when and how to do this?

For this we're gonna need a little bit of JavaScript, and more specifically the IntersectionObserver API. This API allows us to observe when an element enters or leaves the viewport.

Depending on the framework you are using, you can either use the JavaScript API directly, or for example in the case of React opt for a library like react-intersection-observer. I used the latter.

import { useInView } from "react-intersection-observer";

const App = () => {
const { ref, inView, entry } = useInView({
threshold: [0, 0.9, 1],
});

return (
<div ref={ref} className="aspect-video">
<video className="aspect-video" src="video.mp4" controls/>
</div>
);
}

In the example above I used the useInView hook from the react-intersection-observer library. This hook returns a ref, which you can attach to the element you want to observe.

We want to observe the wrapper div, and not the video itself, since the video will never go out of view (because it becomes sticky).

Next to that we get a inView boolean, and an entry object containing data about the element that's being tracked.

Lastly you see the tresholds are set to [0, 0.9, 1]. These thresholds tell the intersection observer when it should 'trigger'. In our case that means the intersection observer will update itself when the element is fully in view, when it's 90% in view, and when it's fully out of view. Watch the full video to see why I chose these thresholds.

Making the player sticky

Now that we have the inView boolean, we can use this to make the player sticky. I'm using tailwind-merge here to merge CSS classes together.

import { useInView } from "react-intersection-observer";
import { twMerge } from "tailwind-merge";

const App = () => {
const { ref, inView, entry } = useInView({
threshold: [0, 0.9, 1],
});

return (
<div ref={ref} className="aspect-video">
<video
className={twMerge("aspect-video",
inView
? "relative"
: "fixed bottom-6 right-6 w-[300px]"
)}
src="video.mp4" controls/>
</div>
);
}

It really is that easy! Just using position fixed and ensuring the parent doesn't collapse.

In the video and playground at the top of this page, we do however go quite a bit further. We explore how we can only make the video sticky if it's still playing, we will also add a nice fade-in animation too!

So definitely check out the video at the top of this page to learn even more.