Skip to content

Commit

Permalink
Add THEOlive UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jeroen-tempels committed Mar 22, 2024
1 parent f273e22 commit c2522a5
Show file tree
Hide file tree
Showing 15 changed files with 771 additions and 0 deletions.
228 changes: 228 additions & 0 deletions src/THEOliveUI.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
:host {
box-sizing: border-box;
position: relative;
display: inline-block;
width: 100%;
font-family: Helvetica, Arial, sans-serif;
}

:host([hidden]) {
display: none !important;
}

theoplayer-ui {
font-family: Helvetica, Arial, sans-serif;
width: 100%;
--theoplayer-loading-delay: 0.1s;
}

:host(:fullscreen),
:host(:fullscreen) theoplayer-ui {
width: 100% !important;
height: 100% !important;
}

theoplayer-menu::part(heading) {
display: none !important;
}

[part='title'] {
user-select: none;
color: var(--theoplayer-text-color, #fff);
text-shadow: 0 0 4px rgba(0, 0, 0, 0.75);
padding: var(--theoplayer-control-padding, 10px);

/* Vertically center any text */
font-size: var(--theoplayer-text-font-size, 14px);
line-height: var(--theoplayer-text-content-height, var(--theoplayer-control-height, 24px));
}

:host(:not([has-title])) [part='title'] {
display: none;
}

[part='centered-chrome'] * {
--theoplayer-control-height: 48px;
}

[part='middle-chrome'] {
position: relative;
display: flex;
flex-flow: column nowrap;
/* Align to bottom for Chromecast display */
justify-content: flex-end;
flex-grow: 1;
pointer-events: none;
}

[part='bottom-chrome'] {
position: relative;
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}

/*
* On mobile, put a backdrop color on the entire player when showing controls.
*/
:host([mobile]) theoplayer-ui {
--theoplayer-control-backdrop-background: rgba(0, 0, 0, 0.5);
}

/*
* On desktop, put a soft gradient behind the top and bottom control bars.
*/
:host {
/*
* Smooth transparent-to-black gradient from Chrome's <video> controls.
* See: https://bugs.chromium.org/p/chromium/issues/detail?id=1404684
*/
/* prettier-ignore */
--theoplayer-control-background-gradient-stops: rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.01) 8.1%,
rgba(0, 0, 0, 0.037) 15.5%,
rgba(0, 0, 0, 0.078) 22.5%,
rgba(0, 0, 0, 0.131) 29%,
rgba(0, 0, 0, 0.195) 35.3%,
rgba(0, 0, 0, 0.264) 41.2%,
rgba(0, 0, 0, 0.337) 47.1%,
rgba(0, 0, 0, 0.413) 52.9%,
rgba(0, 0, 0, 0.486) 58.8%,
rgba(0, 0, 0, 0.555) 64.7%,
rgba(0, 0, 0, 0.619) 71%,
rgba(0, 0, 0, 0.672) 77.5%,
rgba(0, 0, 0, 0.713) 84.5%,
rgba(0, 0, 0, 0.74) 91.9%,
rgba(0, 0, 0, 0.75) 100%;
}

:host(:not([mobile])) [part='top-chrome']::before,
:host(:not([mobile])) [part='bottom-chrome']::before {
content: '';
display: block;
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
}

:host(:not([mobile])) [part='top-chrome']::before {
background: linear-gradient(to top, var(--theoplayer-control-background-gradient-stops));
}

:host(:not([mobile])) [part='bottom-chrome']::before {
background: linear-gradient(to bottom, var(--theoplayer-control-background-gradient-stops));
}

.theoplayer-spacer {
flex-grow: 1;
}

theoplayer-time-range {
--theoplayer-control-height: 12px;
--theoplayer-range-track-pointer-background: rgba(255, 255, 255, 0.5);
}

/*
* Mobile-only and mobile-hidden elements
*/
:host([mobile]) [mobile-hidden],
:host(:not([mobile])) [mobile-only] {
display: none !important;
}

/*
* Live-only and live-hidden elements
*/
:host(:not([stream-type='vod'])) [live-hidden],
:host(:not([stream-type='vod'])) theoplayer-control-bar ::slotted([live-hidden]),
:host([stream-type='vod']) [live-only],
:host([stream-type='vod']) theoplayer-control-bar ::slotted([live-only]) {
display: none !important;
}

/*
* Ad-only and ad-hidden elements
*/
theoplayer-ui[playing-ad] [ad-hidden],
theoplayer-ui:not([playing-ad]) [ad-only] {
display: none !important;
}

/*
* Hide all controls before first play, except for the center play button
*/
theoplayer-ui:not([has-first-play]) theoplayer-control-bar,
theoplayer-ui:not([has-first-play]) [part='centered-chrome'] :not(theoplayer-play-button) {
display: none !important;
}

/*
* Hide center play button on desktop after first play
*/
:host(:not([mobile])) theoplayer-ui[has-first-play] [part='centered-chrome'] theoplayer-play-button {
display: none !important;
}

theoplayer-volume-range {
--theoplayer-range-padding-left: 0;
}

theoplayer-mute-button + theoplayer-volume-range {
width: 0;
overflow: hidden;
--theoplayer-range-padding-right: 0;

/* Set the internal width so it reveals, not grows */
--theoplayer-range-track-width: 70px;
transition: width 0.2s ease-in;
}

/* Expand volume control in all relevant states */
theoplayer-mute-button:hover + theoplayer-volume-range,
theoplayer-mute-button:focus + theoplayer-volume-range,
theoplayer-mute-button + theoplayer-volume-range:hover,
theoplayer-mute-button + theoplayer-volume-range:focus {
width: 70px;
}

/* IE doesn't support :focus-within, so keep these separate (and use a polyfill?) */
theoplayer-mute-button:focus-within + theoplayer-volume-range,
theoplayer-mute-button + theoplayer-volume-range:focus-within {
width: 70px;
}

/* Reduce space between live button and remaining time display */
theoplayer-live-button + theoplayer-time-display {
padding-left: 0;
}

/* Hide remaining time display when playing at live edge */
theoplayer-live-button[live] + theoplayer-time-display {
display: none !important;
}

p {
color: var(--theoplayer-text-color, #fff);
font-size: var(--theoplayer-text-font-size, 20px);
}

#loading-announcement {
display: none;
}

#offline-announcement {
display: none;
}

#announcement {
display: none;
}

theolive-bad-network-button {
display: var(--theolive-bad-network-button-display, none);
}

theoplayer-settings-menu-button {
display: var(--theolive-quality-button-display, flex);
}
30 changes: 30 additions & 0 deletions src/THEOliveUI.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<theoplayer-ui>
<theolive-logo no-auto-hide slot="top-chrome" part="logo"></theolive-logo>
<p id="loading-announcement" no-auto-hide slot="centered-chrome"><slot name="loading-announcement">Loading...</slot></p>
<p id="offline-announcement" no-auto-hide slot="centered-chrome"><slot name="offline-announcement">The live stream hasn't started yet</slot></p>
<p id="announcement" no-auto-hide slot="centered-chrome"></p>
<theolive-watermark no-auto-hide slot="centered-chrome" part="watermark"></theolive-watermark>
<theoplayer-loading-indicator slot="centered-loading" no-auto-hide part="loading-indicator"></theoplayer-loading-indicator>
<div slot="centered-chrome" part="centered-chrome">
<theoplayer-play-button part="center-play-button"></theoplayer-play-button>
</div>
<div part="bottom-chrome">
<theoplayer-control-bar>
<theoplayer-play-button mobile-hidden part="play-button"></theoplayer-play-button>
<theoplayer-mute-button part="mute-button"></theoplayer-mute-button>
<theoplayer-volume-range mobile-hidden part="volume-range"></theoplayer-volume-range>
<theoplayer-live-button ad-hidden live-only part="live-button"></theoplayer-live-button>
<span class="theoplayer-spacer"></span>
<theoplayer-settings-menu-button ad-hidden menu="all-quality-menu" part="quality-button"></theoplayer-settings-menu-button>
<theolive-bad-network-button ad-hidden menu="quality-menu" part="theolive-bad-network-button"></theolive-bad-network-button>
<theoplayer-fullscreen-button part="fullscreen-button"></theoplayer-fullscreen-button>
</theoplayer-control-bar>
</div>
<theoplayer-menu id="quality-menu" slot="menu" menu-close-on-input hidden>
<theolive-bad-network-menu></theolive-bad-network-menu>
</theoplayer-menu>
<theoplayer-menu id="all-quality-menu" slot="menu" menu-close-on-input hidden>
<theoplayer-quality-radio-group></theoplayer-quality-radio-group>
</theoplayer-menu>
<theoplayer-error-display slot="error" part="error-display"></theoplayer-error-display>
</theoplayer-ui>
116 changes: 116 additions & 0 deletions src/THEOliveUI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import "./components/theolive/Logo";
import "./components/theolive/quality/BadNetworkModeButton";
import "./components/theolive/quality/BadNetworkModeMenu";
import css from './THEOliveUI.css'
import html from './THEOliveUI.html';
import type {ErrorEvent, PlayerConfiguration} from "theoplayer/chromeless";
import {DefaultUI} from "./DefaultUI";
import {READY_EVENT} from "./events/ReadyEvent";
import {ErrorDisplay, PlayButton} from "./components";

const template = document.createElement('template');
template.innerHTML = `<style>${css}</style>${html}`;

export class THEOLiveUI extends DefaultUI {
private readonly _loading: HTMLParagraphElement;
private readonly _offline: HTMLParagraphElement;
private readonly _announcement: HTMLParagraphElement;
private readonly _errorDisplay: ErrorDisplay;
private readonly _playButton: PlayButton;
private readonly _root: HTMLElement;

constructor(configuration: PlayerConfiguration = {}) {
super(configuration);
this._loading = this._shadowRoot.querySelector<HTMLParagraphElement>("#loading-announcement")!;
this._offline = this._shadowRoot.querySelector<HTMLParagraphElement>("#offline-announcement")!;
this._announcement = this._shadowRoot.querySelector<HTMLParagraphElement>("#announcement")!;
this._errorDisplay = this._shadowRoot.querySelector<ErrorDisplay>("theoplayer-error-display")!;
this._playButton = this._shadowRoot.querySelector<PlayButton>("theoplayer-play-button")!;
this._root = this._shadowRoot.querySelector<HTMLElement>('theoplayer-ui')!

this._ui.addEventListener(READY_EVENT, this.onReady);
}

protected initShadowRoot(): ShadowRoot {
const shadowRoot = this.attachShadow({mode: 'open', delegatesFocus: true});
shadowRoot.appendChild(template.content.cloneNode(true));
return shadowRoot;
}

private readonly onReady = () => {
this._ui.removeEventListener(READY_EVENT, this.onReady);
const player = this.player;
if (player) {
player.theolive?.addEventListener('loadchannelstart', this.onLoadChannelStart);

Check failure on line 44 in src/THEOliveUI.ts

View workflow job for this annotation

GitHub Actions / build (20)

Property 'theolive' does not exist on type 'ChromelessPlayer'.
player.theolive?.addEventListener('channeloffline', this.onChannelOffline);

Check failure on line 45 in src/THEOliveUI.ts

View workflow job for this annotation

GitHub Actions / build (20)

Property 'theolive' does not exist on type 'ChromelessPlayer'.
player.theolive?.addEventListener('channelloaded', this.onChannelLoaded);

Check failure on line 46 in src/THEOliveUI.ts

View workflow job for this annotation

GitHub Actions / build (20)

Property 'theolive' does not exist on type 'ChromelessPlayer'.
player.addEventListener('error', this.onError);
}
}

private onLoadChannelStart = () => {
this.showMessage_('loading', undefined);
};

private onChannelOffline = () => {
this.showMessage_('offline', undefined);
};

private onChannelLoaded = () => {
this.hidePlayerError();
this.hideMessage_();
}

private onError = (e: ErrorEvent) => {
const errorCode = e.errorObject.code
if (errorCode < 13_000 || errorCode >= 14_000) {
this.showMessage_('offline', undefined);
return;
}
this.stopHidingPlayerError();
this.hideMessage_();
};

private hidePlayerError(): void {
this._root.removeAttribute('has-error');
this._errorDisplay.style.display = "none";
}

private stopHidingPlayerError(): void {
this._root.setAttribute('has-error', '');
this._errorDisplay.style.display = "flex";
}

private hidePlayerPlayButton_(): void {
this._playButton.style.display = "none";
}

private stopHidingPlayerPlayButton(): void {
this._playButton.style.display = "inline-flex";
}

private showMessage_(type: 'offline' | 'loading' | 'announcement', text: string | undefined): void {
this.hidePlayerError();
this._loading.style.display = 'none';
this._offline.style.display = 'none';
this._announcement.style.display = 'none';
if (type === 'loading') {
this._loading.style.display = 'block';
} else if (type === 'offline') {
this._offline.style.display = 'block';
} else {
this._announcement.textContent = text ?? '';
this._announcement.style.display = 'block';
}
this.hidePlayerPlayButton_();
}

private hideMessage_(): void {
this._loading.style.display = 'none';
this._offline.style.display = 'none';
this._announcement.style.display = 'none';
this.stopHidingPlayerPlayButton();
}
}

customElements.define('theo-live-default-ui', THEOLiveUI);
1 change: 1 addition & 0 deletions src/UIContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export class UIContainer extends HTMLElement {
this._player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updateStreamType);
this._player.addEventListener('ratechange', this._updatePlaybackRate);
this._player.addEventListener('sourcechange', this._onSourceChange);
this._player.theolive?.addEventListener('loadchannelstart', this._onSourceChange);
this._player.videoTracks.addEventListener(['addtrack', 'removetrack', 'change'], this._updateActiveVideoTrack);
this._player.cast?.addEventListener('castingchange', this._updateCasting);
this._player.addEventListener(['durationchange', 'sourcechange', 'emptied'], this._updatePlayingAd);
Expand Down
Loading

0 comments on commit c2522a5

Please sign in to comment.