mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-10-06 22:32:41 +07:00
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:
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@ class CustomChaptersOverlay extends Component {
|
||||
this.createOverlay = this.createOverlay.bind(this);
|
||||
this.updateCurrentChapter = this.updateCurrentChapter.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
|
||||
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() {
|
||||
if (!this.chaptersData || this.chaptersData.length === 0) {
|
||||
console.log('⚠ No chapters data available for overlay');
|
||||
@@ -46,10 +63,16 @@ class CustomChaptersOverlay extends Component {
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
pointer-events: auto;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
`;
|
||||
|
||||
this.overlay.addEventListener('click', (event) => {
|
||||
if (event.target === this.overlay) {
|
||||
this.closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'video-chapter';
|
||||
container.style.cssText = `
|
||||
@@ -129,8 +152,10 @@ class CustomChaptersOverlay extends Component {
|
||||
sub.className = 'meta-sub';
|
||||
const dynamic = document.createElement('span');
|
||||
dynamic.className = 'meta-dynamic';
|
||||
dynamic.textContent = this.channelName;
|
||||
const chapterTimeRange = this.getChapterTimeRange(chapter);
|
||||
dynamic.textContent = chapterTimeRange;
|
||||
dynamic.setAttribute('data-duration', timeStr);
|
||||
dynamic.setAttribute('data-time-range', chapterTimeRange);
|
||||
sub.appendChild(dynamic);
|
||||
meta.appendChild(titleEl);
|
||||
meta.appendChild(sub);
|
||||
@@ -215,7 +240,7 @@ class CustomChaptersOverlay extends Component {
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
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 {
|
||||
el.classList.remove('selected');
|
||||
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() {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
|
||||
@@ -17,6 +17,7 @@ class CustomSettingsMenu extends Component {
|
||||
this.subtitlesSubmenu = null;
|
||||
this.userPreferences = options?.userPreferences || new UserPreferences();
|
||||
this.providedQualities = options?.qualities || null;
|
||||
this.hasSubtitles = options?.hasSubtitles || false;
|
||||
|
||||
// Bind methods
|
||||
this.createSettingsButton = this.createSettingsButton.bind(this);
|
||||
@@ -153,10 +154,10 @@ class CustomSettingsMenu extends Component {
|
||||
activeQuality?.label ||
|
||||
(currentQuality ? String(currentQuality) : "Auto");
|
||||
|
||||
// Settings menu content
|
||||
this.settingsOverlay.innerHTML = `
|
||||
<div class="settings-header">Settings</div>
|
||||
// Settings menu content - split into separate variables for maintainability
|
||||
const settingsHeader = `<div class="settings-header">Settings</div>`;
|
||||
|
||||
const playbackSpeedSection = `
|
||||
<div class="settings-item" data-setting="playback-speed">
|
||||
<span class="settings-left">
|
||||
<span class="vjs-icon-placeholder settings-item-svg">
|
||||
@@ -167,8 +168,9 @@ class CustomSettingsMenu extends Component {
|
||||
<span class="current-speed">${playbackRateLabel}</span>
|
||||
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const qualitySection = `
|
||||
<div class="settings-item" data-setting="quality">
|
||||
<span class="settings-left">
|
||||
<span class="vjs-icon-placeholder settings-item-svg">
|
||||
@@ -179,8 +181,9 @@ class CustomSettingsMenu extends Component {
|
||||
<span class="current-quality">${qualityLabelHTML}</span>
|
||||
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const subtitlesSection = `
|
||||
<div class="settings-item" data-setting="subtitles">
|
||||
<span class="settings-left">
|
||||
<span class="vjs-icon-placeholder settings-item-svg">
|
||||
@@ -191,8 +194,17 @@ class CustomSettingsMenu extends Component {
|
||||
<span class="current-subtitles">${currentSubtitleLabel}</span>
|
||||
<span class="vjs-icon-placeholder vjs-icon-navigate-next"></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
|
||||
this.createSpeedSubmenu();
|
||||
@@ -459,13 +471,8 @@ class CustomSettingsMenu extends Component {
|
||||
return this.sortAndDecorateQualities(mapped, desiredOrder);
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
// Build full ordered list without src so UI is consistent; switching will require src in JSON
|
||||
const fallback = desiredOrder.map((v) => ({
|
||||
label: v === "auto" ? "Auto" : v,
|
||||
value: v,
|
||||
}));
|
||||
return this.sortAndDecorateQualities(fallback, desiredOrder);
|
||||
// Default fallback - return empty array if no valid sources found
|
||||
return [];
|
||||
}
|
||||
|
||||
sortAndDecorateQualities(list, desiredOrder) {
|
||||
@@ -473,7 +480,11 @@ class CustomSettingsMenu extends Component {
|
||||
const i = desiredOrder.indexOf(String(val).toLowerCase());
|
||||
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) => {
|
||||
const val = (q.value || q.label || "").toString().toLowerCase();
|
||||
const baseLabel = q.label || q.value || "";
|
||||
@@ -485,20 +496,6 @@ class CustomSettingsMenu extends Component {
|
||||
})
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,63 @@ function VideoJSPlayer() {
|
||||
? window.MEDIA_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: [
|
||||
{
|
||||
friendly_token: 'jgLkic37V',
|
||||
@@ -564,18 +621,12 @@ function VideoJSPlayer() {
|
||||
},
|
||||
siteUrl: '',
|
||||
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: [
|
||||
{ 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' },
|
||||
{ startTime: 240, endTime: 320, text: 'Ocean Conservation' },
|
||||
{ startTime: 0, endTime: 4, text: 'Introduction' },
|
||||
{ startTime: 5, endTime: 10, text: 'Overview of Marine Life' },
|
||||
{ startTime: 10, endTime: 15, text: 'Coral Reef Ecosystems' },
|
||||
{ startTime: 15, endTime: 20, text: 'Deep Sea Creatures' },
|
||||
{ startTime: 20, endTime: 30, text: 'Ocean Conservation' },
|
||||
{ startTime: 320, endTime: 400, text: 'Climate Change Impact' },
|
||||
{ startTime: 400, endTime: 480, text: 'Marine Protected Areas' },
|
||||
{ startTime: 480, endTime: 560, text: 'Sustainable Fishing' },
|
||||
@@ -600,10 +651,11 @@ function VideoJSPlayer() {
|
||||
// Define chapters as JSON object
|
||||
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
|
||||
const chaptersData = mediaData.chaptersData ?? [
|
||||
{ startTime: 0, endTime: 30, text: 'Introduction' },
|
||||
{ startTime: 30, endTime: 90, text: 'Overview of Marine Life' },
|
||||
{ startTime: 90, endTime: 180, text: 'Coral Reef Ecosystems' },
|
||||
{ startTime: 180, endTime: 240, text: 'Deep Sea Creatures' },
|
||||
{ startTime: 0, endTime: 4, text: 'Introduction' },
|
||||
{ startTime: 5, endTime: 10, text: 'Overview of Marine Life' },
|
||||
{ startTime: 10, endTime: 15, text: 'Coral Reef Ecosystems' },
|
||||
{ startTime: 15, endTime: 20, text: 'Deep Sea Creatures' },
|
||||
{ startTime: 20, endTime: 30, text: 'Ocean Conservation' },
|
||||
{ startTime: 240, endTime: 320, text: 'Ocean Conservation' },
|
||||
{ startTime: 320, endTime: 400, text: 'Climate Change Impact' },
|
||||
{ startTime: 400, endTime: 480, text: 'Marine Protected Areas' },
|
||||
@@ -625,33 +677,102 @@ function VideoJSPlayer() {
|
||||
|
||||
// Get video data from mediaData
|
||||
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',
|
||||
title: mediaData.data?.title || 'Video',
|
||||
poster: mediaData.siteUrl + mediaData.data?.poster_url || '',
|
||||
previewSprite: mediaData?.previewSprite || {},
|
||||
related_media: mediaData.data?.related_media || [],
|
||||
nextLink: mediaData?.nextLink || null,
|
||||
sources: mediaData.data?.original_media_url
|
||||
? [
|
||||
{
|
||||
src: mediaData.siteUrl + mediaData.data.original_media_url,
|
||||
type: 'video/mp4',
|
||||
sources: getVideoSources(),
|
||||
};
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
src: '/videos/sample-video.mp4',
|
||||
type: 'video/mp4',
|
||||
},
|
||||
],
|
||||
}),
|
||||
[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 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 norm = arr.map((q) => ({
|
||||
@@ -660,20 +781,17 @@ function VideoJSPlayer() {
|
||||
src: q.src || q.url || q.href,
|
||||
type: q.type || 'video/mp4',
|
||||
}));
|
||||
// ensure all desired present
|
||||
const have = new Set(norm.map((q) => q.value));
|
||||
desiredOrder.forEach((v) => {
|
||||
if (!have.has(v)) {
|
||||
norm.push({ label: v === 'auto' ? 'Auto' : v, value: v });
|
||||
}
|
||||
});
|
||||
// sort
|
||||
|
||||
// Only include qualities that have actual sources
|
||||
const validQualities = norm.filter(q => q.src);
|
||||
|
||||
// sort based on desired order
|
||||
const idx = (v) => {
|
||||
const i = desiredOrder.indexOf(String(v).toLowerCase());
|
||||
return i === -1 ? 999 : i;
|
||||
};
|
||||
norm.sort((a, b) => idx(a.value) - idx(b.value));
|
||||
return norm;
|
||||
validQualities.sort((a, b) => idx(a.value) - idx(b.value));
|
||||
return validQualities;
|
||||
};
|
||||
|
||||
const jsonList = mediaData?.data?.qualities;
|
||||
@@ -681,17 +799,81 @@ function VideoJSPlayer() {
|
||||
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 type = (currentVideo?.sources && currentVideo.sources[0]?.type) || 'video/mp4';
|
||||
|
||||
const buildFromBase = desiredOrder.map((v) => ({
|
||||
label: v === 'auto' ? 'Auto' : v,
|
||||
value: v,
|
||||
src: baseSrc || undefined,
|
||||
if (baseSrc) {
|
||||
const buildFromBase = [{
|
||||
label: 'Auto',
|
||||
value: 'auto',
|
||||
src: baseSrc,
|
||||
type,
|
||||
}));
|
||||
}];
|
||||
return normalize(buildFromBase);
|
||||
}
|
||||
|
||||
// Return empty array if no valid sources found
|
||||
return [];
|
||||
}, [mediaData, currentVideo]);
|
||||
|
||||
// Get related videos from mediaData instead of static data
|
||||
@@ -736,12 +918,12 @@ function VideoJSPlayer() {
|
||||
default: false,
|
||||
},
|
||||
];
|
||||
// const demoSubtitleTracks = []; // NO Subtitles. TODO: hide it on production
|
||||
|
||||
// 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 hasSubtitles = backendSubtitles.length > 0 || isDevelopment;
|
||||
const backendSubtitles = mediaData?.data?.subtitles_info || (isDevelopment ? demoSubtitleTracks : []);
|
||||
const hasSubtitles = backendSubtitles.length > 0;
|
||||
const subtitleTracks = hasSubtitles
|
||||
? backendSubtitles.map(track => ({
|
||||
kind: 'subtitles',
|
||||
@@ -750,7 +932,11 @@ function VideoJSPlayer() {
|
||||
label: track.label,
|
||||
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
|
||||
const goToNextVideo = () => {
|
||||
@@ -1078,8 +1264,6 @@ function VideoJSPlayer() {
|
||||
}
|
||||
|
||||
// BEGIN: Add subtitle tracks
|
||||
|
||||
|
||||
hasSubtitles && subtitleTracks.forEach((track) => {
|
||||
playerRef.current.addRemoteTextTrack(track, false);
|
||||
});
|
||||
@@ -1426,6 +1610,7 @@ function VideoJSPlayer() {
|
||||
customComponents.current.settingsMenu = new CustomSettingsMenu(playerRef.current, {
|
||||
userPreferences: userPreferences.current,
|
||||
qualities: availableQualities,
|
||||
hasSubtitles: hasSubtitles,
|
||||
});
|
||||
|
||||
// If qualities change per video (e.g., via MEDIA_DATA update), refresh menu
|
||||
|
||||
@@ -12,12 +12,7 @@ const mountComponents = () => {
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<div className='video-wrapper'>
|
||||
<div className='video-box'>
|
||||
<VideoJS />
|
||||
</div>
|
||||
{/* <ChapterList /> */}
|
||||
</div>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-6
@@ -3105,9 +3105,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz",
|
||||
"integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==",
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
|
||||
"integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -6393,9 +6393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-property-aliases-ecmascript": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
|
||||
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
|
||||
"integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
+9
-9
@@ -3787,9 +3787,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz",
|
||||
"integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==",
|
||||
"version": "2.8.4",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.4.tgz",
|
||||
"integrity": "sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
@@ -5858,9 +5858,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
@@ -13530,9 +13530,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unicode-property-aliases-ecmascript": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
|
||||
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
|
||||
"integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
|
||||
File diff suppressed because one or more lines are too long
+115
-120
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user