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

Setup reference examples for video blog #122

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions plugins/aem-assets-plugin/blocks/video/video.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ main>.section.video-container {
.video-hero video,
.video-hero .vjs-poster img {
object-fit: cover;
width: 100%;
height: 100%;
}

.custom-play-button {
Expand Down
141 changes: 121 additions & 20 deletions plugins/aem-assets-plugin/blocks/video/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ const VIDEO_JS_SCRIPT = '/blocks/video/videojs/video.min.js';
const VIDEO_JS_CSS = '/blocks/video/videojs/video-js.min.css';
const VIDEO_JS_LOAD_EVENT = 'videojs-loaded';

function getDeviceSpecificVideoUrl(videoUrl) {
function getDeviceSpecificVideoUrl(videoUrl, isProgressive) {
if (isProgressive) {
return videoUrl.replace(/manifest\.mpd|manifest\.m3u8|play/, 'original/as/video.mp4');
}

const { userAgent } = navigator;
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
const isSafari = (/Safari/i).test(userAgent) && !(/Chrome/i).test(userAgent) && !(/CriOs/i).test(userAgent) && !(/Android/i).test(userAgent) && !(/Edg/i).test(userAgent);
Expand All @@ -15,7 +19,8 @@ function getDeviceSpecificVideoUrl(videoUrl) {

function parseConfig(block) {
const isAutoPlay = block.classList.contains('autoplay');

const isProgressive = block.classList.contains('progressive');
const isHighestBitrate = block.classList.contains('highestbitrate');
if (block.classList.contains('hero')) {
const posterImage = block.querySelector('picture');
const videoUrl = block.querySelector('div > div:first-child a').href;
Expand All @@ -25,12 +30,13 @@ function parseConfig(block) {

return {
type: 'hero',
videoUrl: getDeviceSpecificVideoUrl(videoUrl),
videoUrl: getDeviceSpecificVideoUrl(videoUrl, isProgressive),
isAutoPlay,
title,
description,
button,
posterImage,
highestBitrate: isHighestBitrate,
};
}

Expand All @@ -42,17 +48,19 @@ function parseConfig(block) {
const description = child.querySelector('div:nth-child(2) > p')?.textContent;

return {
videoUrl: getDeviceSpecificVideoUrl(videoUrl),
videoUrl: getDeviceSpecificVideoUrl(videoUrl, isProgressive),
isAutoPlay,
title,
description,
posterImage,
highestBitrate: isHighestBitrate,
};
});

return {
type: 'cards',
cards,
highestBitrate: isHighestBitrate,
};
}

Expand All @@ -61,8 +69,9 @@ function parseConfig(block) {

return {
type: 'modal',
videoUrl: getDeviceSpecificVideoUrl(videoUrl),
videoUrl: getDeviceSpecificVideoUrl(videoUrl, isProgressive),
posterImage,
highestBitrate: isHighestBitrate,
};
}

Expand All @@ -73,6 +82,8 @@ function getVideojsScripts() {
};
}

// This function waits for the videojs script and css to be loaded
// and returns a promise which resolves when both are loaded
async function waitForVideoJs() {
return new Promise((resolve) => {
const { scriptTag, cssLink } = getVideojsScripts();
Expand All @@ -92,17 +103,23 @@ async function waitForVideoJs() {
}

async function loadVideoJs() {
// If videojs script and css html tags
// are already present but not loaded, wait for them to be loaded and return
const { scriptTag, cssLink } = getVideojsScripts();
if (scriptTag && cssLink) {
await waitForVideoJs();
return;
}

// We are here means that the videojs script and css html tags
// are not present in the head, so we need to load them
await Promise.all([
loadCSS(VIDEO_JS_CSS),
loadScript(VIDEO_JS_SCRIPT),
]);

// Once the script and css are loaded, set the dataset.loaded attribute
// to true and dispatch a custom event to notify that videojs is loaded
const { scriptTag: jsScript, cssLink: css } = getVideojsScripts();
jsScript.dataset.loaded = true;
css.dataset.loaded = true;
Expand Down Expand Up @@ -205,6 +222,31 @@ function setupAutopause(videoElement, player) {
observer.observe(videoElement);
}

function setupProgressiveVideo(url, videoContainer, config) {
const videoElement = document.createElement('video');

// Add basic attributes
videoElement.setAttribute('loop', '');
videoElement.setAttribute('playsinline', '');
videoElement.removeAttribute('controls');
videoElement.addEventListener('canplay', () => {
videoElement.muted = true;
if (config.autoplay) videoElement.play();
});

// Set source
videoElement.src = url;

// Add poster if available
if (config.poster) {
videoElement.poster = getPosterImage(config.poster);
}

videoContainer.append(videoElement);

return videoElement;
}

function setupPlayer(url, videoContainer, config) {
const videoElement = document.createElement('video');
videoElement.classList.add('video-js');
Expand All @@ -219,15 +261,39 @@ function setupPlayer(url, videoContainer, config) {
const videojsConfig = {
...config,
preload: poster && !config.autoplay ? 'none' : 'auto',
poster,
};

if (poster) {
videojsConfig.poster = poster;
}

if (!videojsConfig.posterImage) {
delete videojsConfig.posterImage;
}

if (config.autoplay) {
videojsConfig.muted = true;
videojsConfig.loop = true;
videojsConfig.autoplay = true;
}

videojsConfig.html5 = {
vhs: {
useDevicePixelRatio: true,
customPixelRatio: window.devicePixelRatio ?? 1,
// Start with highest quality for highestbitrate class
limitRenditionByPlayerDimensions: !config.highestBitrate,
// Select highest bitrate variant
selectionCallback: config.highestBitrate
? (playlistController, representations) => representations.reduce((highest, current) => (
(!highest || current.bandwidth > highest.bandwidth)
? current
: highest
))
: undefined,
},
};

// eslint-disable-next-line no-undef
const player = videojs(videoElement, videojsConfig);
player.src(url);
Expand All @@ -250,6 +316,22 @@ async function decorateVideoPlayer(url, videoContainer, config) {
videoContainer.append(config.posterImage);
}

// Check if progressive playback is requested
if (config.progressive) {
const player = setupProgressiveVideo(url, videoContainer, config);
player.addEventListener('loadeddata', () => {
const posterImage = videoContainer.querySelector('picture');
if (posterImage) {
posterImage.style.display = 'none';
setTimeout(() => {
player.play();
}, 1000);
}
});
return;
}

// Existing VideoJS implementation
await waitForVideoJs();
const player = setupPlayer(url, videoContainer, config);
player.on('loadeddata', () => {
Expand Down Expand Up @@ -296,6 +378,8 @@ async function decorateVideoCard(container, config) {
hasCustomPlayButton: true,
fill: true,
posterImage: config.posterImage,
progressive: config.progressive,
highestBitrate: config.highestBitrate,
});
}

Expand Down Expand Up @@ -335,6 +419,8 @@ async function decorateHeroBlock(block, config) {
hasCustomPlayButton: true,
fill: true,
posterImage: config.posterImage,
progressive: config.progressive,
highestBitrate: config.highestBitrate,
});
}

Expand Down Expand Up @@ -442,7 +528,6 @@ async function decorateVideoModal(block, config) {
const container = document.createElement('div');
container.classList.add('video-component');

const posterImage = config.posterImage.cloneNode(true);
const playButton = document.createElement('button');
playButton.setAttribute('aria-label', 'Play video');
playButton.classList.add('video-play-button');
Expand All @@ -457,9 +542,13 @@ async function decorateVideoModal(block, config) {
await openModal(config);
});

container.append(posterImage);
container.append(playButton);

if (config.posterImage) {
const posterImage = config.posterImage.cloneNode(true);
container.append(posterImage);
}

block.innerHTML = '';
block.append(container);

Expand All @@ -470,23 +559,35 @@ async function decorateVideoModal(block, config) {
}

export default async function decorate(block) {
if (typeof window.DELAYED_PHASE !== 'undefined') {
// DELAYED_PHASE is defined, so hook to delayed-phase
if (window.DELAYED_PHASE) {
loadVideoJs();
// Check for progressive class
const isProgressive = block.classList.contains('progressive');

if (!isProgressive) {
let vjsLoadingPhase;
if (block.classList.contains('lazy')) {
vjsLoadingPhase = 'lazy';
} else if (block.classList.contains('delayed')) {
vjsLoadingPhase = 'delayed';
}

if (vjsLoadingPhase) {
if (window.LOADING_PHASE === vjsLoadingPhase) {
loadVideoJs();
} else {
const vjsLoadingPhaseHandler = async () => {
document.removeEventListener(`${vjsLoadingPhase}-phase`, vjsLoadingPhaseHandler);
await loadVideoJs();
};
document.addEventListener(`${vjsLoadingPhase}-phase`, vjsLoadingPhaseHandler);
}
} else {
const delayedPhaseHandler = async () => {
document.removeEventListener('delayed-phase', delayedPhaseHandler);
await loadVideoJs();
};
document.addEventListener('delayed-phase', delayedPhaseHandler);
await loadVideoJs();
}
} else {
// DELAYED_PHASE is not defined, so don't hook to delayed-phase event
setTimeout(loadVideoJs, 3000);
}

const config = parseConfig(block);
// Add progressive flag to config
config.progressive = isProgressive;

if (config.type === 'hero') {
await decorateHeroBlock(block, config);
Expand Down
2 changes: 1 addition & 1 deletion scripts/delayed.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
document.dispatchEvent(new Event('delayed-phase'));
Window.DELAYED_PHASE = true;
Window.LOADING_PHASE = 'delayed';
// add delayed functionality here
8 changes: 7 additions & 1 deletion scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ async function loadEager(doc) {
decorateMain(main);
document.body.classList.add('appear');
await loadSection(main.querySelector('.section'), waitForFirstImage);
// Artificial Delay eager loading of the page for 1 seconds - for demo only
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}

sampleRUM.enhance();
Expand All @@ -213,6 +217,8 @@ async function loadEager(doc) {
* @param {Element} doc The container element
*/
async function loadLazy(doc) {
document.dispatchEvent(new Event('lazy-phase'));
window.LOADING_PHASE = 'lazy';
const main = doc.querySelector('main');
await loadSections(main);

Expand All @@ -233,7 +239,7 @@ async function loadLazy(doc) {
*/
function loadDelayed() {
// eslint-disable-next-line import/no-cycle
window.setTimeout(() => import('./delayed.js'), 3000);
window.setTimeout(() => import('./delayed.js'), 5000);
// load anything that can be postponed to the latest here
}

Expand Down