feat: Video.js (qualities, css improvements)

- Settings menu content - split into separate variables for maintainability
- Only include qualities that have actual sources
- hls_info vs encodings_info
This commit is contained in:
Yiannis Christodoulou 2025-09-15 23:47:43 +03:00
parent 543085c38f
commit 5d99a4e23d
10 changed files with 700 additions and 1064 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,8 @@ class CustomChaptersOverlay extends Component {
this.createOverlay = this.createOverlay.bind(this); this.createOverlay = this.createOverlay.bind(this);
this.updateCurrentChapter = this.updateCurrentChapter.bind(this); this.updateCurrentChapter = this.updateCurrentChapter.bind(this);
this.toggleOverlay = this.toggleOverlay.bind(this); this.toggleOverlay = this.toggleOverlay.bind(this);
this.formatTime = this.formatTime.bind(this);
this.getChapterTimeRange = this.getChapterTimeRange.bind(this);
// Initialize after player is ready // Initialize after player is ready
this.player().ready(() => { this.player().ready(() => {
@ -27,6 +29,21 @@ class CustomChaptersOverlay extends Component {
}); });
} }
formatTime(seconds) {
const totalSec = Math.max(0, Math.floor(seconds));
const hh = Math.floor(totalSec / 3600);
const mm = Math.floor((totalSec % 3600) / 60);
const ss = totalSec % 60;
return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
}
getChapterTimeRange(chapter) {
const startTime = this.formatTime(chapter.startTime);
const endTime = this.formatTime(chapter.endTime || chapter.startTime);
return `${startTime} - ${endTime}`;
}
createOverlay() { createOverlay() {
if (!this.chaptersData || this.chaptersData.length === 0) { if (!this.chaptersData || this.chaptersData.length === 0) {
console.log('⚠ No chapters data available for overlay'); console.log('⚠ No chapters data available for overlay');
@ -46,10 +63,16 @@ class CustomChaptersOverlay extends Component {
height: 100%; height: 100%;
z-index: 9999; z-index: 9999;
display: none; display: none;
pointer-events: none; pointer-events: auto;
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.35);
`; `;
this.overlay.addEventListener('click', (event) => {
if (event.target === this.overlay) {
this.closeOverlay();
}
});
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'video-chapter'; container.className = 'video-chapter';
container.style.cssText = ` container.style.cssText = `
@ -129,8 +152,10 @@ class CustomChaptersOverlay extends Component {
sub.className = 'meta-sub'; sub.className = 'meta-sub';
const dynamic = document.createElement('span'); const dynamic = document.createElement('span');
dynamic.className = 'meta-dynamic'; dynamic.className = 'meta-dynamic';
dynamic.textContent = this.channelName; const chapterTimeRange = this.getChapterTimeRange(chapter);
dynamic.textContent = chapterTimeRange;
dynamic.setAttribute('data-duration', timeStr); dynamic.setAttribute('data-duration', timeStr);
dynamic.setAttribute('data-time-range', chapterTimeRange);
sub.appendChild(dynamic); sub.appendChild(dynamic);
meta.appendChild(titleEl); meta.appendChild(titleEl);
meta.appendChild(sub); meta.appendChild(sub);
@ -215,7 +240,7 @@ class CustomChaptersOverlay extends Component {
} else { } else {
item.classList.remove('selected'); item.classList.remove('selected');
if (handle) handle.textContent = String(index + 1); if (handle) handle.textContent = String(index + 1);
if (dynamic) dynamic.textContent = this.channelName; if (dynamic) dynamic.textContent = dynamic.getAttribute('data-time-range') || this.getChapterTimeRange(chapter);
} }
}); });
} }
@ -232,11 +257,30 @@ class CustomChaptersOverlay extends Component {
} else { } else {
el.classList.remove('selected'); el.classList.remove('selected');
if (handle) handle.textContent = String(idx + 1); if (handle) handle.textContent = String(idx + 1);
if (dynamic) dynamic.textContent = this.channelName; if (dynamic) {
const timeRange = dynamic.getAttribute('data-time-range');
if (timeRange) {
dynamic.textContent = timeRange;
} else {
// Fallback: calculate time range from chapters data
const chapter = this.chaptersData[idx];
if (chapter) {
dynamic.textContent = this.getChapterTimeRange(chapter);
}
}
}
} }
}); });
} }
closeOverlay() {
if (this.overlay) {
this.overlay.style.display = 'none';
const el = this.player().el();
if (el) el.classList.remove('chapters-open');
}
}
dispose() { dispose() {
if (this.overlay) { if (this.overlay) {
this.overlay.remove(); this.overlay.remove();

View File

@ -17,6 +17,7 @@ class CustomSettingsMenu extends Component {
this.subtitlesSubmenu = null; this.subtitlesSubmenu = null;
this.userPreferences = options?.userPreferences || new UserPreferences(); this.userPreferences = options?.userPreferences || new UserPreferences();
this.providedQualities = options?.qualities || null; this.providedQualities = options?.qualities || null;
this.hasSubtitles = options?.hasSubtitles || false;
// Bind methods // Bind methods
this.createSettingsButton = this.createSettingsButton.bind(this); this.createSettingsButton = this.createSettingsButton.bind(this);
@ -153,10 +154,10 @@ class CustomSettingsMenu extends Component {
activeQuality?.label || activeQuality?.label ||
(currentQuality ? String(currentQuality) : "Auto"); (currentQuality ? String(currentQuality) : "Auto");
// Settings menu content // Settings menu content - split into separate variables for maintainability
this.settingsOverlay.innerHTML = ` const settingsHeader = `<div class="settings-header">Settings</div>`;
<div class="settings-header">Settings</div>
const playbackSpeedSection = `
<div class="settings-item" data-setting="playback-speed"> <div class="settings-item" data-setting="playback-speed">
<span class="settings-left"> <span class="settings-left">
<span class="vjs-icon-placeholder settings-item-svg"> <span class="vjs-icon-placeholder settings-item-svg">
@ -167,8 +168,9 @@ class CustomSettingsMenu extends Component {
<span class="current-speed">${playbackRateLabel}</span> <span class="current-speed">${playbackRateLabel}</span>
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span> <span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
</span> </span>
</div> </div>`;
const qualitySection = `
<div class="settings-item" data-setting="quality"> <div class="settings-item" data-setting="quality">
<span class="settings-left"> <span class="settings-left">
<span class="vjs-icon-placeholder settings-item-svg"> <span class="vjs-icon-placeholder settings-item-svg">
@ -179,8 +181,9 @@ class CustomSettingsMenu extends Component {
<span class="current-quality">${qualityLabelHTML}</span> <span class="current-quality">${qualityLabelHTML}</span>
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span> <span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
</span> </span>
</div> </div>`;
const subtitlesSection = `
<div class="settings-item" data-setting="subtitles"> <div class="settings-item" data-setting="subtitles">
<span class="settings-left"> <span class="settings-left">
<span class="vjs-icon-placeholder settings-item-svg"> <span class="vjs-icon-placeholder settings-item-svg">
@ -191,8 +194,17 @@ class CustomSettingsMenu extends Component {
<span class="current-subtitles">${currentSubtitleLabel}</span> <span class="current-subtitles">${currentSubtitleLabel}</span>
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span> <span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
</span> </span>
</div> </div>`;
`;
// Build the complete settings overlay
this.settingsOverlay.innerHTML = settingsHeader;
this.settingsOverlay.innerHTML += playbackSpeedSection;
this.settingsOverlay.innerHTML += qualitySection;
// Check if subtitles are available
if (this.hasSubtitles) {
this.settingsOverlay.innerHTML += subtitlesSection;
}
// Create speed submenu // Create speed submenu
this.createSpeedSubmenu(); this.createSpeedSubmenu();
@ -459,13 +471,8 @@ class CustomSettingsMenu extends Component {
return this.sortAndDecorateQualities(mapped, desiredOrder); return this.sortAndDecorateQualities(mapped, desiredOrder);
} }
// Default fallback // Default fallback - return empty array if no valid sources found
// Build full ordered list without src so UI is consistent; switching will require src in JSON return [];
const fallback = desiredOrder.map((v) => ({
label: v === "auto" ? "Auto" : v,
value: v,
}));
return this.sortAndDecorateQualities(fallback, desiredOrder);
} }
sortAndDecorateQualities(list, desiredOrder) { sortAndDecorateQualities(list, desiredOrder) {
@ -473,7 +480,11 @@ class CustomSettingsMenu extends Component {
const i = desiredOrder.indexOf(String(val).toLowerCase()); const i = desiredOrder.indexOf(String(val).toLowerCase());
return i === -1 ? 999 : i; return i === -1 ? 999 : i;
}; };
const decorated = list
// Only include qualities that have actual sources
const validQualities = list.filter(q => q.src);
const decorated = validQualities
.map((q) => { .map((q) => {
const val = (q.value || q.label || "").toString().toLowerCase(); const val = (q.value || q.label || "").toString().toLowerCase();
const baseLabel = q.label || q.value || ""; const baseLabel = q.label || q.value || "";
@ -485,20 +496,6 @@ class CustomSettingsMenu extends Component {
}) })
.sort((a, b) => orderIndex(a.value) - orderIndex(b.value)); .sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
// Ensure all desired labels appear at least once (even if not provided), for consistent menu
const have = new Set(decorated.map((q) => q.value));
desiredOrder.forEach((val) => {
if (!have.has(val)) {
const baseLabel = val === "auto" ? "Auto" : val;
const displayLabel =
val === "1080p"
? `${baseLabel} <sup class="hd-badge">HD</sup>`
: baseLabel;
decorated.push({ label: baseLabel, value: val, displayLabel });
}
});
// Re-sort after pushing missing
decorated.sort((a, b) => orderIndex(a.value) - orderIndex(b.value));
return decorated; return decorated;
} }

View File

@ -27,6 +27,63 @@ function VideoJSPlayer() {
? window.MEDIA_DATA ? window.MEDIA_DATA
: { : {
data: { data: {
__hls_info: {
"master_file": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/master.m3u8",
"480_iframe": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-1/iframes.m3u8",
"720_iframe": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-2/iframes.m3u8",
"240_iframe": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-3/iframes.m3u8",
"360_iframe": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-4/iframes.m3u8",
"480_playlist": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-1/stream.m3u8",
"720_playlist": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-2/stream.m3u8",
"240_playlist": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-3/stream.m3u8",
"360_playlist": "http://localhost/media/hls/c868df836ac34688b876e741b58ada93/media-4/stream.m3u8"
},
__original_media_url: "http://localhost/media/original/user/admin/c868df836ac34688b876e741b58ada93.SampleVideo_1280x720_30mb.mp4",
__encodings_info: {
"144": {},
"240": {
"h264": {
"title": "h264-240",
"url": "http://localhost/media/encoded/2/admin/c868df836ac34688b876e741b58ada93.c868df836ac34688b876e741b58ada93.SampleVideo_1280x720_30mb.mp4.mp4",
"progress": 100,
"size": "8.2MB",
"encoding_id": 22,
"status": "success"
}
},
"360": {
"h264": {
"title": "h264-360",
"url": "http://localhost/media/encoded/3/admin/c868df836ac34688b876e741b58ada93.c868df836ac34688b876e741b58ada93.SampleVideo_1280x720_30mb.mp4.mp4",
"progress": 100,
"size": "12.3MB",
"encoding_id": 23,
"status": "success"
}
},
"480": {
"h264": {
"title": "h264-480",
"url": "http://localhost/media/encoded/13/admin/c868df836ac34688b876e741b58ada93.c868df836ac34688b876e741b58ada93.SampleVideo_1280x720_30mb.mp4.mp4",
"progress": 100,
"size": "17.4MB",
"encoding_id": 24,
"status": "success"
}
},
"720": {
"h264": {
"title": "h264-720",
"url": "http://localhost/media/encoded/10/admin/c868df836ac34688b876e741b58ada93.c868df836ac34688b876e741b58ada93.SampleVideo_1280x720_30mb.mp4.mp4",
"progress": 100,
"size": "27.4MB",
"encoding_id": 25,
"status": "success"
}
},
"1080": {},
"1440": {},
},
related_media: [ related_media: [
{ {
friendly_token: 'jgLkic37V', friendly_token: 'jgLkic37V',
@ -564,18 +621,12 @@ function VideoJSPlayer() {
}, },
siteUrl: '', siteUrl: '',
nextLink: 'https://demo.mediacms.io/view?m=YjGJafibO', nextLink: 'https://demo.mediacms.io/view?m=YjGJafibO',
chapter_data: [
{ startTime: 0, endTime: 3, text: 'Introduction' },
{ startTime: 3, endTime: 5, text: 'Overview of Marine Life' },
{ startTime: 5, endTime: 10, text: 'Coral Reef Ecosystems' },
{ startTime: 10, endTime: 14, text: 'Deep Sea Creatures' },
],
chaptersData: [ chaptersData: [
{ startTime: 0, endTime: 3, text: 'Introduction' }, { startTime: 0, endTime: 4, text: 'Introduction' },
{ startTime: 3, endTime: 5, text: 'Overview of Marine Life' }, { startTime: 5, endTime: 10, text: 'Overview of Marine Life' },
{ startTime: 5, endTime: 10, text: 'Coral Reef Ecosystems' }, { startTime: 10, endTime: 15, text: 'Coral Reef Ecosystems' },
{ startTime: 10, endTime: 14, text: 'Deep Sea Creatures' }, { startTime: 15, endTime: 20, text: 'Deep Sea Creatures' },
{ startTime: 240, endTime: 320, text: 'Ocean Conservation' }, { startTime: 20, endTime: 30, text: 'Ocean Conservation' },
{ startTime: 320, endTime: 400, text: 'Climate Change Impact' }, { startTime: 320, endTime: 400, text: 'Climate Change Impact' },
{ startTime: 400, endTime: 480, text: 'Marine Protected Areas' }, { startTime: 400, endTime: 480, text: 'Marine Protected Areas' },
{ startTime: 480, endTime: 560, text: 'Sustainable Fishing' }, { startTime: 480, endTime: 560, text: 'Sustainable Fishing' },
@ -600,10 +651,11 @@ function VideoJSPlayer() {
// Define chapters as JSON object // Define chapters as JSON object
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON // Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
const chaptersData = mediaData.chaptersData ?? [ const chaptersData = mediaData.chaptersData ?? [
{ startTime: 0, endTime: 30, text: 'Introduction' }, { startTime: 0, endTime: 4, text: 'Introduction' },
{ startTime: 30, endTime: 90, text: 'Overview of Marine Life' }, { startTime: 5, endTime: 10, text: 'Overview of Marine Life' },
{ startTime: 90, endTime: 180, text: 'Coral Reef Ecosystems' }, { startTime: 10, endTime: 15, text: 'Coral Reef Ecosystems' },
{ startTime: 180, endTime: 240, text: 'Deep Sea Creatures' }, { startTime: 15, endTime: 20, text: 'Deep Sea Creatures' },
{ startTime: 20, endTime: 30, text: 'Ocean Conservation' },
{ startTime: 240, endTime: 320, text: 'Ocean Conservation' }, { startTime: 240, endTime: 320, text: 'Ocean Conservation' },
{ startTime: 320, endTime: 400, text: 'Climate Change Impact' }, { startTime: 320, endTime: 400, text: 'Climate Change Impact' },
{ startTime: 400, endTime: 480, text: 'Marine Protected Areas' }, { startTime: 400, endTime: 480, text: 'Marine Protected Areas' },
@ -625,33 +677,102 @@ function VideoJSPlayer() {
// Get video data from mediaData // Get video data from mediaData
const currentVideo = useMemo( const currentVideo = useMemo(
() => ({ () => {
// Get video sources based on available data
const getVideoSources = () => {
// Check if HLS info is available and not empty
if (mediaData.data?.hls_info && mediaData.data.hls_info.master_file) {
// Use master file as the primary source (auto quality)
return [
{
src: mediaData.siteUrl + mediaData.data.hls_info.master_file,
type: 'application/x-mpegURL', // HLS MIME type
label: 'Auto',
},
];
}
// Fallback to encoded qualities if available
if (mediaData.data?.encodings_info) {
const sources = [];
const encodings = mediaData.data.encodings_info;
// Get available qualities dynamically from encodings_info
const availableQualities = Object.keys(encodings)
.filter(quality => encodings[quality] && encodings[quality].h264 && encodings[quality].h264.url)
.sort((a, b) => parseInt(b) - parseInt(a)); // Sort descending (highest first)
for (const quality of availableQualities) {
sources.push({
src: mediaData.siteUrl + encodings[quality].h264.url,
type: 'video/mp4',
label: `${quality}p`,
});
}
if (sources.length > 0) {
return sources;
}
}
// Final fallback to original media URL or sample video
if (mediaData.data?.original_media_url) {
return [
{
src: mediaData.siteUrl + mediaData.data.original_media_url,
type: 'video/mp4',
},
];
}
// Default sample video
return [
{
src: '/videos/sample-video.mp4',
type: 'video/mp4',
},
];
};
return {
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 || {}, previewSprite: mediaData?.previewSprite || {},
related_media: mediaData.data?.related_media || [], related_media: mediaData.data?.related_media || [],
nextLink: mediaData?.nextLink || null, nextLink: mediaData?.nextLink || null,
sources: mediaData.data?.original_media_url sources: getVideoSources(),
? [ };
{
src: mediaData.siteUrl + mediaData.data.original_media_url,
type: 'video/mp4',
}, },
]
: [
{
src: '/videos/sample-video.mp4',
type: 'video/mp4',
},
],
}),
[mediaData] [mediaData]
); );
// Compute available qualities. Prefer JSON (mediaData.data.qualities), otherwise build a full ordered list using the current source. // Compute available qualities. Prefer JSON (mediaData.data.qualities), otherwise build from encodings_info or current source.
const availableQualities = useMemo(() => { const availableQualities = useMemo(() => {
const desiredOrder = ['auto', '144p', '240p', '360p', '480p', '720p', '1080p']; // Generate desiredOrder dynamically based on available data
const generateDesiredOrder = () => {
const baseOrder = ['auto'];
// Add qualities from encodings_info if available
if (mediaData.data?.encodings_info) {
const availableQualities = Object.keys(mediaData.data.encodings_info)
.filter(quality => {
const encoding = mediaData.data.encodings_info[quality];
return encoding && encoding.h264 && encoding.h264.url;
})
.map(quality => `${quality}p`)
.sort((a, b) => parseInt(a) - parseInt(b)); // Sort ascending
baseOrder.push(...availableQualities);
} else {
// Fallback to standard order
baseOrder.push('144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '2160p');
}
return baseOrder;
};
const desiredOrder = generateDesiredOrder();
const normalize = (arr) => { const normalize = (arr) => {
const norm = arr.map((q) => ({ const norm = arr.map((q) => ({
@ -660,20 +781,17 @@ function VideoJSPlayer() {
src: q.src || q.url || q.href, src: q.src || q.url || q.href,
type: q.type || 'video/mp4', type: q.type || 'video/mp4',
})); }));
// ensure all desired present
const have = new Set(norm.map((q) => q.value)); // Only include qualities that have actual sources
desiredOrder.forEach((v) => { const validQualities = norm.filter(q => q.src);
if (!have.has(v)) {
norm.push({ label: v === 'auto' ? 'Auto' : v, value: v }); // sort based on desired order
}
});
// sort
const idx = (v) => { const idx = (v) => {
const i = desiredOrder.indexOf(String(v).toLowerCase()); const i = desiredOrder.indexOf(String(v).toLowerCase());
return i === -1 ? 999 : i; return i === -1 ? 999 : i;
}; };
norm.sort((a, b) => idx(a.value) - idx(b.value)); validQualities.sort((a, b) => idx(a.value) - idx(b.value));
return norm; return validQualities;
}; };
const jsonList = mediaData?.data?.qualities; const jsonList = mediaData?.data?.qualities;
@ -681,17 +799,81 @@ function VideoJSPlayer() {
return normalize(jsonList); return normalize(jsonList);
} }
// Build from current source // If HLS is available, build qualities from HLS playlists
if (mediaData.data?.hls_info && mediaData.data.hls_info.master_file) {
const hlsInfo = mediaData.data.hls_info;
const qualities = [];
// Add master file as auto quality
qualities.push({
label: 'Auto',
value: 'auto',
src: mediaData.siteUrl + hlsInfo.master_file,
type: 'application/x-mpegURL',
});
// Add individual HLS playlists
Object.keys(hlsInfo).forEach(key => {
if (key.endsWith('_playlist')) {
const quality = key.replace('_playlist', '');
qualities.push({
label: `${quality}p`,
value: `${quality}p`,
src: mediaData.siteUrl + hlsInfo[key],
type: 'application/x-mpegURL',
});
}
});
return normalize(qualities);
}
// Build from encodings_info if available
if (mediaData.data?.encodings_info) {
const encodings = mediaData.data.encodings_info;
const qualities = [];
// Add auto quality first
qualities.push({
label: 'Auto',
value: 'auto',
src: null, // Will use the highest available quality
type: 'video/mp4',
});
// Add available encoded qualities dynamically
Object.keys(encodings).forEach(quality => {
if (encodings[quality] && encodings[quality].h264 && encodings[quality].h264.url) {
qualities.push({
label: `${quality}p`,
value: `${quality}p`,
src: mediaData.siteUrl + encodings[quality].h264.url,
type: 'video/mp4',
});
}
});
if (qualities.length > 1) { // More than just auto
return normalize(qualities);
}
}
// Build from current source as fallback - only if we have a valid source
const baseSrc = (currentVideo?.sources && currentVideo.sources[0]?.src) || null; const baseSrc = (currentVideo?.sources && currentVideo.sources[0]?.src) || null;
const type = (currentVideo?.sources && currentVideo.sources[0]?.type) || 'video/mp4'; const type = (currentVideo?.sources && currentVideo.sources[0]?.type) || 'video/mp4';
const buildFromBase = desiredOrder.map((v) => ({ if (baseSrc) {
label: v === 'auto' ? 'Auto' : v, const buildFromBase = [{
value: v, label: 'Auto',
src: baseSrc || undefined, value: 'auto',
src: baseSrc,
type, type,
})); }];
return normalize(buildFromBase); return normalize(buildFromBase);
}
// Return empty array if no valid sources found
return [];
}, [mediaData, currentVideo]); }, [mediaData, currentVideo]);
// Get related videos from mediaData instead of static data // Get related videos from mediaData instead of static data
@ -736,12 +918,12 @@ function VideoJSPlayer() {
default: false, default: false,
}, },
]; ];
// const demoSubtitleTracks = []; // NO Subtitles. TODO: hide it on production
// Get subtitle tracks from backend response or fallback based on environment // Get subtitle tracks from backend response or fallback based on environment
const backendSubtitles = mediaData?.data?.subtitles_info || [];
const isDevelopment = process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost'; const isDevelopment = process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost';
const backendSubtitles = mediaData?.data?.subtitles_info || (isDevelopment ? demoSubtitleTracks : []);
const hasSubtitles = backendSubtitles.length > 0 || isDevelopment; const hasSubtitles = backendSubtitles.length > 0;
const subtitleTracks = hasSubtitles const subtitleTracks = hasSubtitles
? backendSubtitles.map(track => ({ ? backendSubtitles.map(track => ({
kind: 'subtitles', kind: 'subtitles',
@ -750,7 +932,11 @@ function VideoJSPlayer() {
label: track.label, label: track.label,
default: false, default: false,
})) }))
: (isDevelopment ? demoSubtitleTracks : []); : [];
console.log('mediaData?.data?.thumbnail_time', mediaData?.data?.thumbnail_time);
console.log('mediaData?.data?.sprites_url', mediaData?.data?.sprites_url);
console.log('mediaData', mediaData);
// Function to navigate to next video // Function to navigate to next video
const goToNextVideo = () => { const goToNextVideo = () => {
@ -1078,8 +1264,6 @@ function VideoJSPlayer() {
} }
// BEGIN: Add subtitle tracks // BEGIN: Add subtitle tracks
hasSubtitles && subtitleTracks.forEach((track) => { hasSubtitles && subtitleTracks.forEach((track) => {
playerRef.current.addRemoteTextTrack(track, false); playerRef.current.addRemoteTextTrack(track, false);
}); });
@ -1426,6 +1610,7 @@ function VideoJSPlayer() {
customComponents.current.settingsMenu = new CustomSettingsMenu(playerRef.current, { customComponents.current.settingsMenu = new CustomSettingsMenu(playerRef.current, {
userPreferences: userPreferences.current, userPreferences: userPreferences.current,
qualities: availableQualities, qualities: availableQualities,
hasSubtitles: hasSubtitles,
}); });
// If qualities change per video (e.g., via MEDIA_DATA update), refresh menu // If qualities change per video (e.g., via MEDIA_DATA update), refresh menu

View File

@ -12,12 +12,7 @@ const mountComponents = () => {
const root = createRoot(rootContainer); const root = createRoot(rootContainer);
root.render( root.render(
<StrictMode> <StrictMode>
<div className='video-wrapper'>
<div className='video-box'>
<VideoJS /> <VideoJS />
</div>
{/* <ChapterList /> */}
</div>
</StrictMode> </StrictMode>
); );
} }

View File

@ -3105,9 +3105,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.3", "version": "2.8.4",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
"integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@ -6393,9 +6393,9 @@
} }
}, },
"node_modules/unicode-property-aliases-ecmascript": { "node_modules/unicode-property-aliases-ecmascript": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@ -3787,9 +3787,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.8.3", "version": "2.8.4",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
"integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==", "integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.js" "baseline-browser-mapping": "dist/cli.js"
@ -5858,9 +5858,9 @@
} }
}, },
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-arrayish": "^0.2.1" "is-arrayish": "^0.2.1"
@ -13530,9 +13530,9 @@
} }
}, },
"node_modules/unicode-property-aliases-ecmascript": { "node_modules/unicode-property-aliases-ecmascript": {
"version": "2.1.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long