Version: 2.67.0

How to build a Chromessless UI

IN PROGRESS AS OF 2019-02-13

1. Introduction

This guide explains the components of a video player UI and demonstrates how to implement a UI on top a Chromeless THEOplayer. There's no UI when THEOplayer is Chromeless. The developer built a UI from scratch, and hooks their custom UI into the THEOplayer API.

2. Components of a video player UI

A video player UI can be split in three main components:

  1. Video frame: the frame rendering the actual video content.
  2. Control bar: the container containing the buttons to pause/play the video, to switch to fullscreen, to change the volume, and much more.
  3. Timeline: the seekbar which can be used to navigate to another position in the timeline. The timeline usually illustrates the video duration and the current playhead position, and parts of the video which have been added to the buffer. The timeline is arguably a sub-component of the control bar, but deserves sufficient attention.

There are also less apparent components:

  1. Subtitles: the rendering of the subtitle cues.
  2. UX enchancements: a poster image, a loading icon, ... .
  3. Error handling: visualizing playback issues to the viewer.
  1. Implementing components

This section demonstrates how to implement these 4 components in a Chromeless THEOplayer.

Regardless of which platform (Web, iOS/Xcode, Android, ...) you're targetting, you'll need to hook into the THEOplayer API, so we'll list which APIs are relevant per component.

3.1. Video frame

The video frame is the container (or View) which renders the actual video content. The other two components are often children of this container.

When you initialize a Chromeless THEOplayer instance, you have to pass along the container in which THEOplayer should render the video content. You should use the native platform techniques to style this container. For example, when doing Browser development, and using our Web SDK, a DOM-element (usually a

-element) is used as a container. You can use HTML, CSS and JavaScript to do the styling.

The sub-sections below demonstrate how to initialize a Chromeless THEOplayer instance.

Web SDK

The snippet below is a starting template.

Web SDKExpand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
</style>
</head>
<body>
<div class="theoplayer-container"></div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
</script>
</body>
</html>

You could run this on a local web server and open up through http://localhost. The video frame appears as a black container because although the stream has been loaded, there hasn't been a video play request. Calling player.play() in the Console will start the video.

3.1.1. Relevant APIs

To initialize a Chromeless video player instance and connect it with a container can be achieved with the APIs below.

Use-caseWeb SDK APIAndroid SDK APIiOS SDK APIInitialize a Chromeless THEOplayer instancePlayer API (new THEOplayer.ChromelessPlayer(...))THEOplayerConfig API(chromeless(true))THEOplayerConfiguration

3.2. Control bar

The control bar is the container which allows users to play and pause the video, to change the volume, to toggle fullscreen, and much more.

The sub-section below describes how you could create such a component. Later on we will describe how you can put separate user controls (e.g. a "play" component) which hook into the THEOplayer API.

Web SDK

The snippet below is an extension of the previous snippet. This snippet adds a controls container at the bottom of your video container.

Web SDK - adding a controls containerExpand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 10%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 100%;
position: relative;
background: yellow;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar"></div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
</script>
</body>
</html>

The image below demonstrates the expected result.

As you can notice, there's a yellow container at the bottom which will contain the control bar controls. You can also subtly observe orange borders on the left and right side of the video frame. This is due to rendering the frame in a 500x280px container, which does not respect the video's aspect ratio of 16:9. THEOplayer will fill the void with the orange background color as expected.

3.2.1. Sub-components

A control bar usually contains the following sub-components:

Sub-componentWeb SDK APIAndroid SDK APIiOS SDK APIPlay / pause buttonPlayer API (e.g. player.play() / player.pause()) Player APIVolume mute buttonPlayer API (e.g. player.muted = true) Player APIVolume increase sliderPlayer API (e.g. player.volume = 0.5) Player APIFullscreen / inline / picture-in-picture buttonYou need to build it yourself - as would be expected from a Chromeless UISameSameSwitch-video-quality buttonVideoTrack API (e.g. player.videoTracks[0].targetQuality = player.videoTracks[0].qualities[0]) iOS SDK only supports 'auto' ABR as of 2019-02-18Switch-audio-track buttonAudioTrack API (e.g. player.audioTracks[0].enabled = true) AudioTrack APISwitch-subtitle-track buttonTextTrack API (e.g. player.textTracks[0].mode = 'showing') TextTrack APIChromecast buttonCast API (e.g. player.cast.chromecast.start()) Chromecast API Airplay button Cast API (e.g. player.cast.airplay.start()) You need to build it yourself

3.2.2. Play / pause button

To add a play / pause button to your UI, you need to hook your on-click (or on-touch or ...) behavior with the THEOplayer API. The THEOplayer API exposes a play() method to initiate playback and a pause() button to halt playback.

If the player is in a PAUSED state you want to show a play button, and when the player is in a PLAYING state you want to show a pause button. The THEOplayer API exposes event listeners to track play and pause events.

Web SDK

The snippet below adds a play and pause button to the UI. Only the pause button will be visible if the video is playing, and only the play button will be visible if the video is paused.

Web SDK - adding a play and pause buttonExpand source

<!DOCTYPE html>
<html lang="en">
<head>
...
<style>
... .hidden {
display: none;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
var buttonPlay = document.querySelector(".button-play"),
buttonPause = document.querySelector(".button-pause");
buttonPlay.addEventListener("click", function() {
player.play();
});
buttonPause.addEventListener("click", function() {
player.pause();
});
player.addEventListener("play", function(e) {
buttonPlay.classList.toggle("hidden");
buttonPause.classList.toggle("hidden");
});
player.addEventListener("pause", function(e) {
buttonPause.classList.toggle("hidden");
buttonPlay.classList.toggle("hidden");
});
}
</script>
</body>
</html>

The image below demonstrates the expected result.

3.2.3. Volume mute button

To add a mute / unmute button to your UI, you need to connect your on-click (or on-touch or ...) behavior with the THEOplayer API. The THEOplayer API exposes a muted property (or method) to (un)mute the player.

If the player is in a MUTED state you want to show a unmute button, and when the player is in a UNMUTED state you want to show a mute button. The THEOplayer API exposes event listeners to track volumechange events.

Web SDK

The snippet below adds a mute and unmute button. Only one button is shown, depending whether the player is already muted

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
var buttonPlay = document.querySelector(".button-play"),
buttonPause = document.querySelector(".button-pause"),
buttonMuted = document.querySelector(".button-muted"),
buttonUnmuted = document.querySelector(".button-unmuted");
buttonPlay.addEventListener("click", function() {
player.play();
});
buttonPause.addEventListener("click", function() {
player.pause();
});
buttonMuted.addEventListener("click", function() {
player.muted = true;
});
buttonUnmuted.addEventListener("click", function() {
player.muted = false;
});
player.addEventListener("play", function(e) {
buttonPlay.classList.toggle("hidden");
buttonPause.classList.toggle("hidden");
});
player.addEventListener("pause", function(e) {
buttonPause.classList.toggle("hidden");
buttonPlay.classList.toggle("hidden");
});
player.addEventListener("volumechange", function(e) {
buttonMuted.classList.toggle("hidden");
buttonUnmuted.classList.toggle("hidden");
});
}
</script>
</body>
</html>

The image blow demonstrates the expected result.

3.2.4. Volume increase slider

To add a volume slider to your UI, you need to connect your on-click (or on-touch or ...) behavior with the THEOplayer API. The THEOplayer API exposes a volume property (or method) to adjust the player volume.

Web SDK

The snippet below implements a slider which allows the user to change the volume levle.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
</div>
</div>
</div>
<script type="text/javascript">
...
function registerControlBarComponents() {
var buttonPlay = document.querySelector('.button-play'),
buttonPause = document.querySelector('.button-pause'),
buttonMuted = document.querySelector('.button-muted'),
buttonUnmuted = document.querySelector('.button-unmuted'),
sliderVolume = document.querySelector('.slider-volume');
...
buttonUnmuted.addEventListener('click', function () {
player.muted = false;
});
sliderVolume.addEventListener('input', function (e) {
player.volume = e.target.value;
});
player.addEventListener('play', function(e) {
buttonPlay.classList.toggle('hidden');
buttonPause.classList.toggle('hidden');
});
player.addEventListener('pause', function(e) {
buttonPause.classList.toggle('hidden');
buttonPlay.classList.toggle('hidden');
});
var wasPreviouslyMuted = false;
player.addEventListener('volumechange', function(e) {
if (player.muted != wasPreviouslyMuted) {
wasPreviouslyMuted = player.muted;
buttonMuted.classList.toggle('hidden');
buttonUnmuted.classList.toggle('hidden');
}
});
}
</script>
</body>
</html>

3.2.5. Fullscreen / inline / picture-in-picture button

To add a fullscreen / inline / picture-in-picture button to your UI, you need to connect your on-click (or on-touch or ...) behavior with the platform-native API. (THEOplayer's Presentation API is naturally not available in Chromeless mode.)

If the player is in a INLINE state you want to show a fullscreen button, and when the player is in a FULLSCREEN state you want to show a inline button.

Web SDK

The snippet below implements three buttons which allows you to switch between presentation states. The presentation state changes are triggered through platform-native APIs.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
</div>
</div>
</div>
<script type="text/javascript">
...
function registerControlBarComponents() {
var buttonPlay = document.querySelector('.button-play'),
buttonPause = document.querySelector('.button-pause'),
buttonMuted = document.querySelector('.button-muted'),
buttonUnmuted = document.querySelector('.button-unmuted'),
sliderVolume = document.querySelector('.slider-volume'),
buttonFullscreen = document.querySelector('.button-fullscreen'),
buttonInline = document.querySelector('.button-inline'),
buttonPictureInPicture = document.querySelector('.button-picture-in-picture');
buttonPlay.addEventListener('click', function() {
player.play();
});
buttonPause.addEventListener('click', function() {
player.pause();
});
buttonMuted.addEventListener('click', function () {
player.muted = true;
});
buttonUnmuted.addEventListener('click', function () {
player.muted = false;
});
sliderVolume.addEventListener('input', function (e) {
player.volume = e.target.value;
});
buttonFullscreen.addEventListener('click', function () {
openFullscreen();
});
buttonInline.addEventListener('click', function () {
closeFullscreen();
});
var video = element.querySelectorAll('video')[1],
isPictureInPictureEnabled = false;
buttonPictureInPicture.addEventListener('click', function () {
if (!isPictureInPictureEnabled) {
video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
});
video.addEventListener('enterpictureinpicture', function() {
isPictureInPictureEnabled = true;
});
video.addEventListener('leavepictureinpicture', function() {
isPictureInPictureEnabled = false;
});
player.addEventListener('play', function(e) {
buttonPlay.classList.toggle('hidden');
buttonPause.classList.toggle('hidden');
});
player.addEventListener('pause', function(e) {
buttonPause.classList.toggle('hidden');
buttonPlay.classList.toggle('hidden');
});
var wasPreviouslyMuted = false;
player.addEventListener('volumechange', function(e) {
if (player.muted != wasPreviouslyMuted) {
wasPreviouslyMuted = player.muted;
buttonMuted.classList.toggle('hidden');
buttonUnmuted.classList.toggle('hidden');
}
});
function openFullscreen() {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { /* Firefox */
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { /* IE/Edge */
element.msRequestFullscreen();
}
}
function closeFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) { /* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) { /* IE/Edge */
document.msExitFullscreen();
}
}
}
</script>
</body>
</html>

The image below demonstrates the expected result.

3.2.6. Switch-video-quality button

To add a switch-video-quality dropdown to your UI, you need to connect your on-change behavior with the THEOplayer VideoTrack API. The VideoTrack API exposes a targetQuality property (or method) to change the video quality.

The VideoTrack API exposes event handlers which allows you to identify the qualities present in the stream.

Web SDK

The snippet below adds a select-element which allows users to switch between qualities. Additionally, it uses event listeners to identify the available qualities and renders them in the UI.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 20%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 100%;
position: relative;
background: yellow;
opacity: 0.7;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
<select class="select-video-qualities"
><option value="">Auto</option></select
>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
/**
* Play, Pause, Muted, Unmuted, Volume, Fullscreen, Inline, Picture-in-Picture
* */
var buttonPlay = document.querySelector(".button-play"),
buttonPause = document.querySelector(".button-pause"),
buttonMuted = document.querySelector(".button-muted"),
buttonUnmuted = document.querySelector(".button-unmuted"),
sliderVolume = document.querySelector(".slider-volume"),
buttonFullscreen = document.querySelector(".button-fullscreen"),
buttonInline = document.querySelector(".button-inline"),
buttonPictureInPicture = document.querySelector(
".button-picture-in-picture"
);
buttonPlay.addEventListener("click", function() {
player.play();
});
buttonPause.addEventListener("click", function() {
player.pause();
});
buttonMuted.addEventListener("click", function() {
player.muted = true;
});
buttonUnmuted.addEventListener("click", function() {
player.muted = false;
});
sliderVolume.addEventListener("input", function(e) {
player.volume = e.target.value;
});
buttonFullscreen.addEventListener("click", function() {
openFullscreen();
});
buttonInline.addEventListener("click", function() {
closeFullscreen();
});
var video = element.querySelectorAll("video")[1],
isPictureInPictureEnabled = false;
buttonPictureInPicture.addEventListener("click", function() {
if (!isPictureInPictureEnabled) {
video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
});
video.addEventListener("enterpictureinpicture", function() {
isPictureInPictureEnabled = true;
});
video.addEventListener("leavepictureinpicture", function() {
isPictureInPictureEnabled = false;
});
player.addEventListener("play", function(e) {
buttonPlay.classList.toggle("hidden");
buttonPause.classList.toggle("hidden");
});
player.addEventListener("pause", function(e) {
buttonPause.classList.toggle("hidden");
buttonPlay.classList.toggle("hidden");
});
var wasPreviouslyMuted = false;
player.addEventListener("volumechange", function(e) {
if (player.muted != wasPreviouslyMuted) {
wasPreviouslyMuted = player.muted;
buttonMuted.classList.toggle("hidden");
buttonUnmuted.classList.toggle("hidden");
}
});
function openFullscreen() {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
/* Firefox */
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
/* IE/Edge */
element.msRequestFullscreen();
}
}
function closeFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen();
}
}
/**
* Video quality
*/
var selectVideoQualities = document.querySelector(
".select-video-qualities"
);
selectVideoQualities.addEventListener("change", function() {
var value = selectVideoQualities.value;
if (value == "") {
player.videoTracks[0].targetQuality = null;
} else {
player.videoTracks[0].targetQuality =
player.videoTracks[0].qualities[value];
}
});
player.videoTracks.addEventListener("addtrack", function(e) {
e.track.qualities.forEach(function(quality, index) {
var option = document.createElement("option");
option.value = index;
option.innerText = quality.height;
selectVideoQualities.appendChild(option);
});
});
}
</script>
</body>
</html>

The image below demonstrates the expected result.

3.2.7. Switch-audio-track button

To add a switch-audio-track dropdown to your UI, you need to connect your on-change behavior with the THEOplayer AudioTrack API. The AudioTrack API exposes an enabled property (or method) to change the video quality.

The TextTrack API exposes event handlers which allows you to identify the audio tracks present in the stream.

Web SDK

The snippet below adds a select-element which allows users to switch between audio tracks. Additionally, it uses event listeners to identify the available audio tracks and renders them in the UI.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 30%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 70%;
position: relative;
background: yellow;
opacity: 0.7;
}
.subtitles-container {
height: 30%;
width: 100%;
background: grey;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="subtitles-container"></div>
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
<select class="select-video-qualities"
><option value="">Auto</option></select
>
<select class="select-audio-track"
><option value="">Default</option></select
>
<select class="select-subtitle-track"
><option value="">Off</option></select
>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
/**
* Play, Pause, Muted, Unmuted, Volume, Fullscreen, Inline, Picture-in-Picture
* */
var buttonPlay = document.querySelector(".button-play"),
buttonPause = document.querySelector(".button-pause"),
buttonMuted = document.querySelector(".button-muted"),
buttonUnmuted = document.querySelector(".button-unmuted"),
sliderVolume = document.querySelector(".slider-volume"),
buttonFullscreen = document.querySelector(".button-fullscreen"),
buttonInline = document.querySelector(".button-inline"),
buttonPictureInPicture = document.querySelector(
".button-picture-in-picture"
);
buttonPlay.addEventListener("click", function() {
player.play();
});
buttonPause.addEventListener("click", function() {
player.pause();
});
buttonMuted.addEventListener("click", function() {
player.muted = true;
});
buttonUnmuted.addEventListener("click", function() {
player.muted = false;
});
sliderVolume.addEventListener("input", function(e) {
player.volume = e.target.value;
});
buttonFullscreen.addEventListener("click", function() {
openFullscreen();
});
buttonInline.addEventListener("click", function() {
closeFullscreen();
});
var video = element.querySelectorAll("video")[1],
isPictureInPictureEnabled = false;
buttonPictureInPicture.addEventListener("click", function() {
if (!isPictureInPictureEnabled) {
video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
});
video.addEventListener("enterpictureinpicture", function() {
isPictureInPictureEnabled = true;
});
video.addEventListener("leavepictureinpicture", function() {
isPictureInPictureEnabled = false;
});
player.addEventListener("play", function(e) {
buttonPlay.classList.toggle("hidden");
buttonPause.classList.toggle("hidden");
});
player.addEventListener("pause", function(e) {
buttonPause.classList.toggle("hidden");
buttonPlay.classList.toggle("hidden");
});
var wasPreviouslyMuted = false;
player.addEventListener("volumechange", function(e) {
if (player.muted != wasPreviouslyMuted) {
wasPreviouslyMuted = player.muted;
buttonMuted.classList.toggle("hidden");
buttonUnmuted.classList.toggle("hidden");
}
});
function openFullscreen() {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
/* Firefox */
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
/* IE/Edge */
element.msRequestFullscreen();
}
}
function closeFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen();
}
}
/**
* Video quality
*/
var selectVideoQualities = document.querySelector(
".select-video-qualities"
);
selectVideoQualities.addEventListener("change", function() {
var value = selectVideoQualities.value;
if (value == "") {
player.videoTracks[0].targetQuality = null;
} else {
player.videoTracks[0].targetQuality =
player.videoTracks[0].qualities[value];
}
});
player.videoTracks.addEventListener("addtrack", function(e) {
e.track.qualities.forEach(function(quality, index) {
var option = document.createElement("option");
option.value = index;
option.innerText = quality.height;
selectVideoQualities.appendChild(option);
});
});
/**
* Audio quality
*/
var selectAudioTrack = document.querySelector(".select-audio-track"),
defaultAudioTrack;
selectAudioTrack.addEventListener("change", function() {
var value = selectAudioTrack.value;
player.audioTracks.forEach(function(track) {
track.enabled = false;
});
if (value == "") {
player.audioTracks[0].enabled = true;
} else {
player.audioTracks[value].enabled = true;
}
});
player.audioTracks.addEventListener("addtrack", function(e) {
var option = document.createElement("option");
option.value = player.audioTracks.indexOf(e.track);
option.innerText = e.track.label;
selectAudioTrack.appendChild(option);
if (e.track.enabled) {
defaultAudioTrack = option.value;
}
});
}
</script>
</body>
</html>

3.2.8. Switch-subtitle-track button

To add a switch-video-quality dropdown to your UI, you need to connect your on-change behavior with the THEOplayer VideoTrack API. The VideoTrack API exposes a targetQuality property (or method) to change the video quality.

The VideoTrack API exposes event handlers which allows you to identify the qualities present in the stream.

Web SDK

The snippet below adds a select-element which allows users to switch between subtitle tracks, but also to disable them. Additionally, it uses event listeners to identify the available subtitle tracks and renders them in the UI.

Web SDK - add subtitle select and render subtitle cueExpand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 30%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 70%;
position: relative;
background: yellow;
opacity: 0.7;
}
.subtitles-container {
height: 30%;
width: 100%;
background: grey;
}
.hidden {
display: none;
}
.theoplayer-container .theoplayer-texttracks {
display: none !important;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="subtitles-container"></div>
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
<select class="select-video-qualities"
><option value="">Auto</option></select
>
<select class="select-audio-track"
><option value="">Default</option></select
>
<select class="select-subtitle-track"
><option value="">Off</option></select
>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector(".theoplayer-container");
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: "//cdn.theoplayer.com/dash/theoplayer/"
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
/**
* Play, Pause, Muted, Unmuted, Volume, Fullscreen, Inline, Picture-in-Picture
* */
var buttonPlay = document.querySelector(".button-play"),
buttonPause = document.querySelector(".button-pause"),
buttonMuted = document.querySelector(".button-muted"),
buttonUnmuted = document.querySelector(".button-unmuted"),
sliderVolume = document.querySelector(".slider-volume"),
buttonFullscreen = document.querySelector(".button-fullscreen"),
buttonInline = document.querySelector(".button-inline"),
buttonPictureInPicture = document.querySelector(
".button-picture-in-picture"
);
buttonPlay.addEventListener("click", function() {
player.play();
});
buttonPause.addEventListener("click", function() {
player.pause();
});
buttonMuted.addEventListener("click", function() {
player.muted = true;
});
buttonUnmuted.addEventListener("click", function() {
player.muted = false;
});
sliderVolume.addEventListener("input", function(e) {
player.volume = e.target.value;
});
buttonFullscreen.addEventListener("click", function() {
openFullscreen();
});
buttonInline.addEventListener("click", function() {
closeFullscreen();
});
var video = element.querySelectorAll("video")[1],
isPictureInPictureEnabled = false;
buttonPictureInPicture.addEventListener("click", function() {
if (!isPictureInPictureEnabled) {
video.requestPictureInPicture();
} else {
document.exitPictureInPicture();
}
});
video.addEventListener("enterpictureinpicture", function() {
isPictureInPictureEnabled = true;
});
video.addEventListener("leavepictureinpicture", function() {
isPictureInPictureEnabled = false;
});
player.addEventListener("play", function(e) {
buttonPlay.classList.toggle("hidden");
buttonPause.classList.toggle("hidden");
});
player.addEventListener("pause", function(e) {
buttonPause.classList.toggle("hidden");
buttonPlay.classList.toggle("hidden");
});
var wasPreviouslyMuted = false;
player.addEventListener("volumechange", function(e) {
if (player.muted != wasPreviouslyMuted) {
wasPreviouslyMuted = player.muted;
buttonMuted.classList.toggle("hidden");
buttonUnmuted.classList.toggle("hidden");
}
});
function openFullscreen() {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
/* Firefox */
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
/* IE/Edge */
element.msRequestFullscreen();
}
}
function closeFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen();
}
}
/**
* Video quality
*/
var selectVideoQualities = document.querySelector(
".select-video-qualities"
);
selectVideoQualities.addEventListener("change", function() {
var value = selectVideoQualities.value;
if (value == "") {
player.videoTracks[0].targetQuality = null;
} else {
player.videoTracks[0].targetQuality =
player.videoTracks[0].qualities[value];
}
});
player.videoTracks.addEventListener("addtrack", function(e) {
e.track.qualities.forEach(function(quality, index) {
var option = document.createElement("option");
option.value = index;
option.innerText = quality.height;
selectVideoQualities.appendChild(option);
});
});
/**
* Audio quality
*/
var selectAudioTrack = document.querySelector(".select-audio-track"),
defaultAudioTrack;
selectAudioTrack.addEventListener("change", function() {
var value = selectAudioTrack.value;
player.audioTracks.forEach(function(track) {
track.enabled = false;
});
if (value == "") {
player.audioTracks[0].enabled = true;
} else {
player.audioTracks[value].enabled = true;
}
});
player.audioTracks.addEventListener("addtrack", function(e) {
var option = document.createElement("option");
option.value = player.audioTracks.indexOf(e.track);
option.innerText = e.track.label;
selectAudioTrack.appendChild(option);
if (e.track.enabled) {
defaultAudioTrack = option.value;
}
});
/**
* Subtitle track
*/
var selectSubtitleTrack = document.querySelector(
".select-subtitle-track"
),
subtitlesContainer = document.querySelector(".subtitles-container");
selectSubtitleTrack.addEventListener("change", function() {
var value = selectSubtitleTrack.value;
player.textTracks.forEach(function(track) {
track.mode = "disabled";
});
if (value == "") {
} else {
player.textTracks[value].mode = "showing";
}
});
player.textTracks.addEventListener("addtrack", function(e) {
var option = document.createElement("option");
option.value = player.textTracks.indexOf(e.track);
option.innerText = e.track.label;
selectSubtitleTrack.appendChild(option);
e.track.addEventListener("addcue", function(e1) {
e1.cue.addEventListener("enter", function(e2) {
subtitlesContainer.innerText = e2.cue.content;
});
e1.cue.addEventListener("exit", function(e2) {
subtitlesContainer.innerText = "";
});
});
});
}
</script>
</body>
</html>

The image below demonstrates the expected result.

3.2.9. Chromecast button

In progress ...

Web SDK

fff

Web SDK - Expand source

3.2.10. Airplay button

In progress ...

Web SDK

fff

Web SDK - Expand source

3.3.Timeline

The timeline consists of a set of controls which allow you to interpret and manipulate the playhead position of the video.

3.3.1. Sub-components

A timeline usually contains the following sub-components:

Web SDK APIAndroid SDK APIiOS SDK API Playhead position: the current time in the timeline. Player API(e.g.player.currentTime) Player API Duration: the duration of the stream. Player API(e.g.player.duration) Player API Seekbar: allows you to change the playhead position. Player API(e.g.player.currentTime/player.duration) Player API Buffered blocks: shows you which parts of the timeline have been added to the buffer. Player API(e.g.player.buffered) Player API

3.3.2. Playhead position

The current playhead position can be requested through the THEOplayer Player API. The Player API exposes a currentTime property (or method) to request the current playhead position.

The playhead position is always changing duration playback (or when seeking to a new playhead position). The Player API exposes a timeupdate event listener which is being dispatched periodically during playback, and is appropriate to update the playhead position field.

Web SDK

A timeline-container has been added to the DOM through HTML, as well as a placeholder for the playhead position. We update this placeholder value in every callback of the timeupdate event through player.currentTime. We also add some CSS.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 50%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 40%;
position: relative;
background: yellow;
opacity: 0.7;
}
.subtitles-container {
height: 30%;
width: 100%;
background: grey;
}
.timeline-container {
height: 30%;
width: 100%;
background: green;
}
.playhead-position {
display: inline-block;
width: 10%;
}
.hidden {
display: none;
}
.theoplayer-container .theoplayer-texttracks {
display: none !important;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="subtitles-container"></div>
<div class="timeline-container">
<div class="playhead-position">0</div>
</div>
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
<select class="select-video-qualities"
><option value="">Auto</option></select
>
<select class="select-audio-track"
><option value="">Default</option></select
>
<select class="select-subtitle-track"
><option value="">Off</option></select
>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector('.theoplayer-container');
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: '//cdn.theoplayer.com/dash/theoplayer/'
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
/**
* Play, Pause, Muted, Unmuted, Volume, Fullscreen, Inline, Picture-in-Picture
* */
...
/**
* Playhead position
*/
var divPlayheadPosition = document.querySelector('.playhead-position');
player.addEventListener('timeupdate', function(e) {
var isLive = (player.duration === Infinity);
if (!isLive) { // if VOD
divPlayheadPosition.innerText = formatSecondsToHHMMSS(player.currentTime);
} else if (player.currentProgramDateTime) { // if stream exposes PDT timestamp
divPlayheadPosition.innerText = formatDate(player.currentProgramDateTime);
} else { // if stream doesn't expose PDT timestamp
divPlayheadPosition.innerText = formatDate(calculateLivePlayheadPosition(player.currentTime, player.seekable.end(player.seekable.length-1)));
}
});
// formate date as HH:MM:SS
function formatDate(date) {
var seconds = (date.getSeconds()<10?'0':'') + date.getSeconds();
var minutes = (date.getMinutes()<10?'0':'') + date.getMinutes();
var hour = (date.getHours()<10?'0':'') + date.getHours();
return hour+':'+minutes+':'+seconds;
}
// format total seconds as HH:MM:SS
function formatSecondsToHHMMSS(seconds) {
var hrs = Math.floor(seconds / 3600);
var min = Math.floor((seconds - (hrs * 3600)) / 60);
var sec = seconds - (hrs * 3600) - (min * 60);
sec = Math.round(Math.round(sec * 100) / 100);
var result = (hrs == 0) ? '' : ((hrs < 10 ? '0' + hrs : hrs)+':');
result += (min < 10 ? '0' + min : min)+':';
result += (sec < 10 ? '0' + sec : sec);
return result;
}
// return and calculate playhead position as a date for livestreams which don't expose PDT timestamp
function calculateLivePlayheadPosition(currentTime, seekableEnd) {
var toDeduct = seekableEnd - currentTime;
var date = new Date();
date.setSeconds(date.getSeconds() - toDeduct);
return date;
}
}
</script>
</body>
</html>

3.3.3. Duration

The duration of the asset can be requested through the THEOplayer Player API. The Player API exposes a duration property (or method) to request the stream's duration.

The Player API exposes a durationchange event listener which is dispatched when the value of the duration changes; but also when the initial duration value is set. This event can be used to update the duration field value at an appropriate moment.

Web SDK

The snippet above is very similar to the playhead-position snippet. We leverage the durationchange event and update through player.duration. We also add some CSS.

Web SDK - Expand source

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Chromeless THEOplayer</title>
<script
type="text/javascript"
src="//cdn.theoplayer.com/dash/theoplayer/THEOplayer.js"
></script>
<style>
.theoplayer-container {
width: 500px;
height: 280px;
background: orange;
position: relative;
}
.controls {
width: 100%;
height: 50%;
position: absolute;
z-index: 2;
bottom: 0;
background: grey;
opacity: 0.7;
}
.control-bar {
width: 100%;
height: 40%;
position: relative;
background: yellow;
opacity: 0.7;
}
.subtitles-container {
height: 30%;
width: 100%;
background: grey;
}
.timeline-container {
height: 30%;
width: 100%;
background: green;
}
.playhead-position {
display: inline-block;
width: 10%;
}
.duration {
display: inline-block;
width: 10%;
}
.hidden {
display: none;
}
.theoplayer-container .theoplayer-texttracks {
display: none !important;
}
</style>
</head>
<body>
<div class="theoplayer-container">
<div class="controls">
<div class="subtitles-container"></div>
<div class="timeline-container">
<div class="playhead-position">0</div>
<div class="duration">0</div>
</div>
<div class="control-bar">
<button class="button-play">Play</button>
<button class="button-pause hidden">Pause</button>
<button class="button-muted">Mute</button>
<button class="button-unmuted hidden">Unmute</button>
<input
class="slider-volume"
type="range"
min="0"
max="1"
step="0.01"
value="1"
/>
<button class="button-fullscreen">Fullscreen</button>
<button class="button-inline">Inline</button>
<button class="button-picture-in-picture">
Toggle Picture-in-Picture
</button>
<select class="select-video-qualities"
><option value="">Auto</option></select
>
<select class="select-audio-track"
><option value="">Default</option></select
>
<select class="select-subtitle-track"
><option value="">Off</option></select
>
</div>
</div>
</div>
<script type="text/javascript">
var element = document.querySelector('.theoplayer-container');
var player = new THEOplayer.ChromelessPlayer(element, {
libraryLocation: '//cdn.theoplayer.com/dash/theoplayer/'
});
registerControlBarComponents();
player.src = "//cdn.theoplayer.com/video/elephants-dream/playlist.m3u8";
function registerControlBarComponents() {
/**
* Play, Pause, Muted, Unmuted, Volume, Fullscreen, Inline, Picture-in-Picture
* */
...
/**
* Playhead position
*/
var divPlayheadPosition = document.querySelector('.playhead-position');
player.addEventListener('timeupdate', function(e) {
var isLive = (player.duration === Infinity);
if (!isLive) { // if VOD
divPlayheadPosition.innerText = formatSecondsToHHMMSS(player.currentTime);
} else if (player.currentProgramDateTime) { // if stream exposes PDT timestamp
divPlayheadPosition.innerText = formatDate(player.currentProgramDateTime);
} else { // if stream doesn't expose PDT timestamp
divPlayheadPosition.innerText = formatDate(calculateLivePlayheadPosition(player.currentTime, player.seekable.end(player.seekable.length-1)));
}
});
// formate date as HH:MM:SS
function formatDate(date) {
var seconds = (date.getSeconds()<10?'0':'') + date.getSeconds();
var minutes = (date.getMinutes()<10?'0':'') + date.getMinutes();
var hour = (date.getHours()<10?'0':'') + date.getHours();
return hour+':'+minutes+':'+seconds;
}
// format total seconds as HH:MM:SS
function formatSecondsToHHMMSS(seconds) {
var hrs = Math.floor(seconds / 3600);
var min = Math.floor((seconds - (hrs * 3600)) / 60);
var sec = seconds - (hrs * 3600) - (min * 60);
sec = Math.round(Math.round(sec * 100) / 100);
var result = (hrs == 0) ? '' : ((hrs < 10 ? '0' + hrs : hrs)+':');
result += (min < 10 ? '0' + min : min)+':';
result += (sec < 10 ? '0' + sec : sec);
return result;
}
// return and calculate playhead position as a date for livestreams which don't expose PDT timestamp
function calculateLivePlayheadPosition(currentTime, seekableEnd) {
var toDeduct = seekableEnd - currentTime;
var date = new Date();
date.setSeconds(date.getSeconds() - toDeduct);
return date;
}
/**
* Duration
*/
var divDuration = document.querySelector('.duration');
player.addEventListener('durationchange', function(e) {
var isLive = (player.duration === Infinity);
if (!isLive) {
divDuration.innerText = formatSecondsToHHMMSS(player.duration);
} else if (player.currentProgramDateTime) {
divDuration.innerText = formatDate(calculateLiveDuration(player.currentTime, player.currentProgramDateTime, player.seekable.end(player.seekable.length-1)));
} else {
divDuration.innerText = player.duration;
}
});
// return and calculate duration as a date for livestreams which expose PDT timestamp
function calculateLiveDuration(currentTime, currentProgramDateTime, seekableEnd) {
var toAdd = seekableEnd - currentTime;
var date = new Date(currentProgramDateTime);
date.setSeconds(date.getSeconds() + toAdd);
return date;
}
}
</script>
</body>
<