Files
mediacms/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
T

2159 lines
116 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef, useMemo } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';
// Import the separated components
import EndScreenOverlay from '../overlays/EndScreenOverlay';
import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay';
import ChapterMarkers from '../markers/ChapterMarkers';
import SpritePreview from '../markers/SpritePreview';
import NextVideoButton from '../controls/NextVideoButton';
import AutoplayToggleButton from '../controls/AutoplayToggleButton';
import CustomRemainingTime from '../controls/CustomRemainingTime';
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
import CustomSettingsMenu from '../controls/CustomSettingsMenu';
import SeekIndicator from '../controls/SeekIndicator';
import UserPreferences from '../../utils/UserPreferences';
function VideoJSPlayer() {
const videoRef = useRef(null);
const playerRef = useRef(null); // Track the player instance
const userPreferences = useRef(new UserPreferences()); // User preferences instance
const customComponents = useRef({}); // Store custom components for cleanup
const isDevelopment = process.env.NODE_ENV === 'development' || window.location.hostname === 'localhost';
// Safely access window.MEDIA_DATA with fallback using useMemo
const mediaData = useMemo(
() =>
typeof window !== 'undefined' && window.MEDIA_DATA
? 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": {},
},
__chapter_data: [
{ startTime: 0, endTime: 4, text: 'A1 test' },
{ startTime: 5, endTime: 10, text: 'A2 of Marine Life' },
{ startTime: 10, endTime: 15, text: 'A3 Reef Ecosystems' },
],
related_media: [
{
friendly_token: 'jgLkic37V',
url: 'http://localhost/view?m=jgLkic37V',
api_url: 'http://localhost/api/v1/media/jgLkic37V',
user: 'admin',
title: 'Screenshot20250312at15.58.35.png',
description: '',
add_date: '2025-03-12T14:28:24.757807Z',
views: 5,
media_type: 'image',
state: 'public',
duration: 0,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/ad827f53568c4565b69ee59da88966c8.Screenshot20250312at15.58.35.png.jpg',
is_reviewed: true,
preview_url: null,
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: null,
},
{
friendly_token: '54Ip78oPK',
url: 'http://localhost/view?m=54Ip78oPK',
api_url: 'http://localhost/api/v1/media/54Ip78oPK',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) 2',
description: '',
add_date: '2025-05-19T01:22:10.354407+01:00',
views: 8,
media_type: 'video',
state: 'public',
duration: 1,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/ca19044d26af4eada1669c3373c771cb_nhCvKcl.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/ca19044d26af4eada1669c3373c771cb.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '0.6MB',
},
{
friendly_token: '0VpZtWduU',
url: 'http://localhost/view?m=0VpZtWduU',
api_url: 'http://localhost/api/v1/media/0VpZtWduU',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) (Trimmed)',
description: '',
add_date: '2025-05-19T01:30:55.374863+01:00',
views: 13,
media_type: 'video',
state: 'public',
duration: 6,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/46b2bec8d79f496682640ce99b5c53af_RA16Oba.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/46b2bec8d79f496682640ce99b5c53af.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '4.0MB',
},
{
friendly_token: 'efM2aLP4P',
url: 'http://localhost/view?m=efM2aLP4P',
api_url: 'http://localhost/api/v1/media/efM2aLP4P',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) 1',
description: '',
add_date: '2025-05-19T01:22:08.117300+01:00',
views: 11,
media_type: 'video',
state: 'public',
duration: 8,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/c27f570a84d64cdc857f87c50dace993_Ma3ap4p.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/c27f570a84d64cdc857f87c50dace993.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 2,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '4.8MB',
},
{
friendly_token: '8RUHRjnji',
url: 'http://localhost/view?m=8RUHRjnji',
api_url: 'http://localhost/api/v1/media/8RUHRjnji',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed)',
description: '',
add_date: '2025-05-19T01:29:48.308159+01:00',
views: 7,
media_type: 'video',
state: 'public',
duration: 7,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/884cac6a306e4ae9995726fcd3c7aa49_fT7pnM4.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '4.6MB',
},
{
friendly_token: 'N5Vpnqfpf',
url: 'http://localhost/view?m=N5Vpnqfpf',
api_url: 'http://localhost/api/v1/media/N5Vpnqfpf',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov',
description: '',
add_date: '2025-05-19T01:15:03.594646+01:00',
views: 11,
media_type: 'video',
state: 'public',
duration: 37,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/e549ebb4b1dc41a48cf76ebccf592836_1xqZHHc.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/e549ebb4b1dc41a48cf76ebccf592836.tmpu6x4f3js.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '22.3MB',
},
{
friendly_token: 'MmyuYCEfC',
url: 'http://localhost/view?m=MmyuYCEfC',
api_url: 'http://localhost/api/v1/media/MmyuYCEfC',
user: 'admin',
title: 'JmailLogIdeasforYiannis.mp4',
description: 'check the 00:12 and then the 00:39',
add_date: '2025-03-12T02:37:42Z',
views: 49,
media_type: 'video',
state: 'public',
duration: 112,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/3d4e6f1c7e014720bc6390ed9d61aba5_BppzBiz.JmailLogIdeasforYiannis.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/3d4e6f1c7e014720bc6390ed9d61aba5.tmpcjd0bvr5.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '40.6MB',
},
{
friendly_token: 'E8ia0lfir',
url: 'http://localhost/view?m=E8ia0lfir',
api_url: 'http://localhost/api/v1/media/E8ia0lfir',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed)',
description: '',
add_date: '2025-05-19T01:22:27.251447+01:00',
views: 12,
media_type: 'video',
state: 'public',
duration: 37,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/8572a325a9d649f79cc60181fbece78c_0H5arHy.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/8572a325a9d649f79cc60181fbece78c.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '22.4MB',
},
{
friendly_token: 'eZAl84zg6',
url: 'http://localhost/view?m=eZAl84zg6',
api_url: 'http://localhost/api/v1/media/eZAl84zg6',
user: 'admin',
title: '20257855hd_1920_1080_60fps.mp4',
description: '',
add_date: '2025-03-12T02:35:59.779098Z',
views: 29,
media_type: 'video',
state: 'public',
duration: 90,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/ed9c61a72c0445debfbedee011b6eba1_8HjWGZX.20257855hd_1920_1080_60fps.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/ed9c61a72c0445debfbedee011b6eba1.tmpil72jnbv.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '64.4MB',
},
{
friendly_token: '6WShYNxZx',
url: 'http://localhost/view?m=6WShYNxZx',
api_url: 'http://localhost/api/v1/media/6WShYNxZx',
user: 'admin',
title: 'SampleVideo_1280x720_30mb.mp4 test abc',
description: 'test 123 yiannis',
add_date: '2025-04-14T00:00:00+01:00',
views: 43,
media_type: 'video',
state: 'public',
duration: 171,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/c868df836ac34688b876e741b58ada93_LRvu388.SampleVideo_1280x720_30mb.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/c868df836ac34688b876e741b58ada93.tmpyl_r01_l.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 3,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '31.5MB',
},
{
friendly_token: 'SOgLTsrAH',
url: 'http://localhost/view?m=SOgLTsrAH',
api_url: 'http://localhost/api/v1/media/SOgLTsrAH',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) (Trimmed) 1',
description: '',
add_date: '2025-05-19T01:31:11.057879+01:00',
views: 8,
media_type: 'video',
state: 'public',
duration: 2,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/46c75315947e4a5f9dd464c1721206fd_6KJzqBH.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/46c75315947e4a5f9dd464c1721206fd.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '1.5MB',
},
{
friendly_token: 'PoDk009ue',
url: 'http://localhost/view?m=PoDk009ue',
api_url: 'http://localhost/api/v1/media/PoDk009ue',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov',
description: '',
add_date: '2025-05-19T01:17:44.296445+01:00',
views: 12,
media_type: 'video',
state: 'public',
duration: 37,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/fd1d31460e614ca9b4645228e62c9eec_DB8nan8.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '22.4MB',
},
{
friendly_token: 'b7e06rUz5',
url: 'http://localhost/view?m=b7e06rUz5',
api_url: 'http://localhost/api/v1/media/b7e06rUz5',
user: 'admin',
title: 'Proto.cyTimesheet03_03_202507_03_2025.pdf',
description: '',
add_date: '2025-03-12T14:29:15.245599Z',
views: 16,
media_type: 'pdf',
state: 'public',
duration: 0,
thumbnail_url: null,
is_reviewed: true,
preview_url: null,
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: null,
},
{
friendly_token: 'wUu76zL5a',
url: 'http://localhost/view?m=wUu76zL5a',
api_url: 'http://localhost/api/v1/media/wUu76zL5a',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) 2 (Trimmed)',
description: '',
add_date: '2025-05-19T01:28:52.829907+01:00',
views: 2,
media_type: 'video',
state: 'public',
duration: 0,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/1b3e7dc2431b46a5b7e40b297662137a_1V0DJz5.ca19044d26af4eada1669c3373c771cb.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/1b3e7dc2431b46a5b7e40b297662137a.ca19044d26af4eada1669c3373c771cb.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '0.3MB',
},
{
friendly_token: 'JBjMHLfLZ',
url: 'http://localhost/view?m=JBjMHLfLZ',
api_url: 'http://localhost/api/v1/media/JBjMHLfLZ',
user: 'admin',
title: 'att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov (Trimmed) (Trimmed) 2',
description: '',
add_date: '2025-05-19T01:31:11.848403+01:00',
views: 19,
media_type: 'video',
state: 'public',
duration: 4,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/7759ca3e0ba1482aa9149201bd14139d_ISBMqD4.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.att.24Cz35hRdjJ9FV0RlxCPVVtc6XVAdJvrU4eb8Gkykc.mov.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/7759ca3e0ba1482aa9149201bd14139d.884cac6a306e4ae9995726fcd3c7aa49.fd1d31460e614ca9b4645228e62c9eec.tmpjohj96hu.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '2.5MB',
},
{
friendly_token: 'w13x5uUXc',
url: 'http://localhost/view?m=w13x5uUXc',
api_url: 'http://localhost/api/v1/media/w13x5uUXc',
user: 'admin',
title: 'Web357202503112025_05_33_AM.png',
description: '',
add_date: '2025-03-12T14:27:55.078360Z',
views: 3,
media_type: 'image',
state: 'public',
duration: 0,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/1da967bcdca540efa1d2362af2f3a34b.Web357202503112025_05_33_AM.png.jpg',
is_reviewed: true,
preview_url: null,
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: null,
},
{
friendly_token: 'sZxPpdon5',
url: 'http://localhost/view?m=sZxPpdon5',
api_url: 'http://localhost/api/v1/media/sZxPpdon5',
user: 'admin',
title: 'JmailLogIdeasforYiannis.mp4 (Trimmed)',
description: 'check the 00:12 and then the 00:39',
add_date: '2025-05-15T02:16:19.580745+01:00',
views: 9,
media_type: 'video',
state: 'public',
duration: 17,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/7191a59cbfcd402ebed400e604a76840_5WRh0cp.3d4e6f1c7e014720bc6390ed9d61aba5.JmailLogIdeasforYiannis.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/7191a59cbfcd402ebed400e604a76840.3d4e6f1c7e014720bc6390ed9d61aba5.tmpcjd0bvr5.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '4.5MB',
},
{
friendly_token: 'TCycDZROI',
url: 'http://localhost/view?m=TCycDZROI',
api_url: 'http://localhost/api/v1/media/TCycDZROI',
user: 'admin',
title: 'JmailLogIdeasforYiannis.mp4 (Trimmed)',
description: 'check the 00:12 and then the 00:39',
add_date: '2025-05-15T02:14:58.855935+01:00',
views: 8,
media_type: 'video',
state: 'public',
duration: 112,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/5cbfbe93d06c453487c65d9d54749318_CUIZl8y.3d4e6f1c7e014720bc6390ed9d61aba5.JmailLogIdeasforYiannis.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/5cbfbe93d06c453487c65d9d54749318.3d4e6f1c7e014720bc6390ed9d61aba5.tmpcjd0bvr5.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 1,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '40.6MB',
},
{
friendly_token: 'YjGJafibO',
url: 'http://localhost/view?m=YjGJafibO',
api_url: 'http://localhost/api/v1/media/YjGJafibO',
user: 'admin',
title: 'file_example_MP4_480_1_5MG.mp4',
description: '',
add_date: '2025-04-14T00:07:11.074523+01:00',
views: 18,
media_type: 'video',
state: 'public',
duration: 30,
thumbnail_url:
'http://localhost/media/original/thumbnails/user/admin/e76f4fff2b3d42adbadb0f1204b21b81_s8EEvSE.file_example_MP4_480_1_5MG.mp4.jpg',
is_reviewed: true,
preview_url:
'/media/encoded/1/admin/e76f4fff2b3d42adbadb0f1204b21b81.tmp04azp_tc.gif',
author_name: '',
author_profile: 'http://localhost/user/admin/',
author_thumbnail: 'http://localhost/media/userlogos/user.jpg',
encoding_status: 'success',
likes: 2,
dislikes: 0,
reported_times: 0,
featured: false,
user_featured: false,
size: '1.6MB',
},
],
},
previewSprite: {
url: 'https://demo.mediacms.io/media/original/thumbnails/user/markos/fe4933d67b884d4da507dd60e77f7438.VID_20200909_141053.mp4sprites.jpg',
frame: { width: 160, height: 90, seconds: 10 },
},
siteUrl: '',
nextLink: 'https://demo.mediacms.io/view?m=YjGJafibO',
},
[]
);
// Define chapters as JSON object
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
// CONDITIONAL LOGIC:
// - When chaptersData has content: Uses original ChapterMarkers with sprite preview
// - When chaptersData is empty: Uses separate SpritePreview component
// Toggle between these two lines to test both scenarios:
const chaptersData = mediaData?.data?.chapter_data && mediaData?.data?.chapter_data.length > 0 ? mediaData?.data?.chapter_data : isDevelopment ? [
{ 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' },
{ startTime: 480, endTime: 560, text: 'Sustainable Fishing' },
{ startTime: 560, endTime: 640, text: 'Research Methods' },
{ startTime: 640, endTime: 720, text: 'Future Challenges' },
{ startTime: 720, endTime: 800, text: 'Conclusion' },
{ startTime: 800, endTime: 880, text: 'Marine Biodiversity Hotspots' },
{ startTime: 880, endTime: 960, text: 'Underwater Photography' },
{ startTime: 960, endTime: 1040, text: 'Whale Migration Patterns' },
{ startTime: 1040, endTime: 1120, text: 'Plastic Pollution Crisis' },
{ startTime: 1120, endTime: 1200, text: 'Seagrass Meadows' },
{ startTime: 1200, endTime: 1280, text: 'Ocean Acidification' },
{ startTime: 1280, endTime: 1360, text: 'Marine Archaeology' },
{ startTime: 1360, endTime: 1440, text: 'Tidal Pool Ecosystems' },
{ startTime: 1440, endTime: 1520, text: 'Commercial Aquaculture' },
{ startTime: 1520, endTime: 1600, text: 'Ocean Exploration Technology' },
] : [];
// const chaptersData = []; // NO CHAPTERS (uses separate SpritePreview)
// 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: getVideoSources(),
};
},
[mediaData]
);
// Compute available qualities. Prefer JSON (mediaData.data.qualities), otherwise build from encodings_info or current source.
const availableQualities = useMemo(() => {
// 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) => ({
label: q.label || q.value || 'Auto',
value: (q.value || q.label || 'auto').toString().toLowerCase(),
src: q.src || q.url || q.href,
type: q.type || 'video/mp4',
}));
// 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;
};
validQualities.sort((a, b) => idx(a.value) - idx(b.value));
return validQualities;
};
const jsonList = mediaData?.data?.qualities;
if (Array.isArray(jsonList) && jsonList.length) {
return normalize(jsonList);
}
// 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';
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
const relatedVideos = useMemo(() => {
if (!mediaData?.data?.related_media) {
return [];
}
return mediaData.data.related_media
.slice(0, 12) // Limit to maximum 12 items
.map((media) => ({
id: media.friendly_token,
title: media.title,
author: media.user || media.author_name || 'Unknown',
views: `${media.views} views`,
thumbnail: media.thumbnail_url || media.author_thumbnail,
category: media.media_type,
url: media.url,
duration: media.duration,
size: media.size,
likes: media.likes,
dislikes: media.dislikes,
add_date: media.add_date,
description: media.description,
}));
}, [mediaData]);
// Demo array for testing purposes
const demoSubtitleTracks = [
{
kind: 'subtitles',
src: '/sample-subtitles.vtt',
srclang: 'en',
label: 'English Subtitles',
default: false,
},
{
kind: 'subtitles',
src: '/sample-subtitles-greek.vtt',
srclang: 'el',
label: 'Greek Subtitles (Ελληνικά)',
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 || (isDevelopment ? demoSubtitleTracks : []);
const hasSubtitles = backendSubtitles.length > 0;
const subtitleTracks = hasSubtitles
? backendSubtitles.map(track => ({
kind: 'subtitles',
src: track.src,
srclang: track.srclang,
label: track.label,
default: false,
}))
: [];
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 = () => {
console.log('Next video functionality disabled for single video mode');
if (mediaData.onClickNextCallback && typeof mediaData.onClickNextCallback === 'function') {
mediaData.onClickNextCallback();
}
};
useEffect(() => {
// Only initialize if we don't already have a player and element exists
if (videoRef.current && !playerRef.current) {
// Check if element is already a Video.js player
if (videoRef.current.player) {
// console.log('Video.js already initialized on this element');
return;
}
const timer = setTimeout(() => {
// Double-check that we still don't have a player and element exists
if (!playerRef.current && videoRef.current && !videoRef.current.player) {
playerRef.current = videojs(videoRef.current, {
// ===== STANDARD <video> ELEMENT OPTIONS =====
// Controls whether player has user-interactive controls
controls: true,
// Player dimensions - removed for responsive design
// Autoplay behavior: Use 'muted' to comply with browser policies
autoplay: 'muted',
// Start video over when it ends
loop: false,
// Start video muted
muted: false,
// Poster image URL displayed before video starts
poster: currentVideo.poster,
// Preload behavior: 'auto', 'metadata', 'none'
preload: 'auto',
// Video sources from current video
sources: currentVideo.sources,
// ===== VIDEO.JS-SPECIFIC OPTIONS =====
// Aspect ratio for fluid mode (e.g., '16:9', '4:3')
aspectRatio: '16:9',
// Hide all components except control bar for audio-only mode
audioOnlyMode: false,
// Display poster persistently for audio poster mode
audioPosterMode: false,
// Prevent autoSetup for elements with data-setup attribute
autoSetup: undefined,
// Custom breakpoints for responsive design
breakpoints: {
tiny: 210,
xsmall: 320,
small: 425,
medium: 768,
large: 1440,
xlarge: 2560,
huge: 2561,
},
// Disable picture-in-picture mode
disablePictureInPicture: false,
// Enable document picture-in-picture API
enableDocumentPictureInPicture: false,
// Enable smooth seeking experience
enableSmoothSeeking: false,
// Use experimental SVG icons instead of font icons
experimentalSvgIcons: false,
// Make player scale to fit container
fluid: true,
// Fullscreen options
fullscreen: {
options: {
navigationUI: 'hide',
},
},
// Player element ID
id: undefined,
// Milliseconds of inactivity before user considered inactive (0 = never)
inactivityTimeout: 2000,
// Language code for player (e.g., 'en', 'es', 'fr')
language: 'en',
// Custom language definitions
languages: {},
// Enable live UI with progress bar and live edge button
liveui: false,
// Live tracker options
liveTracker: {
trackingThreshold: 20, // Seconds threshold for showing live UI
liveTolerance: 15, // Seconds tolerance for being "live"
},
// Force native controls for touch devices
nativeControlsForTouch: false,
// Normalize autoplay behavior
normalizeAutoplay: false,
// Custom message when media cannot be played
notSupportedMessage: undefined,
// Prevent title attributes on UI elements for better accessibility
noUITitleAttributes: false,
// Array of playback speed options (e.g., [0.5, 1, 1.5, 2])
playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
// Prefer non-fullscreen playback on mobile
playsinline: true,
// Plugin initialization options
plugins: {},
// Control poster image display
posterImage: true,
// Prefer full window over fullscreen on some devices
preferFullWindow: false,
// Enable responsive player based on breakpoints
responsive: true,
// Restore element when player is disposed
restoreEl: false,
// Suppress "not supported" error until user interaction
suppressNotSupportedError: false,
// Allow techs to override poster
techCanOverridePoster: false,
// Order of preferred playback technologies
techOrder: ['html5'],
// User interaction options
userActions: {
// Enable/disable or customize click behavior
click: true,
// Enable/disable or customize double-click behavior (fullscreen toggle)
doubleClick: true,
// Hotkey configuration
hotkeys: {
// Function to override fullscreen key (default: 'f')
fullscreenKey: function (event) {
return event.which === 70; // 'f' key
},
// Function to override mute key (default: 'm')
muteKey: function (event) {
return event.which === 77; // 'm' key
},
// Function to override play/pause key (default: 'k' and Space)
playPauseKey: function (event) {
return event.which === 75 || event.which === 32; // 'k' or Space
},
// Custom seek functions for arrow keys
seekForwardKey: function (event) {
return event.which === 39; // Right arrow key
},
seekBackwardKey: function (event) {
return event.which === 37; // Left arrow key
},
},
},
// URL to vtt.js for WebVTT support
'vtt.js': undefined,
// Spatial navigation for smart TV/remote control navigation
spatialNavigation: {
enabled: false,
horizontalSeek: false,
},
// ===== CONTROL BAR OPTIONS =====
controlBar: {
progressControl: {
seekBar: {
timeTooltip: {
// Customize TimeTooltip behavior
displayNegative: false, // Don't show negative time
},
},
},
// Remaining time display configuration
remainingTimeDisplay: false,
/* remainingTimeDisplay: {
displayNegative: true,
}, */
// Volume panel configuration
volumePanel: {
inline: true, // Display volume control inline
vertical: false, // Use horizontal volume slider
},
// Fullscreen toggle button
fullscreenToggle: true,
// Picture-in-picture toggle button
pictureInPictureToggle: true,
// Remove default playback speed dropdown from control bar
playbackRateMenuButton: false,
// Descriptions button
descriptionsButton: true,
// Subtitles (CC) button should be visible
subtitlesButton: hasSubtitles ? true : false,
// Captions button (keep disabled to avoid duplicate with subtitles)
captionsButton: false,
// Audio track button
audioTrackButton: true,
// Live display
liveDisplay: true,
// Seek to live button
seekToLive: true,
// Custom control spacer
customControlSpacer: true,
// Chapters menu button (moved after subtitles/captions)
chaptersButton: true,
},
// ===== HTML5 TECH OPTIONS =====
html5: {
// Force native controls for touch devices
nativeControlsForTouch: false,
// Use native audio tracks instead of emulated
nativeAudioTracks: true,
// Use native text tracks instead of emulated
nativeTextTracks: true,
// Use native video tracks instead of emulated
nativeVideoTracks: true,
// Preload text tracks
preloadTextTracks: true,
},
// ===== COMPONENT CONFIGURATION =====
children: [
'mediaLoader',
'posterImage',
'textTrackDisplay',
'loadingSpinner',
'bigPlayButton',
'liveTracker',
'controlBar',
'errorDisplay',
'textTrackSettings',
'resizeManager',
],
});
// Event listeners
/* playerRef.current.on('ready', () => {
console.log('Video.js player ready');
}); */
playerRef.current.ready(() => {
// Apply user preferences to player
userPreferences.current.applyToPlayer(playerRef.current);
// Set up auto-save for preference changes
userPreferences.current.setupAutoSave(playerRef.current);
// Get control bar and its children
const controlBar = playerRef.current.getChild('controlBar');
const playToggle = controlBar.getChild('playToggle');
const currentTimeDisplay = controlBar.getChild('currentTimeDisplay');
const progressControl = controlBar.getChild('progressControl');
const seekBar = progressControl.getChild('seekBar');
const chaptersButton = controlBar.getChild('chaptersButton');
const fullscreenToggle = controlBar.getChild('fullscreenToggle');
// Auto-play video when navigating from next button
const urlParams = new URLSearchParams(window.location.search);
const hasVideoParam = urlParams.get('m');
if (hasVideoParam) {
// Small delay to ensure everything is loaded
setTimeout(() => {
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.play().catch((error) => {
console.log('️ Browser prevented autoplay (normal behavior):', error.message);
});
}
}, 100);
}
// BEGIN: Add subtitle tracks
hasSubtitles && subtitleTracks.forEach((track) => {
playerRef.current.addRemoteTextTrack(track, false);
});
// Apply saved subtitle preference immediately
userPreferences.current.applySubtitlePreference(playerRef.current);
// END: Add subtitle tracks
// BEGIN: Chapters Implementation
if (chaptersData && chaptersData.length > 0) {
const chaptersTrack = playerRef.current.addTextTrack('chapters', 'Chapters', 'en');
// Add cues to the chapters track
chaptersData.forEach((chapter) => {
const cue = new (window.VTTCue || window.TextTrackCue)(
chapter.startTime,
chapter.endTime,
chapter.text
);
chaptersTrack.addCue(cue);
});
}
// END: Chapters Implementation
// Force chapter markers update after chapters are loaded
/* setTimeout(() => {
if (chapterMarkers && chapterMarkers.updateChapterMarkers) {
chapterMarkers.updateChapterMarkers();
}
}, 500); */
// BEGIN: Implement custom time display component
const customRemainingTime = new CustomRemainingTime(playerRef.current, {
displayNegative: false,
customPrefix: '',
customSuffix: '',
});
// Insert it in the desired position (e.g., after current time display)
if (currentTimeDisplay) {
const currentTimeIndex = controlBar.children().indexOf(currentTimeDisplay);
controlBar.addChild(customRemainingTime, {}, currentTimeIndex + 1);
} else {
controlBar.addChild(customRemainingTime, {}, 2);
}
// END: Implement custom time display component
// BEGIN: Implement custom next video button
if (mediaData?.nextLink || 1===1) { // it seems that the nextLink is not always available, and it is need the this.player().trigger('nextVideo'); from NextVideoButton.js // TODO: remove the 1===1 and the mediaData?.nextLink
console.log('mediaData.nextLink edw', mediaData.nextLink);
const nextVideoButton = new NextVideoButton(playerRef.current, {
nextLink: mediaData.nextLink,
});
const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
controlBar.addChild(nextVideoButton, {}, playToggleIndex + 1);
}
// END: Implement custom next video button
// BEGIN: Implement autoplay toggle button - simplified
try {
const autoplayToggleButton = new AutoplayToggleButton(playerRef.current, {
userPreferences: userPreferences.current,
});
// Add it after the play button
const fullscreenToggleIndex = controlBar.children().indexOf(fullscreenToggle);
controlBar.addChild(autoplayToggleButton, {}, fullscreenToggleIndex - 1);
// Store reference for later use
customComponents.current.autoplayToggleButton = autoplayToggleButton;
// Force update icon after adding to DOM to ensure correct display
setTimeout(() => {
autoplayToggleButton.updateIcon();
console.log('✓ Autoplay toggle button icon updated after DOM insertion');
}, 100);
console.log('✓ Autoplay toggle button added successfully');
} catch (error) {
console.error('✗ Failed to add autoplay toggle button:', error);
}
// END: Implement autoplay toggle button
// Remove duplicate captions button and move chapters to end
/* const cleanupControls = () => {
// Log all current children for debugging
const allChildren = controlBar.children();
// Try to find and remove captions/subs-caps button (but keep subtitles)
const possibleCaptionButtons = ['captionsButton', 'subsCapsButton'];
possibleCaptionButtons.forEach((buttonName) => {
const button = controlBar.getChild(buttonName);
if (button) {
try {
controlBar.removeChild(button);
console.log(`✓ Removed ${buttonName}`);
} catch (e) {
console.log(`✗ Failed to remove ${buttonName}:`, e);
}
}
});
// Alternative: hide buttons we can't remove
allChildren.forEach((child, index) => {
const name = (child.name_ || child.constructor.name || '').toLowerCase();
if (name.includes('caption') && !name.includes('subtitle')) {
child.hide();
console.log(`✓ Hidden button at index ${index}: ${name}`);
}
});
// Move chapters button to the very end
const chaptersButton = controlBar.getChild('chaptersButton');
if (chaptersButton) {
try {
controlBar.removeChild(chaptersButton);
controlBar.addChild(chaptersButton);
console.log('✓ Chapters button moved to last position');
} catch (e) {
console.log('✗ Failed to move chapters button:', e);
}
}
}; */
// Try multiple times with different delays
/* setTimeout(cleanupControls, 200);
setTimeout(cleanupControls, 500);
setTimeout(cleanupControls, 1000); */
// Make menus clickable instead of hover-only
setTimeout(() => {
const setupClickableMenus = () => {
// Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay
const menuButtons = ['subtitlesButton', 'playbackRateMenuButton'];
menuButtons.forEach((buttonName) => {
const button = controlBar.getChild(buttonName);
if (button && button.menuButton_) {
// Override the menu button behavior
const menuButton = button.menuButton_;
// Disable hover events
menuButton.off('mouseenter');
menuButton.off('mouseleave');
// Add click-to-toggle behavior
menuButton.on('click', function () {
if (this.menu.hasClass('vjs-lock-showing')) {
this.menu.removeClass('vjs-lock-showing');
this.menu.hide();
} else {
this.menu.addClass('vjs-lock-showing');
this.menu.show();
}
});
console.log(`✓ Made ${buttonName} clickable`);
} else if (button) {
// For buttons without menuButton_ property
const buttonEl = button.el();
if (buttonEl) {
// Add click handler to show/hide menu
buttonEl.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const menu = buttonEl.querySelector('.vjs-menu');
if (menu) {
if (menu.style.display === 'block') {
menu.style.display = 'none';
} else {
// Hide other menus first
document.querySelectorAll('.vjs-menu').forEach((m) => {
if (m !== menu) m.style.display = 'none';
});
menu.style.display = 'block';
}
}
});
console.log(`✓ Added click handler to ${buttonName}`);
}
}
});
// Add YouTube-like subtitles toggle with red underline
const ccNames = ['subtitlesButton', 'captionsButton', 'subsCapsButton'];
for (const n of ccNames) {
const cc = controlBar.getChild(n);
if (cc && cc.el()) {
const el = cc.el();
const menu = el.querySelector('.vjs-menu');
if (menu) menu.style.display = 'none';
const toggleSubs = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const tracks = playerRef.current.textTracks();
let any = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles' && t.mode === 'showing') { any = true; break; }
}
if (any) {
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') t.mode = 'disabled';
}
el.classList.remove('vjs-subs-active');
// Do not change saved language on quick toggle off; save enabled=false
try { userPreferences.current.setPreference('subtitleEnabled', false, true); } catch (e) {}
} else {
// Show using previously chosen language only; do not change it
const preferred = userPreferences.current.getPreference('subtitleLanguage');
if (!preferred) {
// If no language chosen yet, enable first available and save it
let first = null;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') { first = t.language; break; }
}
if (first) {
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') t.mode = t.language === first ? 'showing' : 'disabled';
}
try { userPreferences.current.setPreference('subtitleLanguage', first, true); } catch (e) {}
try { userPreferences.current.setPreference('subtitleEnabled', true, true); } catch (e) {}
el.classList.add('vjs-subs-active');
}
return;
}
let found = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') {
const show = t.language === preferred;
t.mode = show ? 'showing' : 'disabled';
if (show) found = true;
}
}
if (found) {
el.classList.add('vjs-subs-active');
try { userPreferences.current.setPreference('subtitleEnabled', true, true); } catch (e) {}
}
}
};
el.addEventListener('click', toggleSubs, { capture: true });
// Add mobile touch support
el.addEventListener('touchend', (e) => {
e.preventDefault();
e.stopPropagation();
toggleSubs(e);
}, { passive: false });
// Sync underline state on external changes
playerRef.current.on('texttrackchange', () => {
const tracks = playerRef.current.textTracks();
let any = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles' && t.mode === 'showing') { any = true; break; }
}
if (any) el.classList.add('vjs-subs-active'); else el.classList.remove('vjs-subs-active');
});
// Initialize state immediately
const tracks = playerRef.current.textTracks();
let any = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles' && t.mode === 'showing') { any = true; break; }
}
if (any) el.classList.add('vjs-subs-active');
break;
}
}
};
setupClickableMenus();
}, 1500);
// BEGIN: Add chapter markers and sprite preview to progress control
if (progressControl && seekBar) {
console.log('Setting up sprite preview and chapter markers...');
console.log('mediaData.previewSprite:', mediaData.previewSprite);
console.log('chaptersData:', chaptersData);
// Check if we have chapters
const hasChapters = chaptersData && chaptersData.length > 0;
if (hasChapters) {
// Use original ChapterMarkers with sprite functionality when chapters exist
console.log('✓ Adding ChapterMarkers component with sprite functionality (chapters exist)');
const chapterMarkers = new ChapterMarkers(playerRef.current, {
previewSprite: mediaData.previewSprite,
});
seekBar.addChild(chapterMarkers);
} else if (mediaData.previewSprite) {
// Use separate SpritePreview component only when no chapters but sprite data exists
console.log('✓ Adding SpritePreview component (no chapters, but sprite data available)');
const spritePreview = new SpritePreview(playerRef.current, {
previewSprite: mediaData.previewSprite,
});
seekBar.addChild(spritePreview);
// Setup sprite preview hover functionality
setTimeout(() => {
console.log('✓ Setting up sprite preview hover functionality');
spritePreview.setupProgressBarHover();
}, 100);
} else {
console.log('✗ No chapters and no sprite data available');
}
}
// END: Add chapter markers and sprite preview to progress control
// BEGIN: Simple button layout fix - use CSS approach
setTimeout(() => {
console.log('Setting up simplified button layout...');
// Add a simple spacer div using DOM manipulation (simpler approach)
const spacerDiv = document.createElement('div');
spacerDiv.className = 'vjs-spacer-control vjs-control';
spacerDiv.style.flex = '1';
spacerDiv.style.minWidth = '1px';
spacerDiv.style.height = '100%';
// Find insertion point after duration display
const durationDisplay = controlBar.getChild('durationDisplay');
if (durationDisplay && durationDisplay.el()) {
const controlBarEl = controlBar.el();
const durationEl = durationDisplay.el();
const nextSibling = durationEl.nextSibling;
controlBarEl.insertBefore(spacerDiv, nextSibling);
console.log('✓ Simple spacer added after duration display');
}
}, 300);
// END: Simple button layout fix
// BEGIN: Move chapters button after fullscreen toggle
if (chaptersButton && fullscreenToggle) {
try {
const fullscreenIndex = controlBar.children().indexOf(fullscreenToggle);
controlBar.addChild(chaptersButton, {}, fullscreenIndex + 1);
console.log('✓ Chapters button moved after fullscreen toggle');
} catch (e) {
console.log('✗ Failed to move chapters button:', e);
}
}
// END: Move chapters button after fullscreen toggle
console.log('chaptersData', chaptersData);
// BEGIN: Add Chapters Overlay Component
if (chaptersData && chaptersData.length > 0) {
customComponents.current.chaptersOverlay = new CustomChaptersOverlay(playerRef.current, {
chaptersData: chaptersData,
seriesTitle: mediaData?.data?.title || 'Chapters',
channelName: mediaData?.data?.user || mediaData?.data?.author_name || 'Channel',
thumbnail: mediaData?.data?.thumbnail_url || mediaData?.data?.author_thumbnail || '',
});
} else {
console.log('⚠ No chapters data available for overlay');
}
// END: Add Chapters Overlay Component
// BEGIN: Add Settings Menu Component
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
try {
playerRef.current.on('loadedmetadata', () => {
if (customComponents.current.settingsMenu && customComponents.current.settingsMenu.setQualities) {
const md = typeof window !== 'undefined' ? window.MEDIA_DATA : null;
const newQualities = md?.data?.qualities || availableQualities;
customComponents.current.settingsMenu.setQualities(newQualities);
}
});
} catch (e) {}
// END: Add Settings Menu Component
// BEGIN: Add Seek Indicator Component
customComponents.current.seekIndicator = new SeekIndicator(playerRef.current, {
seekAmount: 5, // 5 seconds seek amount
});
// Add the component but ensure it's hidden initially
playerRef.current.addChild(customComponents.current.seekIndicator);
customComponents.current.seekIndicator.hide(); // Explicitly hide on creation
// END: Add Seek Indicator Component
// Store components reference for potential cleanup
// BEGIN: Add custom arrow key seek functionality
const handleKeyDown = (event) => {
// Only handle if the player has focus or no input elements are focused
const activeElement = document.activeElement;
const isInputFocused =
activeElement &&
(activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.contentEditable === 'true');
if (isInputFocused) {
return; // Don't interfere with input fields
}
const seekAmount = 5; // 5 seconds
if (event.key === 'ArrowRight' || event.keyCode === 39) {
event.preventDefault();
const currentTime = playerRef.current.currentTime();
const duration = playerRef.current.duration();
const newTime = Math.min(currentTime + seekAmount, duration);
playerRef.current.currentTime(newTime);
customComponents.current.seekIndicator.show('forward', seekAmount);
} else if (event.key === 'ArrowLeft' || event.keyCode === 37) {
event.preventDefault();
const currentTime = playerRef.current.currentTime();
const newTime = Math.max(currentTime - seekAmount, 0);
playerRef.current.currentTime(newTime);
customComponents.current.seekIndicator.show('backward', seekAmount);
}
};
// Add keyboard event listener to the document
document.addEventListener('keydown', handleKeyDown);
// Store cleanup function
customComponents.current.cleanupKeyboardHandler = () => {
document.removeEventListener('keydown', handleKeyDown);
};
// END: Add custom arrow key seek functionality
// Log current user preferences
console.log('Current user preferences:', userPreferences.current.getPreferences());
window.debugSubtitles = {
showTracks: () => {
const textTracks = playerRef.current.textTracks();
console.log('=== Available Text Tracks ===');
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
console.log(
`${i}: ${track.kind} | ${track.language} | ${track.label} | mode: ${track.mode}`
);
}
},
enableEnglish: () => {
const textTracks = playerRef.current.textTracks();
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === 'subtitles' && track.language === 'en') {
track.mode = 'showing';
console.log('Enabled English subtitles');
break;
}
}
},
enableGreek: () => {
const textTracks = playerRef.current.textTracks();
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === 'subtitles' && track.language === 'el') {
track.mode = 'showing';
console.log('Enabled Greek subtitles');
break;
}
}
},
disableAll: () => {
const textTracks = playerRef.current.textTracks();
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === 'subtitles') {
track.mode = 'disabled';
}
}
console.log('Disabled all subtitles');
},
getPrefs: () => {
console.log('Saved preferences:', userPreferences.current.getPreferences());
},
reapplyPrefs: () => {
userPreferences.current.applySubtitlePreference(playerRef.current);
},
showMenu: () => {
const controlBar = playerRef.current.getChild('controlBar');
// Try different button names
const possibleNames = ['subtitlesButton', 'captionsButton', 'subsCapsButton'];
let subtitlesButton = null;
for (const name of possibleNames) {
const button = controlBar.getChild(name);
if (button) {
console.log(`Found subtitle button: ${name}`);
subtitlesButton = button;
break;
}
}
if (subtitlesButton && subtitlesButton.menu) {
console.log('=== Subtitle Menu Items ===');
subtitlesButton.menu.children_.forEach((item, index) => {
if (item.track) {
console.log(
`${index}: ${item.track.label} (${item.track.language}) - selected: ${item.selected()}`
);
} else {
console.log(
`${index}: ${item.label || 'Unknown'} - selected: ${item.selected()}`
);
}
});
} else {
console.log('No subtitle menu found, checking DOM...');
// Check DOM for subtitle menu items
const menuItems = playerRef.current.el().querySelectorAll('.vjs-menu-item');
console.log(`Found ${menuItems.length} menu items in DOM`);
menuItems.forEach((item, index) => {
if (
item.textContent.toLowerCase().includes('subtitle') ||
item.textContent.toLowerCase().includes('caption') ||
item.textContent.toLowerCase().includes('off')
) {
console.log(
`DOM item ${index}: ${item.textContent} - classes: ${item.className}`
);
}
});
}
},
testMenuClick: (index) => {
const controlBar = playerRef.current.getChild('controlBar');
const possibleNames = ['subtitlesButton', 'captionsButton', 'subsCapsButton'];
let subtitlesButton = null;
for (const name of possibleNames) {
const button = controlBar.getChild(name);
if (button) {
subtitlesButton = button;
break;
}
}
if (subtitlesButton && subtitlesButton.menu && subtitlesButton.menu.children_[index]) {
const menuItem = subtitlesButton.menu.children_[index];
console.log('Simulating click on menu item:', index);
menuItem.handleClick();
} else {
console.log('Menu item not found at index:', index, 'trying DOM approach...');
// Try DOM approach
const menuItems = playerRef.current.el().querySelectorAll('.vjs-menu-item');
const subtitleItems = Array.from(menuItems).filter(
(item) =>
item.textContent.toLowerCase().includes('subtitle') ||
item.textContent.toLowerCase().includes('caption') ||
item.textContent.toLowerCase().includes('off')
);
if (subtitleItems[index]) {
console.log('Clicking DOM element:', subtitleItems[index].textContent);
subtitleItems[index].click();
} else {
console.log('No DOM subtitle item found at index:', index);
}
}
},
forceEnableEnglish: () => {
console.log('Force enabling English subtitles...');
const textTracks = playerRef.current.textTracks();
for (let i = 0; i < textTracks.length; i++) {
const track = textTracks[i];
if (track.kind === 'subtitles') {
track.mode = track.language === 'en' ? 'showing' : 'disabled';
}
}
userPreferences.current.setPreference('subtitleLanguage', 'en');
console.log('English subtitles enabled and saved');
},
watchSubtitleChanges: () => {
console.log('👀 Watching subtitle preference changes...');
const originalSetPreference = userPreferences.current.setPreference;
userPreferences.current.setPreference = function (key, value) {
if (key === 'subtitleLanguage') {
console.log(`🎯 SUBTITLE CHANGE: ${value} at ${new Date().toISOString()}`);
console.trace('Change origin:');
}
return originalSetPreference.call(this, key, value);
};
console.log('Subtitle change monitoring activated');
},
checkRestorationFlag: () => {
console.log('Restoration flag:', userPreferences.current.isRestoringSubtitles);
console.log('Auto-save disabled:', userPreferences.current.subtitleAutoSaveDisabled);
},
forceSaveGreek: () => {
console.log('🚀 Force saving Greek subtitle preference...');
userPreferences.current.forceSetSubtitleLanguage('el');
console.log('Check result:', userPreferences.current.getPreferences());
},
forceSaveEnglish: () => {
console.log('🚀 Force saving English subtitle preference...');
userPreferences.current.forceSetSubtitleLanguage('en');
console.log('Check result:', userPreferences.current.getPreferences());
},
forceSaveNull: () => {
console.log('🚀 Force saving null subtitle preference...');
userPreferences.current.forceSetSubtitleLanguage(null);
console.log('Check result:', userPreferences.current.getPreferences());
},
};
});
// Listen for next video event
playerRef.current.on('nextVideo', () => {
console.log('Next video requested');
goToNextVideo();
});
playerRef.current.on('play', () => {
console.log('Video started playing');
// Only show play indicator if not changing quality
if (!playerRef.current.isChangingQuality) {
customComponents.current.seekIndicator.show('play');
}
});
playerRef.current.on('pause', () => {
console.log('Video paused');
// Only show pause indicator if not changing quality
if (!playerRef.current.isChangingQuality) {
customComponents.current.seekIndicator.show('pause');
}
});
// Store reference to end screen and autoplay countdown for cleanup
let endScreen = null;
let autoplayCountdown = null;
playerRef.current.on('ended', () => {
console.log('Video ended');
console.log('Available relatedVideos:', relatedVideos);
// Keep controls active after video ends
setTimeout(() => {
if (playerRef.current && !playerRef.current.isDisposed()) {
// Remove vjs-ended class if it disables controls
const playerEl = playerRef.current.el();
if (playerEl) {
// Keep the visual ended state but ensure controls work
const controlBar = playerRef.current.getChild('controlBar');
if (controlBar) {
controlBar.show();
controlBar.el().style.opacity = '1';
controlBar.el().style.pointerEvents = 'auto';
}
}
}
}, 50);
console.log('mediaData.previewSprite', mediaData.previewSprite);
console.log('mediaData.nextLink', mediaData.nextLink);
console.log('userPreferences', userPreferences);
// Check if autoplay is enabled and there's a next video
const isAutoplayEnabled = userPreferences.current.getAutoplayPreference();
const hasNextVideo = mediaData.nextLink !== null;
console.log('isAutoplayEnabled', isAutoplayEnabled);
console.log('hasNextVideo', hasNextVideo);
if (isAutoplayEnabled && hasNextVideo) {
// Get next video data for countdown display - find the next video in related videos
let nextVideoData = {
title: 'Next Video',
author: '',
duration: 0,
thumbnail: '',
};
// Try to find the next video by URL matching or just use the first related video
if (relatedVideos.length > 0) {
// For now, use the first related video as the next video
// In a real implementation, you might want to find the specific next video
const nextVideo = relatedVideos[0];
nextVideoData = {
title: nextVideo.title || 'Next Video',
author: nextVideo.author || '',
duration: nextVideo.duration || 0,
thumbnail: nextVideo.thumbnail || '',
};
}
// Clean up any existing overlays
if (endScreen) {
playerRef.current.removeChild(endScreen);
endScreen = null;
}
if (autoplayCountdown) {
playerRef.current.removeChild(autoplayCountdown);
autoplayCountdown = null;
}
// Show autoplay countdown
autoplayCountdown = new AutoplayCountdownOverlay(playerRef.current, {
nextVideoData: nextVideoData,
countdownSeconds: 5,
onPlayNext: () => {
console.log('Autoplay: Navigating to next video');
goToNextVideo();
},
onCancel: () => {
console.log('Autoplay: User cancelled, showing related videos');
// Hide countdown and show end screen instead
if (autoplayCountdown) {
playerRef.current.removeChild(autoplayCountdown);
autoplayCountdown = null;
}
showEndScreen();
},
});
playerRef.current.addChild(autoplayCountdown);
autoplayCountdown.startCountdown();
} else {
// Autoplay disabled or no next video - show regular end screen
showEndScreen();
}
// Function to show the regular end screen
function showEndScreen() {
// Prevent creating multiple end screens
if (endScreen) {
console.log('End screen already exists, removing previous one');
playerRef.current.removeChild(endScreen);
endScreen = null;
}
// Show end screen with related videos
endScreen = new EndScreenOverlay(playerRef.current, {
relatedVideos: relatedVideos,
});
// Also store the data directly on the component as backup
endScreen.relatedVideos = relatedVideos;
playerRef.current.addChild(endScreen);
endScreen.show();
}
});
// Hide end screen and autoplay countdown when user wants to replay
playerRef.current.on('play', () => {
if (endScreen) {
endScreen.hide();
}
if (autoplayCountdown) {
autoplayCountdown.stopCountdown();
}
});
// Hide end screen and autoplay countdown when user seeks
playerRef.current.on('seeking', () => {
if (endScreen) {
endScreen.hide();
}
if (autoplayCountdown) {
autoplayCountdown.stopCountdown();
}
});
// Handle replay button functionality
playerRef.current.on('replay', () => {
if (endScreen) {
endScreen.hide();
}
playerRef.current.currentTime(0);
playerRef.current.play();
});
playerRef.current.on('error', (error) => {
console.error('Video.js error:', error);
});
playerRef.current.on('fullscreenchange', () => {
console.log('Fullscreen changed:', playerRef.current.isFullscreen());
});
playerRef.current.on('volumechange', () => {
console.log('Volume changed:', playerRef.current.volume(), 'Muted:', playerRef.current.muted());
});
playerRef.current.on('ratechange', () => {
console.log('Playback rate changed:', playerRef.current.playbackRate());
});
playerRef.current.on('texttrackchange', () => {
console.log('Text track changed');
const textTracks = playerRef.current.textTracks();
for (let i = 0; i < textTracks.length; i++) {
console.log(
'Track',
i,
':',
textTracks[i].kind,
textTracks[i].label,
'Mode:',
textTracks[i].mode
);
}
});
// Focus the player element so keyboard controls work
// This ensures spacebar can pause/play the video
playerRef.current.ready(() => {
// Focus the player element
if (playerRef.current.el()) {
playerRef.current.el().focus();
console.log('Video player focused for keyboard controls');
}
// Start playing the video immediately if autoplay is enabled
if (playerRef.current.autoplay()) {
playerRef.current.play().catch((error) => {
console.log('️ Browser prevented autoplay (normal behavior):', error.message);
// If autoplay fails, we can still focus the element
// so the user can manually start and use keyboard controls
});
}
});
}
}, 0);
return () => {
clearTimeout(timer);
};
}
// Cleanup function
return () => {
// Clean up keyboard event listener if it exists
if (customComponents.current && customComponents.current.cleanupKeyboardHandler) {
customComponents.current.cleanupKeyboardHandler();
}
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, []);
// Additional effect to ensure video gets focus for keyboard controls
useEffect(() => {
const focusVideo = () => {
if (playerRef.current && playerRef.current.el()) {
playerRef.current.el().focus();
console.log('Video element focused for keyboard controls');
}
};
// Focus when the page becomes visible or gains focus
const handleVisibilityChange = () => {
if (!document.hidden) {
setTimeout(focusVideo, 100);
}
};
const handleWindowFocus = () => {
setTimeout(focusVideo, 100);
};
// Add event listeners
document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('focus', handleWindowFocus);
// Initial focus attempt
setTimeout(focusVideo, 500);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('focus', handleWindowFocus);
};
}, []);
return <video ref={videoRef} className="video-js vjs-default-skin" tabIndex="0" />;
}
export default VideoJSPlayer;