feat: Add the sprites functionality

This commit is contained in:
Yiannis Christodoulou 2025-07-21 18:57:05 +03:00
parent dca9ef4014
commit 11465523b1
4 changed files with 192 additions and 83 deletions

View File

@ -11,6 +11,7 @@ class ChapterMarkers extends Component {
this.chaptersData = [];
this.tooltip = null;
this.isHovering = false;
this.previewSprite = options.previewSprite || null;
}
createEl() {
@ -72,9 +73,7 @@ class ChapterMarkers extends Component {
}
setupProgressBarHover() {
const progressControl = this.player()
.getChild('controlBar')
.getChild('progressControl');
const progressControl = this.player().getChild('controlBar').getChild('progressControl');
if (!progressControl) return;
const seekBar = progressControl.getChild('seekBar');
@ -103,11 +102,61 @@ class ChapterMarkers extends Component {
bottom: '45px',
transform: 'translateX(-50%)',
display: 'none',
maxWidth: '250px',
minWidth: '200px',
maxWidth: '280px',
width: 'auto',
textAlign: 'center',
border: '1px solid rgba(255, 255, 255, 0.2)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
});
// Create stable DOM structure to avoid trembling
this.chapterTitle = videojs.dom.createEl('div', {
className: 'chapter-title',
});
Object.assign(this.chapterTitle.style, {
fontWeight: 'bold',
marginBottom: '4px',
color: '#fff',
});
this.chapterInfo = videojs.dom.createEl('div', {
className: 'chapter-info',
});
Object.assign(this.chapterInfo.style, {
fontSize: '11px',
opacity: '0.8',
marginBottom: '2px',
});
this.positionInfo = videojs.dom.createEl('div', {
className: 'position-info',
});
Object.assign(this.positionInfo.style, {
fontSize: '10px',
opacity: '0.6',
});
this.chapterImage = videojs.dom.createEl('div', {
className: 'chapter-image-sprite',
});
Object.assign(this.chapterImage.style, {
width: '160px',
height: '90px',
marginTop: '8px',
borderRadius: '4px',
border: '1px solid rgba(255,255,255,0.1)',
display: 'block',
overflow: 'hidden',
backgroundRepeat: 'no-repeat',
backgroundSize: 'auto',
});
// Append all elements to tooltip
this.tooltip.appendChild(this.chapterTitle);
this.tooltip.appendChild(this.chapterInfo);
this.tooltip.appendChild(this.positionInfo);
this.tooltip.appendChild(this.chapterImage);
}
// Add tooltip to seekBar if not already added
@ -124,18 +173,9 @@ class ChapterMarkers extends Component {
const progressControlEl = progressControl.el();
// Remove existing listeners to prevent duplicates
progressControlEl.removeEventListener(
'mouseenter',
this.handleMouseEnter
);
progressControlEl.removeEventListener(
'mouseleave',
this.handleMouseLeave
);
progressControlEl.removeEventListener(
'mousemove',
this.handleMouseMove
);
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
// Bind methods to preserve context
this.handleMouseEnter = () => {
@ -171,10 +211,7 @@ class ChapterMarkers extends Component {
// Use seekBar for horizontal calculation but allow vertical tolerance
const offsetX = event.clientX - seekBarRect.left;
const percentage = Math.max(
0,
Math.min(1, offsetX / seekBarRect.width)
);
const percentage = Math.max(0, Math.min(1, offsetX / seekBarRect.width));
const currentTime = percentage * duration;
// Position tooltip relative to progress control area
@ -195,21 +232,49 @@ class ChapterMarkers extends Component {
const endTime = formatTime(currentChapter.endTime);
const timeAtPosition = formatTime(currentTime);
this.tooltip.innerHTML = `
<div style="font-weight: bold; margin-bottom: 4px; color: #fff;">${currentChapter.text}</div>
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 2px;">Chapter: ${startTime} - ${endTime}</div>
<div style="font-size: 10px; opacity: 0.6;">Position: ${timeAtPosition}</div>
`;
// Update text content without rebuilding DOM
this.chapterTitle.textContent = currentChapter.text;
this.chapterInfo.textContent = `Chapter: ${startTime} - ${endTime}`;
this.positionInfo.textContent = `Position: ${timeAtPosition}`;
// Update sprite thumbnail
this.updateSpriteThumbnail(currentTime);
this.chapterImage.style.display = 'block';
} else {
const timeAtPosition = this.formatTime(currentTime);
this.tooltip.innerHTML = `
<div style="font-weight: bold; margin-bottom: 2px;">No Chapter</div>
<div style="font-size: 10px; opacity: 0.6;">Position: ${timeAtPosition}</div>
`;
this.chapterTitle.textContent = 'No Chapter';
this.chapterInfo.textContent = '';
this.positionInfo.textContent = `Position: ${timeAtPosition}`;
// Still show sprite thumbnail even when not in a chapter
this.updateSpriteThumbnail(currentTime);
this.chapterImage.style.display = 'block';
}
// Position tooltip relative to progress control container
this.tooltip.style.left = `${tooltipOffsetX}px`;
// Position tooltip with smart boundary detection
// Force tooltip to be visible momentarily to get accurate dimensions
this.tooltip.style.visibility = 'hidden';
this.tooltip.style.display = 'block';
const tooltipWidth = this.tooltip.offsetWidth || 240; // Fallback width
const progressControlWidth = progressControlRect.width;
const halfTooltipWidth = tooltipWidth / 2;
// Calculate ideal position (where mouse is)
let idealLeft = tooltipOffsetX;
// Check and adjust boundaries
if (idealLeft - halfTooltipWidth < 0) {
// Too far left - align to left edge with small margin
idealLeft = halfTooltipWidth + 5;
} else if (idealLeft + halfTooltipWidth > progressControlWidth) {
// Too far right - align to right edge with small margin
idealLeft = progressControlWidth - halfTooltipWidth - 5;
}
// Apply position and make visible
this.tooltip.style.left = `${idealLeft}px`;
this.tooltip.style.visibility = 'visible';
this.tooltip.style.display = 'block';
}
@ -222,6 +287,61 @@ class ChapterMarkers extends Component {
return null;
}
updateSpriteThumbnail(currentTime) {
if (!this.previewSprite || !this.previewSprite.url) {
// Hide image if no sprite data available
this.chapterImage.style.display = 'none';
console.log('No sprite data available:', this.previewSprite);
return;
}
const { url, frame } = this.previewSprite;
const { width, height } = frame;
// Calculate which frame to show based on current time
// Use sprite interval from frame data, fallback to 10 seconds
const frameInterval = frame.seconds || 10;
// Try to detect total frames based on video duration vs frame interval
const videoDuration = this.player().duration() || 45; // fallback duration
const calculatedMaxFrames = Math.ceil(videoDuration / frameInterval);
const maxFrames = Math.min(calculatedMaxFrames, 6); // Cap at 6 frames to be safe
let frameIndex = Math.floor(currentTime / frameInterval);
// Clamp frameIndex to available frames to prevent showing empty areas
frameIndex = Math.min(frameIndex, maxFrames - 1);
// Based on the sprite image you shared, it appears to have frames arranged vertically
// Let's try a vertical layout first (1 column, multiple rows)
const frameRow = frameIndex; // Each frame is on its own row
const frameCol = 0; // Always first (and only) column
// Calculate background position (negative values to shift the sprite)
const xPos = -(frameCol * width);
const yPos = -(frameRow * height);
console.log(
`Time: ${currentTime}s, Duration: ${this.player().duration()}s, Interval: ${frameInterval}s, Frame: ${frameIndex}/${maxFrames - 1}, Row: ${frameRow}, Col: ${frameCol}, Pos: ${xPos}px ${yPos}px, URL: ${url}`
);
// Apply sprite background
this.chapterImage.style.backgroundImage = `url("${url}")`;
this.chapterImage.style.backgroundPosition = `${xPos}px ${yPos}px`;
this.chapterImage.style.backgroundSize = 'auto';
this.chapterImage.style.backgroundRepeat = 'no-repeat';
// Ensure the image is visible
this.chapterImage.style.display = 'block';
// Fallback: if we're beyond frame 3 (30s+), try showing frame 2 instead (20-30s frame)
if (frameIndex >= 3 && currentTime > 30) {
const fallbackYPos = -(2 * height); // Frame 2 (20-30s range)
this.chapterImage.style.backgroundPosition = `${xPos}px ${fallbackYPos}px`;
console.log(`Fallback: Using frame 2 instead of frame ${frameIndex} for time ${currentTime}s`);
}
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
@ -259,23 +379,12 @@ class ChapterMarkers extends Component {
dispose() {
// Clean up event listeners
const progressControl = this.player()
.getChild('controlBar')
?.getChild('progressControl');
const progressControl = this.player().getChild('controlBar')?.getChild('progressControl');
if (progressControl) {
const progressControlEl = progressControl.el();
progressControlEl.removeEventListener(
'mouseenter',
this.handleMouseEnter
);
progressControlEl.removeEventListener(
'mouseleave',
this.handleMouseLeave
);
progressControlEl.removeEventListener(
'mousemove',
this.handleMouseMove
);
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
}
// Remove tooltip

View File

@ -23,6 +23,10 @@ function VideoJSPlayer() {
? window.MEDIA_DATA
: {
data: {},
previewSprite: {
url: 'https://demo.mediacms.io/media/original/thumbnails/user/markos/fe4933d67b884d4da507dd60e77f7438.VID_20200909_141053.mp4sprites.jpg',
frame: { width: 160, height: 90, seconds: 10 },
},
siteUrl: '',
hasNextLink: true,
},
@ -38,9 +42,9 @@ function VideoJSPlayer() {
{ startTime: 15, endTime: 20, text: 'Parcel Discounts - EuroHPC' },
{ startTime: 20, endTime: 25, text: 'Class Studies - EuroHPC' },
{ startTime: 25, endTime: 30, text: 'Sustainability - EuroHPC' },
{ startTime: 30, endTime: 35, text: 'Funding and Finance - EuroHPC' },
{ startTime: 30, endTime: 31, text: 'Funding and - EuroHPC' } /*
{ startTime: 35, endTime: 40, text: 'Virtual HPC Academy - EuroHPC' },
{ startTime: 40, endTime: 45, text: 'Wrapping up - EuroHPC' },
{ startTime: 40, endTime: 45, text: 'Wrapping up - EuroHPC' }, */,
];
// Get video data from mediaData
@ -49,6 +53,7 @@ function VideoJSPlayer() {
id: mediaData.data?.friendly_token || 'default-video',
title: mediaData.data?.title || 'Video',
poster: mediaData.siteUrl + mediaData.data?.poster_url || '',
previewSprite: mediaData?.previewSprite || {},
sources: mediaData.data?.original_media_url
? [
{
@ -640,7 +645,9 @@ function VideoJSPlayer() {
// BEGIN: Add chapter markers to progress control
if (progressControl && seekBar) {
const chapterMarkers = new ChapterMarkers(playerRef.current);
const chapterMarkers = new ChapterMarkers(playerRef.current, {
previewSprite: mediaData.previewSprite,
});
seekBar.addChild(chapterMarkers);
}
// END: Add chapter markers to progress control

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long