2025-07-21 19:45:19 +03:00
import React , { useEffect , useRef , useMemo } from 'react' ;
2025-07-13 03:37:08 +03:00
import videojs from 'video.js' ;
import 'video.js/dist/video-js.css' ;
// Import the separated components
import EndScreenOverlay from '../overlays/EndScreenOverlay' ;
2025-07-22 05:18:43 +03:00
import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay' ;
2025-07-13 03:37:08 +03:00
import ChapterMarkers from '../markers/ChapterMarkers' ;
import NextVideoButton from '../controls/NextVideoButton' ;
2025-07-22 05:18:43 +03:00
import AutoplayToggleButton from '../controls/AutoplayToggleButton' ;
2025-07-13 15:34:36 +03:00
import CustomRemainingTime from '../controls/CustomRemainingTime' ;
2025-07-14 03:19:27 +03:00
import CustomChaptersOverlay from '../controls/CustomChaptersOverlay' ;
import CustomSettingsMenu from '../controls/CustomSettingsMenu' ;
2025-07-22 01:09:55 +03:00
import SeekIndicator from '../controls/SeekIndicator' ;
2025-07-14 04:20:45 +03:00
import UserPreferences from '../../utils/UserPreferences' ;
2025-07-13 03:37:08 +03:00
function VideoJSPlayer ( ) {
const videoRef = useRef ( null ) ;
const playerRef = useRef ( null ) ; // Track the player instance
2025-07-14 04:20:45 +03:00
const userPreferences = useRef ( new UserPreferences ( ) ) ; // User preferences instance
2025-07-22 01:09:55 +03:00
const customComponents = useRef ( { } ) ; // Store custom components for cleanup
2025-07-13 03:37:08 +03:00
// Safely access window.MEDIA_DATA with fallback using useMemo
const mediaData = useMemo (
( ) =>
typeof window !== 'undefined' && window . MEDIA _DATA
? window . MEDIA _DATA
: {
2025-09-10 13:53:37 +03:00
data : {
2025-09-15 23:47:43 +03:00
_ _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" : { } ,
} ,
2025-09-10 13:53:37 +03:00
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' ,
chaptersData : [
2025-09-15 23:47:43 +03:00
{ 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' } ,
2025-09-15 14:41:28 +03:00
{ 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' } ,
2025-09-10 13:53:37 +03:00
] ,
} ,
2025-07-13 03:37:08 +03:00
[ ]
) ;
// Define chapters as JSON object
// Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
2025-09-15 14:41:28 +03:00
const chaptersData = mediaData . chaptersData ? ? [
2025-09-15 23:47:43 +03:00
{ 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' } ,
2025-09-15 14:41:28 +03:00
{ 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' } ,
] ;
2025-07-13 03:37:08 +03:00
// Get video data from mediaData
const currentVideo = useMemo (
2025-09-15 23:47:43 +03:00
( ) => {
// 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 [
2025-09-10 13:53:37 +03:00
{
src : '/videos/sample-video.mp4' ,
type : 'video/mp4' ,
} ,
2025-09-15 23:47:43 +03:00
] ;
} ;
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 ( ) ,
} ;
} ,
2025-07-13 03:37:08 +03:00
[ mediaData ]
) ;
2025-09-15 23:47:43 +03:00
// Compute available qualities. Prefer JSON (mediaData.data.qualities), otherwise build from encodings_info or current source.
2025-09-10 13:53:37 +03:00
const availableQualities = useMemo ( ( ) => {
2025-09-15 23:47:43 +03:00
// 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 ( ) ;
2025-09-10 13:53:37 +03:00
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' ,
} ) ) ;
2025-09-15 23:47:43 +03:00
// Only include qualities that have actual sources
const validQualities = norm . filter ( q => q . src ) ;
// sort based on desired order
2025-09-10 13:53:37 +03:00
const idx = ( v ) => {
const i = desiredOrder . indexOf ( String ( v ) . toLowerCase ( ) ) ;
return i === - 1 ? 999 : i ;
} ;
2025-09-15 23:47:43 +03:00
validQualities . sort ( ( a , b ) => idx ( a . value ) - idx ( b . value ) ) ;
return validQualities ;
2025-09-10 13:53:37 +03:00
} ;
const jsonList = mediaData ? . data ? . qualities ;
if ( Array . isArray ( jsonList ) && jsonList . length ) {
return normalize ( jsonList ) ;
}
2025-09-15 23:47:43 +03:00
// 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
2025-09-10 13:53:37 +03:00
const baseSrc = ( currentVideo ? . sources && currentVideo . sources [ 0 ] ? . src ) || null ;
const type = ( currentVideo ? . sources && currentVideo . sources [ 0 ] ? . type ) || 'video/mp4' ;
2025-09-15 23:47:43 +03:00
if ( baseSrc ) {
const buildFromBase = [ {
label : 'Auto' ,
value : 'auto' ,
src : baseSrc ,
type ,
} ] ;
return normalize ( buildFromBase ) ;
}
// Return empty array if no valid sources found
return [ ] ;
2025-09-10 13:53:37 +03:00
} , [ mediaData , currentVideo ] ) ;
2025-07-21 19:45:19 +03:00
// 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 ] ) ;
2025-07-13 03:37:08 +03:00
2025-09-15 23:47:43 +03:00
// Demo array for testing purposes
2025-09-15 14:41:28 +03:00
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 ,
} ,
] ;
2025-09-15 23:47:43 +03:00
// const demoSubtitleTracks = []; // NO Subtitles. TODO: hide it on production
2025-09-15 14:41:28 +03:00
// Get subtitle tracks from backend response or fallback based on environment
const isDevelopment = process . env . NODE _ENV === 'development' || window . location . hostname === 'localhost' ;
2025-09-15 23:47:43 +03:00
const backendSubtitles = mediaData ? . data ? . subtitles _info || ( isDevelopment ? demoSubtitleTracks : [ ] ) ;
const hasSubtitles = backendSubtitles . length > 0 ;
2025-09-15 14:41:28 +03:00
const subtitleTracks = hasSubtitles
? backendSubtitles . map ( track => ( {
kind : 'subtitles' ,
src : track . src ,
srclang : track . srclang ,
label : track . label ,
default : false ,
} ) )
2025-09-15 23:47:43 +03:00
: [ ] ;
console . log ( 'mediaData?.data?.thumbnail_time' , mediaData ? . data ? . thumbnail _time ) ;
console . log ( 'mediaData?.data?.sprites_url' , mediaData ? . data ? . sprites _url ) ;
console . log ( 'mediaData' , mediaData ) ;
2025-09-15 14:41:28 +03:00
2025-07-13 16:04:41 +03:00
// Function to navigate to next video
2025-07-13 03:37:08 +03:00
const goToNextVideo = ( ) => {
2025-07-13 16:04:41 +03:00
console . log ( 'Next video functionality disabled for single video mode' ) ;
if ( mediaData . onClickNextCallback && typeof mediaData . onClickNextCallback === 'function' ) {
mediaData . onClickNextCallback ( ) ;
}
2025-07-13 03:37:08 +03:00
} ;
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
2025-07-13 15:34:36 +03:00
if ( ! playerRef . current && videoRef . current && ! videoRef . current . player ) {
2025-07-13 03:37:08 +03:00
playerRef . current = videojs ( videoRef . current , {
// ===== STANDARD <video> ELEMENT OPTIONS =====
// Controls whether player has user-interactive controls
controls : true ,
// Player dimensions - removed for responsive design
2025-07-22 05:18:43 +03:00
// Autoplay behavior: Use 'muted' to comply with browser policies
autoplay : 'muted' ,
2025-07-13 03:37:08 +03:00
// 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 ) {
2025-07-13 15:34:36 +03:00
return event . which === 75 || event . which === 32 ; // 'k' or Space
2025-07-13 03:37:08 +03:00
} ,
2025-07-22 01:09:55 +03:00
// 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
} ,
2025-07-13 03:37:08 +03:00
} ,
} ,
// 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
2025-07-13 15:34:36 +03:00
remainingTimeDisplay : false ,
/* remainingTimeDisplay: {
2025-07-13 03:37:08 +03:00
displayNegative: true,
2025-07-13 15:34:36 +03:00
}, */
2025-07-13 03:37:08 +03:00
// 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 ,
2025-09-10 13:53:37 +03:00
// Remove default playback speed dropdown from control bar
playbackRateMenuButton : false ,
2025-07-13 03:37:08 +03:00
// Descriptions button
descriptionsButton : true ,
2025-09-10 13:53:37 +03:00
// Subtitles (CC) button should be visible
2025-09-15 14:41:28 +03:00
subtitlesButton : hasSubtitles ? true : false ,
2025-07-13 03:37:08 +03:00
2025-09-10 13:53:37 +03:00
// Captions button (keep disabled to avoid duplicate with subtitles)
2025-07-13 03:37:08 +03:00
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
2025-07-14 03:19:27 +03:00
/* playerRef.current.on('ready', () => {
console.log('Video.js player ready');
}); */
playerRef . current . ready ( ( ) => {
2025-07-14 04:20:45 +03:00
// Apply user preferences to player
userPreferences . current . applyToPlayer ( playerRef . current ) ;
// Set up auto-save for preference changes
userPreferences . current . setupAutoSave ( playerRef . current ) ;
2025-07-14 03:19:27 +03:00
// 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' ) ;
2025-07-13 03:37:08 +03:00
// Auto-play video when navigating from next button
2025-07-13 15:34:36 +03:00
const urlParams = new URLSearchParams ( window . location . search ) ;
2025-07-13 03:37:08 +03:00
const hasVideoParam = urlParams . get ( 'm' ) ;
if ( hasVideoParam ) {
// Small delay to ensure everything is loaded
setTimeout ( ( ) => {
2025-07-13 15:34:36 +03:00
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
2025-07-13 03:37:08 +03:00
playerRef . current . play ( ) . catch ( ( error ) => {
2025-07-22 05:18:43 +03:00
console . log ( 'ℹ ️ Browser prevented autoplay (normal behavior):' , error . message ) ;
2025-07-13 03:37:08 +03:00
} ) ;
}
} , 100 ) ;
}
2025-07-14 03:19:27 +03:00
// BEGIN: Add subtitle tracks
2025-09-15 14:41:28 +03:00
hasSubtitles && subtitleTracks . forEach ( ( track ) => {
2025-07-14 03:19:27 +03:00
playerRef . current . addRemoteTextTrack ( track , false ) ;
2025-07-13 03:37:08 +03:00
} ) ;
2025-07-14 04:20:45 +03:00
2025-09-15 14:41:28 +03:00
// Apply saved subtitle preference immediately
userPreferences . current . applySubtitlePreference ( playerRef . current ) ;
2025-07-14 03:19:27 +03:00
// END: Add subtitle tracks
// BEGIN: Chapters Implementation
2025-09-10 13:53:37 +03:00
console . log ( 'chaptersData1' , chaptersData ) ;
2025-07-14 03:19:27 +03:00
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
2025-07-13 03:37:08 +03:00
// Force chapter markers update after chapters are loaded
2025-07-14 03:19:27 +03:00
/* setTimeout(() => {
if (chapterMarkers && chapterMarkers.updateChapterMarkers) {
chapterMarkers.updateChapterMarkers();
2025-07-13 03:37:08 +03:00
}
2025-07-14 03:19:27 +03:00
}, 500); */
2025-07-13 15:34:36 +03:00
2025-07-13 16:04:41 +03:00
// BEGIN: Implement custom time display component
2025-07-13 15:34:36 +03:00
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 ) ;
}
2025-07-13 16:04:41 +03:00
// END: Implement custom time display component
2025-07-13 15:34:36 +03:00
2025-07-13 16:04:41 +03:00
// BEGIN: Implement custom next video button
2025-09-15 14:41:28 +03:00
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
2025-07-22 05:18:43 +03:00
console . log ( 'mediaData.nextLink edw' , mediaData . nextLink ) ;
2025-07-22 02:07:01 +03:00
const nextVideoButton = new NextVideoButton ( playerRef . current , {
nextLink : mediaData . nextLink ,
} ) ;
2025-07-13 16:04:41 +03:00
const playToggleIndex = controlBar . children ( ) . indexOf ( playToggle ) ; // Insert it after play button
controlBar . addChild ( nextVideoButton , { } , playToggleIndex + 1 ) ;
}
// END: Implement custom next video button
2025-07-13 03:37:08 +03:00
2025-07-22 05:18:43 +03:00
// BEGIN: Implement autoplay toggle button - simplified
try {
const autoplayToggleButton = new AutoplayToggleButton ( playerRef . current , {
userPreferences : userPreferences . current ,
} ) ;
// Add it after the play button
2025-09-10 13:53:37 +03:00
const fullscreenToggleIndex = controlBar . children ( ) . indexOf ( fullscreenToggle ) ;
controlBar . addChild ( autoplayToggleButton , { } , fullscreenToggleIndex - 1 ) ;
2025-07-22 05:18:43 +03:00
// 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
2025-07-13 03:37:08 +03:00
// Remove duplicate captions button and move chapters to end
2025-07-14 03:19:27 +03:00
/* const cleanupControls = () => {
2025-07-13 03:37:08 +03:00
// Log all current children for debugging
const allChildren = controlBar.children();
// Try to find and remove captions/subs-caps button (but keep subtitles)
2025-07-13 15:34:36 +03:00
const possibleCaptionButtons = ['captionsButton', 'subsCapsButton'];
2025-07-13 03:37:08 +03:00
possibleCaptionButtons.forEach((buttonName) => {
const button = controlBar.getChild(buttonName);
if (button) {
try {
controlBar.removeChild(button);
console.log(`✓ Removed ${buttonName}`);
} catch (e) {
2025-07-13 15:34:36 +03:00
console.log(`✗ Failed to remove ${buttonName}:`, e);
2025-07-13 03:37:08 +03:00
}
}
});
// Alternative: hide buttons we can't remove
allChildren.forEach((child, index) => {
2025-07-13 15:34:36 +03:00
const name = (child.name_ || child.constructor.name || '').toLowerCase();
if (name.includes('caption') && !name.includes('subtitle')) {
2025-07-13 03:37:08 +03:00
child.hide();
2025-07-13 15:34:36 +03:00
console.log(`✓ Hidden button at index ${index}: ${name}`);
2025-07-13 03:37:08 +03:00
}
});
// Move chapters button to the very end
2025-07-13 15:34:36 +03:00
const chaptersButton = controlBar.getChild('chaptersButton');
2025-07-13 03:37:08 +03:00
if (chaptersButton) {
try {
controlBar.removeChild(chaptersButton);
controlBar.addChild(chaptersButton);
2025-07-13 15:34:36 +03:00
console.log('✓ Chapters button moved to last position');
2025-07-13 03:37:08 +03:00
} catch (e) {
2025-07-13 15:34:36 +03:00
console.log('✗ Failed to move chapters button:', e);
2025-07-13 03:37:08 +03:00
}
}
2025-07-14 03:19:27 +03:00
}; */
2025-07-13 03:37:08 +03:00
// Try multiple times with different delays
2025-07-14 03:19:27 +03:00
/* setTimeout(cleanupControls, 200);
2025-07-13 03:37:08 +03:00
setTimeout(cleanupControls, 500);
2025-07-14 03:19:27 +03:00
setTimeout(cleanupControls, 1000); */
2025-07-13 03:37:08 +03:00
// Make menus clickable instead of hover-only
setTimeout ( ( ) => {
const setupClickableMenus = ( ) => {
2025-09-15 14:41:28 +03:00
// Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay
const menuButtons = [ 'subtitlesButton' , 'playbackRateMenuButton' ] ;
2025-07-13 03:37:08 +03:00
menuButtons . forEach ( ( buttonName ) => {
2025-07-13 15:34:36 +03:00
const button = controlBar . getChild ( buttonName ) ;
2025-07-13 03:37:08 +03:00
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 ( ) {
2025-07-13 15:34:36 +03:00
if ( this . menu . hasClass ( 'vjs-lock-showing' ) ) {
this . menu . removeClass ( 'vjs-lock-showing' ) ;
2025-07-13 03:37:08 +03:00
this . menu . hide ( ) ;
} else {
2025-07-13 15:34:36 +03:00
this . menu . addClass ( 'vjs-lock-showing' ) ;
2025-07-13 03:37:08 +03:00
this . menu . show ( ) ;
}
} ) ;
2025-07-13 15:34:36 +03:00
console . log ( ` ✓ Made ${ buttonName } clickable ` ) ;
2025-07-13 03:37:08 +03:00
} else if ( button ) {
// For buttons without menuButton_ property
const buttonEl = button . el ( ) ;
if ( buttonEl ) {
// Add click handler to show/hide menu
2025-07-13 15:34:36 +03:00
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' ;
2025-07-13 03:37:08 +03:00
}
}
2025-07-13 15:34:36 +03:00
} ) ;
2025-07-13 03:37:08 +03:00
2025-07-13 15:34:36 +03:00
console . log ( ` ✓ Added click handler to ${ buttonName } ` ) ;
2025-07-13 03:37:08 +03:00
}
}
} ) ;
2025-09-15 14:41:28 +03:00
// 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 ;
}
}
2025-07-13 03:37:08 +03:00
} ;
setupClickableMenus ( ) ;
} , 1500 ) ;
2025-07-14 03:19:27 +03:00
// BEGIN: Add chapter markers to progress control
if ( progressControl && seekBar ) {
2025-07-21 18:57:05 +03:00
const chapterMarkers = new ChapterMarkers ( playerRef . current , {
previewSprite : mediaData . previewSprite ,
} ) ;
2025-07-14 03:19:27 +03:00
seekBar . addChild ( chapterMarkers ) ;
}
// END: Add chapter markers to progress control
2025-07-22 05:18:43 +03:00
// BEGIN: Simple button layout fix - use CSS approach
2025-07-21 19:45:19 +03:00
setTimeout ( ( ) => {
2025-07-22 05:18:43 +03:00
console . log ( 'Setting up simplified button layout...' ) ;
2025-07-22 00:12:15 +03:00
2025-07-22 05:18:43 +03:00
// 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%' ;
2025-07-22 00:12:15 +03:00
2025-07-22 05:18:43 +03:00
// 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' ) ;
2025-07-21 19:45:19 +03:00
}
2025-07-22 05:18:43 +03:00
} , 300 ) ;
// END: Simple button layout fix
2025-07-21 19:45:19 +03:00
2025-07-14 03:19:27 +03:00
// 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 ) ;
2025-07-13 03:37:08 +03:00
}
}
2025-07-14 03:19:27 +03:00
// END: Move chapters button after fullscreen toggle
2025-09-10 13:53:37 +03:00
console . log ( 'chaptersData' , chaptersData ) ;
2025-07-14 03:19:27 +03:00
// BEGIN: Add Chapters Overlay Component
if ( chaptersData && chaptersData . length > 0 ) {
2025-07-22 01:09:55 +03:00
customComponents . current . chaptersOverlay = new CustomChaptersOverlay ( playerRef . current , {
2025-07-14 03:19:27 +03:00
chaptersData : chaptersData ,
2025-09-10 13:53:37 +03:00
seriesTitle : mediaData ? . data ? . title || 'Chapters' ,
channelName : mediaData ? . data ? . user || mediaData ? . data ? . author _name || 'Channel' ,
thumbnail : mediaData ? . data ? . thumbnail _url || mediaData ? . data ? . author _thumbnail || '' ,
2025-07-14 03:19:27 +03:00
} ) ;
} else {
console . log ( '⚠ No chapters data available for overlay' ) ;
}
// END: Add Chapters Overlay Component
// BEGIN: Add Settings Menu Component
2025-07-22 01:09:55 +03:00
customComponents . current . settingsMenu = new CustomSettingsMenu ( playerRef . current , {
2025-07-14 04:20:45 +03:00
userPreferences : userPreferences . current ,
2025-09-10 13:53:37 +03:00
qualities : availableQualities ,
2025-09-15 23:47:43 +03:00
hasSubtitles : hasSubtitles ,
2025-07-14 04:20:45 +03:00
} ) ;
2025-07-22 01:12:11 +03:00
2025-09-15 14:41:28 +03:00
// 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 ) { }
2025-07-14 03:19:27 +03:00
// END: Add Settings Menu Component
2025-07-22 01:09:55 +03:00
// 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
2025-07-14 03:19:27 +03:00
// Store components reference for potential cleanup
2025-07-22 01:09:55 +03:00
// 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
2025-07-14 04:20:45 +03:00
// 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 ( ) ) ;
} ,
} ;
2025-07-13 03:37:08 +03:00
} ) ;
// Listen for next video event
playerRef . current . on ( 'nextVideo' , ( ) => {
console . log ( 'Next video requested' ) ;
goToNextVideo ( ) ;
} ) ;
playerRef . current . on ( 'play' , ( ) => {
console . log ( 'Video started playing' ) ;
2025-09-15 14:41:28 +03:00
// Only show play indicator if not changing quality
if ( ! playerRef . current . isChangingQuality ) {
customComponents . current . seekIndicator . show ( 'play' ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'pause' , ( ) => {
console . log ( 'Video paused' ) ;
2025-09-15 14:41:28 +03:00
// Only show pause indicator if not changing quality
if ( ! playerRef . current . isChangingQuality ) {
customComponents . current . seekIndicator . show ( 'pause' ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
2025-07-22 05:18:43 +03:00
// Store reference to end screen and autoplay countdown for cleanup
2025-07-13 03:37:08 +03:00
let endScreen = null ;
2025-07-22 05:18:43 +03:00
let autoplayCountdown = null ;
2025-07-13 03:37:08 +03:00
playerRef . current . on ( 'ended' , ( ) => {
console . log ( 'Video ended' ) ;
console . log ( 'Available relatedVideos:' , relatedVideos ) ;
// Keep controls active after video ends
setTimeout ( ( ) => {
2025-07-13 15:34:36 +03:00
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
2025-07-13 03:37:08 +03:00
// Remove vjs-ended class if it disables controls
const playerEl = playerRef . current . el ( ) ;
if ( playerEl ) {
// Keep the visual ended state but ensure controls work
2025-07-13 15:34:36 +03:00
const controlBar = playerRef . current . getChild ( 'controlBar' ) ;
2025-07-13 03:37:08 +03:00
if ( controlBar ) {
controlBar . show ( ) ;
controlBar . el ( ) . style . opacity = '1' ;
2025-07-13 15:34:36 +03:00
controlBar . el ( ) . style . pointerEvents = 'auto' ;
2025-07-13 03:37:08 +03:00
}
}
}
} , 50 ) ;
2025-07-22 05:18:43 +03:00
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 ( ) ;
2025-07-13 03:37:08 +03:00
}
2025-07-22 05:18:43 +03:00
// 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 ;
}
2025-07-13 03:37:08 +03:00
2025-07-22 05:18:43 +03:00
// 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 ;
2025-07-13 03:37:08 +03:00
2025-07-22 05:18:43 +03:00
playerRef . current . addChild ( endScreen ) ;
endScreen . show ( ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
2025-07-22 05:18:43 +03:00
// Hide end screen and autoplay countdown when user wants to replay
2025-07-13 03:37:08 +03:00
playerRef . current . on ( 'play' , ( ) => {
if ( endScreen ) {
endScreen . hide ( ) ;
}
2025-07-22 05:18:43 +03:00
if ( autoplayCountdown ) {
autoplayCountdown . stopCountdown ( ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
2025-07-22 05:18:43 +03:00
// Hide end screen and autoplay countdown when user seeks
2025-07-13 03:37:08 +03:00
playerRef . current . on ( 'seeking' , ( ) => {
if ( endScreen ) {
endScreen . hide ( ) ;
}
2025-07-22 05:18:43 +03:00
if ( autoplayCountdown ) {
autoplayCountdown . stopCountdown ( ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
// 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' , ( ) => {
2025-07-13 15:34:36 +03:00
console . log ( 'Fullscreen changed:' , playerRef . current . isFullscreen ( ) ) ;
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'volumechange' , ( ) => {
2025-07-13 15:34:36 +03:00
console . log ( 'Volume changed:' , playerRef . current . volume ( ) , 'Muted:' , playerRef . current . muted ( ) ) ;
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'ratechange' , ( ) => {
2025-07-13 15:34:36 +03:00
console . log ( 'Playback rate changed:' , playerRef . current . playbackRate ( ) ) ;
2025-07-13 03:37:08 +03:00
} ) ;
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 ( ) ;
2025-07-13 15:34:36 +03:00
console . log ( 'Video player focused for keyboard controls' ) ;
2025-07-13 03:37:08 +03:00
}
// Start playing the video immediately if autoplay is enabled
if ( playerRef . current . autoplay ( ) ) {
playerRef . current . play ( ) . catch ( ( error ) => {
2025-07-22 05:18:43 +03:00
console . log ( 'ℹ ️ Browser prevented autoplay (normal behavior):' , error . message ) ;
2025-07-13 03:37:08 +03:00
// 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 ( ) => {
2025-07-22 01:09:55 +03:00
// Clean up keyboard event listener if it exists
if ( customComponents . current && customComponents . current . cleanupKeyboardHandler ) {
customComponents . current . cleanupKeyboardHandler ( ) ;
}
2025-07-13 03:37:08 +03:00
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 ( ) => {
2025-07-13 15:34:36 +03:00
document . removeEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
2025-07-13 03:37:08 +03:00
window . removeEventListener ( 'focus' , handleWindowFocus ) ;
} ;
} , [ ] ) ;
2025-07-22 05:18:43 +03:00
return < video ref = { videoRef } className = "video-js vjs-default-skin" tabIndex = "0" / > ;
2025-07-13 03:37:08 +03:00
}
export default VideoJSPlayer ;