Skip to content

Commit

Permalink
Merge pull request #58 from THEOplayer/feature/theolive-ui
Browse files Browse the repository at this point in the history
Feature/theolive UI
  • Loading branch information
MattiasBuelens committed Apr 12, 2024
2 parents 7e0bcf0 + cf995ad commit 12933a4
Show file tree
Hide file tree
Showing 19 changed files with 787 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sidebar_custom_props: { 'icon': '📰' }
## Unreleased

- 💥 **Breaking Change**: This project now requires THEOplayer version 7.0.0 or higher. ([#60](https://github.com/THEOplayer/web-ui/pull/60))
- 🚀 Added `<theolive-default-ui>` that provides a default UI for THEOlive streams. ([#58](https://github.com/THEOplayer/web-ui/pull/58))

## v1.7.2 (2024-03-18)

Expand Down
1 change: 1 addition & 0 deletions react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ sidebar_custom_props: { 'icon': '📰' }
## Unreleased

- 💥 **Breaking Change**: This project now requires THEOplayer version 7.0.0 or higher. ([#60](https://github.com/THEOplayer/web-ui/pull/60))
- 🚀 Added `<THEOliveDefaultUI>` that provides a default UI for THEOlive streams. ([#58](https://github.com/THEOplayer/web-ui/pull/58))

## v1.7.2 (2024-03-18)

Expand Down
60 changes: 60 additions & 0 deletions react/src/THEOliveDefaultUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as React from 'react';
import { type PropsWithoutRef, type ReactNode, useState } from 'react';
import { THEOliveDefaultUI as THEOliveDefaultUIElement } from '@theoplayer/web-ui';
import type { ChromelessPlayer } from 'theoplayer/chromeless';
import { createComponent, type WebComponentProps } from '@lit/react';
import { usePlayer } from './util';
import { PlayerContext } from './context';
import { SlotContainer } from './components';

const RawTHEOliveDefaultUI = createComponent({
tagName: 'theolive-default-ui',
displayName: 'THEOliveDefaultUI',
elementClass: THEOliveDefaultUIElement,
react: React,
events: {
onReady: 'theoplayerready'
} as const
});

export interface THEOliveDefaultUIProps extends PropsWithoutRef<WebComponentProps<THEOliveDefaultUIElement>> {
/**
* A slot for the loading announcement, shown before the publication is loaded.
*/
loadingAnnouncement?: ReactNode;
/**
* A slot for the offline announcement, shown when all publications are offline.
*/
offlineAnnouncement?: ReactNode;
/**
* Use a named slot instead, such as:
* - {@link loadingAnnouncement}
* - {@link offlineAnnouncement}
*/
children?: never;
/**
* Called when the backing player is created.
*
* @param player
*/
onReady?: (player: ChromelessPlayer) => void;
}

/**
* A default UI for THEOlive.
*
* @group Components
*/
export const THEOliveDefaultUI = (props: THEOliveDefaultUIProps) => {
const { loadingAnnouncement, offlineAnnouncement, onReady, ...otherProps } = props;
const [ui, setUi] = useState<THEOliveDefaultUIElement | null>(null);
const player = usePlayer(ui, onReady);
return (
<RawTHEOliveDefaultUI {...otherProps} ref={setUi}>
<PlayerContext.Provider value={player}>
{loadingAnnouncement && <SlotContainer slot="loading-announcement">{loadingAnnouncement}</SlotContainer>}
{offlineAnnouncement && <SlotContainer slot="offline-announcement">{offlineAnnouncement}</SlotContainer>}
</PlayerContext.Provider>
</RawTHEOliveDefaultUI>
);
};
1 change: 1 addition & 0 deletions react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { PlayerContext } from './context';
export * from './UIContainer';
export * from './DefaultUI';
export * from './THEOliveDefaultUI';
export * from './components/index';
export * from './hooks/index';
export * from './version';
20 changes: 20 additions & 0 deletions react/test/ssr.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,24 @@ describe('Server-side rendering (SSR)', () => {
'</theoplayer-ui>';
assert.equal(actual, expected);
});

it('can render <THEOliveDefaultUI> to string', async () => {
const { THEOliveDefaultUI, PlayButton, TimeRange } = await import('@theoplayer/react-ui');
const actual = renderToString(
React.createElement(THEOliveDefaultUI, {
// Properties are ignored during SSR
configuration: { libraryLocation: 'foo', license: 'bar' },
onReady: () => console.log('ready!'),
// Slots are inserted as elements
loadingAnnouncement: 'Loading',
offlineAnnouncement: 'Offline'
})
);
const expected =
'<theolive-default-ui>' +
'<theoplayer-slot-container slot="loading-announcement">Loading</theoplayer-slot-container>' +
'<theoplayer-slot-container slot="offline-announcement">Offline</theoplayer-slot-container>' +
'</theolive-default-ui>';
assert.equal(actual, expected);
});
});
34 changes: 22 additions & 12 deletions src/DefaultUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export class DefaultUI extends HTMLElement {
];
}

private readonly _ui: UIContainer;
private readonly _titleSlot: HTMLSlotElement;
private readonly _timeRange: TimeRange;
protected readonly _shadowRoot: ShadowRoot;
protected readonly _ui: UIContainer;
private readonly _titleSlot: HTMLSlotElement | undefined;
private readonly _timeRange: TimeRange | undefined;
private _appliedExtensions: boolean = false;

/**
Expand All @@ -101,18 +102,17 @@ export class DefaultUI extends HTMLElement {
*/
constructor(configuration: PlayerConfiguration = {}) {
super();
const shadowRoot = this.attachShadow({ mode: 'open', delegatesFocus: true });
shadowRoot.appendChild(template().content.cloneNode(true));
this._shadowRoot = this.initShadowRoot();

this._ui = shadowRoot.querySelector('theoplayer-ui')!;
this._ui = this._shadowRoot.querySelector('theoplayer-ui')!;
this._ui.addEventListener(READY_EVENT, this._dispatchReadyEvent);
this._ui.addEventListener(STREAM_TYPE_CHANGE_EVENT, this._updateStreamType);
this.setConfiguration_(configuration);

this._titleSlot = shadowRoot.querySelector('slot[name="title"]')!;
this._titleSlot.addEventListener('slotchange', this._onTitleSlotChange);
this._titleSlot = this._shadowRoot.querySelector<HTMLSlotElement>('slot[name="title"]') ?? undefined;
this._titleSlot?.addEventListener('slotchange', this._onTitleSlotChange);

this._timeRange = shadowRoot.querySelector('theoplayer-time-range')!;
this._timeRange = this._shadowRoot.querySelector('theoplayer-time-range') ?? undefined;

this._upgradeProperty('configuration');
this._upgradeProperty('source');
Expand All @@ -124,6 +124,12 @@ export class DefaultUI extends HTMLElement {
this._upgradeProperty('dvrThreshold');
}

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

private _upgradeProperty(prop: keyof this) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
Expand Down Expand Up @@ -304,16 +310,20 @@ export class DefaultUI extends HTMLElement {

private readonly _updateStreamType = () => {
this.setAttribute(Attribute.STREAM_TYPE, this.streamType);
// Hide seekbar when stream is live with no DVR
toggleAttribute(this._timeRange, Attribute.HIDDEN, this.streamType === 'live');
if (this._timeRange) {
// Hide seekbar when stream is live with no DVR
toggleAttribute(this._timeRange, Attribute.HIDDEN, this.streamType === 'live');
}
};

private readonly _dispatchReadyEvent = () => {
this.dispatchEvent(createCustomEvent(READY_EVENT));
};

private readonly _onTitleSlotChange = () => {
toggleAttribute(this, Attribute.HAS_TITLE, this._titleSlot.assignedNodes().length > 0);
if (this._titleSlot) {
toggleAttribute(this, Attribute.HAS_TITLE, this._titleSlot.assignedNodes().length > 0);
}
};
}

Expand Down
213 changes: 213 additions & 0 deletions src/THEOliveDefaultUI.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
: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='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, inline-flex);
}
Loading

0 comments on commit 12933a4

Please sign in to comment.