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.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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -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": {
|
||||||
|
|||||||
+9
-9
@@ -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
+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