Skip to main content

Making a custom component

A custom play button

First, we'll build a simple button that plays or pauses the player when clicked.

tip

The built-in <theoplayer-play-button> also provides this functionality.

Step 1: Create a custom button

Create a new JavaScript file called my-play-button.js with the following code:

import { Button, buttonTemplate } from '@theoplayer/web-ui';

// This <template> contains the content of our custom button.
// Its contents will be cloned into every <my-play-button>.
const template = document.createElement('template');
template.innerHTML = buttonTemplate('Play');

// Define a new class for our custom element.
// We extend the `Button` class from Open Video UI for Web,
// so we can inherit some logic and styles.
export class MyPlayButton extends Button {
constructor() {
// Pass our custom template to the super constructor.
super({ template });
}

// This method is called whenever the button gets clicked.
handleClick() {
alert('My play button was clicked!');
}
}

// Define our class as the constructor for our custom element.
// This allows us to use <my-play-button> anywhere in our HTML.
customElements.define('my-play-button', MyPlayButton);

Now add your new button to your custom UI. In our example, we'll place it in the centered-chrome slot, so it'll appear in the center of the player:

<script type="module" src="./my-play-button.js"></script>
<theoplayer-ui
configuration='{"libraryLocation":"/path/to/node_modules/theoplayer/","license":"your_theoplayer_license_goes_here"}'
source='{"sources":{"src":"https://example.com/stream.m3u8"}}'
>
<my-play-button slot="centered-chrome"></my-play-button>
</theoplayer-ui>

It should look something like this:

Try clicking the "Play" button in the middle of the screen. You should see an alert window popping up saying My play button was clicked!.

Step 2: Integrate with the backing player

Of course, we want the player to start playing instead of showing an alert! For this, we need to get access to the backing THEOplayer instance.

Open Video UI for Web provides a built-in mechanism to automatically inject dependencies into UI components, such as the player instance, or other fullscreen state of the UI. When a UI component is added as a child (or descendant) of a <theoplayer-default-ui> or <theoplayer-ui>, the parent UI will automatically inject those dependencies.

First, the UI component needs to opt into this mechanism by mixing in StateReceiverMixin into its superclass (see API documentation). This mixin takes the original superclass, and an array of dependencies which need to be injected:

import { Button, buttonTemplate, StateReceiverMixin } from '@theoplayer/web-ui';

export class MyPlayButton extends StateReceiverMixin(Button, ['player']) {
// ...
}

Once this button is added to a <theoplayer-ui>, it'll automatically receive the backing THEOplayer instance in its player property. If you want to do some custom logic when this happens, you can implement a setter for this property:

export class MyPlayButton extends StateReceiverMixin(Button, ['player']) {
set player(player) {
this._player = player;
console.log('My play button received a player!');
}
}

Change your handleClick() method to call play() or pause() on the player. You can also update the text content of your button to reflect the new state:

import { Button, buttonTemplate, StateReceiverMixin } from '@theoplayer/web-ui';

const template = document.createElement('template');
// Wrap the "Play" text in a <span>, so we can query and modify it later.
template.innerHTML = buttonTemplate('<span>Play</span>');

export class MyPlayButton extends StateReceiverMixin(Button, ['player']) {
constructor() {
super({ template });
this._buttonSpan = this.shadowRoot.querySelector('span');
}

get player() {
return this._player;
}
set player(player) {
this._player = player;
console.log('My play button received a player!');
}

handleClick() {
if (!this._player) {
// Not (yet) attached to a player.
return;
}
// Toggle the player's playing state,
// and update the text inside the <span>.
if (this._player.paused) {
this._player.play();
this._buttonSpan.textContent = 'Pause';
} else {
this._player.pause();
this._buttonSpan.textContent = 'Play';
}
}
}

customElements.define('my-play-button', MyPlayButton);

It should look something like this:

Try clicking the "Play" button in the middle of the screen. The player starts playing!
Clicking it again should pause the player.

Congratulations, you've built your very own play button! 🎉

A custom quality label

Next, let's create a label that displays the resolution of the player's current video quality.

tip

The built-in <theoplayer-active-quality-display> also provides this functionality.

Step 1: Create a custom component

Create a new JavaScript file called my-quality-label.js with the following code:

// This <template> contains the content of our custom label.
// Its contents will be cloned into every <my-quality-label>.
const template = document.createElement('template');
template.innerHTML = `
<style>
/* This rule targets the element itself, i.e. <my-quality-label> */
:host {
/* Use the same text and background color as the rest of the UI controls */
color: var(--theoplayer-text-color, #fff);
background: var(--theoplayer-control-background, transparent);

/* Add some padding */
padding: var(--theoplayer-control-padding, 10px);
}
</style>
<span></span>
`;

// Define a new class for our custom element.
export class MyQualityLabel extends HTMLElement {
constructor() {
super();

// Create a shadow root, and clone our template into it
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.content.cloneNode(true));

// Show something (for testing)
this._labelSpan = shadowRoot.querySelector('span');
this._labelSpan.textContent = '1080p';
}
}

// Define our class as the constructor for our custom element.
// This allows us to use <my-quality-label> anywhere in our HTML.
customElements.define('my-quality-label', MyQualityLabel);

Now add your new label to your custom UI. In our example, we'll place it inside a <theoplayer-control-bar> in the default slot, so it'll appear at the bottom of the player:

<script type="module" src="./my-quality-label.js"></script>
<theoplayer-ui
configuration='{"libraryLocation":"/path/to/node_modules/theoplayer/","license":"your_theoplayer_license_goes_here"}'
source='{"sources":{"src":"https://example.com/stream.m3u8"}}'
>
<theoplayer-control-bar>
<!-- A seek bar -->
<theoplayer-time-range></theoplayer-time-range>
</theoplayer-control-bar>
<theoplayer-control-bar>
<!-- A few commonly used built-in controls -->
<theoplayer-play-button></theoplayer-play-button>
<theoplayer-mute-button></theoplayer-mute-button>
<!-- A spacer, to fill up the remaining space in the middle -->
<span style="flex-grow: 1"></span>
<!-- Your brand new quality label! -->
<my-quality-label></my-quality-label>
<!-- Some other controls -->
<theoplayer-settings-menu-button menu="settings-menu"></theoplayer-settings-menu-button>
<theoplayer-fullscreen-button></theoplayer-fullscreen-button>
</theoplayer-control-bar>
<!-- A settings menu, so you can manually change the active quality -->
<theoplayer-settings-menu slot="menu" id="settings-menu"></theoplayer-settings-menu>
</theoplayer-ui>

It should look something like this:

Step 2: Listen to quality changes

Right now, the quality label is static, it doesn't actually update when the player's quality changes. Let's fix that!

  1. Once again, we use the StateReceiverMixin to gain access to the backing THEOplayer instance.
  2. When we receive the player, we add an event listener for the addtrack event of player.videoTracks, so we'll be notified when the video track becomes available.
  3. We register an activequalitychanged event listener on that video track, so we can respond when the active video quality changes.
  4. When our activequalitychanged listener fires, we update the contents of our <span> with the quality's height.
import { StateReceiverMixin } from '@theoplayer/web-ui';

const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
color: var(--theoplayer-text-color, #fff);
background: var(--theoplayer-control-background, transparent);
padding: var(--theoplayer-control-padding, 10px);
}
</style>
<span></span>
`;

export class MyQualityLabel extends StateReceiverMixin(HTMLElement, ['player']) {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.content.cloneNode(true));
this._labelSpan = shadowRoot.querySelector('span');
this._labelSpan.textContent = '';
}

get player() {
return this._player;
}
set player(player) {
if (this._player) {
// Clean up
this._player.videoTracks.removeEventListener('addtrack', this.handleAddTrack);
this.updateActiveTrack(undefined);
}
this._player = player;
if (this._player) {
// Listen for the 'addtrack' event
this._player.videoTracks.addEventListener('addtrack', this.handleAddTrack);
// If the player already has an active video track,
// start using it right away!
if (this._player.videoTracks.length > 0) {
this.updateActiveTrack(this._player.videoTracks[0]);
}
}
}

handleAddTrack = (event) => {
this.updateActiveTrack(event.track);
};
updateActiveTrack(track) {
if (this._activeVideoTrack) {
// Clean up
this._activeVideoTrack.removeEventListener('activequalitychanged', this.handleActiveQualityChanged);
}
this._activeVideoTrack = track;
if (this._activeVideoTrack) {
// Listen for the 'activequalitychanged' event
this._activeVideoTrack.addEventListener('activequalitychanged', this.handleActiveQualityChanged);
// If the track already has an active quality,
// start using it right away!
if (this._activeVideoTrack.activeQuality) {
this.updateActiveTrack(this._activeVideoTrack.activeQuality);
}
}
}

handleActiveQualityChanged = (event) => {
this.updateActiveQuality(event.quality);
};
updateActiveQuality(quality) {
if (quality) {
// Show the quality's height in our <span>
this._labelSpan.textContent = `${quality.height}p`;
} else {
// No active quality yet...
this._labelSpan.textContent = '';
}
}
}

customElements.define('my-quality-label', MyQualityLabel);

It should look something like this:

Try changing the active quality by clicking the ⚙️ (Settings) button, and changing "Automatic" to a specific quality. You should see your custom label update to show the height of the new quality.

That's it! You now have your very own active quality label! 🥳