mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-12 18:38:52 -05: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:
parent
543085c38f
commit
5d99a4e23d
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(
|
||||
() => ({
|
||||
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',
|
||||
},
|
||||
]
|
||||
: [
|
||||
() => {
|
||||
// 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: getVideoSources(),
|
||||
};
|
||||
},
|
||||
[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,
|
||||
type,
|
||||
}));
|
||||
return normalize(buildFromBase);
|
||||
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
|
||||
@ -719,7 +901,7 @@ function VideoJSPlayer() {
|
||||
}));
|
||||
}, [mediaData]);
|
||||
|
||||
// Demo array for testing purposes
|
||||
// Demo array for testing purposes
|
||||
const demoSubtitleTracks = [
|
||||
{
|
||||
kind: 'subtitles',
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
12
frontend/packages/player/package-lock.json
generated
12
frontend/packages/player/package-lock.json
generated
@ -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": {
|
||||
|
||||
18
frontend/packages/scripts/package-lock.json
generated
18
frontend/packages/scripts/package-lock.json
generated
@ -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
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