Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a countdown timer example to the demo #48

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/layout/content/showcase/showcase-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import rawDisplayCurrentChapterExample from '../../../../static/showcases/chapte
import rawSkipCreditsExample from '../../../../static/showcases/skip-credits.html?raw';
import rawPlaylistExample from '../../../../static/showcases/playlist.html?raw';
import rawqualityMenuExample from '../../../../static/showcases/quality-menu.html?raw';
import rawCountdown from '../../../../static/showcases/countdown.html?raw';
import { getTextFromHTML } from './example-parser.js';

const startTimeExampleTxt = getTextFromHTML(rawStartTimeExample);
Expand All @@ -22,6 +23,7 @@ const displayCurrentChapterExampleTxt =
const skipCreditsExampleTxt = getTextFromHTML(rawSkipCreditsExample);
const playlistExampleTxt = getTextFromHTML(rawPlaylistExample);
const qualityMenuExampleTxt = getTextFromHTML(rawqualityMenuExample);
const countdownExampleTxt = getTextFromHTML(rawCountdown);

export class ShowCasePage extends LitElement {
static styles = [theme, animations, unsafeCSS(showcasePageCss)];
Expand All @@ -35,6 +37,7 @@ export class ShowCasePage extends LitElement {
${this.#renderSkipCredits()}
${this.#renderPlaylist()}
${this.#renderQualityMenu()}
${this.#renderCountdown()}
`;
}

Expand Down Expand Up @@ -149,7 +152,7 @@ export class ShowCasePage extends LitElement {
<showcase-component href="playlist.html">
<h2 slot="title">Playlist</h2>
<p slot="description">
This example shows how to fetch media data for a set of video sources
This example shows how to fetch media data for a set of video sources
and load them into the <a href="https://github.com/SRGSSR/pillarbox-web-suite/tree/main/packages/pillarbox-playlist#readme" target="_blank">Pillarbox Playlist plugin</a>
with metadata such as title and duration.
</p>
Expand Down Expand Up @@ -181,6 +184,24 @@ export class ShowCasePage extends LitElement {
</div>
`;
}

#renderCountdown() {
return html`
<div class="fade-in"
@animationend="${e => e.target.classList.remove('fade-in')}">
<showcase-component href="countdown.html">
<h2 slot="title">Countdown Timer</h2>
<p slot="description">
In this showcase, we'll demonstrate how to display a countdown timer.
</p>
<code-block slot="code" language="javascript">${countdownExampleTxt}</code-block>
</showcase-component>
<a part="showcase-link" href="./static/showcases/countdown.html" target="_blank">
Open this showcase
</a>
</div>
`;
}
}

customElements.define('showcase-page', ShowCasePage);
Expand Down
226 changes: 226 additions & 0 deletions static/showcases/countdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pillarbox Demo - Display Countdown</title>
<link rel="icon" type="image/x-icon" href="../../img/favicon.ico">
<link rel="stylesheet" href="./countdown.scss" />
</head>

<body>
<core-demo-header></core-demo-header>
<div class="showcase-content">
<h2>Display Countdown</h2>
<div class="video-container">
<video-js id="video-element-id" class="pillarbox-js" controls></video-js>
</div>

<button class="showcase-btn" id="close-btn">Close this window</button>
</div>

<script type="module" data-implementation>
// Import the pillarbox library
import pillarbox from '@srgssr/pillarbox-web';

const ModalDialog = pillarbox.getComponent('ModalDialog');

/**
* Provides a countdown timer functionality within a modal dialog.
*/
class Countdown extends ModalDialog {
/**
* Creates an instance of Countdown.
*
* @param {Player} player The video.js player instance
* @param {Object} options Configuration options for the modal dialog
*/
constructor(player, options) {
const opts = pillarbox.obj.merge(options, {
pauseOnOpen: false,
fillAlways: true,
temporary: false,
uncloseable: true
});

super(player, opts);

this.intervalId = undefined;
this.reset = this.reset.bind(this);

this.on(player, ['loadstart', 'playerreset', 'dispose', 'error'], this.reset);
}

/**
* The CSS class name for the countdown modal dialog.
*
* @returns {string} The CSS class name
*/
buildCSSClass() {
return `pillarbox-countdown ${super.buildCSSClass()}`;
}

/**
* Disposes of the countdown component.
*
* Cleans up any resources and event listeners.
*/
dispose() {
this.reset();
this.off(this.player(), ['loadstart', 'playerreset', 'dispose', 'error'], this.reset);

super.dispose();
}

/**
* Resets the countdown timer.
*
* Clears the interval, closes the modal, and empties its content.
*/
reset() {
this.clearInterval(this.intervalId);
this.close();
this.empty();

this.intervalId = undefined;
}

/**
* Starts the countdown timer.
*
* @param {number} timestamp The target timestamp in milliseconds
* @param {string} source The source to play when the countdown ends
*
* @returns {boolean} True if the countdown started successfully
*/
start(timestamp, source) {
this.reset();

if (typeof timestamp !== 'number') return;

this.intervalId = this.setInterval(() => {
const remainingDuration = this.remainingDuration(timestamp);

if (remainingDuration.totalInMilliseconds <= 0) {
this.reset();
this.player().src(source);

return;
}

this.fillWith(`${remainingDuration.days
}d ${remainingDuration.hours
}h ${remainingDuration.minutes
}m ${remainingDuration.seconds
}s remaining`);
}, 1_000);

this.open();

return true;
}

/**
* The remaining duration until the target timestamp.
*
* @param {number} timestamp The target timestamp in milliseconds
*
* @returns {Object} An object containing the remaining days, hours, minutes, seconds, and total in milliseconds
*/
remainingDuration(timestamp) {
const SECONDS_IN_MS = 1_000;
const MINUTES = 60 * SECONDS_IN_MS;
const HOURS = 60 * MINUTES;
const DAYS = 24 * HOURS;
const totalInMilliseconds = timestamp - Date.now();

let diff = totalInMilliseconds;
// Calculate days, hours, minutes, and seconds
let days = Math.floor(diff / DAYS);
diff -= days * DAYS;

let hours = Math.floor(diff / HOURS);
diff -= hours * HOURS;

let minutes = Math.floor(diff / MINUTES);
diff -= minutes * MINUTES;

let seconds = Math.floor(diff / SECONDS_IN_MS);

return {
days: days.toString().padStart(2, '0'),
hours: hours.toString().padStart(2, '0'),
minutes: minutes.toString().padStart(2, '0'),
seconds: seconds.toString().padStart(2, '0'),
totalInMilliseconds
}
}
}

// Register Countdown component
pillarbox.registerComponent('Countdown', Countdown);

// Create a pillarbox player instance with the currentChapter plugin
const player = pillarbox(
'video-element-id',
{
fill: true,
// Add the countdown component to the player
countdown: true,
}
);

// Listen for player errors
player.on('error', () => {
// Get the metadata associated with the error
const { metadata = {} } = player.error() || {};

// If the error is not of type STARTDATE we do nothing
if (metadata.errorType !== 'STARTDATE') return;

// Extract the validFrom property from the mediaData
const {
src: {
mediaData: {
chapters: [{ validFrom } = {}] = []
} = {}
} = {}
} = metadata;
const timestamp = new Date(validFrom).getTime();

if (!player.countdown.start(timestamp, player.currentSource())) return;

// Closes error display component to prevent overlapping
if (player.errorDisplay && player.errorDisplay.opened()) {
player.errorDisplay.close();
}
});

// In the absence of media generating a STARTDATE error, this function
// manually activates the countdown
player.countdown.start(Date.now() + 5_977_235_000, { src: 'urn:rts:video:10894383', type: 'srgssr/urn' });

// If a media sends a STARTDATE error, the countdown is automatically
// activated, since the logic for activating the countdown is handled by
// the error event handler
// player.src({ src: 'urn:rts:video:10894383', type: 'srgssr/urn' });

window.player = player;
</script>

<script type="module">
import pillarbox from '@srgssr/pillarbox-web';
import '../../src/layout/header/core-demo-header-component.js';

document.querySelector('#close-btn').addEventListener('click', () => {
window.close();
});

window.pillarbox = pillarbox;
</script>

</body>

</html>
11 changes: 11 additions & 0 deletions static/showcases/countdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import './static-showcase';


.pillarbox-countdown .vjs-modal-dialog-content {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 2em;
text-align: center;
}
Loading