mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-10 01:18:55 -05:00
feat: Add the sprites functionality
This commit is contained in:
parent
dca9ef4014
commit
11465523b1
@ -11,6 +11,7 @@ class ChapterMarkers extends Component {
|
|||||||
this.chaptersData = [];
|
this.chaptersData = [];
|
||||||
this.tooltip = null;
|
this.tooltip = null;
|
||||||
this.isHovering = false;
|
this.isHovering = false;
|
||||||
|
this.previewSprite = options.previewSprite || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
createEl() {
|
createEl() {
|
||||||
@ -72,9 +73,7 @@ class ChapterMarkers extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupProgressBarHover() {
|
setupProgressBarHover() {
|
||||||
const progressControl = this.player()
|
const progressControl = this.player().getChild('controlBar').getChild('progressControl');
|
||||||
.getChild('controlBar')
|
|
||||||
.getChild('progressControl');
|
|
||||||
if (!progressControl) return;
|
if (!progressControl) return;
|
||||||
|
|
||||||
const seekBar = progressControl.getChild('seekBar');
|
const seekBar = progressControl.getChild('seekBar');
|
||||||
@ -103,11 +102,61 @@ class ChapterMarkers extends Component {
|
|||||||
bottom: '45px',
|
bottom: '45px',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
display: 'none',
|
display: 'none',
|
||||||
maxWidth: '250px',
|
minWidth: '200px',
|
||||||
|
maxWidth: '280px',
|
||||||
|
width: 'auto',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
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
|
// Add tooltip to seekBar if not already added
|
||||||
@ -124,18 +173,9 @@ class ChapterMarkers extends Component {
|
|||||||
const progressControlEl = progressControl.el();
|
const progressControlEl = progressControl.el();
|
||||||
|
|
||||||
// Remove existing listeners to prevent duplicates
|
// Remove existing listeners to prevent duplicates
|
||||||
progressControlEl.removeEventListener(
|
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||||
'mouseenter',
|
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||||
this.handleMouseEnter
|
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
);
|
|
||||||
progressControlEl.removeEventListener(
|
|
||||||
'mouseleave',
|
|
||||||
this.handleMouseLeave
|
|
||||||
);
|
|
||||||
progressControlEl.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
this.handleMouseMove
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bind methods to preserve context
|
// Bind methods to preserve context
|
||||||
this.handleMouseEnter = () => {
|
this.handleMouseEnter = () => {
|
||||||
@ -171,10 +211,7 @@ class ChapterMarkers extends Component {
|
|||||||
|
|
||||||
// Use seekBar for horizontal calculation but allow vertical tolerance
|
// Use seekBar for horizontal calculation but allow vertical tolerance
|
||||||
const offsetX = event.clientX - seekBarRect.left;
|
const offsetX = event.clientX - seekBarRect.left;
|
||||||
const percentage = Math.max(
|
const percentage = Math.max(0, Math.min(1, offsetX / seekBarRect.width));
|
||||||
0,
|
|
||||||
Math.min(1, offsetX / seekBarRect.width)
|
|
||||||
);
|
|
||||||
const currentTime = percentage * duration;
|
const currentTime = percentage * duration;
|
||||||
|
|
||||||
// Position tooltip relative to progress control area
|
// Position tooltip relative to progress control area
|
||||||
@ -195,21 +232,49 @@ class ChapterMarkers extends Component {
|
|||||||
const endTime = formatTime(currentChapter.endTime);
|
const endTime = formatTime(currentChapter.endTime);
|
||||||
const timeAtPosition = formatTime(currentTime);
|
const timeAtPosition = formatTime(currentTime);
|
||||||
|
|
||||||
this.tooltip.innerHTML = `
|
// Update text content without rebuilding DOM
|
||||||
<div style="font-weight: bold; margin-bottom: 4px; color: #fff;">${currentChapter.text}</div>
|
this.chapterTitle.textContent = currentChapter.text;
|
||||||
<div style="font-size: 11px; opacity: 0.8; margin-bottom: 2px;">Chapter: ${startTime} - ${endTime}</div>
|
this.chapterInfo.textContent = `Chapter: ${startTime} - ${endTime}`;
|
||||||
<div style="font-size: 10px; opacity: 0.6;">Position: ${timeAtPosition}</div>
|
this.positionInfo.textContent = `Position: ${timeAtPosition}`;
|
||||||
`;
|
|
||||||
|
// Update sprite thumbnail
|
||||||
|
this.updateSpriteThumbnail(currentTime);
|
||||||
|
this.chapterImage.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
const timeAtPosition = this.formatTime(currentTime);
|
const timeAtPosition = this.formatTime(currentTime);
|
||||||
this.tooltip.innerHTML = `
|
this.chapterTitle.textContent = 'No Chapter';
|
||||||
<div style="font-weight: bold; margin-bottom: 2px;">No Chapter</div>
|
this.chapterInfo.textContent = '';
|
||||||
<div style="font-size: 10px; opacity: 0.6;">Position: ${timeAtPosition}</div>
|
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
|
// Position tooltip with smart boundary detection
|
||||||
this.tooltip.style.left = `${tooltipOffsetX}px`;
|
// 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';
|
this.tooltip.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +287,61 @@ class ChapterMarkers extends Component {
|
|||||||
return null;
|
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) {
|
formatTime(seconds) {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
@ -259,23 +379,12 @@ class ChapterMarkers extends Component {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
// Clean up event listeners
|
// Clean up event listeners
|
||||||
const progressControl = this.player()
|
const progressControl = this.player().getChild('controlBar')?.getChild('progressControl');
|
||||||
.getChild('controlBar')
|
|
||||||
?.getChild('progressControl');
|
|
||||||
if (progressControl) {
|
if (progressControl) {
|
||||||
const progressControlEl = progressControl.el();
|
const progressControlEl = progressControl.el();
|
||||||
progressControlEl.removeEventListener(
|
progressControlEl.removeEventListener('mouseenter', this.handleMouseEnter);
|
||||||
'mouseenter',
|
progressControlEl.removeEventListener('mouseleave', this.handleMouseLeave);
|
||||||
this.handleMouseEnter
|
progressControlEl.removeEventListener('mousemove', this.handleMouseMove);
|
||||||
);
|
|
||||||
progressControlEl.removeEventListener(
|
|
||||||
'mouseleave',
|
|
||||||
this.handleMouseLeave
|
|
||||||
);
|
|
||||||
progressControlEl.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
this.handleMouseMove
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove tooltip
|
// Remove tooltip
|
||||||
|
|||||||
@ -23,6 +23,10 @@ function VideoJSPlayer() {
|
|||||||
? window.MEDIA_DATA
|
? window.MEDIA_DATA
|
||||||
: {
|
: {
|
||||||
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: '',
|
siteUrl: '',
|
||||||
hasNextLink: true,
|
hasNextLink: true,
|
||||||
},
|
},
|
||||||
@ -38,9 +42,9 @@ function VideoJSPlayer() {
|
|||||||
{ startTime: 15, endTime: 20, text: 'Parcel Discounts - EuroHPC' },
|
{ startTime: 15, endTime: 20, text: 'Parcel Discounts - EuroHPC' },
|
||||||
{ startTime: 20, endTime: 25, text: 'Class Studies - EuroHPC' },
|
{ startTime: 20, endTime: 25, text: 'Class Studies - EuroHPC' },
|
||||||
{ startTime: 25, endTime: 30, text: 'Sustainability - 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: 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
|
// Get video data from mediaData
|
||||||
@ -49,6 +53,7 @@ function VideoJSPlayer() {
|
|||||||
id: mediaData.data?.friendly_token || 'default-video',
|
id: mediaData.data?.friendly_token || 'default-video',
|
||||||
title: mediaData.data?.title || 'Video',
|
title: mediaData.data?.title || 'Video',
|
||||||
poster: mediaData.siteUrl + mediaData.data?.poster_url || '',
|
poster: mediaData.siteUrl + mediaData.data?.poster_url || '',
|
||||||
|
previewSprite: mediaData?.previewSprite || {},
|
||||||
sources: mediaData.data?.original_media_url
|
sources: mediaData.data?.original_media_url
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -640,7 +645,9 @@ function VideoJSPlayer() {
|
|||||||
|
|
||||||
// BEGIN: Add chapter markers to progress control
|
// BEGIN: Add chapter markers to progress control
|
||||||
if (progressControl && seekBar) {
|
if (progressControl && seekBar) {
|
||||||
const chapterMarkers = new ChapterMarkers(playerRef.current);
|
const chapterMarkers = new ChapterMarkers(playerRef.current, {
|
||||||
|
previewSprite: mediaData.previewSprite,
|
||||||
|
});
|
||||||
seekBar.addChild(chapterMarkers);
|
seekBar.addChild(chapterMarkers);
|
||||||
}
|
}
|
||||||
// END: Add chapter markers to progress control
|
// 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
Loading…
x
Reference in New Issue
Block a user