mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-10-06 22:32:41 +07:00
2159 lines
116 KiB
React
2159 lines
116 KiB
React
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;
|