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' ;
2025-09-16 09:58:41 +03:00
import SpritePreview from '../markers/SpritePreview' ;
2025-07-13 03:37:08 +03:00
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
2025-09-18 11:15:26 +03:00
function VideoJSPlayer ( { videoId = 'default-video' } ) {
2025-07-13 03:37:08 +03:00
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
2025-09-18 12:03:00 +03:00
// Check if this is an embed player (disable next video and autoplay features)
const isEmbedPlayer = videoId === 'video-embed' ;
2025-09-18 12:13:53 +03:00
// Environment-based development mode configuration
const isDevMode = import . meta . env . VITE _DEV _MODE === 'true' || window . location . hostname . includes ( 'vercel.app' ) ;
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-18 10:28:10 +03:00
data : {
// COMMON
poster _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/7dedcb56bde9463dbc0766768a99be0f_C8E5GFY.20250605_110647.mp4.jpg' ,
2025-09-19 09:16:42 +03:00
chapter _data : [
2025-09-20 02:39:40 +03:00
{ startTime : '00:00:00.000' , endTime : '00:00:08.295' , chapterTitle : 'A1 test' } ,
2025-09-19 12:27:26 +03:00
{ startTime : '00:00:24.295' , endTime : '00:00:48.590' , chapterTitle : 'A2 of Marine Life' } ,
{
startTime : '00:00:48.590' ,
endTime : '00:01:12.885' ,
chapterTitle : 'A3 Reef Ecosystems' ,
} ,
2025-09-18 10:28:10 +03:00
] ,
related _media : [
{
friendly _token : 'dktSm7iEo' ,
url : 'https://demo.mediacms.io/view?m=dktSm7iEo' ,
api _url : 'https://demo.mediacms.io/api/v1/media/dktSm7iEo' ,
user : 'markos' ,
title : 'Sed aliquam consectetur dolor.' ,
description :
'Voluptatem quiquia dolorem labore dolore. Dolor etincidunt non etincidunt etincidunt sed. Adipisci eius etincidunt dolor magnam dolor. Dolorem porro etincidunt quaerat. Eius magnam dolorem tempora voluptatem labore. Dolore sed porro ipsum aliquam numquam non dolor. Labore aliquam labore dolor sit quisquam quaerat.' ,
add _date : '2024-10-02T05:28:18.784775-04:00' ,
views : 803 ,
media _type : 'video' ,
state : 'public' ,
duration : 12 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/8624c4080afc46eba8b4f27a81eccf27.Birch.mp4_myELKan.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/8624c4080afc46eba8b4f27a81eccf27.tmpb7kerjb2.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 13 ,
dislikes : 2 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '124.5MB' ,
} ,
{
friendly _token : 'zK2nirNLC' ,
url : 'https://demo.mediacms.io/view?m=zK2nirNLC' ,
api _url : 'https://demo.mediacms.io/api/v1/media/zK2nirNLC' ,
user : 'markos' ,
title : 'University of Copenhagen Mærsk Tower' ,
description : 'https://maps.app.goo.gl/ewVAGgqdrb1MD1sF7' ,
add _date : '2025-06-06T00:00:00-04:00' ,
views : 632 ,
media _type : 'video' ,
state : 'public' ,
duration : 27 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/6497e960081b4b8abddcf4cbdf2bf4eb_38hpsj6.20250604_080632.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/6497e960081b4b8abddcf4cbdf2bf4eb.tmpjc3_yx1g.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 13 ,
dislikes : 0 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '58.8MB' ,
} ,
{
friendly _token : 'o7lKzt664' ,
url : 'https://demo.mediacms.io/view?m=o7lKzt664' ,
api _url : 'https://demo.mediacms.io/api/v1/media/o7lKzt664' ,
user : 'markos' ,
title : 'Magnam velit ipsum quisquam amet magnam etincidunt.' ,
description :
'Magnam sed quisquam quiquia dolor est. Tempora sit etincidunt dolor dolore magnam. Numquam non dolorem eius aliquam non. Consectetur sit consectetur dolor quaerat est. Consectetur amet dolor ut dolor ipsum. Mpla mpla antalya' ,
add _date : '2024-10-02T05:35:10-04:00' ,
views : 1378 ,
media _type : 'video' ,
state : 'public' ,
duration : 6 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/95eb092b57c24f52b75691fa382d16bb_Bg99UmX.20240526_123312.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/95eb092b57c24f52b75691fa382d16bb.tmpthrejon6.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 22 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '13.0MB' ,
} ,
{
friendly _token : 'mFELnYWko' ,
url : 'https://demo.mediacms.io/view?m=mFELnYWko' ,
api _url : 'https://demo.mediacms.io/api/v1/media/mFELnYWko' ,
user : 'markos' ,
title : 'kubectl-cheat-sheet.pdf' ,
description : '' ,
add _date : '2024-10-25T04:24:39-04:00' ,
views : 1391 ,
media _type : 'pdf' ,
state : 'public' ,
duration : 0 ,
thumbnail _url : null ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 15 ,
dislikes : 9 ,
reported _times : 1 ,
featured : false ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'ZLjVzLcCE' ,
url : 'https://demo.mediacms.io/view?m=ZLjVzLcCE' ,
api _url : 'https://demo.mediacms.io/api/v1/media/ZLjVzLcCE' ,
user : 'markos' ,
title : 'Quaerat velit sed numquam ipsum magnam.' ,
description :
'Dolore numquam aliquam dolore modi modi. Dolor quaerat est voluptatem ut. Dolor eius tempora magnam etincidunt ipsum modi porro. Etincidunt consectetur est est sed ut. Porro neque sed dolorem dolore. Sed velit quisquam ipsum quisquam consectetur porro.' ,
add _date : '2024-10-02T05:34:03.836032-04:00' ,
views : 888 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/b5e8dea6a0a3477885db786f2e89fb51.IMG_20240324_141309.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 6 ,
dislikes : 2 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'bvMsRGRxE' ,
url : 'https://demo.mediacms.io/view?m=bvMsRGRxE' ,
api _url : 'https://demo.mediacms.io/api/v1/media/bvMsRGRxE' ,
user : 'markos' ,
title : 'Numquam quisquam amet dolore quisquam ipsum ut.' ,
description :
'Modi numquam magnam numquam eius labore est dolorem. Voluptatem etincidunt neque ipsum non. Non tempora etincidunt magnam etincidunt. Sed dolor dolore amet quiquia porro sit non. Tempora etincidunt modi sed etincidunt est aliquam. Magnam aliquam ipsum modi dolore. Etincidunt sit eius dolore sed neque porro labore. Eius etincidunt dolorem est quiquia amet aliquam. Quaerat velit labore est dolor.' ,
add _date : '2024-10-02T05:33:02.972212-04:00' ,
views : 773 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/b9717b02cd8b45ec91d07470933810db.IMG_20231226_140530.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 10 ,
dislikes : 2 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'lsNWKKq5N' ,
url : 'https://demo.mediacms.io/view?m=lsNWKKq5N' ,
api _url : 'https://demo.mediacms.io/api/v1/media/lsNWKKq5N' ,
user : 'markos' ,
title : 'Quaerat quaerat numquam porro dolor' ,
description :
'Modi dolorem non non neque dolor magnam quisquam. Magnam amet magnam porro. Dolorem quiquia dolorem etincidunt labore ipsum aliquam sed. Eius sed eius sit consectetur quaerat. Voluptatem dolorem porro etincidunt labore aliquam quisquam. Adipisci quisquam dolorem dolorem magnam dolorem ipsum. Consectetur quaerat magnam sit voluptatem.' ,
add _date : '2024-10-02T00:00:00-04:00' ,
views : 666 ,
media _type : 'video' ,
state : 'public' ,
duration : 13 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/58008fdc69d34c229a85f29076004639.VID_20230917_094453.mp4_Pe8a1dv.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/58008fdc69d34c229a85f29076004639.tmp85b478_u.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 10 ,
dislikes : 3 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : '35.0MB' ,
} ,
{
friendly _token : 'tsgNaSe6E' ,
url : 'https://demo.mediacms.io/view?m=tsgNaSe6E' ,
api _url : 'https://demo.mediacms.io/api/v1/media/tsgNaSe6E' ,
user : 'markos' ,
title : 'Magnam ipsum eius numquam quiquia non adipisci.' ,
description :
'Adipisci labore dolorem ipsum quaerat non dolore ut. Velit porro neque non consectetur neque neque. Ut sit tempora tempora. Ipsum ut velit neque. Quaerat labore amet porro porro amet tempora. Sed voluptatem est amet quisquam sed numquam velit. Est ipsum non labore. Consectetur amet neque consectetur dolor ipsum.' ,
add _date : '2024-10-02T05:32:46.253917-04:00' ,
views : 824 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/0405f61e131f431793644be3742fcc1a.20240628_235522.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 12 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'f9xqzbbJE' ,
url : 'https://demo.mediacms.io/view?m=f9xqzbbJE' ,
api _url : 'https://demo.mediacms.io/api/v1/media/f9xqzbbJE' ,
user : 'markos' ,
title : 'Magnam quaerat numquam modi dolore sed amet.' ,
description :
'Non non voluptatem neque velit labore. Eius labore non aliquam quisquam adipisci neque. Aliquam ipsum sed ipsum quisquam. Sit quaerat sed dolore non tempora. Ipsum sed labore dolore consectetur. Modi non quisquam sed ut ut dolor quaerat.' ,
add _date : '2024-10-02T05:34:18.498303-04:00' ,
views : 844 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/369f44b14f944941881a20e8d5285e78.IMG_20240324_151737.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 3 ,
dislikes : 1 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'DE3KByBeo' ,
url : 'https://demo.mediacms.io/view?m=DE3KByBeo' ,
api _url : 'https://demo.mediacms.io/api/v1/media/DE3KByBeo' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 272 ,
media _type : 'video' ,
state : 'public' ,
duration : 29 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/58b35efa3aca454196227c0eb5e2ca75_pkgSAK2.20250517_101207.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/58b35efa3aca454196227c0eb5e2ca75.tmpsw6vmsfo.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 6 ,
dislikes : 2 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '62.0MB' ,
} ,
{
friendly _token : 'M8ktwf8kF' ,
url : 'https://demo.mediacms.io/view?m=M8ktwf8kF' ,
api _url : 'https://demo.mediacms.io/api/v1/media/M8ktwf8kF' ,
user : 'markos' ,
title : 'Quaerat voluptatem quisquam neque velit neque.' ,
description :
'Aliquam ipsum quisquam dolor. Modi quisquam neque ut ipsum amet. Tempora quaerat ipsum aliquam velit velit porro est. Consectetur neque eius quisquam porro amet sit neque. Modi voluptatem neque modi. Ipsum aliquam labore quaerat.' ,
add _date : '2024-10-02T05:34:32.027708-04:00' ,
views : 852 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/48a682227b544388a1b547736668d0ad.IMG_20240324_151743.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 7 ,
dislikes : 2 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 's2qAmRTJ9' ,
url : 'https://demo.mediacms.io/view?m=s2qAmRTJ9' ,
api _url : 'https://demo.mediacms.io/api/v1/media/s2qAmRTJ9' ,
user : 'markos' ,
title : 'Labore neque ipsum labore modi tempora aliquam neque.' ,
description :
'Eius voluptatem aliquam sit sit ipsum consectetur. Dolorem velit amet modi. Porro quisquam velit neque dolorem. Dolorem modi quiquia aliquam. Numquam est magnam non numquam modi quisquam est. Sit velit ut labore sit dolore velit modi. Aliquam modi dolorem ut.' ,
add _date : '2024-10-02T05:34:27.197596-04:00' ,
views : 779 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/2b786916111947e3ba960d7146ae0424.IMG_20230708_133437.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 16 ,
dislikes : 2 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : '8pWsxkOS5' ,
url : 'https://demo.mediacms.io/view?m=8pWsxkOS5' ,
api _url : 'https://demo.mediacms.io/api/v1/media/8pWsxkOS5' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 321 ,
media _type : 'video' ,
state : 'public' ,
duration : 47 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/d970760faf5745b4b3d0d0cff2b95d86_cvyjd5y.20250517_140535.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/d970760faf5745b4b3d0d0cff2b95d86.tmp3vtt3uip.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 5 ,
dislikes : 2 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '101.2MB' ,
} ,
{
friendly _token : 'swcx8A2h1' ,
url : 'https://demo.mediacms.io/view?m=swcx8A2h1' ,
api _url : 'https://demo.mediacms.io/api/v1/media/swcx8A2h1' ,
user : 'markos' ,
title : 'Etincidunt dolore eius ut non numquam dolore dolorem.' ,
description :
'Etincidunt amet dolorem quisquam tempora. Dolorem dolor modi sit modi labore sit. Labore est sed non numquam. Porro non quaerat dolorem porro tempora sit. Ut neque est etincidunt velit eius. Etincidunt aliquam adipisci sed quiquia modi. Adipisci non sed adipisci velit.' ,
add _date : '2024-10-02T05:38:43-04:00' ,
views : 7973 ,
media _type : 'video' ,
state : 'public' ,
duration : 31 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/fe4933d67b884d4da507dd60e77f7438.VID_20200909_141053.mp4_bU90dbl.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/fe4933d67b884d4da507dd60e77f7438.tmpdd72kiwh.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 230 ,
dislikes : 62 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : '65.9MB' ,
} ,
{
friendly _token : 'rNefa4WtV' ,
url : 'https://demo.mediacms.io/view?m=rNefa4WtV' ,
api _url : 'https://demo.mediacms.io/api/v1/media/rNefa4WtV' ,
user : 'markos' ,
title : 'Quaerat modi non eius.' ,
description :
'Quisquam ut dolorem dolorem quisquam dolore. Non modi etincidunt labore sit quisquam. Sed neque quaerat quisquam voluptatem. Numquam labore neque etincidunt. Magnam etincidunt porro adipisci.' ,
add _date : '2024-10-02T05:36:46.062913-04:00' ,
views : 2683 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/ca0e3af507c64fc5995b9d97e4a8c779.20240527_091011.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 45 ,
dislikes : 16 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'LP09dv0mx' ,
url : 'https://demo.mediacms.io/view?m=LP09dv0mx' ,
api _url : 'https://demo.mediacms.io/api/v1/media/LP09dv0mx' ,
user : 'markos' ,
title : 'Est ipsum non etincidunt voluptatem adipisci labore.' ,
description :
'Est sit voluptatem numquam ut etincidunt. Adipisci sed dolor voluptatem labore. Quiquia est sit eius eius labore velit. Tempora ut tempora neque. Ipsum eius sit labore amet dolorem non non. Quiquia velit amet eius sit ut ut voluptatem. Quiquia sit ut ipsum ipsum neque. Amet etincidunt aliquam consectetur voluptatem sed etincidunt quiquia. Sit eius dolore magnam sed velit consectetur. Etincidunt amet numquam sit porro.' ,
add _date : '2024-10-02T05:31:41.087014-04:00' ,
views : 711 ,
media _type : 'video' ,
state : 'public' ,
duration : 26 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/980decd203f245bbb3723cba73a94a11.VID_20230813_104846.mp4_3rwZtxQ.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/980decd203f245bbb3723cba73a94a11.tmpmddawiqe.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 11 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '66.4MB' ,
} ,
{
friendly _token : 'just_somethi' ,
url : 'https://demo.mediacms.io/view?m=just_somethi' ,
api _url : 'https://demo.mediacms.io/api/v1/media/just_somethi' ,
user : 'markos' ,
title : 'Sit consectetur dolore numquam.' ,
description :
'Consectetur adipisci neque neque tempora. Amet quiquia ut labore non sit. Dolor aliquam quiquia adipisci dolor dolorem quiquia. Dolore porro modi labore quisquam adipisci numquam non. Dolor consectetur ut est neque.' ,
add _date : '2024-10-02T00:00:00-04:00' ,
views : 1288 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/7aaa65ac24224fe3a768aa6b7a723b58.20240527_090952.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 28 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'PbsYTGEol' ,
url : 'https://demo.mediacms.io/view?m=PbsYTGEol' ,
api _url : 'https://demo.mediacms.io/api/v1/media/PbsYTGEol' ,
user : 'markos' ,
title : 'Voluptatem porro neque tempora dolorem quiquia est dolor.' ,
description :
'Labore aliquam dolorem quiquia est ipsum quiquia. Sed est amet non ipsum. Labore etincidunt etincidunt quiquia amet tempora tempora. Aliquam velit ipsum consectetur. Ipsum labore quaerat quiquia aliquam magnam. Quisquam ut velit velit dolorem dolorem. Aliquam quaerat tempora quisquam ut voluptatem voluptatem quiquia.' ,
add _date : '2024-10-02T05:33:48.024941-04:00' ,
views : 785 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/f01faaa2e7be4c9aa7598ab755898a09.IMG_20240324_141304.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 14 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'mkpfy31bY' ,
url : 'https://demo.mediacms.io/view?m=mkpfy31bY' ,
api _url : 'https://demo.mediacms.io/api/v1/media/mkpfy31bY' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 212 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/c2d40995ce7640e3b8cbfee1a2890c51.20250517_183010.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 2 ,
dislikes : 0 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'w2lYWaW8e' ,
url : 'https://demo.mediacms.io/view?m=w2lYWaW8e' ,
api _url : 'https://demo.mediacms.io/api/v1/media/w2lYWaW8e' ,
user : 'markos' ,
title : 'Plane view approaching Copenhagen airport' ,
description : 'plane view' ,
add _date : '2025-06-06T00:00:00-04:00' ,
views : 664 ,
media _type : 'video' ,
state : 'public' ,
duration : 50 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/e84f1caf58f44d838456625ffe96173b_LQCWsAe.20250603_110810.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/e84f1caf58f44d838456625ffe96173b.tmp7hm26nok.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 13 ,
dislikes : 3 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '108.8MB' ,
} ,
{
friendly _token : 'qLMrr970w' ,
url : 'https://demo.mediacms.io/view?m=qLMrr970w' ,
api _url : 'https://demo.mediacms.io/api/v1/media/qLMrr970w' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 215 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/558f53227cc5418c9b7d66a3740fe2f8.20250517_082340.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 2 ,
dislikes : 0 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'elygiagorgechania' ,
url : 'https://demo.mediacms.io/view?m=elygiagorgechania' ,
api _url : 'https://demo.mediacms.io/api/v1/media/elygiagorgechania' ,
user : 'markos' ,
title : 'Exit of Elygia Gorge, Chania, Crete' ,
description :
'This video is from the exit of Elygia Gorge, Chania, Crete, where it meets the sea!' ,
add _date : '2025-06-15T00:00:00-04:00' ,
views : 688 ,
media _type : 'video' ,
state : 'public' ,
duration : 29 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/c1ab03cab3bb46b5854a5e217cfe3013_Nete6ao.VID_20230813_144422.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/c1ab03cab3bb46b5854a5e217cfe3013.tmpjlxkhy0i.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 6 ,
dislikes : 0 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '75.6MB' ,
} ,
{
friendly _token : 'OxO6BMVZb' ,
url : 'https://demo.mediacms.io/view?m=OxO6BMVZb' ,
api _url : 'https://demo.mediacms.io/api/v1/media/OxO6BMVZb' ,
user : 'markos' ,
title : 'Eius velit etincidunt amet tempora ut.' ,
description :
'Aliquam eius adipisci adipisci. Quaerat dolor quaerat magnam. Amet ut quaerat sit sed magnam quaerat neque. Neque velit porro labore modi ut ut ut. Non quaerat consectetur dolor eius voluptatem. Quisquam modi amet sed magnam eius. Quisquam dolor dolore aliquam quisquam neque dolore. Quisquam dolor ut ipsum quiquia. Voluptatem quisquam neque quisquam quiquia adipisci. Est modi eius est etincidunt numquam quisquam ut.' ,
add _date : '2024-10-02T05:34:33.461809-04:00' ,
views : 1023 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/13299e6838e143fda776bacf7081484e.IMG_20230820_200357.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 16 ,
dislikes : 4 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'hDHXkdwy0' ,
url : 'https://demo.mediacms.io/view?m=hDHXkdwy0' ,
api _url : 'https://demo.mediacms.io/api/v1/media/hDHXkdwy0' ,
user : 'markos' ,
title : 'Modi tempora est quaerat numquam' ,
description :
'Magnam voluptatem est magnam dolorem. Etincidunt quiquia aliquam velit tempora porro. Magnam neque eius eius etincidunt ut ipsum. Adipisci labore quaerat modi. Ipsum modi quaerat consectetur est non quaerat sed. Neque ut modi adipisci dolore adipisci dolor ut. Dolor tempora adipisci quisquam. Dolorem consectetur velit adipisci etincidunt voluptatem. Non quisquam voluptatem adipisci. Voluptatem est aliquam porro labore non.' ,
add _date : '2024-10-02T05:36:42-04:00' ,
views : 1679 ,
media _type : 'video' ,
state : 'public' ,
duration : 24 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/a3c5642e13624149897f193981ebccf3.VID_20210307_111552.mp4_uEHcD0C.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/a3c5642e13624149897f193981ebccf3.tmpempjz6eh.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 32 ,
dislikes : 12 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '52.4MB' ,
} ,
{
friendly _token : 'vDKrrkIVc' ,
url : 'https://demo.mediacms.io/view?m=vDKrrkIVc' ,
api _url : 'https://demo.mediacms.io/api/v1/media/vDKrrkIVc' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 228 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/50f296fd588240d2ad80a6fb9a5ce7d6.20250518_093811.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 6 ,
dislikes : 1 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : '4h3nsvXb1' ,
url : 'https://demo.mediacms.io/view?m=4h3nsvXb1' ,
api _url : 'https://demo.mediacms.io/api/v1/media/4h3nsvXb1' ,
user : 'markos' ,
title : 'Dolor voluptatem non quiquia consectetur est numquam sed.' ,
description :
'Aliquam ipsum etincidunt neque ipsum. Consectetur ut non velit quaerat porro. Eius ut voluptatem velit aliquam dolor. Non etincidunt est quaerat quaerat. Quiquia est non ipsum numquam. Quisquam amet magnam sed eius quaerat. Magnam porro dolorem dolor. Numquam numquam quaerat est. Quisquam tempora ut quaerat est.' ,
add _date : '2024-10-02T05:32:43.865153-04:00' ,
views : 981 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/baca04d3009d4daba302919c25b4325e.IMG_1936.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 16 ,
dislikes : 1 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'HdUU8boQP' ,
url : 'https://demo.mediacms.io/view?m=HdUU8boQP' ,
api _url : 'https://demo.mediacms.io/api/v1/media/HdUU8boQP' ,
user : 'markos' ,
title : 'Consectetur adipisci porro quiquia ipsum aliquam etincidunt ut.' ,
description :
'Consectetur numquam eius amet est dolor neque modi. Consectetur est amet voluptatem quaerat numquam sed. Porro tempora ut ut. Non dolor amet sit. Labore porro neque dolorem numquam dolore ut. Modi sed adipisci dolore. Numquam magnam est tempora. Neque aliquam labore dolor ipsum porro.' ,
add _date : '2024-10-02T05:34:13.171233-04:00' ,
views : 795 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/e1df55b16b3b456ea88bf7feb7db6051.IMG_20230708_120717.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 8 ,
dislikes : 3 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'vy5PTWJZ6' ,
url : 'https://demo.mediacms.io/view?m=vy5PTWJZ6' ,
api _url : 'https://demo.mediacms.io/api/v1/media/vy5PTWJZ6' ,
user : 'markos' ,
title : 'Non etincidunt numquam velit.' ,
description :
'Etincidunt ut velit ipsum. Labore modi magnam eius quisquam. Dolorem magnam sit quiquia non dolorem tempora. Aliquam labore sed quaerat magnam est aliquam porro. Adipisci adipisci aliquam tempora ut aliquam eius amet. Est etincidunt quiquia dolorem amet consectetur. Ipsum neque dolorem dolore etincidunt.\r\n' ,
add _date : '2024-10-02T05:33:26-04:00' ,
views : 713 ,
media _type : 'image' ,
state : 'public' ,
duration : 0 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/7a9ec6be9ce24a569a246c61d9b03690.IMG_20220528_135153.jpg.jpg' ,
is _reviewed : true ,
preview _url : null ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 9 ,
dislikes : 1 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : null ,
} ,
{
friendly _token : 'TAdmfDUlu' ,
url : 'https://demo.mediacms.io/view?m=TAdmfDUlu' ,
api _url : 'https://demo.mediacms.io/api/v1/media/TAdmfDUlu' ,
user : 'markos' ,
title : 'Sed neque adipisci dolorem sed.' ,
description :
'Quiquia ipsum velit amet. Consectetur porro numquam numquam magnam adipisci dolore. Dolor ipsum ut ut consectetur modi labore. Neque est non amet. Sit quiquia quisquam dolorem. Modi dolore modi dolorem ipsum ipsum. Neque modi modi dolorem quisquam numquam modi quaerat.\r\n\r\nbest scenes at 00:00:12 and 00:14' ,
add _date : '2024-10-02T00:00:00-04:00' ,
views : 821 ,
media _type : 'video' ,
state : 'public' ,
duration : 30 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/f371a6b2c157451d924bc4f612bf2667_Kh4GigX.Pexels_Videos_2079217_1.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/f371a6b2c157451d924bc4f612bf2667.tmp2jqxf9sr.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 20 ,
dislikes : 4 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '90.0MB' ,
} ,
{
friendly _token : 'kHd7EKAVH' ,
url : 'https://demo.mediacms.io/view?m=kHd7EKAVH' ,
api _url : 'https://demo.mediacms.io/api/v1/media/kHd7EKAVH' ,
user : 'markos' ,
title : 'Tempora magnam velit ipsum neque aliquam adipisci.' ,
description :
'Porro dolorem eius sed non eius. Non dolor quiquia dolorem. Modi ut dolor aliquam dolor. Non est dolorem amet consectetur neque quiquia numquam. Aliquam adipisci quiquia voluptatem ipsum quisquam magnam adipisci. Sit adipisci dolor consectetur dolor quaerat. Magnam ut modi tempora. Modi non ipsum tempora etincidunt porro. Ut ut dolor ipsum non consectetur neque quiquia.' ,
add _date : '2024-10-02T05:33:57.651288-04:00' ,
views : 1051 ,
media _type : 'video' ,
state : 'public' ,
duration : 54 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/9d3b8425eb08400fa08d90f988bc5ff4.VID_20220821_110509.mp4_JzAol5C.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/9d3b8425eb08400fa08d90f988bc5ff4.tmpwlnjum5k.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 26 ,
dislikes : 8 ,
reported _times : 1 ,
featured : true ,
user _featured : false ,
size : '136.8MB' ,
} ,
{
friendly _token : 'Otbc37Yj4' ,
url : 'https://demo.mediacms.io/view?m=Otbc37Yj4' ,
api _url : 'https://demo.mediacms.io/api/v1/media/Otbc37Yj4' ,
user : 'markos' ,
title : 'Kastania Evrytanias, Central Greece' ,
description : '' ,
add _date : '2025-05-19T00:00:00-04:00' ,
views : 311 ,
media _type : 'video' ,
state : 'public' ,
duration : 25 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/dd3af0e1dece43b490bbafc9400a407a_YtfxVr4.20250517_105515.mp4.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/dd3af0e1dece43b490bbafc9400a407a.tmpl3iqzl10.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 12 ,
dislikes : 1 ,
reported _times : 0 ,
featured : false ,
user _featured : false ,
size : '54.3MB' ,
} ,
{
friendly _token : 'a1BP6J0fM' ,
url : 'https://demo.mediacms.io/view?m=a1BP6J0fM' ,
api _url : 'https://demo.mediacms.io/api/v1/media/a1BP6J0fM' ,
user : 'markos' ,
title : 'Velit sed magnam quiquia amet.' ,
description :
'Numquam quiquia numquam ut etincidunt numquam. Dolore ut sit eius dolorem sed. Neque porro modi dolor ipsum amet dolore quisquam. Ipsum dolore dolor voluptatem eius quiquia etincidunt. Dolore etincidunt amet velit amet ipsum ut. Aliquam etincidunt consectetur est. Consectetur non quiquia voluptatem velit sed quisquam.' ,
add _date : '2024-10-02T05:35:15.434023-04:00' ,
views : 997 ,
media _type : 'video' ,
state : 'public' ,
duration : 11 ,
thumbnail _url :
'https://demo.mediacms.io/media/original/thumbnails/user/markos/32e2cf3ff5fe498da93251034e977d9c.20240527_090548.mp4_qiF5S9H.jpg' ,
is _reviewed : true ,
preview _url :
'https://demo.mediacms.io/media/encoded/1/markos/32e2cf3ff5fe498da93251034e977d9c.tmpheuxmj3y.gif' ,
author _name : 'Markos Gogoulos' ,
author _profile : 'https://demo.mediacms.io/user/markos/' ,
author _thumbnail : 'https://demo.mediacms.io/media/userlogos/2024/10/02/markos.jpeg' ,
encoding _status : 'success' ,
likes : 14 ,
dislikes : 1 ,
reported _times : 0 ,
featured : true ,
user _featured : false ,
size : '3.5MB' ,
} ,
] ,
// VIDEO
media _type : 'video' ,
original _media _url :
'https://demo.mediacms.io/media/original/user/markos/7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4' ,
hls _info : {
master _file :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/master.m3u8' ,
'1080_iframe' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-1/iframes.m3u8' ,
'720_iframe' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-2/iframes.m3u8' ,
'360_iframe' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-3/iframes.m3u8' ,
'240_iframe' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-4/iframes.m3u8' ,
'480_iframe' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-5/iframes.m3u8' ,
'1080_playlist' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-1/stream.m3u8' ,
'720_playlist' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-2/stream.m3u8' ,
'360_playlist' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-3/stream.m3u8' ,
'240_playlist' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-4/stream.m3u8' ,
'480_playlist' :
'https://demo.mediacms.io/media/hls/7dedcb56bde9463dbc0766768a99be0f/media-5/stream.m3u8' ,
} ,
encodings _info : {
2160 : { } ,
1440 : { } ,
1080 : {
h264 : {
title : 'h264-1080' ,
url : 'https://demo.mediacms.io/media/encoded/7/markos/7dedcb56bde9463dbc0766768a99be0f.7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4.mp4' ,
progress : 100 ,
size : '31.4MB' ,
encoding _id : 4688 ,
status : 'success' ,
} ,
} ,
720 : {
h264 : {
title : 'h264-720' ,
url : 'https://demo.mediacms.io/media/encoded/10/markos/7dedcb56bde9463dbc0766768a99be0f.7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4.mp4' ,
progress : 100 ,
size : '17.7MB' ,
encoding _id : 4687 ,
status : 'success' ,
} ,
} ,
480 : {
h264 : {
title : 'h264-480' ,
url : 'https://demo.mediacms.io/media/encoded/13/markos/7dedcb56bde9463dbc0766768a99be0f.7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4.mp4' ,
progress : 100 ,
size : '7.4MB' ,
encoding _id : 4686 ,
status : 'success' ,
} ,
} ,
360 : {
h264 : {
title : 'h264-360' ,
url : 'https://demo.mediacms.io/media/encoded/3/markos/7dedcb56bde9463dbc0766768a99be0f.7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4.mp4' ,
progress : 100 ,
size : '4.0MB' ,
encoding _id : 4685 ,
status : 'success' ,
} ,
} ,
240 : {
h264 : {
title : 'h264-240' ,
url : 'https://demo.mediacms.io/media/encoded/2/markos/7dedcb56bde9463dbc0766768a99be0f.7dedcb56bde9463dbc0766768a99be0f.20250605_110647.mp4.mp4' ,
progress : 100 ,
size : '2.7MB' ,
encoding _id : 4684 ,
status : 'success' ,
} ,
} ,
144 : { } ,
} ,
// AUDIO
/*media_type: 'audio',
original_media_url:
'https://videojs.mediacms.io/media/original/user/markos/174be7a1ecb04850a6927a0af2887ccc.SizzlaHardGround.mp3',
hls_info: {},
encodings_info: {},*/
} ,
// other
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' ,
2025-09-19 08:31:38 +03:00
urlAutoplay : true ,
2025-09-20 02:39:40 +03:00
urlMuted : false ,
2025-09-18 10:28:10 +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-18 10:28:10 +03:00
// CONDITIONAL LOGIC:
2025-09-16 09:58:41 +03:00
// - When chaptersData has content: Uses original ChapterMarkers with sprite preview
// - When chaptersData is empty: Uses separate SpritePreview component
2025-09-19 09:16:42 +03:00
// Utility function to convert time string (HH:MM:SS.mmm) to seconds
const convertTimeStringToSeconds = ( timeString ) => {
if ( typeof timeString === 'number' ) {
return timeString ; // Already in seconds
}
if ( typeof timeString !== 'string' ) {
return 0 ;
}
const parts = timeString . split ( ':' ) ;
if ( parts . length !== 3 ) {
return 0 ;
}
const hours = parseInt ( parts [ 0 ] , 10 ) || 0 ;
const minutes = parseInt ( parts [ 1 ] , 10 ) || 0 ;
const seconds = parseFloat ( parts [ 2 ] ) || 0 ;
return hours * 3600 + minutes * 60 + seconds ;
} ;
// Convert chapters data from backend format to required format with memoization
const convertChaptersData = useMemo ( ( ) => {
return ( rawChaptersData ) => {
if ( ! rawChaptersData || ! Array . isArray ( rawChaptersData ) ) {
return [ ] ;
}
const convertedData = rawChaptersData . map ( ( chapter ) => ( {
startTime : convertTimeStringToSeconds ( chapter . startTime ) ,
endTime : convertTimeStringToSeconds ( chapter . endTime ) ,
2025-09-19 12:27:26 +03:00
chapterTitle : chapter . chapterTitle ,
2025-09-19 09:16:42 +03:00
} ) ) ;
return convertedData ;
} ;
} , [ ] ) ;
// Memoized chapters data conversion
const chaptersData = useMemo ( ( ) => {
if ( mediaData ? . data ? . chapter _data && mediaData ? . data ? . chapter _data . length > 0 ) {
return convertChaptersData ( mediaData ? . data ? . chapter _data ) ;
}
return isDevMode
? [
2025-09-19 12:27:26 +03:00
{ startTime : '00:00:00.000' , endTime : '00:00:04.000' , chapterTitle : 'Introduction' } ,
{ startTime : '00:00:05.000' , endTime : '00:00:10.000' , chapterTitle : 'Overview of Marine Life' } ,
{ startTime : '00:00:10.000' , endTime : '00:00:15.000' , chapterTitle : 'Coral Reef Ecosystems' } ,
{ startTime : '00:00:15.000' , endTime : '00:00:20.000' , chapterTitle : 'Deep Sea Creatures' } ,
{ startTime : '00:00:20.000' , endTime : '00:00:30.000' , chapterTitle : 'Ocean Conservation' } ,
{ startTime : '00:00:24.000' , endTime : '00:00:32.000' , chapterTitle : 'Ocean Conservation' } ,
{ startTime : '00:00:32.000' , endTime : '00:00:40.000' , chapterTitle : 'Climate Change Impact' } ,
{ startTime : '00:00:40.000' , endTime : '00:00:48.000' , chapterTitle : 'Marine Protected Areas' } ,
{ startTime : '00:00:48.000' , endTime : '00:00:56.000' , chapterTitle : 'Sustainable Fishing' } ,
{ startTime : '00:00:56.000' , endTime : '00:00:64.000' , chapterTitle : 'Research Methods' } ,
{ startTime : '00:00:64.000' , endTime : '00:00:72.000' , chapterTitle : 'Future Challenges' } ,
{ startTime : '00:00:72.000' , endTime : '00:00:80.000' , chapterTitle : 'Conclusion' } ,
{ startTime : '00:00:80.000' , endTime : '00:00:88.000' , chapterTitle : 'Marine Biodiversity Hotspots' } ,
{ startTime : '00:00:88.000' , endTime : '00:00:96.000' , chapterTitle : 'Marine Biodiversity test' } ,
{ startTime : '00:00:96.000' , endTime : '00:01:04.000' , chapterTitle : 'Whale Migration Patterns' } ,
{ startTime : '00:01:04.000' , endTime : '00:01:12.000' , chapterTitle : 'Plastic Pollution Crisis' } ,
{ startTime : '00:01:12.000' , endTime : '00:01:20.000' , chapterTitle : 'Seagrass Meadows' } ,
{ startTime : '00:01:20.000' , endTime : '00:01:28.000' , chapterTitle : 'Ocean Acidification' } ,
{ startTime : '00:01:28.000' , endTime : '00:01:36.000' , chapterTitle : 'Marine Archaeology' } ,
{ startTime : '00:01:28.000' , endTime : '00:01:36.000' , chapterTitle : 'Tidal Pool Ecosystems' } ,
{ startTime : '00:01:36.000' , endTime : '00:01:44.000' , chapterTitle : 'Commercial Aquaculture' } ,
{ startTime : '00:01:44.000' , endTime : '00:01:52.000' , chapterTitle : 'Ocean Exploration Technology' } ,
2025-09-19 09:16:42 +03:00
] . map ( ( chapter ) => ( {
startTime : convertTimeStringToSeconds ( chapter . startTime ) ,
endTime : convertTimeStringToSeconds ( chapter . endTime ) ,
2025-09-19 12:27:26 +03:00
chapterTitle : chapter . chapterTitle ,
2025-09-19 09:16:42 +03:00
} ) )
: [ ] ;
} , [ mediaData ? . data ? . chapter _data , isDevMode , convertChaptersData ] ) ;
2025-09-18 10:28:10 +03:00
// Helper function to determine MIME type based on file extension or media type
const getMimeType = ( url , mediaType ) => {
if ( mediaType === 'audio' ) {
if ( url && url . toLowerCase ( ) . includes ( '.mp3' ) ) {
return 'audio/mpeg' ;
}
if ( url && url . toLowerCase ( ) . includes ( '.ogg' ) ) {
return 'audio/ogg' ;
}
if ( url && url . toLowerCase ( ) . includes ( '.wav' ) ) {
return 'audio/wav' ;
}
if ( url && url . toLowerCase ( ) . includes ( '.m4a' ) ) {
return 'audio/mp4' ;
}
// Default audio MIME type
return 'audio/mpeg' ;
}
// Default to video/mp4 for video content
if ( url && url . toLowerCase ( ) . includes ( '.webm' ) ) {
return 'video/webm' ;
}
if ( url && url . toLowerCase ( ) . includes ( '.ogg' ) ) {
return 'video/ogg' ;
}
// Default video MIME type
return 'video/mp4' ;
} ;
2025-07-13 03:37:08 +03:00
2025-09-20 02:39:40 +03:00
// Get user's quality preference for dependency tracking
const userQualityPreference = userPreferences . current . getQualityPreference ( ) ;
2025-07-13 03:37:08 +03:00
// Get video data from mediaData
2025-09-18 10:28:10 +03:00
const currentVideo = useMemo ( ( ) => {
2025-09-20 02:39:40 +03:00
// Get video sources based on available data and user preferences
2025-09-18 10:28:10 +03:00
const getVideoSources = ( ) => {
2025-09-20 02:39:40 +03:00
// Use the extracted quality preference
const userQuality = userQualityPreference ;
2025-09-18 10:28:10 +03:00
// Check if HLS info is available and not empty
2025-09-20 02:39:40 +03:00
if ( mediaData . data ? . hls _info ) {
// If user prefers auto quality or master file doesn't exist for specific quality
if ( userQuality === 'auto' && mediaData . data . hls _info . master _file ) {
return [
{
src : mediaData . siteUrl + mediaData . data . hls _info . master _file ,
type : 'application/x-mpegURL' , // HLS MIME type
label : 'Auto' ,
} ,
] ;
}
// If user has selected a specific quality, try to use that playlist
if ( userQuality !== 'auto' ) {
const qualityKey = ` ${ userQuality . replace ( 'p' , '' ) } _playlist ` ;
if ( mediaData . data . hls _info [ qualityKey ] ) {
return [
{
src : mediaData . data . hls _info [ qualityKey ] ,
type : 'application/x-mpegURL' , // HLS MIME type
label : ` ${ userQuality } p ` ,
} ,
] ;
}
}
// Fallback to master file if specific quality not available
if ( mediaData . data . hls _info . master _file ) {
return [
{
src : mediaData . siteUrl + mediaData . data . hls _info . master _file ,
type : 'application/x-mpegURL' , // HLS MIME type
label : 'Auto' ,
} ,
] ;
}
2025-09-18 10:28:10 +03:00
}
2025-09-15 23:47:43 +03:00
2025-09-18 10:28:10 +03:00
// Fallback to encoded qualities if available
if ( mediaData . data ? . encodings _info ) {
const encodings = mediaData . data . encodings _info ;
2025-09-20 02:39:40 +03:00
const userQuality = userQualityPreference ;
// If user has selected a specific quality, try to use that encoding first
if ( userQuality !== 'auto' ) {
const qualityNumber = userQuality . replace ( 'p' , '' ) ; // Remove 'p' from '240p' -> '240'
if (
encodings [ qualityNumber ] &&
encodings [ qualityNumber ] . h264 &&
encodings [ qualityNumber ] . h264 . url
) {
return [
{
src : encodings [ qualityNumber ] . h264 . url ,
type : getMimeType ( encodings [ qualityNumber ] . h264 . url , mediaData . data ? . media _type ) ,
label : ` ${ qualityNumber } p ` ,
} ,
] ;
}
}
// If auto quality or specific quality not available, return all available qualities
const sources = [ ] ;
2025-09-18 10:28:10 +03:00
// 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 ) {
const sourceUrl = encodings [ quality ] . h264 . url ;
sources . push ( {
src : sourceUrl ,
type : getMimeType ( sourceUrl , mediaData . data ? . media _type ) ,
label : ` ${ quality } p ` ,
} ) ;
2025-09-15 23:47:43 +03:00
}
2025-09-18 10:28:10 +03:00
if ( sources . length > 0 ) {
return sources ;
2025-09-15 23:47:43 +03:00
}
2025-09-18 10:28:10 +03:00
}
2025-09-15 23:47:43 +03:00
2025-09-18 10:28:10 +03:00
// Final fallback to original media URL or sample video
if ( mediaData . data ? . original _media _url ) {
const sourceUrl = mediaData . data . original _media _url ;
2025-09-15 23:47:43 +03:00
return [
2025-09-10 13:53:37 +03:00
{
2025-09-18 10:28:10 +03:00
src : sourceUrl ,
type : getMimeType ( sourceUrl , mediaData . data ? . media _type ) ,
2025-09-10 13:53:37 +03:00
} ,
2025-09-15 23:47:43 +03:00
] ;
2025-09-18 10:28:10 +03:00
}
2025-09-15 23:47:43 +03:00
2025-09-18 10:28:10 +03:00
// Default sample video
return [
{
2025-09-20 02:39:40 +03:00
src : '/videos/sample-video-white.mp4' ,
type : 'video/mp4' ,
} ,
/* {
2025-09-18 10:28:10 +03:00
src: '/videos/sample-video.mp3',
type: 'audio/mpeg',
2025-09-20 02:39:40 +03:00
}, */
2025-09-18 10:28:10 +03:00
] ;
} ;
return {
id : mediaData . data ? . friendly _token || 'default-video' ,
title : mediaData . data ? . title || 'Video' ,
poster : mediaData . data ? . poster _url ? mediaData . siteUrl + mediaData . data . poster _url : '' ,
previewSprite : mediaData ? . previewSprite || { } ,
related _media : mediaData . data ? . related _media || [ ] ,
nextLink : mediaData ? . nextLink || null ,
2025-09-19 08:31:38 +03:00
urlAutoplay : mediaData ? . urlAutoplay || true ,
2025-09-20 02:39:40 +03:00
urlMuted : mediaData ? . urlMuted || false ,
2025-09-18 10:28:10 +03:00
sources : getVideoSources ( ) ,
} ;
2025-09-20 02:39:40 +03:00
} , [ mediaData , userQualityPreference ] ) ;
2025-07-13 03:37:08 +03:00
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' ] ;
2025-09-18 10:28:10 +03:00
2025-09-15 23:47:43 +03:00
// Add qualities from encodings_info if available
if ( mediaData . data ? . encodings _info ) {
const availableQualities = Object . keys ( mediaData . data . encodings _info )
2025-09-18 10:28:10 +03:00
. filter ( ( quality ) => {
2025-09-15 23:47:43 +03:00
const encoding = mediaData . data . encodings _info [ quality ] ;
return encoding && encoding . h264 && encoding . h264 . url ;
} )
2025-09-18 10:28:10 +03:00
. map ( ( quality ) => ` ${ quality } p ` )
2025-09-15 23:47:43 +03:00
. sort ( ( a , b ) => parseInt ( a ) - parseInt ( b ) ) ; // Sort ascending
2025-09-18 10:28:10 +03:00
2025-09-15 23:47:43 +03:00
baseOrder . push ( ... availableQualities ) ;
} else {
// Fallback to standard order
baseOrder . push ( '144p' , '240p' , '360p' , '480p' , '720p' , '1080p' , '1440p' , '2160p' ) ;
}
2025-09-18 10:28:10 +03:00
2025-09-15 23:47:43 +03:00
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 ,
2025-09-18 10:28:10 +03:00
type : q . type || getMimeType ( q . src || q . url || q . href , mediaData . data ? . media _type ) ,
2025-09-10 13:53:37 +03:00
} ) ) ;
2025-09-18 10:28:10 +03:00
2025-09-15 23:47:43 +03:00
// Only include qualities that have actual sources
2025-09-18 10:28:10 +03:00
const validQualities = norm . filter ( ( q ) => q . src ) ;
2025-09-15 23:47:43 +03:00
// 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
2025-09-18 10:28:10 +03:00
Object . keys ( hlsInfo ) . forEach ( ( key ) => {
2025-09-15 23:47:43 +03:00
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
2025-09-18 10:28:10 +03:00
type : getMimeType ( null , mediaData . data ? . media _type ) ,
2025-09-15 23:47:43 +03:00
} ) ;
// Add available encoded qualities dynamically
2025-09-18 10:28:10 +03:00
Object . keys ( encodings ) . forEach ( ( quality ) => {
2025-09-15 23:47:43 +03:00
if ( encodings [ quality ] && encodings [ quality ] . h264 && encodings [ quality ] . h264 . url ) {
2025-09-18 10:28:10 +03:00
const sourceUrl = encodings [ quality ] . h264 . url ;
2025-09-15 23:47:43 +03:00
qualities . push ( {
label : ` ${ quality } p ` ,
value : ` ${ quality } p ` ,
2025-09-18 10:28:10 +03:00
src : sourceUrl ,
type : getMimeType ( sourceUrl , mediaData . data ? . media _type ) ,
2025-09-15 23:47:43 +03:00
} ) ;
}
} ) ;
2025-09-18 10:28:10 +03:00
if ( qualities . length > 1 ) {
// More than just auto
2025-09-15 23:47:43 +03:00
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 ;
2025-09-18 10:28:10 +03:00
const type =
( currentVideo ? . sources && currentVideo . sources [ 0 ] ? . type ) ||
getMimeType ( baseSrc , mediaData . data ? . media _type ) ;
2025-09-10 13:53:37 +03:00
2025-09-15 23:47:43 +03:00
if ( baseSrc ) {
2025-09-18 10:28:10 +03:00
const buildFromBase = [
{
label : 'Auto' ,
value : 'auto' ,
src : baseSrc ,
type ,
} ,
] ;
2025-09-15 23:47:43 +03:00
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-18 10:28:10 +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
2025-09-18 12:13:53 +03:00
const backendSubtitles = mediaData ? . data ? . subtitles _info || ( isDevMode ? demoSubtitleTracks : [ ] ) ;
2025-09-15 23:47:43 +03:00
const hasSubtitles = backendSubtitles . length > 0 ;
2025-09-18 10:28:10 +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
: [ ] ;
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
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 ) {
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-09-20 02:39:40 +03:00
// Autoplay behavior: Try unmuted first, fallback to muted if needed
autoplay : true , // Try unmuted autoplay first (true/false, play, muted, any)
2025-07-13 03:37:08 +03:00
// Start video over when it ends
loop : false ,
2025-09-19 08:31:38 +03:00
// Start video muted (check URL parameter or default)
muted : mediaData . urlMuted || false ,
2025-07-13 03:37:08 +03:00
// 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
2025-09-18 10:28:10 +03:00
audioPosterMode : mediaData . data ? . media _type === 'audio' ,
2025-07-13 03:37:08 +03:00
// 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
2025-09-18 11:15:26 +03:00
id : mediaData . id ,
2025-07-13 03:37:08 +03:00
// 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 ,
2025-09-18 10:28:10 +03:00
// Ensures consistent autoplay behavior across browsers (prevents unexpected blocking or auto-play issues)
normalizeAutoplay : true ,
2025-07-13 03:37:08 +03:00
// 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 . 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-09-19 08:31:38 +03:00
// Expose the player instance globally for timestamp functionality
if ( typeof window !== 'undefined' ) {
if ( ! window . videojsPlayers ) {
window . videojsPlayers = { } ;
}
window . videojsPlayers [ videoId ] = playerRef . current ;
}
// Call the onPlayerInitCallback if provided via MEDIA_DATA
if ( mediaData . onPlayerInitCallback && typeof mediaData . onPlayerInitCallback === 'function' ) {
mediaData . onPlayerInitCallback ( { player : playerRef . current } , playerRef . current . el ( ) ) ;
}
// Handle URL timestamp parameter
if ( mediaData . urlTimestamp !== null && mediaData . urlTimestamp >= 0 ) {
const timestamp = mediaData . urlTimestamp ;
// Wait for video metadata to be loaded before seeking
if ( playerRef . current . readyState ( ) >= 1 ) {
// Metadata is already loaded, seek immediately
if ( timestamp < playerRef . current . duration ( ) ) {
playerRef . current . currentTime ( timestamp ) ;
} else if ( timestamp >= 0 ) {
playerRef . current . play ( ) ;
}
} else {
// Wait for metadata to load
playerRef . current . one ( 'loadedmetadata' , ( ) => {
if ( timestamp >= 0 && timestamp < playerRef . current . duration ( ) ) {
playerRef . current . currentTime ( timestamp ) ;
} else if ( timestamp >= 0 ) {
playerRef . current . play ( ) ;
}
} ) ;
}
}
2025-09-20 02:39:40 +03:00
// Detect if user has interacted with the page
const hasUserInteracted = ( ) => {
// Check various indicators of user interaction
return (
document . hasFocus ( ) ||
document . visibilityState === 'visible' ||
sessionStorage . getItem ( 'userInteracted' ) === 'true'
) ;
} ;
// Handle autoplay while respecting user's saved preferences
const handleAutoplay = async ( ) => {
const userInteracted = hasUserInteracted ( ) ;
const savedMuteState = userPreferences . current . getPreference ( 'muted' ) ;
try {
// Respect user's saved mute preference, but try unmuted if user interacted and hasn't explicitly muted
if ( ! mediaData . urlMuted && userInteracted && savedMuteState !== true ) {
playerRef . current . muted ( false ) ;
}
// First attempt: try to play with current mute state
await playerRef . current . play ( ) ;
} catch ( error ) {
// Fallback to muted autoplay unless user explicitly wants to stay unmuted
if ( ! playerRef . current . muted ( ) ) {
try {
playerRef . current . muted ( true ) ;
await playerRef . current . play ( ) ;
// Only try to restore sound if user hasn't explicitly saved mute=true
if ( savedMuteState !== true ) {
// Aggressively try to restore sound
const restoreSound = ( ) => {
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
playerRef . current . muted ( false ) ;
playerRef . current . trigger ( 'notify' , '🔊 Sound enabled!' ) ;
}
} ;
// Try to restore sound immediately if user has interacted
if ( userInteracted ) {
setTimeout ( restoreSound , 100 ) ;
} else {
// Show notification for manual interaction
setTimeout ( ( ) => {
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
playerRef . current . trigger (
'notify' ,
'🔇 Click anywhere to enable sound'
) ;
}
} , 1000 ) ;
// Set up interaction listeners
const enableSound = ( ) => {
restoreSound ( ) ;
// Mark user interaction for future videos
sessionStorage . setItem ( 'userInteracted' , 'true' ) ;
// Remove listeners
document . removeEventListener ( 'click' , enableSound ) ;
document . removeEventListener ( 'keydown' , enableSound ) ;
document . removeEventListener ( 'touchstart' , enableSound ) ;
} ;
document . addEventListener ( 'click' , enableSound , { once : true } ) ;
document . addEventListener ( 'keydown' , enableSound , { once : true } ) ;
document . addEventListener ( 'touchstart' , enableSound , { once : true } ) ;
}
}
} catch ( mutedError ) {
console . error ( '❌ Even muted autoplay was blocked:' , mutedError . message ) ;
}
}
}
} ;
2025-09-19 08:31:38 +03:00
if ( mediaData ? . urlAutoplay ) {
2025-09-20 02:39:40 +03:00
// Explicit autoplay requested via URL parameter
handleAutoplay ( ) ;
2025-09-19 08:31:38 +03:00
} else {
2025-09-20 02:39:40 +03:00
// Auto-start video on page load/reload with fallback strategy
handleAutoplay ( ) ;
2025-09-19 08:31:38 +03:00
}
2025-09-18 10:28:10 +03:00
const setupMobilePlayPause = ( ) => {
const playerEl = playerRef . current . el ( ) ;
const videoEl = playerEl . querySelector ( 'video' ) ;
if ( videoEl ) {
// Remove default touch handling that might interfere
videoEl . style . touchAction = 'manipulation' ;
// Add mobile-specific touch event handlers
let touchStartTime = 0 ;
let touchStartPos = { x : 0 , y : 0 } ;
const handleTouchStart = ( e ) => {
touchStartTime = Date . now ( ) ;
const touch = e . touches [ 0 ] ;
touchStartPos = { x : touch . clientX , y : touch . clientY } ;
} ;
const handleTouchEnd = ( e ) => {
const touchEndTime = Date . now ( ) ;
const touchDuration = touchEndTime - touchStartTime ;
// Only handle if it's a quick tap
if ( touchDuration < 500 ) {
const touch = e . changedTouches [ 0 ] ;
const touchEndPos = { x : touch . clientX , y : touch . clientY } ;
const distance = Math . sqrt (
Math . pow ( touchEndPos . x - touchStartPos . x , 2 ) +
Math . pow ( touchEndPos . y - touchStartPos . y , 2 )
) ;
// Only trigger if it's a tap (not a swipe)
if ( distance < 50 ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
if ( playerRef . current . paused ( ) ) {
playerRef . current . play ( ) ;
} else {
playerRef . current . pause ( ) ;
}
}
}
} ;
videoEl . addEventListener ( 'touchstart' , handleTouchStart , { passive : true } ) ;
videoEl . addEventListener ( 'touchend' , handleTouchEnd , { passive : false } ) ;
}
} ;
setTimeout ( setupMobilePlayPause , 100 ) ;
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' ) ;
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
2025-09-20 02:39:40 +03:00
setTimeout ( async ( ) => {
2025-07-13 15:34:36 +03:00
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
2025-09-20 02:39:40 +03:00
try {
await playerRef . current . play ( ) ;
} catch ( error ) {
console . error ( 'ℹ ️ Browser prevented play:' , error . message ) ;
// Try muted playback as fallback
if ( ! playerRef . current . muted ( ) ) {
try {
playerRef . current . muted ( true ) ;
await playerRef . current . play ( ) ;
} catch ( mutedError ) {
console . error ( 'ℹ ️ Even muted play was blocked:' , mutedError . message ) ;
}
}
}
2025-07-13 03:37:08 +03:00
}
} , 100 ) ;
}
2025-07-14 03:19:27 +03:00
// BEGIN: Add subtitle tracks
2025-09-18 10:28:10 +03:00
if ( hasSubtitles ) {
try {
const savedLang = userPreferences . current . getPreference ( 'subtitleLanguage' ) ;
const enabled = userPreferences . current . getPreference ( 'subtitleEnabled' ) ;
const matchLang = ( t , target ) => {
const tl = String ( t . srclang || t . language || '' ) . toLowerCase ( ) ;
const sl = String ( target || '' ) . toLowerCase ( ) ;
if ( ! tl || ! sl ) return false ;
return tl === sl || tl . startsWith ( sl + '-' ) || sl . startsWith ( tl + '-' ) ;
} ;
const tracksToAdd = subtitleTracks . map ( ( t ) => ( {
... t ,
// Hint iOS by marking default on the matched track when enabled
default : ! ! ( enabled && savedLang && matchLang ( t , savedLang ) ) ,
} ) ) ;
tracksToAdd . forEach ( ( track ) => {
playerRef . current . addRemoteTextTrack ( track , false ) ;
} ) ;
} catch ( e ) {
// Fallback: add as-is
subtitleTracks . forEach ( ( track ) => {
playerRef . current . addRemoteTextTrack ( track , false ) ;
} ) ;
}
}
2025-07-14 04:20:45 +03:00
2025-09-18 10:28:10 +03:00
// Apply saved subtitle preference immediately and on key readiness events
2025-09-15 14:41:28 +03:00
userPreferences . current . applySubtitlePreference ( playerRef . current ) ;
2025-09-18 10:28:10 +03:00
playerRef . current . one ( 'loadeddata' , ( ) =>
userPreferences . current . applySubtitlePreference ( playerRef . current )
) ;
playerRef . current . one ( 'canplay' , ( ) =>
userPreferences . current . applySubtitlePreference ( playerRef . current )
) ;
2025-07-14 03:19:27 +03:00
// 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 ,
2025-09-20 02:39:40 +03:00
chapter . chapterTitle
2025-07-14 03:19:27 +03:00
) ;
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-18 12:13:53 +03:00
if ( ! isEmbedPlayer && ( mediaData ? . nextLink || isDevMode ) ) {
2025-09-18 10:28:10 +03:00
// 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 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
2025-09-18 12:03:00 +03:00
if ( ! isEmbedPlayer ) {
try {
const autoplayToggleButton = new AutoplayToggleButton ( playerRef . current , {
userPreferences : userPreferences . current ,
} ) ;
2025-09-20 02:39:40 +03:00
// Add it before the chapters button (or at a suitable position)
const chaptersButtonIndex = controlBar . children ( ) . indexOf ( chaptersButton ) ;
const insertIndex =
chaptersButtonIndex > 0 ? chaptersButtonIndex : controlBar . children ( ) . length - 3 ;
controlBar . addChild ( autoplayToggleButton , { } , insertIndex ) ;
2025-07-22 05:18:43 +03:00
2025-09-18 12:03:00 +03:00
// Store reference for later use
customComponents . current . autoplayToggleButton = autoplayToggleButton ;
2025-07-22 05:18:43 +03:00
2025-09-18 12:03:00 +03:00
// Force update icon after adding to DOM to ensure correct display
setTimeout ( ( ) => {
autoplayToggleButton . updateIcon ( ) ;
} , 100 ) ;
} catch ( error ) {
console . error ( '✗ Failed to add autoplay toggle button:' , error ) ;
}
2025-07-22 05:18:43 +03:00
}
// END: Implement autoplay toggle button
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 ( ) ;
}
} ) ;
} 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-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 ] ;
2025-09-18 10:28:10 +03:00
if ( t . kind === 'subtitles' && t . mode === 'showing' ) {
any = true ;
break ;
}
2025-09-15 14:41:28 +03:00
}
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
2025-09-18 10:28:10 +03:00
try {
userPreferences . current . setPreference (
'subtitleEnabled' ,
false ,
true
) ;
} catch ( e ) { }
2025-09-15 14:41:28 +03:00
} else {
// Show using previously chosen language only; do not change it
2025-09-18 10:28:10 +03:00
const preferred =
userPreferences . current . getPreference ( 'subtitleLanguage' ) ;
2025-09-15 14:41:28 +03:00
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 ] ;
2025-09-18 10:28:10 +03:00
if ( t . kind === 'subtitles' ) {
first = t . language ;
break ;
}
2025-09-15 14:41:28 +03:00
}
if ( first ) {
for ( let i = 0 ; i < tracks . length ; i ++ ) {
const t = tracks [ i ] ;
2025-09-18 10:28:10 +03:00
if ( t . kind === 'subtitles' )
t . mode = t . language === first ? 'showing' : 'disabled' ;
2025-09-15 14:41:28 +03:00
}
2025-09-18 10:28:10 +03:00
try {
userPreferences . current . setPreference (
'subtitleLanguage' ,
first ,
true
) ;
} catch ( e ) { }
try {
userPreferences . current . setPreference (
'subtitleEnabled' ,
true ,
true
) ;
} catch ( e ) { }
2025-09-15 14:41:28 +03:00
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' ) ;
2025-09-18 10:28:10 +03:00
try {
userPreferences . current . setPreference (
'subtitleEnabled' ,
true ,
true
) ;
} catch ( e ) { }
2025-09-15 14:41:28 +03:00
}
}
} ;
el . addEventListener ( 'click' , toggleSubs , { capture : true } ) ;
2025-09-18 10:28:10 +03:00
2025-09-15 14:41:28 +03:00
// Add mobile touch support
2025-09-18 10:28:10 +03:00
el . addEventListener (
'touchend' ,
( e ) => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
toggleSubs ( e ) ;
} ,
{ passive : false }
) ;
2025-09-15 14:41:28 +03:00
// 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 ] ;
2025-09-18 10:28:10 +03:00
if ( t . kind === 'subtitles' && t . mode === 'showing' ) {
any = true ;
break ;
}
2025-09-15 14:41:28 +03:00
}
2025-09-18 10:28:10 +03:00
if ( any ) el . classList . add ( 'vjs-subs-active' ) ;
else el . classList . remove ( 'vjs-subs-active' ) ;
2025-09-15 14:41:28 +03:00
} ) ;
// Initialize state immediately
const tracks = playerRef . current . textTracks ( ) ;
let any = false ;
for ( let i = 0 ; i < tracks . length ; i ++ ) {
const t = tracks [ i ] ;
2025-09-18 10:28:10 +03:00
if ( t . kind === 'subtitles' && t . mode === 'showing' ) {
any = true ;
break ;
}
2025-09-15 14:41:28 +03:00
}
if ( any ) el . classList . add ( 'vjs-subs-active' ) ;
break ;
}
}
2025-07-13 03:37:08 +03:00
} ;
setupClickableMenus ( ) ;
} , 1500 ) ;
2025-09-16 09:58:41 +03:00
// BEGIN: Add chapter markers and sprite preview to progress control
2025-07-14 03:19:27 +03:00
if ( progressControl && seekBar ) {
2025-09-16 09:58:41 +03:00
// Check if we have chapters
const hasChapters = chaptersData && chaptersData . length > 0 ;
2025-09-18 10:28:10 +03:00
2025-09-16 09:58:41 +03:00
if ( hasChapters ) {
// Use original ChapterMarkers with sprite functionality when 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
const spritePreview = new SpritePreview ( playerRef . current , {
previewSprite : mediaData . previewSprite ,
} ) ;
seekBar . addChild ( spritePreview ) ;
2025-09-18 10:28:10 +03:00
2025-09-16 09:58:41 +03:00
// Setup sprite preview hover functionality
setTimeout ( ( ) => {
spritePreview . setupProgressBarHover ( ) ;
} , 100 ) ;
}
2025-07-14 03:19:27 +03:00
}
2025-09-16 09:58:41 +03:00
// END: Add chapter markers and sprite preview to progress control
2025-07-14 03:19:27 +03:00
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
// 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 ) ;
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-09-20 02:39:40 +03:00
// BEGIN: Move Picture-in-Picture and Fullscreen buttons to the very end
setTimeout ( ( ) => {
2025-07-14 03:19:27 +03:00
try {
2025-09-20 02:39:40 +03:00
const pictureInPictureToggle = controlBar . getChild ( 'pictureInPictureToggle' ) ;
const fullscreenToggle = controlBar . getChild ( 'fullscreenToggle' ) ;
// Move Picture-in-Picture button to the very end (if it exists)
if ( pictureInPictureToggle ) {
controlBar . removeChild ( pictureInPictureToggle ) ;
controlBar . addChild ( pictureInPictureToggle ) ;
}
// Move Fullscreen button to the very end (after PiP)
if ( fullscreenToggle ) {
controlBar . removeChild ( fullscreenToggle ) ;
controlBar . addChild ( fullscreenToggle ) ;
}
2025-07-14 03:19:27 +03:00
} catch ( e ) {
2025-09-20 02:39:40 +03:00
console . error ( '✗ Failed to move PiP/Fullscreen buttons to end:' , e ) ;
2025-07-13 03:37:08 +03:00
}
2025-09-20 02:39:40 +03:00
} , 100 ) ;
// END: Move Picture-in-Picture and Fullscreen buttons to the very end
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' ,
2025-09-18 10:28:10 +03:00
channelName : 'Chapter' ,
2025-09-10 13:53:37 +03:00
thumbnail : mediaData ? . data ? . thumbnail _url || mediaData ? . data ? . author _thumbnail || '' ,
2025-07-14 03:19:27 +03:00
} ) ;
}
// 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' , ( ) => {
2025-09-18 10:28:10 +03:00
if (
customComponents . current . settingsMenu &&
customComponents . current . settingsMenu . setQualities
) {
2025-09-15 14:41:28 +03:00
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
2025-09-22 22:48:40 +03:00
// BEGIN: Add comprehensive keyboard event handling
const handleAllKeyboardEvents = ( event ) => {
// Only handle if no input elements are focused
2025-07-22 01:09:55 +03:00
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
}
2025-09-22 22:48:40 +03:00
// Handle space key for play/pause
if ( event . code === 'Space' || event . key === ' ' ) {
event . preventDefault ( ) ;
if ( playerRef . current ) {
if ( playerRef . current . paused ( ) ) {
playerRef . current . play ( ) ;
} else {
playerRef . current . pause ( ) ;
}
}
return ;
}
// Handle arrow keys for seeking
2025-07-22 01:09:55 +03:00
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 ) ;
2025-09-19 08:31:38 +03:00
if ( customComponents . current . seekIndicator ) {
customComponents . current . seekIndicator . show ( 'forward' , seekAmount ) ;
}
2025-07-22 01:09:55 +03:00
} 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 ) ;
2025-09-19 08:31:38 +03:00
if ( customComponents . current . seekIndicator ) {
customComponents . current . seekIndicator . show ( 'backward' , seekAmount ) ;
}
2025-07-22 01:09:55 +03:00
}
} ;
// Add keyboard event listener to the document
2025-09-22 22:48:40 +03:00
document . addEventListener ( 'keydown' , handleAllKeyboardEvents ) ;
2025-07-22 01:09:55 +03:00
2025-09-22 22:48:40 +03:00
// Store cleanup function for arrow keys
customComponents . current . cleanupArrowKeyHandler = ( ) => {
document . removeEventListener ( 'keydown' , handleAllKeyboardEvents ) ;
2025-07-22 01:09:55 +03:00
} ;
2025-09-22 22:48:40 +03:00
// END: Add comprehensive keyboard event handling
2025-07-13 03:37:08 +03:00
} ) ;
// Listen for next video event
playerRef . current . on ( 'nextVideo' , ( ) => {
goToNextVideo ( ) ;
} ) ;
playerRef . current . on ( 'play' , ( ) => {
2025-09-15 14:41:28 +03:00
// Only show play indicator if not changing quality
2025-09-19 08:31:38 +03:00
if ( ! playerRef . current . isChangingQuality && customComponents . current . seekIndicator ) {
2025-09-15 14:41:28 +03:00
customComponents . current . seekIndicator . show ( 'play' ) ;
}
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'pause' , ( ) => {
2025-09-15 14:41:28 +03:00
// Only show pause indicator if not changing quality
2025-09-19 08:31:38 +03:00
if ( ! playerRef . current . isChangingQuality && customComponents . current . seekIndicator ) {
2025-09-15 14:41:28 +03:00
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' , ( ) => {
// 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
// Check if autoplay is enabled and there's a next video
const isAutoplayEnabled = userPreferences . current . getAutoplayPreference ( ) ;
const hasNextVideo = mediaData . nextLink !== null ;
2025-09-18 12:03:00 +03:00
if ( ! isEmbedPlayer && isAutoplayEnabled && hasNextVideo ) {
2025-07-22 05:18:43 +03:00
// 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 : ( ) => {
goToNextVideo ( ) ;
} ,
onCancel : ( ) => {
// 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 ) {
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 ) => {
2025-09-20 02:39:40 +03:00
// console.error('Video.js error:', error);
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'fullscreenchange' , ( ) => {
2025-09-20 02:39:40 +03:00
// console.log('Fullscreen changed:', playerRef.current.isFullscreen());
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'volumechange' , ( ) => {
2025-09-20 02:39:40 +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-09-20 02:39:40 +03:00
// console.log('Playback rate changed:', playerRef.current.playbackRate());
2025-07-13 03:37:08 +03:00
} ) ;
playerRef . current . on ( 'texttrackchange' , ( ) => {
2025-09-20 02:39:40 +03:00
// console.log('Text track changed');
2025-07-13 03:37:08 +03:00
} ) ;
// Focus the player element so keyboard controls work
2025-09-22 22:48:40 +03:00
// This ensures keyboard events work properly in both normal and fullscreen modes
2025-07-13 03:37:08 +03:00
playerRef . current . ready ( ( ) => {
2025-09-22 22:48:40 +03:00
// Focus the player element and set up focus handling
2025-07-13 03:37:08 +03:00
if ( playerRef . current . el ( ) ) {
2025-09-19 09:16:42 +03:00
// Make the video element focusable
const videoElement = playerRef . current . el ( ) ;
videoElement . setAttribute ( 'tabindex' , '0' ) ;
videoElement . focus ( ) ;
2025-09-22 22:48:40 +03:00
}
} ) ;
2025-09-19 09:16:42 +03:00
2025-09-22 22:48:40 +03:00
// Handle focus when entering/exiting fullscreen to ensure keyboard events work
playerRef . current . on ( 'fullscreenchange' , ( ) => {
setTimeout ( ( ) => {
if ( playerRef . current && playerRef . current . el ( ) ) {
const videoElement = playerRef . current . el ( ) ;
videoElement . setAttribute ( 'tabindex' , '0' ) ;
videoElement . focus ( ) ;
// In fullscreen mode, ensure the player container has focus
if ( playerRef . current . isFullscreen ( ) ) {
// Focus the fullscreen element to ensure keyboard events are captured
const fullscreenElement =
document . fullscreenElement ||
document . webkitFullscreenElement ||
document . mozFullScreenElement ||
document . msFullscreenElement ;
if ( fullscreenElement ) {
fullscreenElement . setAttribute ( 'tabindex' , '0' ) ;
fullscreenElement . focus ( ) ;
2025-09-19 09:16:42 +03:00
}
}
2025-09-22 22:48:40 +03:00
}
} , 100 ) ; // Small delay to ensure fullscreen transition is complete
2025-07-13 03:37:08 +03:00
} ) ;
}
} , 0 ) ;
return ( ) => {
clearTimeout ( timer ) ;
} ;
}
// Cleanup function
return ( ) => {
2025-09-22 22:48:40 +03:00
// Clean up keyboard event listeners if they exist
if ( customComponents . current && customComponents . current . cleanupArrowKeyHandler ) {
customComponents . current . cleanupArrowKeyHandler ( ) ;
2025-07-22 01:09:55 +03:00
}
2025-07-13 03:37:08 +03:00
if ( playerRef . current && ! playerRef . current . isDisposed ( ) ) {
playerRef . current . dispose ( ) ;
playerRef . current = null ;
}
} ;
} , [ ] ) ;
2025-09-19 09:16:42 +03:00
// Additional effect to ensure video gets focus for keyboard controls on page load
2025-07-13 03:37:08 +03:00
useEffect ( ( ) => {
const focusVideo = ( ) => {
if ( playerRef . current && playerRef . current . el ( ) ) {
2025-09-19 09:16:42 +03:00
const videoElement = playerRef . current . el ( ) ;
videoElement . setAttribute ( 'tabindex' , '0' ) ;
videoElement . focus ( ) ;
2025-07-13 03:37:08 +03:00
}
} ;
// 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 ) ;
2025-09-19 09:16:42 +03:00
// Multiple attempts to ensure focus on page load
const focusAttempts = [ 100 , 500 , 1000 , 2000 ] ;
const timeouts = focusAttempts . map ( ( delay ) => setTimeout ( focusVideo , delay ) ) ;
2025-07-13 03:37:08 +03:00
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-09-19 09:16:42 +03:00
timeouts . forEach ( clearTimeout ) ;
2025-07-13 03:37:08 +03:00
} ;
} , [ ] ) ;
2025-09-18 11:15:26 +03:00
return < video ref = { videoRef } id = { videoId } className = "video-js vjs-default-skin" tabIndex = "0" / > ;
2025-07-13 03:37:08 +03:00
}
export default VideoJSPlayer ;