diff --git a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx
index f7e79066..1f609713 100644
--- a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx
+++ b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx
@@ -589,12 +589,13 @@ const TimelineControls = ({
// Update display time and check for transitions between segments and empty spaces
useEffect(() => {
- // Always update display time to match current video time when playing
+ // Always update display time to match current video time
if (videoRef.current) {
- // If video is playing, always update the displayed time in the tooltip
+ // Always update display time when current time changes (both playing and paused)
+ setDisplayTime(currentTime);
+
+ // If video is playing, also update the tooltip and perform segment checks
if (!videoRef.current.paused) {
- setDisplayTime(currentTime);
-
// Also update clicked time to keep them in sync when playing
// This ensures correct time is shown when pausing
setClickedTime(currentTime);
diff --git a/frontend-tools/video-editor/client/src/App.tsx b/frontend-tools/video-editor/client/src/App.tsx
index a8a48bb6..5428a068 100644
--- a/frontend-tools/video-editor/client/src/App.tsx
+++ b/frontend-tools/video-editor/client/src/App.tsx
@@ -253,7 +253,8 @@ const App = () => {
case 'ArrowLeft':
event.preventDefault();
if (videoRef.current) {
- const newTime = Math.max(currentTime - 10, 0);
+ // Use the video element's current time directly to avoid stale state
+ const newTime = Math.max(videoRef.current.currentTime - 10, 0);
handleMobileSafeSeek(newTime);
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
}
@@ -261,7 +262,8 @@ const App = () => {
case 'ArrowRight':
event.preventDefault();
if (videoRef.current) {
- const newTime = Math.min(currentTime + 10, duration);
+ // Use the video element's current time directly to avoid stale state
+ const newTime = Math.min(videoRef.current.currentTime + 10, duration);
handleMobileSafeSeek(newTime);
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
}
@@ -274,7 +276,7 @@ const App = () => {
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
- }, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
+ }, [handlePlay, handleMobileSafeSeek, duration, videoRef]);
return (
diff --git a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx
index a43fc8d0..f24a9920 100644
--- a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx
+++ b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx
@@ -779,12 +779,13 @@ const TimelineControls = ({
// Update display time and check for transitions between segments and empty spaces
useEffect(() => {
- // Always update display time to match current video time when playing
+ // Always update display time to match current video time
if (videoRef.current) {
- // If video is playing, always update the displayed time in the tooltip
+ // Always update display time when current time changes (both playing and paused)
+ setDisplayTime(currentTime);
+
+ // If video is playing, also update the tooltip and perform segment checks
if (!videoRef.current.paused) {
- setDisplayTime(currentTime);
-
// Also update clicked time to keep them in sync when playing
// This ensures correct time is shown when pausing
setClickedTime(currentTime);
diff --git a/frontend-tools/video-js/index-embed-old.html b/frontend-tools/video-js/index-embed-old.html
deleted file mode 100644
index 71716245..00000000
--- a/frontend-tools/video-js/index-embed-old.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
VideoJS
-
-
-
-
-
-
diff --git a/frontend-tools/video-js/index-old.html b/frontend-tools/video-js/index-old.html
deleted file mode 100644
index d5c6db6d..00000000
--- a/frontend-tools/video-js/index-old.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
VideoJS
-
-
-
-
-
-
diff --git a/frontend-tools/video-js/src/assets/sample-media-file.json b/frontend-tools/video-js/src/assets/sample-media-file.json
index a261c857..02ae30a1 100644
--- a/frontend-tools/video-js/src/assets/sample-media-file.json
+++ b/frontend-tools/video-js/src/assets/sample-media-file.json
@@ -1,79 +1,70 @@
{
- "url": "https://videojs.mediacms.io/view?m=d8vz3n3PZ",
- "user": "markos",
- "title": "ExitofElygiaGorge,Chania,Crete.mp4 (Trimmed)",
- "description": "Checkout an awesome timestamp at 00:00:14 and another one is 00:00:24",
- "add_date": "2025-09-25T13:12:26.160968+01:00",
- "edit_date": "2025-09-25T13:12:26.403883+01:00",
+ "url": "https://deic.mediacms.io/view?m=mNm3XcqOm",
+ "user": "giannis",
+ "title": "samplevideo10m.mp4",
+ "description": "",
+ "add_date": "2025-06-24T12:43:03.640948+01:00",
+ "edit_date": "2025-10-25T12:50:37.898321+01:00",
"media_type": "video",
- "state": "public",
- "duration": 20,
- "thumbnail_url": "/media/original/thumbnails/user/markos/d393b72d279d4d0da70a484eb6b8b44f_XEowv3s.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.jpg",
- "poster_url": "/media/original/thumbnails/user/markos/d393b72d279d4d0da70a484eb6b8b44f_1JFT5EJ.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.jpg",
+ "state": "private",
+ "duration": 608,
+ "thumbnail_url": "/media/original/thumbnails/user/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd_shMkDEC.samplevideo10m.mp4.jpg",
+ "poster_url": "/media/original/thumbnails/user/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd_YkFI3Fg.samplevideo10m.mp4.jpg",
"thumbnail_time": null,
- "sprites_url": "/media/original/thumbnails/user/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4sprites.jpg",
- "preview_url": "/media/encoded/1/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.tmpm_mh1zlk.gif",
- "author_name": "Markos",
- "author_profile": "/user/markos/",
- "author_thumbnail": "/media/userlogos/2025/09/23/markos.jpeg",
+ "sprites_url": "/media/original/thumbnails/user/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4sprites.jpg",
+ "preview_url": "/media/encoded/1/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.tmpvldjbxzb.gif",
+ "author_name": "Giannis Christodoulou",
+ "author_profile": "/user/giannis/",
+ "author_thumbnail": "/media/userlogos/user.jpg",
"encodings_info": {
- "144": {
- "h264": {
- "title": "h264-144",
- "url": "/media/encoded/23/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
- "progress": 100,
- "size": "0.9MB",
- "encoding_id": 157,
- "status": "success"
- }
- },
+ "144": {},
"240": {
"h264": {
"title": "h264-240",
- "url": "/media/encoded/2/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
+ "url": "/media/encoded/2/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4.mp4",
"progress": 100,
- "size": "1.5MB",
- "encoding_id": 160,
+ "size": "3.4MB",
+ "encoding_id": 2367,
"status": "success"
}
},
"360": {
"h264": {
"title": "h264-360",
- "url": "/media/encoded/3/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
+ "url": "/media/encoded/3/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4.mp4",
"progress": 100,
- "size": "2.3MB",
- "encoding_id": 158,
+ "size": "4.7MB",
+ "encoding_id": 2368,
"status": "success"
}
},
"480": {
"h264": {
"title": "h264-480",
- "url": "/media/encoded/13/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
+ "url": "/media/encoded/13/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4.mp4",
"progress": 100,
- "size": "4.2MB",
- "encoding_id": 154,
+ "size": "6.2MB",
+ "encoding_id": 2369,
"status": "success"
}
},
"720": {
"h264": {
"title": "h264-720",
- "url": "/media/encoded/10/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
+ "url": "/media/encoded/10/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4.mp4",
"progress": 100,
- "size": "12.2MB",
- "encoding_id": 155,
+ "size": "8.9MB",
+ "encoding_id": 2370,
"status": "success"
}
},
"1080": {
"h264": {
"title": "h264-1080",
- "url": "/media/encoded/7/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4.mp4",
+ "url": "/media/encoded/7/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4.mp4",
"progress": 100,
- "size": "25.9MB",
- "encoding_id": 159,
+ "size": "13.8MB",
+ "encoding_id": 2371,
"status": "success"
}
},
@@ -81,1160 +72,54 @@
"2160": {}
},
"encoding_status": "success",
- "views": 58,
+ "views": 5,
"likes": 1,
"dislikes": 0,
"reported_times": 0,
"user_featured": false,
- "original_media_url": "/media/original/user/markos/d393b72d279d4d0da70a484eb6b8b44f.c4668407eb354740a35d933ed7d2786c.ExitofElygiaGorgeChaniaCrete.mp4",
- "size": "49.1MB",
+ "original_media_url": "/media/original/user/giannis/5bdc2acc8a254c4da09b5af92fcc7ebd.samplevideo10m.mp4",
+ "size": "43.5MB",
"video_height": 1080,
"enable_comments": true,
"categories_info": [],
"is_reviewed": true,
- "edit_url": "/edit?m=d8vz3n3PZ",
+ "edit_url": "/edit?m=mNm3XcqOm",
"tags_info": [],
"hls_info": {
- "master_file": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/master.m3u8",
- "720_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-1/iframes.m3u8",
- "144_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-2/iframes.m3u8",
- "360_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-3/iframes.m3u8",
- "1080_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-4/iframes.m3u8",
- "240_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-5/iframes.m3u8",
- "480_iframe": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-6/iframes.m3u8",
- "720_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-1/stream.m3u8",
- "144_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-2/stream.m3u8",
- "360_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-3/stream.m3u8",
- "1080_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-4/stream.m3u8",
- "240_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-5/stream.m3u8",
- "480_playlist": "/media/hls/d393b72d279d4d0da70a484eb6b8b44f/media-6/stream.m3u8"
+ "master_file": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/master.m3u8",
+ "1080_iframe": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-1/iframes.m3u8",
+ "720_iframe": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-2/iframes.m3u8",
+ "480_iframe": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-3/iframes.m3u8",
+ "360_iframe": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-4/iframes.m3u8",
+ "240_iframe": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-5/iframes.m3u8",
+ "1080_playlist": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-1/stream.m3u8",
+ "720_playlist": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-2/stream.m3u8",
+ "480_playlist": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-3/stream.m3u8",
+ "360_playlist": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-4/stream.m3u8",
+ "240_playlist": "/media/hls/5bdc2acc8a254c4da09b5af92fcc7ebd/media-5/stream.m3u8"
},
"license": null,
- "__subtitles_info": [
- {
- "src": "https://videojs.mediacms.io/media/original/subtitles/user/markos/7e5aaed284954ae497d09783bf81004f.ExitofElygiaGorgeChaniaCrete.vtt",
- "srclang": "en",
- "label": "English"
- }
- ],
+ "subtitles_info": [],
"chapter_data": [
{
- "startTime": "00:00:00.000",
- "endTime": "00:00:02.322",
- "chapterTitle": "Chapter 1"
+ "startTime": "00:00:37.184",
+ "endTime": "00:01:45.969",
+ "chapterTitle": "test1"
},
{
- "startTime": "00:00:03.786",
- "endTime": "00:00:06.049",
- "chapterTitle": "Chapter 2"
+ "startTime": "00:02:23.423",
+ "endTime": "00:05:18.439",
+ "chapterTitle": "test2"
},
{
- "startTime": "00:00:07.864",
- "endTime": "00:00:10.791",
- "chapterTitle": "Chapter 3"
- },
- {
- "startTime": "00:00:12.411",
- "endTime": "00:00:19.514",
- "chapterTitle": "Chapter 4"
+ "startTime": "00:05:57.580",
+ "endTime": "00:08:12.337",
+ "chapterTitle": "test3"
}
],
"ratings_info": [],
- "add_subtitle_url": "/add_subtitle?m=d8vz3n3PZ",
+ "add_subtitle_url": "/add_subtitle?m=mNm3XcqOm",
"allow_download": true,
"slideshow_items": [],
- "related_media": [
- {
- "friendly_token": "YprzLzICe",
- "url": "https://videojs.mediacms.io/view?m=YprzLzICe",
- "api_url": "https://videojs.mediacms.io/api/v1/media/YprzLzICe",
- "user": "admin",
- "title": "14249923_3840_2160_60fps.mp4",
- "description": "",
- "add_date": "2025-09-23T13:05:09.930906+01:00",
- "views": 47,
- "media_type": "video",
- "state": "public",
- "duration": 15,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/682cdbf87bb14826b5cf2146384e7498_mpSPxdc.14249923_3840_2160_60fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/682cdbf87bb14826b5cf2146384e7498.tmpyusu9afm.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "71.9MB"
- },
- {
- "friendly_token": "2Uk08Il5u",
- "url": "https://videojs.mediacms.io/view?m=2Uk08Il5u",
- "api_url": "https://videojs.mediacms.io/api/v1/media/2Uk08Il5u",
- "user": "markos",
- "title": "A video without HLS",
- "description": "",
- "add_date": "2025-09-16T12:40:55+01:00",
- "views": 68,
- "media_type": "video",
- "state": "public",
- "duration": 27,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/db52140de7204022a1e5f08e078b4ec6_02xAHoZ.UniversityofCopenhagenM%C3%A6rskTower.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/db52140de7204022a1e5f08e078b4ec6.tmpuespp_ik.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "58.8MB"
- },
- {
- "friendly_token": "v7MDJVk6z",
- "url": "https://videojs.mediacms.io/view?m=v7MDJVk6z",
- "api_url": "https://videojs.mediacms.io/api/v1/media/v7MDJVk6z",
- "user": "admin",
- "title": "20233414hd_2048_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:08.710882+01:00",
- "views": 9,
- "media_type": "video",
- "state": "public",
- "duration": 37,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/c4e70c90cc004d6aab2204a057787b17_htzxbCC.20233414hd_2048_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/c4e70c90cc004d6aab2204a057787b17.tmpn4pfbu27.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "26.9MB"
- },
- {
- "friendly_token": "GrtvaYVVc",
- "url": "https://videojs.mediacms.io/view?m=GrtvaYVVc",
- "api_url": "https://videojs.mediacms.io/api/v1/media/GrtvaYVVc",
- "user": "testme",
- "title": "20250725_194342.mp4",
- "description": "",
- "add_date": "2025-07-25T19:54:09.496404+01:00",
- "views": 66,
- "media_type": "video",
- "state": "public",
- "duration": 60,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/testme/6bb22eae99f74ee59e0af5d3aa54ded6_7i3CQ53.20250725_194342.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/testme/6bb22eae99f74ee59e0af5d3aa54ded6.tmpu2_qmcuu.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/testme/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "130.9MB"
- },
- {
- "friendly_token": "cX4A2xLym",
- "url": "https://videojs.mediacms.io/view?m=cX4A2xLym",
- "api_url": "https://videojs.mediacms.io/api/v1/media/cX4A2xLym",
- "user": "markos",
- "title": "VID_20230122_134550.mp4",
- "description": "",
- "add_date": "2025-09-15T14:52:53.159939+01:00",
- "views": 30,
- "media_type": "video",
- "state": "public",
- "duration": 7,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/c2a120930e934a1aa3f7230426800b30_9I7Hvhl.VID_20230122_134550.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/c2a120930e934a1aa3f7230426800b30.tmp4qgw2m2t.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "18.0MB"
- },
- {
- "friendly_token": "ieLetJSzc",
- "url": "https://videojs.mediacms.io/view?m=ieLetJSzc",
- "api_url": "https://videojs.mediacms.io/api/v1/media/ieLetJSzc",
- "user": "casto",
- "title": "test_audio.mp3",
- "description": "",
- "add_date": "2025-10-13T10:45:10.633098+01:00",
- "views": 8,
- "media_type": "audio",
- "state": "public",
- "duration": 1536,
- "thumbnail_url": null,
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "Casper Tollund",
- "author_profile": "https://videojs.mediacms.io/user/casto/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "HXqM3h77T",
- "url": "https://videojs.mediacms.io/view?m=HXqM3h77T",
- "api_url": "https://videojs.mediacms.io/api/v1/media/HXqM3h77T",
- "user": "markos",
- "title": "VID_20231226_003812.mp4",
- "description": "",
- "add_date": "2025-09-15T14:50:20+01:00",
- "views": 72,
- "media_type": "video",
- "state": "public",
- "duration": 52,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/DSC07691_Weawux4.JPG",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/da71ed534ff7431aa96dd3cd4e03b61a.tmpz3nbwhcu.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "132.1MB"
- },
- {
- "friendly_token": "ClYscEzph",
- "url": "https://videojs.mediacms.io/view?m=ClYscEzph",
- "api_url": "https://videojs.mediacms.io/api/v1/media/ClYscEzph",
- "user": "yiannis@web357.com",
- "title": "samplevideo.mp3",
- "description": "",
- "add_date": "2025-09-23T13:31:43+01:00",
- "views": 27,
- "media_type": "audio",
- "state": "public",
- "duration": 221,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/yiannis@web357.com/0_84mclyg0_copy_2_TxO7VxC.jpg",
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "Yiannis",
- "author_profile": "https://videojs.mediacms.io/user/yiannis@web357.com/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "op9FK7V5W",
- "url": "https://videojs.mediacms.io/view?m=op9FK7V5W",
- "api_url": "https://videojs.mediacms.io/api/v1/media/op9FK7V5W",
- "user": "markos",
- "title": "KastaniaEvrytanias,CentralGreece.mp4",
- "description": "",
- "add_date": "2025-09-16T12:40:17+01:00",
- "views": 40,
- "media_type": "video",
- "state": "public",
- "duration": 25,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/99866d1f90354360a6de6d1b269cee8a_ceEIIZx.KastaniaEvrytaniasCentralGreece.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/99866d1f90354360a6de6d1b269cee8a.tmpjalqxuhn.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "54.3MB"
- },
- {
- "friendly_token": "qWZDxnF6j",
- "url": "https://videojs.mediacms.io/view?m=qWZDxnF6j",
- "api_url": "https://videojs.mediacms.io/api/v1/media/qWZDxnF6j",
- "user": "thorkildjensen",
- "title": "13502237_4096_2160_24fps.mp4",
- "description": "",
- "add_date": "2025-09-22T08:24:34+01:00",
- "views": 38,
- "media_type": "video",
- "state": "public",
- "duration": 18,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/thorkildjensen/0a68c8f4fb93454a8c1feceb91c55619_hImy7D7.13502237_4096_2160_24fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/thorkildjensen/0a68c8f4fb93454a8c1feceb91c55619.tmpp70pluvg.gif",
- "author_name": "Thorkild Jensen",
- "author_profile": "https://videojs.mediacms.io/user/thorkildjensen/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "37.6MB"
- },
- {
- "friendly_token": "tHZZaCAbG",
- "url": "https://videojs.mediacms.io/view?m=tHZZaCAbG",
- "api_url": "https://videojs.mediacms.io/api/v1/media/tHZZaCAbG",
- "user": "admin",
- "title": "Tracking_Hvemfolgerdigpaanettet?Source.mp3",
- "description": "",
- "add_date": "2025-09-25T09:49:16+01:00",
- "views": 29,
- "media_type": "audio",
- "state": "public",
- "duration": 1536,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/0_84mclyg0_Ox2vkM6_aH0oho2.jpg",
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "lblIBR20P",
- "url": "https://videojs.mediacms.io/view?m=lblIBR20P",
- "api_url": "https://videojs.mediacms.io/api/v1/media/lblIBR20P",
- "user": "markos",
- "title": "video.mp4 1",
- "description": "",
- "add_date": "2025-07-08T00:00:00+01:00",
- "views": 140,
- "media_type": "video",
- "state": "public",
- "duration": 7,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/9cccc9090798474b98b63937753dde8c_MtuuUc2.video.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/9cccc9090798474b98b63937753dde8c.tmpc9qbk5oj.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "19.2MB"
- },
- {
- "friendly_token": "qzuncxJyg",
- "url": "https://videojs.mediacms.io/view?m=qzuncxJyg",
- "api_url": "https://videojs.mediacms.io/api/v1/media/qzuncxJyg",
- "user": "admin",
- "title": "19247660hd_1920_1080_60fps.mp4",
- "description": "",
- "add_date": "2025-09-30T15:16:28+01:00",
- "views": 21,
- "media_type": "video",
- "state": "public",
- "duration": 8,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/43cc73a8c1604425b7057ad2b50b1798.19247660hd_1920_1080_60fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/43cc73a8c1604425b7057ad2b50b1798.tmpjuivwj8x.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "5.8MB"
- },
- {
- "friendly_token": "gqdbr5GQF",
- "url": "https://videojs.mediacms.io/view?m=gqdbr5GQF",
- "api_url": "https://videojs.mediacms.io/api/v1/media/gqdbr5GQF",
- "user": "markos",
- "title": "ritsoskarlob.jpg",
- "description": "",
- "add_date": "2025-07-08T13:36:29.632853+01:00",
- "views": 23,
- "media_type": "image",
- "state": "public",
- "duration": 0,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/6a8bf33be4b84451bc27ed5a30abd17f.ritsoskarlob.jpg.jpg",
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "GJZlb14qT",
- "url": "https://videojs.mediacms.io/view?m=GJZlb14qT",
- "api_url": "https://videojs.mediacms.io/api/v1/media/GJZlb14qT",
- "user": "admin",
- "title": "20315562hd_1920_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:10.444953+01:00",
- "views": 10,
- "media_type": "video",
- "state": "public",
- "duration": 8,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/44283493026346708eafde49c6ae5743_Cl0JaTI.20315562hd_1920_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/44283493026346708eafde49c6ae5743.tmpmm5xmrfa.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "6.1MB"
- },
- {
- "friendly_token": "keVJnk9Qc",
- "url": "https://videojs.mediacms.io/view?m=keVJnk9Qc",
- "api_url": "https://videojs.mediacms.io/api/v1/media/keVJnk9Qc",
- "user": "admin",
- "title": "18084154hd_1920_1080_50fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:22:52.768899+01:00",
- "views": 20,
- "media_type": "video",
- "state": "public",
- "duration": 11,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/8a1ba14f0a1641768430d0ceb4ae44b6_N06NhEk.18084154hd_1920_1080_50fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/8a1ba14f0a1641768430d0ceb4ae44b6.tmpdvzd3vo9.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "5.2MB"
- },
- {
- "friendly_token": "OXA5YLgbR",
- "url": "https://videojs.mediacms.io/view?m=OXA5YLgbR",
- "api_url": "https://videojs.mediacms.io/api/v1/media/OXA5YLgbR",
- "user": "admin",
- "title": "20325563hd_1920_1080_60fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:12.701363+01:00",
- "views": 26,
- "media_type": "video",
- "state": "public",
- "duration": 5,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/00b2e0c02233417588e362db03840fad_ULyZDYI.20325563hd_1920_1080_60fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/00b2e0c02233417588e362db03840fad.tmpu2rz97fb.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "3.3MB"
- },
- {
- "friendly_token": "3o3F3l9Vb",
- "url": "https://videojs.mediacms.io/view?m=3o3F3l9Vb",
- "api_url": "https://videojs.mediacms.io/api/v1/media/3o3F3l9Vb",
- "user": "markos",
- "title": "ExitofElygiaGorge,Chania,Crete.mp4",
- "description": "Checkout an awesome timestamp at 00:00:14 and another one is 00:00:24",
- "add_date": "2025-09-16T12:41:36+01:00",
- "views": 78,
- "media_type": "video",
- "state": "public",
- "duration": 29,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/c4668407eb354740a35d933ed7d2786c_iDC7Dvv.ExitofElygiaGorgeChaniaCrete.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/c4668407eb354740a35d933ed7d2786c.tmpm_mh1zlk.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "75.6MB"
- },
- {
- "friendly_token": "cxridUpmN",
- "url": "https://videojs.mediacms.io/view?m=cxridUpmN",
- "api_url": "https://videojs.mediacms.io/api/v1/media/cxridUpmN",
- "user": "yiannis@web357.com",
- "title": "samplevideowhite.mp4",
- "description": "",
- "add_date": "2025-09-23T13:22:52.267985+01:00",
- "views": 18,
- "media_type": "video",
- "state": "public",
- "duration": 28,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/yiannis@web357.com/5073e97457004961a163c5b504e2d7e8_TPZjERG.samplevideowhite.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/yiannis@web357.com/5073e97457004961a163c5b504e2d7e8.tmpz7aaqgd2.gif",
- "author_name": "Yiannis",
- "author_profile": "https://videojs.mediacms.io/user/yiannis@web357.com/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "0.2MB"
- },
- {
- "friendly_token": "rcXVukhjR",
- "url": "https://videojs.mediacms.io/view?m=rcXVukhjR",
- "api_url": "https://videojs.mediacms.io/api/v1/media/rcXVukhjR",
- "user": "markos",
- "title": "VID_20230916_203950.mp4",
- "description": "",
- "add_date": "2025-09-15T14:51:34.686737+01:00",
- "views": 54,
- "media_type": "video",
- "state": "public",
- "duration": 17,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/8dda62d489734949a8e45ee7e57dcae3_lNlZGLY.VID_20230916_203950.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/8dda62d489734949a8e45ee7e57dcae3.tmpy0gdlfes.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "44.6MB"
- },
- {
- "friendly_token": "ywaw3XHp2",
- "url": "https://videojs.mediacms.io/view?m=ywaw3XHp2",
- "api_url": "https://videojs.mediacms.io/api/v1/media/ywaw3XHp2",
- "user": "admin",
- "title": "19093555hd_2048_1080_24fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:22:55.398583+01:00",
- "views": 15,
- "media_type": "video",
- "state": "public",
- "duration": 24,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/0be4bbdb807b4c05a1181534bf957114_xqBc2SX.19093555hd_2048_1080_24fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/0be4bbdb807b4c05a1181534bf957114.tmpgv0vmvk_.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "16.8MB"
- },
- {
- "friendly_token": "uNhnQvy44",
- "url": "https://videojs.mediacms.io/view?m=uNhnQvy44",
- "api_url": "https://videojs.mediacms.io/api/v1/media/uNhnQvy44",
- "user": "admin",
- "title": "deic16_06_MartinBechSource.mp4",
- "description": "",
- "add_date": "2025-10-03T10:50:31.589590+01:00",
- "views": 69,
- "media_type": "video",
- "state": "public",
- "duration": 31,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/9c0ebef31e044da18273958b4d7b4f58_Z8Gweou.deic16_06_MartinBechSource.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/9c0ebef31e044da18273958b4d7b4f58.tmpjorgd1it.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "10.5MB"
- },
- {
- "friendly_token": "adcPiBg8N",
- "url": "https://videojs.mediacms.io/view?m=adcPiBg8N",
- "api_url": "https://videojs.mediacms.io/api/v1/media/adcPiBg8N",
- "user": "thorkildjensen",
- "title": "13161886_3840_2160_30fps.mp4",
- "description": "",
- "add_date": "2025-09-22T08:24:23.619459+01:00",
- "views": 35,
- "media_type": "video",
- "state": "public",
- "duration": 17,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/thorkildjensen/876966ca37dd4b48870722f9a1a95365_ZiiI8cx.13161886_3840_2160_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/thorkildjensen/876966ca37dd4b48870722f9a1a95365.tmpgu8k9qm5.gif",
- "author_name": "Thorkild Jensen",
- "author_profile": "https://videojs.mediacms.io/user/thorkildjensen/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "82.6MB"
- },
- {
- "friendly_token": "FupnUqfc4",
- "url": "https://videojs.mediacms.io/view?m=FupnUqfc4",
- "api_url": "https://videojs.mediacms.io/api/v1/media/FupnUqfc4",
- "user": "admin",
- "title": "19128074hd_1920_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:22:57.622428+01:00",
- "views": 29,
- "media_type": "video",
- "state": "public",
- "duration": 37,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/504c2bc1d231406195dad39ac24fb2b6_QEDTRkr.19128074hd_1920_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/504c2bc1d231406195dad39ac24fb2b6.tmp7nrk9byi.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 2,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "24.9MB"
- },
- {
- "friendly_token": "PDDYW3FKT",
- "url": "https://videojs.mediacms.io/view?m=PDDYW3FKT",
- "api_url": "https://videojs.mediacms.io/api/v1/media/PDDYW3FKT",
- "user": "admin",
- "title": "19247660hd_1920_1080_60fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:02.024559+01:00",
- "views": 14,
- "media_type": "video",
- "state": "public",
- "duration": 8,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/f39569e5e5fc45218dbd04a5e484f8c1_IBZ6zrz.19247660hd_1920_1080_60fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/f39569e5e5fc45218dbd04a5e484f8c1.tmpp7fky5cw.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "5.8MB"
- },
- {
- "friendly_token": "8iNDqeAfg",
- "url": "https://videojs.mediacms.io/view?m=8iNDqeAfg",
- "api_url": "https://videojs.mediacms.io/api/v1/media/8iNDqeAfg",
- "user": "yiannis@web357.com",
- "title": "Magnamvelitipsumquisquamametmagnametincidunt.mp4",
- "description": "",
- "add_date": "2025-09-29T17:31:44.981327+01:00",
- "views": 18,
- "media_type": "video",
- "state": "public",
- "duration": 6,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/yiannis@web357.com/ca2def779e93418a9b9da919870bd526_WJegiqZ.Magnamvelitipsumquisquamametmagnametincidunt.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/yiannis@web357.com/ca2def779e93418a9b9da919870bd526.tmpr_cnt2t4.gif",
- "author_name": "Yiannis",
- "author_profile": "https://videojs.mediacms.io/user/yiannis@web357.com/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "13.0MB"
- },
- {
- "friendly_token": "Q7IufzPhn",
- "url": "https://videojs.mediacms.io/view?m=Q7IufzPhn",
- "api_url": "https://videojs.mediacms.io/api/v1/media/Q7IufzPhn",
- "user": "admin",
- "title": "5607902hd_1920_1080_24fps.mp4",
- "description": "",
- "add_date": "2025-09-30T15:16:16.757233+01:00",
- "views": 12,
- "media_type": "video",
- "state": "public",
- "duration": 10,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/16d54aefb6e441f0875a069ef3b1f4b7_HwEDnf5.5607902hd_1920_1080_24fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/16d54aefb6e441f0875a069ef3b1f4b7.tmp3ohrrdrr.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "6.0MB"
- },
- {
- "friendly_token": "9BuFyzHAB",
- "url": "https://videojs.mediacms.io/view?m=9BuFyzHAB",
- "api_url": "https://videojs.mediacms.io/api/v1/media/9BuFyzHAB",
- "user": "casto",
- "title": "blinking_controls.mov",
- "description": "",
- "add_date": "2025-10-02T15:05:29+01:00",
- "views": 32,
- "media_type": "video",
- "state": "public",
- "duration": 6,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/casto/b5f994242d61426daaa637ded4e0215f_Lngth06.blinking_controls.mov.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/casto/b5f994242d61426daaa637ded4e0215f.tmp920kwv6w.gif",
- "author_name": "Casper Tollund",
- "author_profile": "https://videojs.mediacms.io/user/casto/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 2,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "44.0MB"
- },
- {
- "friendly_token": "MPpjNAJ37",
- "url": "https://videojs.mediacms.io/view?m=MPpjNAJ37",
- "api_url": "https://videojs.mediacms.io/api/v1/media/MPpjNAJ37",
- "user": "admin",
- "title": "20576968hd_1920_1080_25fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:15.649931+01:00",
- "views": 23,
- "media_type": "video",
- "state": "public",
- "duration": 27,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/f0d604b8319648b3a5b2fca0ac04760b_MwJq72A.20576968hd_1920_1080_25fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/f0d604b8319648b3a5b2fca0ac04760b.tmp2fmp2ou_.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "19.5MB"
- },
- {
- "friendly_token": "bfdekixxV",
- "url": "https://videojs.mediacms.io/view?m=bfdekixxV",
- "api_url": "https://videojs.mediacms.io/api/v1/media/bfdekixxV",
- "user": "admin",
- "title": "TeknologienhjaelperhumanistiskforskningSource.mp3",
- "description": "",
- "add_date": "2025-09-25T09:49:15+01:00",
- "views": 23,
- "media_type": "audio",
- "state": "public",
- "duration": 2149,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/0_84mclyg0_uxHXW0A_QlEoygu.jpg",
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "aT8RBaCBT",
- "url": "https://videojs.mediacms.io/view?m=aT8RBaCBT",
- "api_url": "https://videojs.mediacms.io/api/v1/media/aT8RBaCBT",
- "user": "markos",
- "title": "Estipsumnonetinciduntvoluptatemadipiscilabore.mp4",
- "description": "go to the best place: go to 00:04",
- "add_date": "2025-09-16T12:39:41+01:00",
- "views": 56,
- "media_type": "video",
- "state": "public",
- "duration": 26,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/4fd9b683a76443cf9d754be55adc0091_U0XpnNl.Estipsumnonetinciduntvoluptatemadipiscilabore.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/4fd9b683a76443cf9d754be55adc0091.tmpyvc1vnrp.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "66.4MB"
- },
- {
- "friendly_token": "7VIy7inGZ",
- "url": "https://videojs.mediacms.io/view?m=7VIy7inGZ",
- "api_url": "https://videojs.mediacms.io/api/v1/media/7VIy7inGZ",
- "user": "admin",
- "title": "Hvemharbrugtnationalesupercomputere?Source.mp3",
- "description": "",
- "add_date": "2025-09-23T08:40:06+01:00",
- "views": 34,
- "media_type": "audio",
- "state": "public",
- "duration": 1839,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/0_84mclyg0_yVNkDBq.jpg",
- "is_reviewed": true,
- "preview_url": null,
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": null
- },
- {
- "friendly_token": "beae7lLVs",
- "url": "https://videojs.mediacms.io/view?m=beae7lLVs",
- "api_url": "https://videojs.mediacms.io/api/v1/media/beae7lLVs",
- "user": "admin",
- "title": "19990812hd_1920_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:06.413778+01:00",
- "views": 19,
- "media_type": "video",
- "state": "public",
- "duration": 36,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/c082bf4d565b47cf849909f92303c4f1_CIG2eav.19990812hd_1920_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/c082bf4d565b47cf849909f92303c4f1.tmp9nt688cs.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "25.2MB"
- },
- {
- "friendly_token": "VX2qAwiFd",
- "url": "https://videojs.mediacms.io/view?m=VX2qAwiFd",
- "api_url": "https://videojs.mediacms.io/api/v1/media/VX2qAwiFd",
- "user": "markos",
- "title": "VID_20230917_094453.mp4",
- "description": "",
- "add_date": "2025-09-15T14:50:54.166449+01:00",
- "views": 28,
- "media_type": "video",
- "state": "public",
- "duration": 13,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/654e4ca525ea4a979ef0065f9991f335_yCPofZH.VID_20230917_094453.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/654e4ca525ea4a979ef0065f9991f335.tmpzwnlnlun.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "35.0MB"
- },
- {
- "friendly_token": "meivs1H3R",
- "url": "https://videojs.mediacms.io/view?m=meivs1H3R",
- "api_url": "https://videojs.mediacms.io/api/v1/media/meivs1H3R",
- "user": "markos",
- "title": "video.mp4",
- "description": "",
- "add_date": "2025-07-08T13:33:24+01:00",
- "views": 107,
- "media_type": "video",
- "state": "public",
- "duration": 7,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/d6ae9093cb1648529432f38ee1198200_8Bqn2pb.video.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/d6ae9093cb1648529432f38ee1198200.tmpshyvhjby.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "19.2MB"
- },
- {
- "friendly_token": "Q531pthDT",
- "url": "https://videojs.mediacms.io/view?m=Q531pthDT",
- "api_url": "https://videojs.mediacms.io/api/v1/media/Q531pthDT",
- "user": "admin",
- "title": "19224432hd_1920_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:22:59.568359+01:00",
- "views": 16,
- "media_type": "video",
- "state": "public",
- "duration": 24,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/743fbae69e3a421f9017b4b26f08f94a_wrABxwh.19224432hd_1920_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/743fbae69e3a421f9017b4b26f08f94a.tmp6gnt2iza.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "16.9MB"
- },
- {
- "friendly_token": "DjplF1kqz",
- "url": "https://videojs.mediacms.io/view?m=DjplF1kqz",
- "api_url": "https://videojs.mediacms.io/api/v1/media/DjplF1kqz",
- "user": "admin",
- "title": "19553130hd_1920_1080_30fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:23:04.080897+01:00",
- "views": 9,
- "media_type": "video",
- "state": "public",
- "duration": 9,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/9c8dc7b294394c8f8ad101556c6ca327_thTz43p.19553130hd_1920_1080_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/9c8dc7b294394c8f8ad101556c6ca327.tmps4d56664.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "6.4MB"
- },
- {
- "friendly_token": "lyNk0HYMx",
- "url": "https://videojs.mediacms.io/view?m=lyNk0HYMx",
- "api_url": "https://videojs.mediacms.io/api/v1/media/lyNk0HYMx",
- "user": "admin",
- "title": "16478029hd_1920_1080_24fps.mp4",
- "description": "",
- "add_date": "2025-10-01T15:22:51.462252+01:00",
- "views": 7,
- "media_type": "video",
- "state": "public",
- "duration": 14,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/82b779012c604afeadacfc3c3bdba584_B9yXEMI.16478029hd_1920_1080_24fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/82b779012c604afeadacfc3c3bdba584.tmp226vuafd.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "10.1MB"
- },
- {
- "friendly_token": "4NxnfB7F8",
- "url": "https://videojs.mediacms.io/view?m=4NxnfB7F8",
- "api_url": "https://videojs.mediacms.io/api/v1/media/4NxnfB7F8",
- "user": "admin",
- "title": "4193130hd_1922_1080_24fps.mp4",
- "description": "",
- "add_date": "2025-09-30T15:16:06.506083+01:00",
- "views": 10,
- "media_type": "video",
- "state": "public",
- "duration": 44,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/admin/96cb2b54607642b6877f309dad8926db_RTfRILY.4193130hd_1922_1080_24fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/admin/96cb2b54607642b6877f309dad8926db.tmp72yep0r5.gif",
- "author_name": "admin",
- "author_profile": "https://videojs.mediacms.io/user/admin/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos_6FncSBT.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "30.9MB"
- },
- {
- "friendly_token": "KWNEepos7",
- "url": "https://videojs.mediacms.io/view?m=KWNEepos7",
- "api_url": "https://videojs.mediacms.io/api/v1/media/KWNEepos7",
- "user": "markos",
- "title": "VID_20230813_104846.mp4",
- "description": "",
- "add_date": "2025-09-15T14:52:33.267086+01:00",
- "views": 38,
- "media_type": "video",
- "state": "public",
- "duration": 26,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/eb69a038fe4c4fbaa95752de95f31605_MrtO8cw.VID_20230813_104846.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/eb69a038fe4c4fbaa95752de95f31605.tmp4wl8jpje.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 2,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "66.4MB"
- },
- {
- "friendly_token": "UtRFqDxc8",
- "url": "https://videojs.mediacms.io/view?m=UtRFqDxc8",
- "api_url": "https://videojs.mediacms.io/api/v1/media/UtRFqDxc8",
- "user": "markos",
- "title": "VID_20220206_123853.mp4",
- "description": "",
- "add_date": "2025-09-15T14:53:30.537614+01:00",
- "views": 32,
- "media_type": "video",
- "state": "public",
- "duration": 15,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/markos/540d03d3a30248448034c018400dd16d_0nHBsiZ.VID_20220206_123853.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/markos/540d03d3a30248448034c018400dd16d.tmpe0s2dtk1.gif",
- "author_name": "Markos",
- "author_profile": "https://videojs.mediacms.io/user/markos/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/2025/09/23/markos.jpeg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "39.6MB"
- },
- {
- "friendly_token": "bMw0ZOVDc",
- "url": "https://videojs.mediacms.io/view?m=bMw0ZOVDc",
- "api_url": "https://videojs.mediacms.io/api/v1/media/bMw0ZOVDc",
- "user": "thorkildjensen",
- "title": "14060819_3840_2160_30fps.mp4",
- "description": "Jump in timeline via link in description: 00:05",
- "add_date": "2025-09-22T08:24:54+01:00",
- "views": 52,
- "media_type": "video",
- "state": "public",
- "duration": 11,
- "thumbnail_url": "https://videojs.mediacms.io/media/original/thumbnails/user/thorkildjensen/fea7bb6a434d4e43b98ec4f2bf1389a2_izGw7ow.14060819_3840_2160_30fps.mp4.jpg",
- "is_reviewed": true,
- "preview_url": "/media/encoded/1/thorkildjensen/fea7bb6a434d4e43b98ec4f2bf1389a2.tmp4y3qxrlh.gif",
- "author_name": "Thorkild Jensen",
- "author_profile": "https://videojs.mediacms.io/user/thorkildjensen/",
- "author_thumbnail": "https://videojs.mediacms.io/media/userlogos/user.jpg",
- "encoding_status": "success",
- "likes": 1,
- "dislikes": 0,
- "reported_times": 0,
- "featured": false,
- "user_featured": false,
- "size": "31.7MB"
- }
- ]
+ "related_media": []
}
diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js
index 034ac015..523bd9b3 100644
--- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js
+++ b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js
@@ -45,7 +45,7 @@ class EndScreenOverlay extends Component {
}
createGrid() {
- const { columns, maxVideos, useSwiper } = this.getGridConfig();
+ const { columns, maxVideos, useSwiper, itemsPerView, gridRows } = this.getGridConfig();
// Get videos to show - access directly from options during createEl
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
@@ -55,7 +55,7 @@ class EndScreenOverlay extends Component {
: this.createSampleVideos().slice(0, maxVideos);
if (useSwiper) {
- return this.createSwiperGrid(videosToShow);
+ return this.createSwiperGrid(videosToShow, itemsPerView || 2, columns, gridRows || 1);
} else {
return this.createRegularGrid(columns, videosToShow);
}
@@ -91,14 +91,14 @@ class EndScreenOverlay extends Component {
return grid;
}
- createSwiperGrid(videosToShow) {
+ createSwiperGrid(videosToShow, itemsPerView = 2, columns = 2, gridRows = 1) {
const container = videojs.dom.createEl('div', {
className: 'vjs-related-videos-swiper-container',
});
// Container styling - ensure it stays within bounds
container.style.position = 'relative';
- container.style.padding = '20px';
+ container.style.padding = gridRows > 1 ? '12px' : '20px'; // Minimal padding for 2x2 grid
container.style.height = '100%';
container.style.width = '100%';
container.style.display = 'flex';
@@ -111,16 +111,32 @@ class EndScreenOverlay extends Component {
className: 'vjs-related-videos-swiper',
});
- swiperWrapper.style.display = 'flex';
- swiperWrapper.style.overflowX = 'auto';
- swiperWrapper.style.overflowY = 'hidden';
- swiperWrapper.style.gap = '12px';
- swiperWrapper.style.paddingBottom = '10px';
- swiperWrapper.style.scrollBehavior = 'smooth';
- swiperWrapper.style.scrollSnapType = 'x mandatory';
- swiperWrapper.style.width = '100%';
- swiperWrapper.style.maxWidth = '100%';
- swiperWrapper.style.boxSizing = 'border-box';
+ if (gridRows > 1) {
+ // Multi-row grid layout (e.g., 2x2 for landscape)
+ swiperWrapper.style.display = 'flex';
+ swiperWrapper.style.overflowX = 'auto';
+ swiperWrapper.style.overflowY = 'hidden';
+ swiperWrapper.style.scrollBehavior = 'smooth';
+ swiperWrapper.style.scrollSnapType = 'x mandatory';
+ swiperWrapper.style.width = '100%';
+ swiperWrapper.style.maxWidth = '100%';
+ swiperWrapper.style.height = '100%';
+ swiperWrapper.style.flex = '1';
+ swiperWrapper.style.boxSizing = 'border-box';
+ swiperWrapper.style.gap = '0'; // Remove gap, we'll handle it in pages
+ } else {
+ // Single row layout (original swiper)
+ swiperWrapper.style.display = 'flex';
+ swiperWrapper.style.overflowX = 'auto';
+ swiperWrapper.style.overflowY = 'hidden';
+ swiperWrapper.style.gap = '12px';
+ swiperWrapper.style.paddingBottom = '10px';
+ swiperWrapper.style.scrollBehavior = 'smooth';
+ swiperWrapper.style.scrollSnapType = 'x mandatory';
+ swiperWrapper.style.width = '100%';
+ swiperWrapper.style.maxWidth = '100%';
+ swiperWrapper.style.boxSizing = 'border-box';
+ }
// Hide scrollbar and prevent scroll propagation
swiperWrapper.style.scrollbarWidth = 'none'; // Firefox
@@ -158,17 +174,56 @@ class EndScreenOverlay extends Component {
{ passive: true }
);
- // Create video items for swiper (show 2 at a time, but allow scrolling through all)
- videosToShow.forEach((video) => {
- const videoItem = this.createVideoItem(video, true); // Pass true for swiper mode
- swiperWrapper.appendChild(videoItem);
- });
+ if (gridRows > 1) {
+ // Create pages with grid layout (e.g., 2x2 grid per page)
+ const itemsPerPage = itemsPerView;
+ const totalPages = Math.ceil(videosToShow.length / itemsPerPage);
+
+ for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
+ const page = videojs.dom.createEl('div', {
+ className: 'vjs-swiper-page',
+ });
+
+ page.style.minWidth = '100%';
+ page.style.width = '100%';
+ page.style.height = '100%';
+ page.style.display = 'grid';
+ page.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
+ page.style.gridTemplateRows = `repeat(${gridRows}, 1fr)`;
+ page.style.gap = '12px'; // Increased gap for better spacing
+ page.style.scrollSnapAlign = 'start';
+ page.style.boxSizing = 'border-box';
+ page.style.alignContent = 'stretch';
+ page.style.justifyContent = 'stretch';
+ page.style.alignItems = 'stretch';
+ page.style.justifyItems = 'stretch';
+
+ // Get videos for this page
+ const startIndex = pageIndex * itemsPerPage;
+ const endIndex = Math.min(startIndex + itemsPerPage, videosToShow.length);
+ const pageVideos = videosToShow.slice(startIndex, endIndex);
+
+ // Create video items for this page
+ pageVideos.forEach((video) => {
+ const videoItem = this.createVideoItem(video, true, itemsPerView, true); // Pass true for grid mode
+ page.appendChild(videoItem);
+ });
+
+ swiperWrapper.appendChild(page);
+ }
+ } else {
+ // Single row - create video items directly
+ videosToShow.forEach((video) => {
+ const videoItem = this.createVideoItem(video, true, itemsPerView, false);
+ swiperWrapper.appendChild(videoItem);
+ });
+ }
container.appendChild(swiperWrapper);
- // Add navigation indicators if there are more than 2 videos
- if (videosToShow.length > 2) {
- const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper);
+ // Add navigation indicators if there are more videos than can fit in one view
+ if (videosToShow.length > itemsPerView) {
+ const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper, itemsPerView);
container.appendChild(indicators);
}
@@ -190,10 +245,48 @@ class EndScreenOverlay extends Component {
// Calculate maximum rows that can fit - be more aggressive
const maxRows = Math.max(2, Math.floor((availableHeight + gap) / (cardHeight + gap)));
- console.log('Grid Config:', { playerWidth, playerHeight, availableHeight, maxRows });
+ // Detect landscape orientation on mobile
+ // Check screen/window orientation first, then player dimensions
+ const screenWidth = window.innerWidth || document.documentElement.clientWidth;
+ const screenHeight = window.innerHeight || document.documentElement.clientHeight;
+ const isScreenLandscape = screenWidth > screenHeight;
+
+ const isLandscape = playerWidth > playerHeight;
+
+ // Detect mobile/touch devices - should always show swiper
+ // Check both width and touch capability for better detection
+ const isTouchDevice = this.isTouchDevice;
+ const isSmallScreen = screenWidth < 700 || playerWidth < 700;
+ const isMobileOrTouch = isTouchDevice || isSmallScreen;
+
+ // For mobile, prioritize screen orientation over player dimensions
+ // Only consider it landscape if BOTH screen and player are in landscape
+ const isDefinitelyLandscape = isMobileOrTouch ? isScreenLandscape && isLandscape : isLandscape;
+
+ console.log('Grid Config:', {
+ screenWidth,
+ screenHeight,
+ isScreenLandscape,
+ playerWidth,
+ playerHeight,
+ availableHeight,
+ maxRows,
+ isLandscape,
+ isDefinitelyLandscape,
+ isTouchDevice,
+ isSmallScreen,
+ isMobileOrTouch,
+ });
// Enhanced grid configuration to fill all available space
- if (playerWidth >= 1600) {
+ // Check mobile/touch conditions first - swiper should ALWAYS be used on mobile/touch devices
+ if (isMobileOrTouch && isDefinitelyLandscape) {
+ // Mobile/Touch landscape: show 2x2 grid (4 items total) with swiper for pagination
+ return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 4, gridRows: 2 };
+ } else if (isMobileOrTouch) {
+ // Mobile/Touch portrait: show 2 items in single row swiper mode
+ return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 2, gridRows: 1 };
+ } else if (playerWidth >= 1600) {
const columns = 5;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
} else if (playerWidth >= 1200) {
@@ -202,11 +295,9 @@ class EndScreenOverlay extends Component {
} else if (playerWidth >= 900) {
const columns = 3;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
- } else if (playerWidth >= 700) {
+ } else {
const columns = 2;
return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows
- } else {
- return { columns: 2, maxVideos: 12, useSwiper: true }; // Use swiper for small screens
}
}
@@ -220,7 +311,7 @@ class EndScreenOverlay extends Component {
return this.createSampleVideos().slice(0, maxVideos);
}
- createVideoItem(video, isSwiperMode = false) {
+ createVideoItem(video, isSwiperMode = false, itemsPerView = 2, isGridMode = false) {
const item = videojs.dom.createEl('div', {
className: `vjs-related-video-item ${isSwiperMode ? 'vjs-swiper-item' : ''}`,
});
@@ -228,7 +319,7 @@ class EndScreenOverlay extends Component {
// Consistent item styling with fixed dimensions
item.style.position = 'relative';
item.style.backgroundColor = '#1a1a1a';
- item.style.borderRadius = '6px';
+ item.style.borderRadius = '8px';
item.style.overflow = 'hidden';
item.style.cursor = 'pointer';
item.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease';
@@ -236,11 +327,21 @@ class EndScreenOverlay extends Component {
item.style.flexDirection = 'column';
// Consistent dimensions for all cards
- if (isSwiperMode) {
- // Calculate proper width for swiper items (2 items visible + gap)
- item.style.minWidth = 'calc(50% - 6px)'; // 50% width minus half the gap
- item.style.width = 'calc(50% - 6px)';
- item.style.maxWidth = '180px'; // Maximum width for larger screens
+ if (isGridMode) {
+ // Grid mode (2x2): items fill their grid cell completely
+ item.style.height = '100%';
+ item.style.minHeight = '0';
+ item.style.width = '100%';
+ item.style.maxWidth = 'none';
+ item.style.flex = '1';
+ } else if (isSwiperMode) {
+ // Single row swiper mode: calculate width based on items per view
+ // Formula: (100% / itemsPerView) - (gap * (itemsPerView - 1) / itemsPerView)
+ const itemsPerRow = itemsPerView / (itemsPerView === 4 ? 2 : 1); // For 4 items in 2 rows, show 2 per row
+ const gapAdjustment = (12 * (itemsPerRow - 1)) / itemsPerRow;
+ item.style.minWidth = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`;
+ item.style.width = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`;
+ item.style.maxWidth = itemsPerView === 4 ? '150px' : '180px'; // Smaller max width for 4 items
// Simpler height since text is overlaid on thumbnail
const cardHeight = '120px'; // Just the thumbnail height
@@ -270,7 +371,7 @@ class EndScreenOverlay extends Component {
}
// Create thumbnail container with overlaid text
- const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode);
+ const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode, itemsPerView);
item.appendChild(thumbnailContainer);
console.log('Created video item with overlay:', item);
@@ -331,7 +432,7 @@ class EndScreenOverlay extends Component {
return container;
}
- createThumbnailWithOverlay(video, isSwiperMode = false) {
+ createThumbnailWithOverlay(video, isSwiperMode = false, itemsPerView = 2) {
const container = videojs.dom.createEl('div', {
className: 'vjs-related-video-thumbnail-container',
});
@@ -339,9 +440,13 @@ class EndScreenOverlay extends Component {
// Container styling - full height since it contains everything
container.style.position = 'relative';
container.style.width = '100%';
- container.style.height = '120px';
+ container.style.height = '100%';
+ container.style.minHeight = '120px';
container.style.overflow = 'hidden';
- container.style.borderRadius = '6px';
+ container.style.borderRadius = '8px';
+ container.style.flex = '1';
+ container.style.display = 'flex';
+ container.style.flexDirection = 'column';
// Create thumbnail image
const thumbnail = videojs.dom.createEl('img', {
@@ -354,6 +459,9 @@ class EndScreenOverlay extends Component {
thumbnail.style.height = '100%';
thumbnail.style.objectFit = 'cover';
thumbnail.style.display = 'block';
+ thumbnail.style.flex = '1';
+ thumbnail.style.minWidth = '0';
+ thumbnail.style.minHeight = '0';
container.appendChild(thumbnail);
@@ -370,7 +478,7 @@ class EndScreenOverlay extends Component {
duration.style.color = 'white';
duration.style.padding = '2px 6px';
duration.style.borderRadius = '3px';
- duration.style.fontSize = '11px';
+ duration.style.fontSize = itemsPerView === 4 ? '10px' : '11px';
duration.style.fontWeight = '600';
duration.style.lineHeight = '1';
duration.style.zIndex = '3';
@@ -384,12 +492,12 @@ class EndScreenOverlay extends Component {
});
textOverlay.style.position = 'absolute';
- textOverlay.style.top = '8px';
- textOverlay.style.left = '8px';
- textOverlay.style.right = '8px';
+ textOverlay.style.top = itemsPerView === 4 ? '6px' : '8px';
+ textOverlay.style.left = itemsPerView === 4 ? '6px' : '8px';
+ textOverlay.style.right = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.background =
'linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 70%, transparent 100%)';
- textOverlay.style.padding = '8px';
+ textOverlay.style.padding = itemsPerView === 4 ? '6px' : '8px';
textOverlay.style.borderRadius = '4px';
textOverlay.style.zIndex = '2';
@@ -399,10 +507,11 @@ class EndScreenOverlay extends Component {
});
title.textContent = video.title || 'Sample Video Title';
title.style.color = '#ffffff';
- title.style.fontSize = isSwiperMode ? '12px' : '13px';
+ // Adjust font sizes based on items per view
+ title.style.fontSize = itemsPerView === 4 ? '11px' : isSwiperMode ? '12px' : '13px';
title.style.fontWeight = '600';
title.style.lineHeight = '1.3';
- title.style.marginBottom = '4px';
+ title.style.marginBottom = itemsPerView === 4 ? '3px' : '4px';
title.style.overflow = 'hidden';
title.style.textOverflow = 'ellipsis';
title.style.display = '-webkit-box';
@@ -428,7 +537,8 @@ class EndScreenOverlay extends Component {
meta.textContent = metaText;
meta.style.color = '#e0e0e0';
- meta.style.fontSize = isSwiperMode ? '10px' : '11px';
+ // Adjust font sizes based on items per view
+ meta.style.fontSize = itemsPerView === 4 ? '9px' : isSwiperMode ? '10px' : '11px';
meta.style.lineHeight = '1.2';
meta.style.overflow = 'hidden';
meta.style.textOverflow = 'ellipsis';
@@ -529,7 +639,7 @@ class EndScreenOverlay extends Component {
return info;
}
- createSwiperIndicators(totalVideos, swiperWrapper) {
+ createSwiperIndicators(totalVideos, swiperWrapper, itemsPerView = 2) {
const indicators = videojs.dom.createEl('div', {
className: 'vjs-swiper-indicators',
});
@@ -539,7 +649,6 @@ class EndScreenOverlay extends Component {
indicators.style.gap = '8px';
indicators.style.marginTop = '10px';
- const itemsPerView = 2;
const totalPages = Math.ceil(totalVideos / itemsPerView);
for (let i = 0; i < totalPages; i++) {
diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
index 43c919dc..ff758b6c 100644
--- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
+++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx
@@ -208,1111 +208,14 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
isPlayList: false,
previewSprite: {
url: sampleMediaData.sprites_url
- ? 'https://videojs.mediacms.io' + sampleMediaData.sprites_url
- : 'https://videojs.mediacms.io/media/original/thumbnails/user/admin/43cc73a8c1604425b7057ad2b50b1798.19247660hd_1920_1080_60fps.mp4sprites.jpg',
+ ? 'https://deic.mediacms.io' + sampleMediaData.sprites_url
+ : 'https://deic.mediacms.io/media/original/thumbnails/user/admin/43cc73a8c1604425b7057ad2b50b1798.19247660hd_1920_1080_60fps.mp4sprites.jpg',
frame: { width: 160, height: 90, seconds: 10 },
},
- siteUrl: 'https://videojs.mediacms.io',
- nextLink: 'https://videojs.mediacms.io/view?m=elygiagorgechania',
+ siteUrl: 'https://deic.mediacms.io',
+ nextLink: 'https://deic.mediacms.io/view?m=elygiagorgechania',
urlAutoplay: true,
urlMuted: false,
-
- // FALLBACK - keep old structure for reference
- __data_old: {
- // COMMON
- title: 'Modi tempora est quaerat numquam',
- author_name: 'Markos Gogoulos',
- author_profile: '/user/markos/',
- author_thumbnail: '/media/userlogos/user.jpg',
- url: 'https://videojs.mediacms.io/view?m=2Uk08Il5u',
- poster_url: '',
- __poster_url:
- '/media/original/thumbnails/user/markos/db52140de7204022a1e5f08e078b4ec6_VKPTF4v.UniversityofCopenhagenMærskTower.mp4.jpg',
- ___chapter_data: [],
- chapter_data: [
- {
- startTime: '00:00:00.000',
- endTime: '00:00:02.295',
- chapterTitle: 'A1 Lorem ipsum dolor sit amet consectetur adipisicing elit.',
- },
- { startTime: '00:00:02.295', endTime: '00:00:04.590', chapterTitle: 'A2 of Marine Life' },
- {
- startTime: '00:00:04.590',
- endTime: '00:00:06.885',
- chapterTitle: 'A3 Reef Ecosystems',
- },
- {
- startTime: '00:00:06.885',
- endTime: '00:00:09.180',
- chapterTitle: 'A4 Coral Formation and Growth',
- },
- {
- startTime: '00:00:09.180',
- endTime: '00:00:11.475',
- chapterTitle: 'A5 Tropical Fish Species',
- },
- {
- startTime: '00:00:11.475',
- endTime: '00:00:13.770',
- chapterTitle: 'A6 Ocean Current Patterns',
- },
- {
- startTime: '00:00:13.770',
- endTime: '00:00:16.065',
- chapterTitle: 'A7 Deep Sea Exploration',
- },
- {
- startTime: '00:00:16.065',
- endTime: '00:00:18.360',
- chapterTitle: 'A8 Marine Conservation Efforts',
- },
- {
- startTime: '00:00:18.360',
- endTime: '00:00:20.655',
- chapterTitle: 'A9 Underwater Photography Techniques',
- },
- {
- startTime: '00:00:20.655',
- endTime: '00:00:22.950',
- chapterTitle: 'A10 Plankton and Microscopic Life',
- },
- {
- startTime: '00:00:22.950',
- endTime: '00:00:25.245',
- chapterTitle: 'A11 Whale Migration Routes',
- },
- {
- startTime: '00:00:25.245',
- endTime: '00:00:27.540',
- chapterTitle: 'A12 Tidal Pool Ecosystems',
- },
- {
- startTime: '00:00:27.540',
- endTime: '00:00:29.835',
- chapterTitle: 'A13 Submarine Technology',
- },
- {
- startTime: '00:00:29.835',
- endTime: '00:00:32.130',
- chapterTitle: 'A14 Ocean Pollution Impact',
- },
- {
- startTime: '00:00:32.130',
- endTime: '00:00:34.425',
- chapterTitle: 'A15 Bioluminescent Creatures',
- },
- {
- startTime: '00:00:34.425',
- endTime: '00:00:36.720',
- chapterTitle: 'A16 Seaweed and Kelp Forests',
- },
- {
- startTime: '00:00:36.720',
- endTime: '00:00:39.015',
- chapterTitle: 'A17 Marine Food Chain Dynamics',
- },
- {
- startTime: '00:00:39.015',
- endTime: '00:00:41.310',
- chapterTitle: 'A18 Coastal Erosion and Climate Change',
- },
- ],
- related_media: [
- {
- friendly_token: 'dktSm7iEo',
- url: 'https://videojs.mediacms.io/view?m=dktSm7iEo',
- api_url: 'https://videojs.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://videojs.mediacms.io/media/original/thumbnails/user/markos/db52140de7204022a1e5f08e078b4ec6_02xAHoZ.UniversityofCopenhagenMærskTower.mp4.jpg',
- is_reviewed: true,
- preview_url:
- 'https://videojs.mediacms.io/media/encoded/1/markos/db52140de7204022a1e5f08e078b4ec6.tmpuespp_ik.gif',
- author_name: 'Markos Gogoulos',
- author_profile: 'https://videojs.mediacms.io/user/markos/',
- author_thumbnail:
- 'https://videojs.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: 'audio',
- original_media_url:
- '/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4',
- hls_info: {
- master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8',
- '240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/iframes.m3u8',
- '480_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/iframes.m3u8',
- '720_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-3/iframes.m3u8',
- '144_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/iframes.m3u8',
- '360_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/iframes.m3u8',
- '240_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/stream.m3u8',
- '480_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/stream.m3u8',
- '720_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-3/stream.m3u8',
- '144_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/stream.m3u8',
- '360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8',
- },
- __hls_info: {},
- __encodings_info: {},
- encodings_info: {
- 144: {
- h264: {
- title: 'h264-144',
- url: '/media/encoded/23/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '0.9MB',
- encoding_id: 84,
- status: 'success',
- },
- },
- 240: {
- h264: {
- title: 'h264-240',
- url: '/media/encoded/2/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '1.7MB',
- encoding_id: 85,
- status: 'success',
- },
- },
- 360: {
- h264: {
- title: 'h264-360',
- url: '/media/encoded/3/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '2.9MB',
- encoding_id: 86,
- status: 'success',
- },
- },
- 480: {
- h264: {
- title: 'h264-480',
- url: '/media/encoded/13/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '4.5MB',
- encoding_id: 87,
- status: 'success',
- },
- },
- 720: {
- h264: {
- title: 'h264-720',
- url: '/media/encoded/10/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '8.3MB',
- encoding_id: 88,
- status: 'success',
- },
- },
- 1080: {
- h264: {
- title: 'h264-1080',
- url: '/media/encoded/7/markos/db52140de7204022a1e5f08e078b4ec6.db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4.mp4',
- progress: 100,
- size: '16.6MB',
- encoding_id: 89,
- status: 'success',
- },
- },
- 1440: {},
- 2160: {},
- },
-
- // AUDIO
- /*media_type: 'audio',
- original_media_url:
- 'https://videojs.mediacms.io/media/original/user/markos/174be7a1ecb04850a6927a0af2887ccc.SizzlaHardGround.mp3',
- hls_info: {},
- encodings_info: {},*/
- },
},
[]
);
@@ -1821,7 +724,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const subtitleTracks = hasSubtitles
? backendSubtitles.map((track) => ({
kind: 'subtitles',
- src: track.src,
+ src: (!isDevMode ? mediaData?.siteUrl : '') + track.src,
srclang: track.srclang,
label: track.label,
default: track.default || false,
@@ -2099,7 +1002,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Use native text tracks on iOS for fullscreen caption support
// On other devices, use Video.js text tracks for full CSS positioning control
- nativeTextTracks: isIOS,
+ nativeTextTracks: isIOS && mediaData.data?.media_type !== 'audio' ? true : false,
// Use native video tracks instead of emulated - disabled for consistency
nativeVideoTracks: false,
diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer__OLD.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer__OLD.jsx
deleted file mode 100644
index 7720620d..00000000
--- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer__OLD.jsx
+++ /dev/null
@@ -1,3334 +0,0 @@
-import React, { useEffect, useRef, useMemo } from 'react';
-import videojs from 'video.js';
-import 'video.js/dist/video-js.css';
-// import '../../VideoJS.css';
-import '../../styles/embed.css';
-//import '../controls/SubtitlesButton.css';
-
-// Import the separated components
-import EndScreenOverlay from '../overlays/EndScreenOverlay';
-import AutoplayCountdownOverlay from '../overlays/AutoplayCountdownOverlay';
-import EmbedInfoOverlay from '../overlays/EmbedInfoOverlay';
-import ChapterMarkers from '../markers/ChapterMarkers';
-import SpritePreview from '../markers/SpritePreview';
-import NextVideoButton from '../controls/NextVideoButton';
-import AutoplayToggleButton from '../controls/AutoplayToggleButton';
-import CustomRemainingTime from '../controls/CustomRemainingTime';
-import CustomChaptersOverlay from '../controls/CustomChaptersOverlay';
-import CustomSettingsMenu from '../controls/CustomSettingsMenu';
-import SeekIndicator from '../controls/SeekIndicator';
-import UserPreferences from '../../utils/UserPreferences';
-
-function VideoJSPlayer({ videoId = 'default-video' }) {
- const videoRef = useRef(null);
- const playerRef = useRef(null); // Track the player instance
- const userPreferences = useRef(new UserPreferences()); // User preferences instance
- const customComponents = useRef({}); // Store custom components for cleanup
-
- // Check if this is an embed player (disable next video and autoplay features)
- const isEmbedPlayer = videoId === 'video-embed';
-
- // Utility function to detect touch devices
- const isTouchDevice = useMemo(() => {
- return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
- }, []);
-
- // Environment-based development mode configuration
- const isDevMode = import.meta.env.VITE_DEV_MODE === 'true' || window.location.hostname.includes('vercel.app');
- // Safely access window.MEDIA_DATA with fallback using useMemo
- const mediaData = useMemo(
- () =>
- typeof window !== 'undefined' && window.MEDIA_DATA
- ? window.MEDIA_DATA
- : {
- data: {
- // COMMON
- title: 'Modi tempora est quaerat numquam',
- author_name: 'Markos Gogoulos',
- author_profile: '/user/markos/',
- author_thumbnail: '/media/userlogos/user.jpg',
- url: 'https://videojs.mediacms.io/view?m=meivs1H3R',
- poster_url:
- '/media/original/thumbnails/user/markos/d6ae9093cb1648529432f38ee1198200_6BfyhyM.video.mp4.jpg',
-
- chapter_data: [
- {
- startTime: '00:00:00.000',
- endTime: '00:00:02.295',
- chapterTitle: 'A1 Lorem ipsum dolor sit amet consectetur adipisicing elit.',
- },
- { startTime: '00:00:02.295', endTime: '00:00:04.590', chapterTitle: 'A2 of Marine Life' },
- {
- startTime: '00:00:04.590',
- endTime: '00:00:06.885',
- chapterTitle: 'A3 Reef Ecosystems',
- },
- ],
- 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: '/media/original/user/markos/d6ae9093cb1648529432f38ee1198200.video.mp4',
- hls_info: {
- master_file: '/media/hls/d6ae9093cb1648529432f38ee1198200/master.m3u8',
- '144_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-1/iframes.m3u8',
- '240_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-2/iframes.m3u8',
- '360_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-3/iframes.m3u8',
- '480_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-4/iframes.m3u8',
- '720_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-5/iframes.m3u8',
- '1080_iframe': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-6/iframes.m3u8',
- '144_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-1/stream.m3u8',
- '240_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-2/stream.m3u8',
- '360_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-3/stream.m3u8',
- '480_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-4/stream.m3u8',
- '720_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-5/stream.m3u8',
- '1080_playlist': '/media/hls/d6ae9093cb1648529432f38ee1198200/media-6/stream.m3u8',
- },
- encodings_info: {
- 144: {
- h264: {
- title: 'h264-144',
- url: '/media/encoded/23/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '0.3MB',
- encoding_id: 1,
- status: 'success',
- },
- },
- 240: {
- h264: {
- title: 'h264-240',
- url: '/media/encoded/2/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '0.6MB',
- encoding_id: 2,
- status: 'success',
- },
- },
- 360: {
- h264: {
- title: 'h264-360',
- url: '/media/encoded/3/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '0.8MB',
- encoding_id: 3,
- status: 'success',
- },
- },
- 480: {
- h264: {
- title: 'h264-480',
- url: '/media/encoded/13/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '1.5MB',
- encoding_id: 4,
- status: 'success',
- },
- },
- 720: {
- h264: {
- title: 'h264-720',
- url: '/media/encoded/10/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '3.5MB',
- encoding_id: 5,
- status: 'success',
- },
- },
- 1080: {
- h264: {
- title: 'h264-1080',
- url: '/media/encoded/7/markos/d6ae9093cb1648529432f38ee1198200.d6ae9093cb1648529432f38ee1198200.video.mp4.mp4',
- progress: 100,
- size: '6.4MB',
- encoding_id: 6,
- status: 'success',
- },
- },
- 1440: {},
- 2160: {},
- },
-
- // AUDIO
- /*media_type: 'audio',
- original_media_url:
- 'https://videojs.mediacms.io/media/original/user/markos/174be7a1ecb04850a6927a0af2887ccc.SizzlaHardGround.mp3',
- hls_info: {},
- encodings_info: {},*/
- },
-
- // other
- useRoundedCorners: false,
- isPlayList: false,
- previewSprite: {
- url: 'https://videojs.mediacms.io/media/original/thumbnails/user/markos/d6ae9093cb1648529432f38ee1198200.video.mp4sprites.jpg',
- frame: { width: 160, height: 90, seconds: 10 },
- },
- siteUrl: 'https://videojs.mediacms.io',
- nextLink: 'https://videojs.mediacms.io/view?m=YjGJafibO',
- urlAutoplay: true,
- urlMuted: false,
- },
- []
- );
-
- // Define chapters as JSON object
- // Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
- // CONDITIONAL LOGIC:
- // - When chaptersData has content: Uses original ChapterMarkers with sprite preview
- // - When chaptersData is empty: Uses separate SpritePreview component
- // 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),
- chapterTitle: chapter.chapterTitle,
- }));
-
- return convertedData;
- };
- }, []);
-
- // Helper function to check if chapters represent a meaningful chapter structure
- // Returns false if there's only one chapter covering the entire video duration with a generic title
- const hasRealChapters = useMemo(() => {
- return (rawChaptersData, videoDuration) => {
- if (!rawChaptersData || !Array.isArray(rawChaptersData) || rawChaptersData.length === 0) {
- return false;
- }
-
- // If there's more than one chapter, assume it's a real chapter structure
- if (rawChaptersData.length > 1) {
- return true;
- }
-
- // If there's only one chapter, check if it's a generic segment marker
- if (rawChaptersData.length === 1) {
- const chapter = rawChaptersData[0];
- const startTime = convertTimeStringToSeconds(chapter.startTime);
- const endTime = convertTimeStringToSeconds(chapter.endTime);
-
- // Check if it's a generic segment with common auto-generated titles
- const isGenericTitle = chapter.chapterTitle
- ?.toLowerCase()
- .match(/^(segment|video|full video|chapter|part)$/);
-
- // If we have video duration info, check if this single chapter spans the whole video
- if (videoDuration && videoDuration > 0) {
- // Allow for small timing differences (1 second tolerance)
- const tolerance = 1;
- const isFullVideo = startTime <= tolerance && Math.abs(endTime - videoDuration) <= tolerance;
-
- // Only hide if it's both full video AND has a generic title
- if (isFullVideo && isGenericTitle) {
- return false;
- }
-
- // If it doesn't span the full video, it's a real chapter
- if (!isFullVideo) {
- return true;
- }
- }
-
- // Fallback: If start time is 0 and the title is generic, assume it's not a real chapter
- if (startTime === 0 && isGenericTitle) {
- return false;
- }
- }
-
- return true;
- };
- }, []);
-
- // Memoized chapters data conversion
- const chaptersData = useMemo(() => {
- if (mediaData?.data?.chapter_data && mediaData?.data?.chapter_data.length > 0) {
- const videoDuration = mediaData?.data?.duration || null;
-
- // Check if we have real chapters or just a single segment
- if (hasRealChapters(mediaData.data.chapter_data, videoDuration)) {
- return convertChaptersData(mediaData?.data?.chapter_data);
- } else {
- // Return empty array if it's just a single segment covering the whole video
- return [];
- }
- }
- return isDevMode
- ? [
- { 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' },
- ].map((chapter) => ({
- startTime: convertTimeStringToSeconds(chapter.startTime),
- endTime: convertTimeStringToSeconds(chapter.endTime),
- chapterTitle: chapter.chapterTitle,
- }))
- : [];
- }, [mediaData?.data?.chapter_data, mediaData?.data?.duration, isDevMode, convertChaptersData, hasRealChapters]);
-
- // 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';
- };
-
- // Get user's quality preference for dependency tracking
- const userQualityPreference = userPreferences.current.getQualityPreference();
-
- // Get video data from mediaData
- const currentVideo = useMemo(() => {
- // Get video sources based on available data and user preferences
- const getVideoSources = () => {
- // Use the extracted quality preference
- const userQuality = userQualityPreference;
-
- // Check if HLS info is available and not empty
- 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.siteUrl + 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',
- },
- ];
- }
- }
-
- // Fallback to encoded qualities if available
- if (mediaData.data?.encodings_info) {
- const encodings = mediaData.data.encodings_info;
- 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 = [];
-
- // 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`,
- });
- }
-
- if (sources.length > 0) {
- return sources;
- }
- }
-
- // Final fallback to original media URL or sample video
- if (mediaData.data?.original_media_url) {
- const sourceUrl = mediaData.siteUrl + mediaData.data.original_media_url;
- return [
- {
- src: sourceUrl,
- type: getMimeType(sourceUrl, mediaData.data?.media_type),
- },
- ];
- }
-
- // Default sample video
- return [
- {
- src: '/videos/sample-video-white.mp4',
- type: 'video/mp4',
- },
- /* {
- src: '/videos/sample-video.mp3',
- type: 'audio/mpeg',
- }, */
- ];
- };
-
- const currentVideo = {
- id: mediaData.data?.friendly_token || 'default-video',
- title: mediaData.data?.title || 'Video',
- author_name: mediaData.data?.author_name || 'Unknown',
- author_profile: mediaData.data?.author_profile ? mediaData.siteUrl + mediaData.data.author_profile : '',
- author_thumbnail: mediaData.data?.author_thumbnail
- ? mediaData.siteUrl + mediaData.data.author_thumbnail
- : '',
- url: mediaData.data?.url || '',
- poster: mediaData.data?.poster_url ? mediaData.siteUrl + mediaData.data.poster_url : '',
- previewSprite: mediaData?.previewSprite || {},
- useRoundedCorners: mediaData?.useRoundedCorners,
- isPlayList: mediaData?.isPlayList,
- related_media: mediaData.data?.related_media || [],
- nextLink: mediaData?.nextLink || null,
- urlAutoplay: mediaData?.urlAutoplay || true,
- urlMuted: mediaData?.urlMuted || false,
- sources: getVideoSources(),
- };
-
- return currentVideo;
- }, [mediaData, userQualityPreference]);
-
- // Compute available qualities. Prefer JSON (mediaData.data.qualities), otherwise build from encodings_info or current source.
- const availableQualities = useMemo(() => {
- // Generate desiredOrder dynamically based on available data
- const generateDesiredOrder = () => {
- const baseOrder = ['auto'];
-
- // Add qualities from encodings_info if available
- if (mediaData.data?.encodings_info) {
- const availableQualities = Object.keys(mediaData.data.encodings_info)
- .filter((quality) => {
- const encoding = mediaData.data.encodings_info[quality];
- return encoding && encoding.h264 && encoding.h264.url;
- })
- .map((quality) => `${quality}p`)
- .sort((a, b) => parseInt(a) - parseInt(b)); // Sort ascending
-
- baseOrder.push(...availableQualities);
- } else {
- // Fallback to standard order
- baseOrder.push('144p', '240p', '360p', '480p', '720p', '1080p', '1440p', '2160p');
- }
-
- return baseOrder;
- };
-
- const desiredOrder = generateDesiredOrder();
-
- const normalize = (arr) => {
- const norm = arr.map((q) => ({
- label: q.label || q.value || 'Auto',
- value: (q.value || q.label || 'auto').toString().toLowerCase(),
- src: q.src || q.url || q.href,
- type: q.type || getMimeType(q.src || q.url || q.href, mediaData.data?.media_type),
- }));
-
- // Only include qualities that have actual sources
- const validQualities = norm.filter((q) => q.src);
-
- // sort based on desired order
- const idx = (v) => {
- const i = desiredOrder.indexOf(String(v).toLowerCase());
- return i === -1 ? 999 : i;
- };
- validQualities.sort((a, b) => idx(a.value) - idx(b.value));
- return validQualities;
- };
-
- const jsonList = mediaData?.data?.qualities;
- if (Array.isArray(jsonList) && jsonList.length) {
- return normalize(jsonList);
- }
-
- // If HLS is available, build qualities from HLS playlists
- if (mediaData.data?.hls_info && mediaData.data.hls_info.master_file) {
- const hlsInfo = mediaData.data.hls_info;
- const qualities = [];
-
- // Add master file as auto quality
- qualities.push({
- label: 'Auto',
- value: 'auto',
- src: mediaData.siteUrl + hlsInfo.master_file,
- type: 'application/x-mpegURL',
- });
-
- // Add individual HLS playlists
- Object.keys(hlsInfo).forEach((key) => {
- if (key.endsWith('_playlist')) {
- const quality = key.replace('_playlist', '');
- qualities.push({
- label: `${quality}p`,
- value: `${quality}p`,
- src: mediaData.siteUrl + hlsInfo[key],
- type: 'application/x-mpegURL',
- });
- }
- });
-
- return normalize(qualities);
- }
-
- // Build from encodings_info if available
- if (mediaData.data?.encodings_info) {
- const encodings = mediaData.data.encodings_info;
- const qualities = [];
-
- // Add auto quality first
- qualities.push({
- label: 'Auto',
- value: 'auto',
- src: null, // Will use the highest available quality
- type: getMimeType(null, mediaData.data?.media_type),
- });
-
- // Add available encoded qualities dynamically
- Object.keys(encodings).forEach((quality) => {
- if (encodings[quality] && encodings[quality].h264 && encodings[quality].h264.url) {
- const sourceUrl = encodings[quality].h264.url;
- qualities.push({
- label: `${quality}p`,
- value: `${quality}p`,
- src: sourceUrl,
- type: getMimeType(sourceUrl, mediaData.data?.media_type),
- });
- }
- });
-
- if (qualities.length > 1) {
- // More than just auto
- return normalize(qualities);
- }
- }
-
- // Build from current source as fallback - only if we have a valid source
- const baseSrc = (currentVideo?.sources && currentVideo.sources[0]?.src) || null;
- const type =
- (currentVideo?.sources && currentVideo.sources[0]?.type) ||
- getMimeType(baseSrc, mediaData.data?.media_type);
-
- if (baseSrc) {
- const buildFromBase = [
- {
- label: 'Auto',
- value: 'auto',
- src: baseSrc,
- type,
- },
- ];
- return normalize(buildFromBase);
- }
-
- // Return empty array if no valid sources found
- return [];
- }, [mediaData, currentVideo]);
-
- // Get related videos from mediaData instead of static data
- const relatedVideos = useMemo(() => {
- if (!mediaData?.data?.related_media) {
- return [];
- }
-
- return mediaData.data.related_media
- .slice(0, 12) // Limit to maximum 12 items
- .map((media) => ({
- id: media.friendly_token,
- title: media.title,
- author: media.user || media.author_name || 'Unknown',
- views: `${media.views} views`,
- thumbnail: media.thumbnail_url || media.author_thumbnail,
- category: media.media_type,
- url: media.url,
- duration: media.duration,
- size: media.size,
- likes: media.likes,
- dislikes: media.dislikes,
- add_date: media.add_date,
- description: media.description,
- }));
- }, [mediaData]);
-
- // Demo array for testing purposes
- const demoSubtitleTracks = [
- {
- kind: 'subtitles',
- src: '/sample-subtitles.vtt',
- srclang: 'en',
- label: 'English Subtitles',
- default: false,
- },
- {
- kind: 'subtitles',
- src: '/sample-subtitles-greek.vtt',
- srclang: 'el',
- label: 'Greek Subtitles (Ελληνικά)',
- default: false,
- },
- ];
- // const demoSubtitleTracks = []; // NO Subtitles. TODO: hide it on production
-
- // Get subtitle tracks from backend response or fallback based on environment
- const backendSubtitles = mediaData?.data?.subtitles_info || (isDevMode ? demoSubtitleTracks : []);
- const hasSubtitles = backendSubtitles.length > 0;
- const subtitleTracks = hasSubtitles
- ? backendSubtitles.map((track) => ({
- kind: 'subtitles',
- src: track.src,
- srclang: track.srclang,
- label: track.label,
- default: false,
- }))
- : [];
-
- // Function to navigate to next video
- const goToNextVideo = () => {
- if (mediaData.onClickNextCallback && typeof mediaData.onClickNextCallback === 'function') {
- mediaData.onClickNextCallback();
- }
- };
-
- useEffect(() => {
- // Only initialize if we don't already have a player and element exists
- if (videoRef.current && !playerRef.current) {
- // Check if element is already a Video.js player
- if (videoRef.current.player) {
- return;
- }
-
- const timer = setTimeout(() => {
- // Double-check that we still don't have a player and element exists
- if (!playerRef.current && videoRef.current && !videoRef.current.player) {
- playerRef.current = videojs(videoRef.current, {
- // ===== STANDARD
ELEMENT OPTIONS =====
-
- // Controls whether player has user-interactive controls
- controls: true,
-
- // Player dimensions - removed for responsive design
- // Autoplay behavior: Try unmuted first, fallback to muted if needed
- // For embed players, disable autoplay to show poster
- autoplay: isEmbedPlayer ? false : true, // Try unmuted autoplay first (true/false, play, muted, any)
-
- // Start video over when it ends
- loop: false,
-
- // Start video muted (check URL parameter or default)
- muted: mediaData.urlMuted || false,
-
- // Poster image URL displayed before video starts
- poster: currentVideo.poster,
-
- // Preload behavior: 'auto', 'metadata', 'none'
- preload: 'auto',
-
- // Video sources from current video
- sources: currentVideo.sources,
-
- // ===== VIDEO.JS-SPECIFIC OPTIONS =====
-
- // Aspect ratio for fluid mode (e.g., '16:9', '4:3')
- aspectRatio: '16:9',
-
- // Hide all components except control bar for audio-only mode
- audioOnlyMode: false,
-
- // Display poster persistently for audio poster mode
- audioPosterMode: mediaData.data?.media_type === 'audio',
-
- // Prevent autoSetup for elements with data-setup attribute
- autoSetup: undefined,
-
- // Custom breakpoints for responsive design
- breakpoints: {
- tiny: 210,
- xsmall: 320,
- small: 425,
- medium: 768,
- large: 1440,
- xlarge: 2560,
- huge: 2561,
- },
-
- // Disable picture-in-picture mode
- disablePictureInPicture: false,
-
- // Enable document picture-in-picture API
- enableDocumentPictureInPicture: false,
-
- // Enable smooth seeking experience
- enableSmoothSeeking: false,
-
- // Use experimental SVG icons instead of font icons
- experimentalSvgIcons: false,
-
- // Make player scale to fit container
- fluid: true,
-
- // Fullscreen options
- fullscreen: {
- options: {
- navigationUI: 'hide',
- },
- },
-
- // Player element ID
- id: mediaData.id,
-
- // Milliseconds of inactivity before user considered inactive (0 = never)
- // For embed players, use longer timeout to keep controls visible
- inactivityTimeout: isEmbedPlayer ? 5000 : 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,
-
- // Ensures consistent autoplay behavior across browsers (prevents unexpected blocking or auto-play issues)
- normalizeAutoplay: true,
-
- // Custom message when media cannot be played
- notSupportedMessage: undefined,
-
- // Prevent title attributes on UI elements for better accessibility
- noUITitleAttributes: true,
-
- // 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,
- tap: true,
-
- // Enable/disable or customize double-click behavior (fullscreen toggle)
- doubleClick: true,
-
- // Hotkey configuration
- hotkeys: {
- // Function to override fullscreen key (default: 'f')
- fullscreenKey: function (event) {
- return event.which === 70; // 'f' key
- },
-
- // Function to override mute key (default: 'm')
- muteKey: function (event) {
- return event.which === 77; // 'm' key
- },
-
- // Function to override play/pause key (default: 'k' and Space)
- playPauseKey: function (event) {
- return event.which === 75 || event.which === 32; // 'k' or Space
- },
-
- // Custom seek functions for arrow keys
- seekForwardKey: function (event) {
- return event.which === 39; // Right arrow key
- },
-
- seekBackwardKey: function (event) {
- return event.which === 37; // Left arrow key
- },
- },
- },
-
- // URL to vtt.js for WebVTT support
- 'vtt.js': undefined,
-
- // Spatial navigation for smart TV/remote control navigation
- spatialNavigation: {
- enabled: false,
- horizontalSeek: false,
- },
-
- // ===== CONTROL BAR OPTIONS =====
- controlBar: {
- progressControl: {
- seekBar: {
- timeTooltip: {
- // Customize TimeTooltip behavior
- displayNegative: false, // Don't show negative time
- },
- },
- },
- // Remaining time display configuration
- remainingTimeDisplay: false,
- /* remainingTimeDisplay: {
- displayNegative: true,
- }, */
-
- // Volume panel configuration
- volumePanel: {
- inline: true, // Display volume control inline
- vertical: false, // Use horizontal volume slider
- },
-
- // Fullscreen toggle button
- fullscreenToggle: true,
-
- // Picture-in-picture toggle button
- pictureInPictureToggle: true,
-
- // Remove default playback speed dropdown from control bar
- playbackRateMenuButton: false,
-
- // Descriptions button
- descriptionsButton: false,
-
- // Subtitles (CC) button should be visible
- subtitlesButton: hasSubtitles ? true : false,
-
- // Captions button (keep disabled to avoid duplicate with subtitles)
- captionsButton: false,
-
- // Audio track button
- audioTrackButton: false,
-
- // Live display
- liveDisplay: false,
-
- // Seek to live button
- seekToLive: false,
-
- // Custom control spacer
- customControlSpacer: false,
-
- // Chapters menu button (only show if we have real chapters)
- chaptersButton: chaptersData && chaptersData.length > 0,
- },
-
- // ===== HTML5 TECH OPTIONS =====
- html5: {
- // Force native controls for touch devices
- nativeControlsForTouch: false,
-
- // Use native audio tracks instead of emulated - disabled for consistency
- nativeAudioTracks: false,
-
- // Use native text tracks instead of emulated - disabled for consistency
- nativeTextTracks: false,
-
- // Use native video tracks instead of emulated - disabled for consistency
- nativeVideoTracks: false,
-
- // Preload text tracks
- preloadTextTracks: true,
-
- // Play inline
- playsinline: true,
- },
-
- // ===== COMPONENT CONFIGURATION =====
- children: [
- 'mediaLoader',
- 'posterImage',
- 'textTrackDisplay',
- 'loadingSpinner',
- 'bigPlayButton',
- 'liveTracker',
- 'controlBar',
- 'errorDisplay',
- 'textTrackSettings',
- 'resizeManager',
- ],
- });
-
- // Event listeners
- playerRef.current.ready(() => {
- // Apply user preferences to player
- userPreferences.current.applyToPlayer(playerRef.current);
-
- // Set up auto-save for preference changes
- userPreferences.current.setupAutoSave(playerRef.current);
-
- // 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();
- }
- });
- }
- }
-
- // 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);
- }
- }
- }
- };
-
- // Skip autoplay for embed players to show poster
- if (!isEmbedPlayer) {
- if (mediaData?.urlAutoplay) {
- // Explicit autoplay requested via URL parameter
- handleAutoplay();
- } else {
- // Auto-start video on page load/reload with fallback strategy
- handleAutoplay();
- }
- } else {
- // For embed players, setup clean appearance with hidden controls
- setTimeout(() => {
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
- const controlBar = playerRef.current.getChild('controlBar');
-
- if (bigPlayButton) {
- bigPlayButton.show();
- // Ensure big play button is prominently displayed in center
- const bigPlayEl = bigPlayButton.el();
- if (bigPlayEl) {
- bigPlayEl.style.display = 'block';
- bigPlayEl.style.visibility = 'visible';
- bigPlayEl.style.opacity = '1';
- // Make it more prominent for embed
- bigPlayEl.style.zIndex = '10';
- }
- }
-
- if (controlBar) {
- // Hide controls by default for embed players
- controlBar.hide();
- const controlBarEl = controlBar.el();
- if (controlBarEl) {
- controlBarEl.style.opacity = '0';
- controlBarEl.style.visibility = 'hidden';
- controlBarEl.style.transition = 'opacity 0.3s ease';
- }
- }
-
- // Fix potential duplicate image issue by ensuring proper poster/video layering
- const embedPlayerEl = playerRef.current.el();
- const videoEl = embedPlayerEl.querySelector('video');
- const posterEl = embedPlayerEl.querySelector('.vjs-poster');
-
- if (videoEl && posterEl) {
- // Ensure video is behind poster when paused
- videoEl.style.opacity = '0';
- posterEl.style.zIndex = '1';
- posterEl.style.position = 'absolute';
- posterEl.style.top = '0';
- posterEl.style.left = '0';
- posterEl.style.width = '100%';
- posterEl.style.height = '100%';
- }
-
- // Set player to inactive state to hide controls initially
- playerRef.current.userActive(false);
-
- // Setup hover behavior to show/hide controls for embed
- if (embedPlayerEl) {
- const showControls = () => {
- if (controlBar) {
- controlBar.show();
- const controlBarEl = controlBar.el();
- if (controlBarEl) {
- controlBarEl.style.opacity = '1';
- controlBarEl.style.visibility = 'visible';
- }
- }
- playerRef.current.userActive(true);
- };
-
- const hideControls = () => {
- // Only hide if video is paused (embed behavior)
- if (playerRef.current.paused()) {
- if (controlBar) {
- const controlBarEl = controlBar.el();
- if (controlBarEl) {
- controlBarEl.style.opacity = '0';
- controlBarEl.style.visibility = 'hidden';
- }
- setTimeout(() => {
- if (playerRef.current.paused()) {
- controlBar.hide();
- }
- }, 300);
- }
- playerRef.current.userActive(false);
- }
- };
-
- embedPlayerEl.addEventListener('mouseenter', showControls);
- embedPlayerEl.addEventListener('mouseleave', hideControls);
-
- // Store cleanup function
- customComponents.current.embedControlsCleanup = () => {
- embedPlayerEl.removeEventListener('mouseenter', showControls);
- embedPlayerEl.removeEventListener('mouseleave', hideControls);
- };
- }
- }, 100);
- }
-
- 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 };
-
- // Check if touch is in seekbar area or the zone above it
- const progressControl = playerRef.current
- .getChild('controlBar')
- ?.getChild('progressControl');
- if (progressControl && progressControl.el()) {
- const progressRect = progressControl.el().getBoundingClientRect();
- const seekbarDeadZone = 8; // Only 8px above seekbar is protected for easier seeking
- const isInSeekbarArea =
- touch.clientY >= progressRect.top - seekbarDeadZone &&
- touch.clientY <= progressRect.bottom;
- if (isInSeekbarArea) {
- playerRef.current.seekbarTouching = true;
- }
- }
- };
-
- const handleTouchEnd = (e) => {
- const touchEndTime = Date.now();
- const touchDuration = touchEndTime - touchStartTime;
-
- // Only handle if it's a quick tap and we're not touching the seekbar
- if (touchDuration < 500 && !playerRef.current.seekbarTouching) {
- 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();
-
- // Check if controls are currently visible by examining control bar
- const controlBar = playerRef.current.getChild('controlBar');
- const controlBarEl = controlBar ? controlBar.el() : null;
- const isControlsVisible =
- controlBarEl &&
- window.getComputedStyle(controlBarEl).opacity !== '0' &&
- window.getComputedStyle(controlBarEl).visibility !== 'hidden';
-
- // Check if center play/pause icon is visible and if tap is on it
- const seekIndicator = customComponents.current.seekIndicator;
- const seekIndicatorEl = seekIndicator ? seekIndicator.el() : null;
- const isSeekIndicatorVisible =
- seekIndicatorEl &&
- window.getComputedStyle(seekIndicatorEl).opacity !== '0' &&
- window.getComputedStyle(seekIndicatorEl).visibility !== 'hidden' &&
- window.getComputedStyle(seekIndicatorEl).display !== 'none';
-
- let isTapOnCenterIcon = false;
- if (seekIndicatorEl && isSeekIndicatorVisible) {
- const iconRect = seekIndicatorEl.getBoundingClientRect();
- isTapOnCenterIcon =
- touch.clientX >= iconRect.left &&
- touch.clientX <= iconRect.right &&
- touch.clientY >= iconRect.top &&
- touch.clientY <= iconRect.bottom;
- }
-
- if (playerRef.current.paused()) {
- // Always play if video is paused
- playerRef.current.play();
- } else if (isTapOnCenterIcon) {
- // Pause if tapping on center icon (highest priority)
- playerRef.current.pause();
- } else if (isControlsVisible) {
- // Pause if controls are visible and not touching seekbar area
- playerRef.current.pause();
- } else {
- // If controls are not visible, show them AND show center pause icon
- playerRef.current.userActive(true);
- if (seekIndicator) {
- seekIndicator.showMobilePauseIcon();
- }
- }
- }
- }
-
- // Always clear seekbar touching flag at the end
- setTimeout(() => {
- if (playerRef.current) {
- playerRef.current.seekbarTouching = false;
- }
- }, 50);
- };
-
- videoEl.addEventListener('touchstart', handleTouchStart, { passive: true });
- videoEl.addEventListener('touchend', handleTouchEnd, { passive: false });
- }
- };
- setTimeout(setupMobilePlayPause, 100);
-
- // 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');
-
- // Auto-play video when navigating from next button (skip for embed players)
- if (!isEmbedPlayer) {
- const urlParams = new URLSearchParams(window.location.search);
- const hasVideoParam = urlParams.get('m');
- if (hasVideoParam) {
- // Small delay to ensure everything is loaded
- setTimeout(async () => {
- if (playerRef.current && !playerRef.current.isDisposed()) {
- 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
- );
- }
- }
- }
- }
- }, 100);
- }
- }
-
- // BEGIN: Add subtitle tracks
- 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);
- });
- }
- }
-
- // Apply saved subtitle preference immediately and on key readiness events
- userPreferences.current.applySubtitlePreference(playerRef.current);
- playerRef.current.one('loadeddata', () =>
- userPreferences.current.applySubtitlePreference(playerRef.current)
- );
- playerRef.current.one('canplay', () =>
- userPreferences.current.applySubtitlePreference(playerRef.current)
- );
- // END: Add subtitle tracks
-
- // BEGIN: Chapters Implementation
- if (chaptersData && chaptersData.length > 0) {
- const chaptersTrack = playerRef.current.addTextTrack('chapters', 'Chapters', 'en');
- // Add cues to the chapters track
- chaptersData.forEach((chapter) => {
- const cue = new (window.VTTCue || window.TextTrackCue)(
- chapter.startTime,
- chapter.endTime,
- chapter.chapterTitle
- );
- chaptersTrack.addCue(cue);
- });
- }
- // END: Chapters Implementation
-
- // Force chapter markers update after chapters are loaded
- /* setTimeout(() => {
- if (chapterMarkers && chapterMarkers.updateChapterMarkers) {
- chapterMarkers.updateChapterMarkers();
- }
- }, 500); */
-
- // BEGIN: Wrap play button in custom div container
- const playButtonEl = playToggle.el();
- const playButtonWrapper = document.createElement('div');
- /* playButtonWrapper.className =
- 'vjs-play-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
-
- // Insert wrapper before the play button and move play button inside
- // playButtonEl.parentNode.insertBefore(playButtonWrapper, playButtonEl);
- // playButtonWrapper.appendChild(playButtonEl);
- // END: Wrap play button in custom div container
-
- // BEGIN: Implement custom next video button
- if (!isEmbedPlayer && (mediaData?.nextLink || isDevMode)) {
- // 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
- const nextVideoButton = new NextVideoButton(playerRef.current, {
- nextLink: mediaData.nextLink,
- });
- const playToggleIndex = controlBar.children().indexOf(playToggle); // Insert it after play button
- controlBar.addChild(nextVideoButton, {}, playToggleIndex + 1); // After time display
-
- // Wrap next video button in custom div container
- // setTimeout(() => {
- // const nextVideoButtonEl = nextVideoButton.el();
- // if (nextVideoButtonEl) {
- // const nextVideoWrapper = document.createElement('div');
- // /* nextVideoWrapper.className =
- // 'vjs-next-video-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button'; */
-
- // // Insert wrapper before the next video button and move button inside
- // nextVideoButtonEl.parentNode.insertBefore(nextVideoWrapper, nextVideoButtonEl);
- // nextVideoWrapper.appendChild(nextVideoButtonEl);
- // }
- // }, 2000); // Small delay to ensure button is fully rendered
- }
- // END: Implement custom next video button
-
- // BEGIN: Implement custom time display component
- const customRemainingTime = new CustomRemainingTime(playerRef.current, {
- displayNegative: false,
- customPrefix: '',
- customSuffix: '',
- });
- const playToggleIndex = controlBar.children().indexOf(playToggle);
- controlBar.addChild(customRemainingTime, {}, playToggleIndex + 2);
- customComponents.current.customRemainingTime = customRemainingTime;
- // END: Implement custom time display component
-
- // BEGIN: Wrap volume panel in custom div container
- setTimeout(() => {
- const volumePanel = controlBar.getChild('volumePanel');
- if (volumePanel) {
- const volumePanelEl = volumePanel.el();
- if (volumePanelEl) {
- const volumeWrapper = document.createElement('div');
- volumeWrapper.className =
- 'vjs-volume-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
-
- // Insert wrapper before the volume panel and move panel inside
- volumePanelEl.parentNode.insertBefore(volumeWrapper, volumePanelEl);
- volumeWrapper.appendChild(volumePanelEl);
- }
- }
- }, 100); // Small delay to ensure volume panel is fully rendered
- // END: Wrap volume panel in custom div container
-
- // BEGIN: Implement autoplay toggle button - simplified
- if (!isEmbedPlayer) {
- try {
- const autoplayToggleButton = new AutoplayToggleButton(playerRef.current, {
- userPreferences: userPreferences.current,
- isTouchDevice: isTouchDevice,
- });
- // Add it before the chapters button (or at a suitable position)
- const chaptersButtonIndex = chaptersButton
- ? controlBar.children().indexOf(chaptersButton)
- : -1;
- const insertIndex =
- chaptersButtonIndex > 0 ? chaptersButtonIndex : controlBar.children().length - 3;
- controlBar.addChild(autoplayToggleButton, {}, insertIndex);
-
- // Store reference for later use
- customComponents.current.autoplayToggleButton = autoplayToggleButton;
-
- // Force update icon after adding to DOM to ensure correct display
- setTimeout(() => {
- autoplayToggleButton.updateIcon();
- }, 100);
-
- // Wrap autoplay toggle button in custom div container
- setTimeout(() => {
- const autoplayButtonEl = autoplayToggleButton.el();
- if (autoplayButtonEl) {
- const autoplayWrapper = document.createElement('div');
- autoplayWrapper.className =
- 'vjs-autoplay-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
-
- // Insert wrapper before the autoplay button and move button inside
- autoplayButtonEl.parentNode.insertBefore(autoplayWrapper, autoplayButtonEl);
- autoplayWrapper.appendChild(autoplayButtonEl);
- }
- }, 150); // Slightly longer delay to ensure button is fully rendered and updated
- } catch (error) {
- console.error('✗ Failed to add autoplay toggle button:', error);
- }
- }
- // END: Implement autoplay toggle button
-
- // Make menus clickable instead of hover-only
- setTimeout(() => {
- const setupClickableMenus = () => {
- // Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay
- const menuButtons = ['subtitlesButton', 'playbackRateMenuButton'];
-
- menuButtons.forEach((buttonName) => {
- const button = controlBar.getChild(buttonName);
- if (button && button.menuButton_) {
- // Override the menu button behavior
- const menuButton = button.menuButton_;
-
- // Disable hover events
- menuButton.off('mouseenter');
- menuButton.off('mouseleave');
-
- // Add click-to-toggle behavior
- menuButton.on('click', function () {
- if (this.menu.hasClass('vjs-lock-showing')) {
- this.menu.removeClass('vjs-lock-showing');
- this.menu.hide();
- } else {
- this.menu.addClass('vjs-lock-showing');
- this.menu.show();
- }
- });
- } else if (button) {
- // For buttons without menuButton_ property
- const buttonEl = button.el();
- if (buttonEl) {
- // Add click handler to show/hide menu
- buttonEl.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- const menu = buttonEl.querySelector('.vjs-menu');
- if (menu) {
- if (menu.style.display === 'block') {
- menu.style.display = 'none';
- } else {
- // Hide other menus first
- document.querySelectorAll('.vjs-menu').forEach((m) => {
- if (m !== menu) m.style.display = 'none';
- });
- menu.style.display = 'block';
- }
- }
- });
- }
- }
- });
-
- // Add YouTube-like subtitles toggle with red underline
- const ccNames = ['subtitlesButton', 'captionsButton', 'subsCapsButton'];
- for (const n of ccNames) {
- const cc = controlBar.getChild(n);
- if (cc && cc.el()) {
- const el = cc.el();
- const menu = el.querySelector('.vjs-menu');
- if (menu) menu.style.display = 'none';
-
- const toggleSubs = (ev) => {
- ev.preventDefault();
- ev.stopPropagation();
- const tracks = playerRef.current.textTracks();
- let any = false;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- any = true;
- break;
- }
- }
- if (any) {
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') t.mode = 'disabled';
- }
- el.classList.remove('vjs-subs-active');
- // Do not change saved language on quick toggle off; save enabled=false
- try {
- userPreferences.current.setPreference(
- 'subtitleEnabled',
- false,
- true
- );
- } catch (e) {}
- } else {
- // Show using previously chosen language only; do not change it
- const preferred =
- userPreferences.current.getPreference('subtitleLanguage');
- if (!preferred) {
- // If no language chosen yet, enable first available and save it
- let first = null;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') {
- first = t.language;
- break;
- }
- }
- if (first) {
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles')
- t.mode = t.language === first ? 'showing' : 'disabled';
- }
- try {
- userPreferences.current.setPreference(
- 'subtitleLanguage',
- first,
- true
- );
- } catch (e) {}
- try {
- userPreferences.current.setPreference(
- 'subtitleEnabled',
- true,
- true
- );
- } catch (e) {}
- el.classList.add('vjs-subs-active');
- }
- return;
- }
- let found = false;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles') {
- const show = t.language === preferred;
- t.mode = show ? 'showing' : 'disabled';
- if (show) found = true;
- }
- }
- if (found) {
- el.classList.add('vjs-subs-active');
- try {
- userPreferences.current.setPreference(
- 'subtitleEnabled',
- true,
- true
- );
- } catch (e) {}
- }
- }
- };
-
- el.addEventListener('click', toggleSubs, { capture: true });
-
- // Add mobile touch support
- el.addEventListener(
- 'touchend',
- (e) => {
- e.preventDefault();
- e.stopPropagation();
- toggleSubs(e);
- },
- { passive: false }
- );
-
- // Sync underline state on external changes
- playerRef.current.on('texttrackchange', () => {
- const tracks = playerRef.current.textTracks();
- let any = false;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- any = true;
- break;
- }
- }
- if (any) el.classList.add('vjs-subs-active');
- else el.classList.remove('vjs-subs-active');
- });
-
- // Initialize state immediately
- const tracks = playerRef.current.textTracks();
- let any = false;
- for (let i = 0; i < tracks.length; i++) {
- const t = tracks[i];
- if (t.kind === 'subtitles' && t.mode === 'showing') {
- any = true;
- break;
- }
- }
- if (any) el.classList.add('vjs-subs-active');
-
- break;
- }
- }
- };
-
- setupClickableMenus();
- }, 1500);
-
- // BEGIN: Add chapter markers and sprite preview to progress control
- if (progressControl && seekBar) {
- // Check if we have chapters
- const hasChapters = chaptersData && chaptersData.length > 0;
-
- if (hasChapters) {
- // Use original ChapterMarkers with sprite functionality when chapters exist
- const chapterMarkers = new ChapterMarkers(playerRef.current, {
- previewSprite: mediaData.previewSprite,
- isTouchDevice: isTouchDevice,
- });
- seekBar.addChild(chapterMarkers);
- } else if (mediaData.previewSprite && !isTouchDevice) {
- // Use separate SpritePreview component only when no chapters but sprite data exists
- // Skip on touch devices to avoid unwanted tooltips
- const spritePreview = new SpritePreview(playerRef.current, {
- previewSprite: mediaData.previewSprite,
- isTouchDevice: isTouchDevice,
- });
- seekBar.addChild(spritePreview);
-
- // Setup sprite preview hover functionality (only on non-touch devices)
- setTimeout(() => {
- spritePreview.setupProgressBarHover();
- }, 100);
- }
- }
- // END: Add chapter markers and sprite preview to progress control
-
- // BEGIN: Simple button layout fix - use CSS approach
- setTimeout(() => {
- // Add a simple spacer div using DOM manipulation (simpler approach)
- const spacerDiv = document.createElement('div');
- spacerDiv.className = 'vjs-spacer-control vjs-control';
- spacerDiv.style.flex = '1';
- spacerDiv.style.minWidth = '1px';
- spacerDiv.style.height = '100%';
-
- // Find insertion point after duration display
- const durationDisplay = controlBar.getChild('durationDisplay');
- if (durationDisplay && durationDisplay.el()) {
- const controlBarEl = controlBar.el();
- const durationEl = durationDisplay.el();
- const nextSibling = durationEl.nextSibling;
- controlBarEl.insertBefore(spacerDiv, nextSibling);
- }
- }, 300);
- // END: Simple button layout fix
-
- // BEGIN: Move Picture-in-Picture and Fullscreen buttons to the very end
- setTimeout(() => {
- try {
- 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);
- }
- } catch (e) {
- console.error('✗ Failed to move PiP/Fullscreen buttons to end:', e);
- }
- }, 100);
- // END: Move Picture-in-Picture and Fullscreen buttons to the very end
-
- // BEGIN: Add Chapters Overlay Component
- if (chaptersData && chaptersData.length > 0) {
- customComponents.current.chaptersOverlay = new CustomChaptersOverlay(playerRef.current, {
- chaptersData: chaptersData,
- seriesTitle: mediaData?.data?.title || 'Chapters',
- channelName: 'Chapter',
- thumbnail: mediaData?.data?.thumbnail_url || mediaData?.data?.author_thumbnail || '',
- });
- }
- // END: Add Chapters Overlay Component
-
- // BEGIN: Add Embed Info Overlay Component (for embed player only)
- if (isEmbedPlayer) {
- customComponents.current.embedInfoOverlay = new EmbedInfoOverlay(playerRef.current, {
- authorName: currentVideo.author_name,
- authorProfile: currentVideo.author_profile,
- authorThumbnail: currentVideo.author_thumbnail,
- videoTitle: currentVideo.title,
- videoUrl: currentVideo.url,
- });
- }
- // END: Add Embed Info Overlay Component
-
- // BEGIN: Add Settings Menu Component
- customComponents.current.settingsMenu = new CustomSettingsMenu(playerRef.current, {
- userPreferences: userPreferences.current,
- qualities: availableQualities,
- hasSubtitles: hasSubtitles,
- isTouchDevice: isTouchDevice,
- });
-
- // If qualities change per video (e.g., via MEDIA_DATA update), refresh menu
- try {
- playerRef.current.on('loadedmetadata', () => {
- if (
- customComponents.current.settingsMenu &&
- customComponents.current.settingsMenu.setQualities
- ) {
- const md = typeof window !== 'undefined' ? window.MEDIA_DATA : null;
- const newQualities = md?.data?.qualities || availableQualities;
- customComponents.current.settingsMenu.setQualities(newQualities);
- }
- });
- } catch (e) {}
-
- // Wrap settings button in custom div container
- setTimeout(() => {
- const controlBar = playerRef.current.getChild('controlBar');
- if (
- controlBar &&
- customComponents.current.settingsMenu &&
- customComponents.current.settingsMenu.settingsButton
- ) {
- const settingsButtonEl = customComponents.current.settingsMenu.settingsButton.el();
- if (settingsButtonEl) {
- const settingsWrapper = document.createElement('div');
- settingsWrapper.className =
- 'vjs-settings-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
-
- // Insert wrapper before the settings button and move button inside
- settingsButtonEl.parentNode.insertBefore(settingsWrapper, settingsButtonEl);
- settingsWrapper.appendChild(settingsButtonEl);
- }
- }
- }, 200); // Longer delay to ensure settings button is fully created
-
- // Wrap picture-in-picture button in custom div container
- setTimeout(() => {
- const controlBar = playerRef.current.getChild('controlBar');
- const pipControl = controlBar?.getChild('pictureInPictureToggle');
- if (pipControl) {
- const pipButtonEl = pipControl.el();
- if (pipButtonEl) {
- const pipWrapper = document.createElement('div');
- pipWrapper.className =
- 'vjs-pip-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
-
- // Insert wrapper before the pip button and move button inside
- pipButtonEl.parentNode.insertBefore(pipWrapper, pipButtonEl);
- pipWrapper.appendChild(pipButtonEl);
- }
- }
- }, 100); // Small delay to ensure pip button is available
-
- // Wrap fullscreen button in custom div container
- setTimeout(() => {
- const controlBar = playerRef.current.getChild('controlBar');
- const fullscreenControl = controlBar?.getChild('fullscreenToggle');
- if (fullscreenControl) {
- const fullscreenButtonEl = fullscreenControl.el();
- if (fullscreenButtonEl) {
- const fullscreenWrapper = document.createElement('div');
- fullscreenWrapper.className =
- 'vjs-fullscreen-wrapper vjs-menu-button vjs-menu-button-popup vjs-control vjs-button';
-
- // Insert wrapper before the fullscreen button and move button inside
- fullscreenButtonEl.parentNode.insertBefore(fullscreenWrapper, fullscreenButtonEl);
- fullscreenWrapper.appendChild(fullscreenButtonEl);
- }
- }
- }, 100); // Small delay to ensure fullscreen button is available
-
- // END: Add Settings Menu Component
-
- // BEGIN: Add Seek Indicator Component
- customComponents.current.seekIndicator = new SeekIndicator(playerRef.current, {
- seekAmount: 5, // 5 seconds seek amount
- isEmbedPlayer: isEmbedPlayer, // Pass embed mode flag
- });
- // Add the component but ensure it's hidden initially
- playerRef.current.addChild(customComponents.current.seekIndicator);
-
- customComponents.current.seekIndicator.hide(); // Explicitly hide on creation
- // END: Add Seek Indicator Component
-
- // Store components reference for potential cleanup
-
- // BEGIN: Fix Android seekbar touch functionality
- /* if (isTouchDevice) {
- setTimeout(() => {
- const progressControl = playerRef.current
- .getChild('controlBar')
- ?.getChild('progressControl');
- const seekBar = progressControl?.getChild('seekBar');
-
- if (seekBar && seekBar.el()) {
- const seekBarEl = seekBar.el();
- const progressHolder = seekBarEl.querySelector('.vjs-progress-holder');
-
- if (progressHolder) {
- let isDragging = false;
-
- const handleTouchStart = (e) => {
- isDragging = true;
- e.preventDefault();
- e.stopPropagation(); // Prevent event from reaching video element
- playerRef.current.userActive(true);
-
- // Mark that we're interacting with seekbar to prevent play/pause
- playerRef.current.seekbarTouching = true;
-
- // Temporarily disable big play button
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
- if (bigPlayButton && bigPlayButton.el()) {
- bigPlayButton.el().style.pointerEvents = 'none';
- bigPlayButton.el().style.touchAction = 'none';
- }
- };
-
- const handleTouchMove = (e) => {
- if (!isDragging) return;
- e.preventDefault();
- e.stopPropagation(); // Prevent event from reaching video element
-
- const touch = e.touches[0];
- const rect = progressHolder.getBoundingClientRect();
- const percentage = Math.max(
- 0,
- Math.min(1, (touch.clientX - rect.left) / rect.width)
- );
- const duration = playerRef.current.duration();
-
- if (duration && !isNaN(duration)) {
- const newTime = percentage * duration;
- playerRef.current.currentTime(newTime);
- }
- };
-
- const handleTouchEnd = (e) => {
- isDragging = false;
- e.preventDefault();
- e.stopPropagation(); // Prevent event from reaching video element
-
- // Re-enable big play button
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
- if (bigPlayButton && bigPlayButton.el()) {
- setTimeout(() => {
- bigPlayButton.el().style.pointerEvents = '';
- bigPlayButton.el().style.touchAction = '';
- }, 200);
- }
-
- // Clear the seekbar touching flag after a longer delay to prevent conflicts
- setTimeout(() => {
- if (playerRef.current) {
- playerRef.current.seekbarTouching = false;
- }
- }, 300);
- };
-
- // Add touch event listeners specifically for Android
- progressHolder.addEventListener('touchstart', handleTouchStart, {
- passive: false,
- });
- progressHolder.addEventListener('touchmove', handleTouchMove, {
- passive: false,
- });
- progressHolder.addEventListener('touchend', handleTouchEnd, { passive: false });
-
- // Store cleanup function
- customComponents.current.cleanupSeekbarTouch = () => {
- progressHolder.removeEventListener('touchstart', handleTouchStart);
- progressHolder.removeEventListener('touchmove', handleTouchMove);
- progressHolder.removeEventListener('touchend', handleTouchEnd);
- };
- }
- }
- }, 500);
- } */
- // END: Fix Android seekbar touch functionality
-
- // BEGIN: Add comprehensive keyboard event handling
- const handleAllKeyboardEvents = (event) => {
- // Only handle if no input elements are focused
- const activeElement = document.activeElement;
- const isInputFocused =
- activeElement &&
- (activeElement.tagName === 'INPUT' ||
- activeElement.tagName === 'TEXTAREA' ||
- activeElement.contentEditable === 'true');
-
- if (isInputFocused) {
- return; // Don't interfere with input fields
- }
-
- // 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
- 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);
- if (customComponents.current.seekIndicator) {
- customComponents.current.seekIndicator.show('forward', seekAmount);
- }
- } else if (event.key === 'ArrowLeft' || event.keyCode === 37) {
- event.preventDefault();
- const currentTime = playerRef.current.currentTime();
- const newTime = Math.max(currentTime - seekAmount, 0);
-
- playerRef.current.currentTime(newTime);
- if (customComponents.current.seekIndicator) {
- customComponents.current.seekIndicator.show('backward', seekAmount);
- }
- }
- };
-
- // Add keyboard event listener to the document
- document.addEventListener('keydown', handleAllKeyboardEvents);
-
- // Store cleanup function for arrow keys
- customComponents.current.cleanupArrowKeyHandler = () => {
- document.removeEventListener('keydown', handleAllKeyboardEvents);
- };
-
- // END: Add comprehensive keyboard event handling
- });
-
- // Listen for next video event
- playerRef.current.on('nextVideo', () => {
- goToNextVideo();
- });
-
- // Simple solution: Force controls to hide periodically when video is playing
- const forceHideInterval = setInterval(() => {
- if (playerRef.current && !playerRef.current.paused() && !playerRef.current.ended()) {
- // Check if any menus are open
- const isMenuOpen =
- document.querySelector('.vjs-settings-overlay.show') ||
- document.querySelector('.vjs-menu-button-popup.vjs-lock-showing') ||
- document.querySelector('.vjs-texttrack-settings') ||
- document.querySelector('.vjs-menu.vjs-lock-showing');
-
- // Only force hide if no menus are open and user has been active for too long
- if (
- !isMenuOpen &&
- playerRef.current.userActivity_ &&
- Date.now() - playerRef.current.userActivity_ > (isEmbedPlayer ? 5500 : 2500)
- ) {
- playerRef.current.userActive(false);
- }
- }
- }, 1000); // Check every second
-
- // Store interval for cleanup
- customComponents.current.forceHideInterval = forceHideInterval;
-
- playerRef.current.on('play', () => {
- // Only show play indicator if not changing quality
- if (!playerRef.current.isChangingQuality && customComponents.current.seekIndicator) {
- customComponents.current.seekIndicator.show('play');
- }
-
- // For embed players, ensure video becomes visible when playing
- if (isEmbedPlayer) {
- const playerEl = playerRef.current.el();
- const videoEl = playerEl.querySelector('video');
- const posterEl = playerEl.querySelector('.vjs-poster');
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
-
- if (videoEl) {
- videoEl.style.opacity = '1';
- }
- if (posterEl) {
- posterEl.style.opacity = '0';
- }
- // Hide big play button when video starts playing
- if (bigPlayButton) {
- bigPlayButton.hide();
- }
- }
- });
-
- playerRef.current.on('pause', () => {
- // Only show pause indicator if not changing quality
- if (!playerRef.current.isChangingQuality && customComponents.current.seekIndicator) {
- customComponents.current.seekIndicator.show('pause');
- }
-
- // For embed players, show poster when paused at beginning
- if (isEmbedPlayer && playerRef.current.currentTime() === 0) {
- const playerEl = playerRef.current.el();
- const videoEl = playerEl.querySelector('video');
- const posterEl = playerEl.querySelector('.vjs-poster');
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
-
- if (videoEl) {
- videoEl.style.opacity = '0';
- }
- if (posterEl) {
- posterEl.style.opacity = '1';
- }
- // Show big play button when paused at beginning
- if (bigPlayButton) {
- bigPlayButton.show();
- }
- }
- });
-
- // Store reference to end screen and autoplay countdown for cleanup
- let endScreen = null;
- let autoplayCountdown = null;
-
- playerRef.current.on('ended', () => {
- // For embed players, show big play button when video ends
- if (isEmbedPlayer) {
- const bigPlayButton = playerRef.current.getChild('bigPlayButton');
- if (bigPlayButton) {
- bigPlayButton.show();
- }
- }
-
- // Keep controls active after video ends
- setTimeout(() => {
- if (playerRef.current && !playerRef.current.isDisposed()) {
- // Remove vjs-ended class if it disables controls
- const playerEl = playerRef.current.el();
- if (playerEl) {
- // Keep the visual ended state but ensure controls work
- const controlBar = playerRef.current.getChild('controlBar');
- if (controlBar) {
- controlBar.show();
- controlBar.el().style.opacity = '1';
- controlBar.el().style.pointerEvents = 'auto';
- }
- }
- }
- }, 50);
-
- // Check if autoplay is enabled and there's a next video
- const isAutoplayEnabled = userPreferences.current.getAutoplayPreference();
- const hasNextVideo = mediaData.nextLink !== null;
-
- if (!isEmbedPlayer && isAutoplayEnabled && hasNextVideo) {
- // If it's a playlist, skip countdown and play directly
- if (currentVideo.isPlayList) {
- // Clean up any existing overlays
- if (endScreen) {
- playerRef.current.removeChild(endScreen);
- endScreen = null;
- }
- if (autoplayCountdown) {
- playerRef.current.removeChild(autoplayCountdown);
- autoplayCountdown = null;
- }
-
- // Play next video directly without countdown
- goToNextVideo();
- } else {
- // 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) {
- 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 immediately!
- 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);
- // Start countdown immediately without any delay
- setTimeout(() => {
- if (autoplayCountdown && !autoplayCountdown.isDisposed()) {
- autoplayCountdown.startCountdown();
- }
- }, 0);
- }
- } else {
- // Autoplay disabled or no next video - show regular end screen
- showEndScreen();
- }
-
- // Function to show the regular end screen
- function showEndScreen() {
- // Prevent creating multiple end screens
- if (endScreen) {
- playerRef.current.removeChild(endScreen);
- endScreen = null;
- }
-
- // Show end screen with related videos
- endScreen = new EndScreenOverlay(playerRef.current, {
- relatedVideos: relatedVideos,
- });
-
- // Also store the data directly on the component as backup
- endScreen.relatedVideos = relatedVideos;
-
- playerRef.current.addChild(endScreen);
- endScreen.show();
- }
- });
-
- // Hide end screen and autoplay countdown when user wants to replay
- playerRef.current.on('play', () => {
- if (endScreen) {
- endScreen.hide();
- }
- if (autoplayCountdown) {
- autoplayCountdown.stopCountdown();
- }
- });
-
- // Hide end screen and autoplay countdown when user seeks
- playerRef.current.on('seeking', () => {
- if (endScreen) {
- endScreen.hide();
- }
- if (autoplayCountdown) {
- autoplayCountdown.stopCountdown();
- }
- });
-
- // Handle replay button functionality
- playerRef.current.on('replay', () => {
- if (endScreen) {
- endScreen.hide();
- }
- playerRef.current.currentTime(0);
- playerRef.current.play();
- });
-
- playerRef.current.on('error', (error) => {
- // console.error('Video.js error:', error);
- });
-
- playerRef.current.on('fullscreenchange', () => {
- // console.log('Fullscreen changed:', playerRef.current.isFullscreen());
- });
-
- playerRef.current.on('volumechange', () => {
- // console.log('Volume changed:', playerRef.current.volume(), 'Muted:', playerRef.current.muted());
- });
-
- playerRef.current.on('ratechange', () => {
- // console.log('Playback rate changed:', playerRef.current.playbackRate());
- });
-
- playerRef.current.on('texttrackchange', () => {
- // console.log('Text track changed');
- });
-
- // Focus the player element so keyboard controls work
- // This ensures keyboard events work properly in both normal and fullscreen modes
- playerRef.current.ready(() => {
- // Focus the player element and set up focus handling
- if (playerRef.current.el()) {
- // Make the video element focusable
- const videoElement = playerRef.current.el();
- videoElement.setAttribute('tabindex', '0');
- videoElement.focus();
- }
- });
-
- // 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();
- }
- }
- }
- }, 100); // Small delay to ensure fullscreen transition is complete
- });
- }
- }, 0);
-
- return () => {
- clearTimeout(timer);
- };
- }
-
- // Cleanup function
- return () => {
- // Clean up keyboard event listeners if they exist
- if (customComponents.current && customComponents.current.cleanupArrowKeyHandler) {
- customComponents.current.cleanupArrowKeyHandler();
- }
-
- // Clean up seekbar touch handlers if they exist
- if (customComponents.current && customComponents.current.cleanupSeekbarTouch) {
- customComponents.current.cleanupSeekbarTouch();
- }
-
- // Clean up embed controls event listeners if they exist
- if (customComponents.current && customComponents.current.embedControlsCleanup) {
- customComponents.current.embedControlsCleanup();
- }
-
- // Clean up force hide interval if it exists
- if (customComponents.current && customComponents.current.forceHideInterval) {
- clearInterval(customComponents.current.forceHideInterval);
- }
-
- if (playerRef.current && !playerRef.current.isDisposed()) {
- playerRef.current.dispose();
- playerRef.current = null;
- }
- };
- }, []);
-
- // Additional effect to ensure video gets focus for keyboard controls on page load
- useEffect(() => {
- const focusVideo = () => {
- if (playerRef.current && playerRef.current.el()) {
- const videoElement = playerRef.current.el();
- videoElement.setAttribute('tabindex', '0');
- videoElement.focus();
- }
- };
-
- // 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);
-
- // Multiple attempts to ensure focus on page load
- const focusAttempts = [100, 500, 1000, 2000];
- const timeouts = focusAttempts.map((delay) => setTimeout(focusVideo, delay));
-
- return () => {
- document.removeEventListener('visibilitychange', handleVisibilityChange);
- window.removeEventListener('focus', handleWindowFocus);
- timeouts.forEach(clearTimeout);
- };
- }, []);
-
- return (
-
- );
-}
-
-// export default VideoJSPlayer;
diff --git a/frontend-tools/video-js/src/styles/embed_OLD.css b/frontend-tools/video-js/src/styles/embed_OLD.css
deleted file mode 100644
index 9652b526..00000000
--- a/frontend-tools/video-js/src/styles/embed_OLD.css
+++ /dev/null
@@ -1,194 +0,0 @@
-/* ===== EMBED PLAYER STYLES ===== */
-/* Styles specific to #page-embed and embedded video players */
-
-/* Fullscreen video styles for embedded video player */
-#page-embed .video-js-root-embed .video-js video {
- width: 100vw !important;
- height: 100vh !important;
- object-fit: cover !important;
- border-radius: 0 !important;
-}
-
-#page-embed .video-js-root-embed .video-js .vjs-poster {
- border-radius: 0 !important;
- width: 100vw !important;
- height: 100vh !important;
- object-fit: cover !important;
-}
-
-/* Fullscreen styles for embedded video player */
-#page-embed .video-js-root-embed .video-container {
- width: 100vw;
- height: 100vh;
- max-width: none;
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- position: fixed;
- top: 0;
- left: 0;
- z-index: 1000;
-}
-
-/* Fullscreen fluid styles for embedded video player */
-#page-embed .video-js-root-embed .video-js.vjs-fluid {
- width: 100vw !important;
- height: 100vh !important;
- max-width: none !important;
- max-height: none !important;
-}
-
-/* Fullscreen video-js player styles for embedded video player */
-#page-embed .video-js-root-embed .video-js {
- width: 100vw !important;
- height: 100vh !important;
- border-radius: 0;
- position: relative;
- overflow: hidden; /* Prevent scrollbars in embed video player */
-}
-
-/* Prevent page scrolling when embed is active */
-#page-embed .video-js-root-embed {
- position: fixed;
- top: 0;
- overflow: hidden; /* Prevent scrollbars in embed mode */
-}
-
-/* Sticky controls for embed player - always at bottom of window */
-#page-embed .video-js-root-embed .video-js .vjs-control-bar {
- position: fixed !important;
- bottom: 0 !important;
- left: 0 !important;
- right: 0 !important;
- width: 100vw !important;
- z-index: 1001 !important;
- background: transparent !important;
- background-color: transparent !important;
- background-image: none !important;
- padding: 0 12px !important;
- margin: 0 !important;
- border: none !important;
- box-shadow: none !important;
-}
-
-/* Ensure progress bar is also sticky for embed player */
-#page-embed .video-js-root-embed .video-js .vjs-progress-control {
- position: fixed !important;
- bottom: 48px !important;
- left: 0 !important;
- right: 0 !important;
- width: 100vw !important;
- z-index: 1000 !important;
- margin: 0 !important;
- padding: 0 !important;
- border: none !important;
-}
-
-/* Ensure gradient overlay extends to full window width for embed */
-#page-embed .video-js-root-embed .video-js::after {
- position: fixed !important;
- bottom: 0 !important;
- left: 0 !important;
- right: 0 !important;
- width: 100vw !important;
- height: 120px !important;
- z-index: 999 !important;
-}
-
-/* Mobile optimizations for embed player sticky controls */
-@media (max-width: 768px) {
- #page-embed .video-js-root-embed .video-js .vjs-control-bar {
- height: 56px !important; /* Larger touch target on mobile */
- padding: 0 16px !important; /* More padding for touch */
- margin: 0 !important;
- border: none !important;
- background: transparent !important;
- background-color: transparent !important;
- background-image: none !important;
- }
-
- #page-embed .video-js-root-embed .video-js .vjs-progress-control {
- bottom: 44px !important; /* Much closer to control bar - minimal gap */
- margin: 0 !important;
- padding: 0 !important;
- }
-
- /* Ensure controls don't interfere with mobile browser chrome */
- #page-embed .video-js-root-embed .video-js .vjs-control-bar {
- padding-bottom: env(safe-area-inset-bottom, 0) !important;
- }
-}
-
-/* Ensure controls are always visible when user is active (embed only) */
-#page-embed .video-js-root-embed .video-js.vjs-user-active .vjs-control-bar,
-#page-embed .video-js-root-embed .video-js.vjs-paused .vjs-control-bar,
-#page-embed .video-js-root-embed .video-js.vjs-ended .vjs-control-bar {
- opacity: 1 !important;
- visibility: visible !important;
- transform: translateY(0) !important;
-}
-
-/* Smooth transitions for control visibility */
-#page-embed .video-js-root-embed .video-js .vjs-control-bar {
- transition:
- opacity 0.3s ease,
- transform 0.3s ease !important;
-}
-
-/* Hide controls when user is inactive (but keep them sticky) */
-#page-embed .video-js-root-embed .video-js.vjs-user-inactive:not(.vjs-paused):not(.vjs-ended) .vjs-control-bar {
- opacity: 0 !important;
- transform: translateY(100%) !important;
-}
-
-#page-embed .video-js-root-embed .video-js.vjs-user-inactive:not(.vjs-paused):not(.vjs-ended) .vjs-progress-control {
- opacity: 0 !important;
-}
-
-/* ===== EMBED-SPECIFIC SEEK INDICATOR POSITIONING ===== */
-/* Ensure play icon (SeekIndicator) stays centered in embed view regardless of window size */
-#page-embed .video-js-root-embed .video-js .vjs-seek-indicator {
- position: fixed !important;
- top: 50vh !important;
- left: 50vw !important;
- transform: translate(-50%, -50%) !important;
- z-index: 10000 !important;
- pointer-events: none !important;
- display: none !important;
- align-items: center !important;
- justify-content: center !important;
- opacity: 0 !important;
- visibility: hidden !important;
- transition: opacity 0.2s ease-in-out !important;
- width: auto !important;
- height: auto !important;
- margin: 0 !important;
- padding: 0 !important;
-}
-
-/* ===== EMBED-SPECIFIC BIG PLAY BUTTON POSITIONING ===== */
-/* Ensure big play button stays centered in embed view regardless of window size */
-#page-embed .video-js-root-embed .video-js .vjs-big-play-button {
- position: absolute !important;
- top: 50% !important;
- left: 50% !important;
- transform: translate(-50%, -50%) !important;
- z-index: 1000 !important;
- pointer-events: auto !important;
- margin: 0 !important;
- padding: 0 !important;
-}
-
-/* ===== EMBED-SPECIFIC CONTROLS HIDING FOR INITIAL STATE ===== */
-/* Hide seekbar and controls when poster is displayed (before first play) in embed mode */
-#page-embed .video-js-root-embed .video-js:not(.vjs-has-started) .vjs-control-bar {
- display: none !important;
- opacity: 0 !important;
- visibility: hidden !important;
-}
-
-#page-embed .video-js-root-embed .video-js:not(.vjs-has-started) .vjs-progress-control {
- display: none !important;
- opacity: 0 !important;
- visibility: hidden !important;
-}
diff --git a/frontend/src/static/js/components/VideoJS/VideoJSEmbed.jsx b/frontend/src/static/js/components/VideoJS/VideoJSEmbed.jsx
index 0107721f..491b935e 100644
--- a/frontend/src/static/js/components/VideoJS/VideoJSEmbed.jsx
+++ b/frontend/src/static/js/components/VideoJS/VideoJSEmbed.jsx
@@ -16,6 +16,7 @@ import React, { useEffect, useRef } from 'react';
const VideoJSEmbed = ({
data,
useRoundedCorners,
+ version,
isPlayList,
playerVolume,
playerSoundMuted,
@@ -67,6 +68,7 @@ const VideoJSEmbed = ({
window.MEDIA_DATA = {
data: data || {},
useRoundedCorners: useRoundedCorners,
+ version: version,
isPlayList: isPlayList,
playerVolume: playerVolume || 0.5,
playerSoundMuted: playerSoundMuted || (urlMuted === '1'),
@@ -204,14 +206,14 @@ const VideoJSEmbed = ({
if (!existingCSS) {
const cssLink = document.createElement('link');
cssLink.rel = 'stylesheet';
- cssLink.href = siteUrl + '/static/video_js/video-js.css';
+ cssLink.href = siteUrl + '/static/video_js/video-js.css?v=' + version;
document.head.appendChild(cssLink);
}
// Load JS if not already loaded
if (!existingJS) {
const script = document.createElement('script');
- script.src = siteUrl + '/static/video_js/video-js.js';
+ script.src = siteUrl + '/static/video_js/video-js.js?v=' + version;
document.head.appendChild(script);
}
};
diff --git a/frontend/src/static/js/components/media-viewer/VideoViewer/index.js b/frontend/src/static/js/components/media-viewer/VideoViewer/index.js
index 2ef829d3..0bc6d434 100644
--- a/frontend/src/static/js/components/media-viewer/VideoViewer/index.js
+++ b/frontend/src/static/js/components/media-viewer/VideoViewer/index.js
@@ -392,6 +392,7 @@ export default class VideoViewer extends React.PureComponent {
return React.createElement(VideoJSEmbed, {
data: this.props.data,
useRoundedCorners: site.useRoundedCorners,
+ version: site.version,
isPlayList: !!MediaPageStore.get('playlist-id'),
playerVolume: this.browserCache.get('player-volume'),
playerSoundMuted: this.browserCache.get('player-sound-muted'),
diff --git a/frontend/src/static/js/pages/MediaVideoPage.js b/frontend/src/static/js/pages/MediaVideoPage.js
deleted file mode 100755
index 04d2aed0..00000000
--- a/frontend/src/static/js/pages/MediaVideoPage.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-import { SiteConsumer } from '../utils/contexts/';
-import VideoViewer from '../components/media-viewer/VideoViewer';
-import { _VideoMediaPage } from './_VideoMediaPage';
-
-export class MediaVideoPage extends _VideoMediaPage {
- viewerContainerContent(mediaData) {
- return <>Not working anymore?>; // TODO: check this if this page not working anymore as MediaPage.js do the same work
- return {(site) => } ;
- }
-
- mediaType() {
- return 'video';
- }
-}
diff --git a/frontend/src/static/js/utils/settings/site.js b/frontend/src/static/js/utils/settings/site.js
index aa2924ce..b6a3844a 100755
--- a/frontend/src/static/js/utils/settings/site.js
+++ b/frontend/src/static/js/utils/settings/site.js
@@ -7,6 +7,7 @@ export function init(settings) {
api: '',
title: '',
useRoundedCorners: true,
+ version: '1.0.0',
};
if (void 0 !== settings) {
@@ -29,6 +30,10 @@ export function init(settings) {
if ('boolean' === typeof settings.useRoundedCorners) {
SITE.useRoundedCorners = settings.useRoundedCorners;
}
+
+ if ('string' === typeof settings.version) {
+ SITE.version = settings.version.trim();
+ }
}
}
diff --git a/frontend/src/templates/config/installation/site.config.js b/frontend/src/templates/config/installation/site.config.js
index de0bab31..80ec6cd8 100755
--- a/frontend/src/templates/config/installation/site.config.js
+++ b/frontend/src/templates/config/installation/site.config.js
@@ -5,6 +5,7 @@ module.exports = {
url: process.env.MEDIACMS_URL || 'UNDEFINED_URL',
api: process.env.MEDIACMS_API || 'UNDEFINED_API',
useRoundedCorners: true,
+ version: '1.0.0',
theme: {
mode: 'light', // Valid values: 'light', 'dark'.
switch: {
diff --git a/static/chapters_editor/chapters-editor.js b/static/chapters_editor/chapters-editor.js
index ab9cbbb7..35a6b9a8 100644
--- a/static/chapters_editor/chapters-editor.js
+++ b/static/chapters_editor/chapters-editor.js
@@ -1,4 +1,4 @@
-(function(){"use strict";var up={exports:{}},_s={exports:{}},rl={exports:{}};rl.exports;var sp;function Fw(){return sp||(sp=1,function(M,Q){/**
+(function(){"use strict";var up={exports:{}},_s={exports:{}},rl={exports:{}};rl.exports;var sp;function Fw(){return sp||(sp=1,function(B,b){/**
* @license React
* react.development.js
*
@@ -6,7 +6,7 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var _="18.3.1",MA=Symbol.for("react.element"),KA=Symbol.for("react.portal"),V=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),De=Symbol.for("react.profiler"),cA=Symbol.for("react.provider"),dA=Symbol.for("react.context"),he=Symbol.for("react.forward_ref"),tA=Symbol.for("react.suspense"),qA=Symbol.for("react.suspense_list"),iA=Symbol.for("react.memo"),pA=Symbol.for("react.lazy"),aA=Symbol.for("react.offscreen"),Oe=Symbol.iterator,fe="@@iterator";function jA(u){if(u===null||typeof u!="object")return null;var p=Oe&&u[Oe]||u[fe];return typeof p=="function"?p:null}var LA={current:null},Ae={transition:null},b={current:null,isBatchingLegacy:!1,didScheduleLegacyUpdate:!1},PA={current:null},I={},Ke=null;function HA(u){Ke=u}I.setExtraStackFrame=function(u){Ke=u},I.getCurrentStack=null,I.getStackAddendum=function(){var u="";Ke&&(u+=Ke);var p=I.getCurrentStack;return p&&(u+=p()||""),u};var Ue=!1,We=!1,TA=!1,O=!1,te=!1,$={ReactCurrentDispatcher:LA,ReactCurrentBatchConfig:Ae,ReactCurrentOwner:PA};$.ReactDebugCurrentFrame=I,$.ReactCurrentActQueue=b;function pe(u){{for(var p=arguments.length,U=new Array(p>1?p-1:0),v=1;v1?p-1:0),v=1;v
1){for(var Ie=Array(Qe),Je=0;Je1){for(var ae=Array(Je),At=0;At is not supported and will be removed in a future major release. Did you mean to render instead?")),p.Provider},set:function(CA){p.Provider=CA}},_currentValue:{get:function(){return p._currentValue},set:function(CA){p._currentValue=CA}},_currentValue2:{get:function(){return p._currentValue2},set:function(CA){p._currentValue2=CA}},_threadCount:{get:function(){return p._threadCount},set:function(CA){p._threadCount=CA}},Consumer:{get:function(){return U||(U=!0,yA("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),p.Consumer}},displayName:{get:function(){return p.displayName},set:function(CA){X||(pe("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.",CA),X=!0)}}}),p.Consumer=FA}return p._currentRenderer=null,p._currentRenderer2=null,p}var yt=-1,gt=0,$t=1,Ai=2;function Ta(u){if(u._status===yt){var p=u._result,U=p();if(U.then(function(FA){if(u._status===gt||u._status===yt){var CA=u;CA._status=$t,CA._result=FA}},function(FA){if(u._status===gt||u._status===yt){var CA=u;CA._status=Ai,CA._result=FA}}),u._status===yt){var v=u;v._status=gt,v._result=U}}if(u._status===$t){var X=u._result;return X===void 0&&yA(`lazy: Expected the result of a dynamic import() call. Instead received: %s
+ */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var _="18.3.1",MA=Symbol.for("react.element"),KA=Symbol.for("react.portal"),V=Symbol.for("react.fragment"),s=Symbol.for("react.strict_mode"),De=Symbol.for("react.profiler"),cA=Symbol.for("react.provider"),dA=Symbol.for("react.context"),he=Symbol.for("react.forward_ref"),tA=Symbol.for("react.suspense"),qA=Symbol.for("react.suspense_list"),iA=Symbol.for("react.memo"),pA=Symbol.for("react.lazy"),aA=Symbol.for("react.offscreen"),Oe=Symbol.iterator,fe="@@iterator";function jA(u){if(u===null||typeof u!="object")return null;var p=Oe&&u[Oe]||u[fe];return typeof p=="function"?p:null}var LA={current:null},Ae={transition:null},Q={current:null,isBatchingLegacy:!1,didScheduleLegacyUpdate:!1},PA={current:null},I={},Ke=null;function HA(u){Ke=u}I.setExtraStackFrame=function(u){Ke=u},I.getCurrentStack=null,I.getStackAddendum=function(){var u="";Ke&&(u+=Ke);var p=I.getCurrentStack;return p&&(u+=p()||""),u};var Ue=!1,We=!1,TA=!1,O=!1,te=!1,$={ReactCurrentDispatcher:LA,ReactCurrentBatchConfig:Ae,ReactCurrentOwner:PA};$.ReactDebugCurrentFrame=I,$.ReactCurrentActQueue=Q;function pe(u){{for(var p=arguments.length,U=new Array(p>1?p-1:0),v=1;v1?p-1:0),v=1;v
1){for(var Ie=Array(Qe),Je=0;Je1){for(var ae=Array(Je),At=0;At is not supported and will be removed in a future major release. Did you mean to render instead?")),p.Provider},set:function(CA){p.Provider=CA}},_currentValue:{get:function(){return p._currentValue},set:function(CA){p._currentValue=CA}},_currentValue2:{get:function(){return p._currentValue2},set:function(CA){p._currentValue2=CA}},_threadCount:{get:function(){return p._threadCount},set:function(CA){p._threadCount=CA}},Consumer:{get:function(){return U||(U=!0,yA("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),p.Consumer}},displayName:{get:function(){return p.displayName},set:function(CA){X||(pe("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.",CA),X=!0)}}}),p.Consumer=FA}return p._currentRenderer=null,p._currentRenderer2=null,p}var yt=-1,gt=0,$t=1,Ai=2;function Ta(u){if(u._status===yt){var p=u._result,U=p();if(U.then(function(FA){if(u._status===gt||u._status===yt){var CA=u;CA._status=$t,CA._result=FA}},function(FA){if(u._status===gt||u._status===yt){var CA=u;CA._status=Ai,CA._result=FA}}),u._status===yt){var v=u;v._status=gt,v._result=U}}if(u._status===$t){var X=u._result;return X===void 0&&yA(`lazy: Expected the result of a dynamic import() call. Instead received: %s
Your code should look like:
const MyComponent = lazy(() => import('./MyComponent'))
@@ -28,7 +28,7 @@ Check the render method of \``+u+"`."}return""}function ti(u){if(u!==void 0){var
Check your code at `+p+":"+U+"."}return""}function Mn(u){return u!=null?ti(u.__source):""}var tn={};function ic(u){var p=ul();if(!p){var U=typeof u=="string"?u:u.displayName||u.name;U&&(p=`
-Check the top-level render call using <`+U+">.")}return p}function Bt(u,p){if(!(!u._store||u._store.validated||u.key!=null)){u._store.validated=!0;var U=ic(p);if(!tn[U]){tn[U]=!0;var v="";u&&u._owner&&u._owner!==PA.current&&(v=" It was passed a child from "+lA(u._owner.type)+"."),Da(u),yA('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',U,v),Da(null)}}}function $e(u,p){if(typeof u=="object"){if(at(u))for(var U=0;U ",X=" Did you accidentally export a JSX literal instead of a component?"):CA=typeof u,yA("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",CA,X)}var JA=YA.apply(this,arguments);if(JA==null)return JA;if(v)for(var oe=2;oe10&&pe("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."),v._updatedFibers.clear()}}}var cl=!1,or=null;function nc(u){if(or===null)try{var p=("require"+Math.random()).slice(0,7),U=M&&M[p];or=U.call(M,"timers").setImmediate}catch{or=function(X){cl===!1&&(cl=!0,typeof MessageChannel>"u"&&yA("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var FA=new MessageChannel;FA.port1.onmessage=X,FA.port2.postMessage(void 0)}}return or(u)}var an=0,Qn=!1;function hl(u){{var p=an;an++,b.current===null&&(b.current=[]);var U=b.isBatchingLegacy,v;try{if(b.isBatchingLegacy=!0,v=u(),!U&&b.didScheduleLegacyUpdate){var X=b.current;X!==null&&(b.didScheduleLegacyUpdate=!1,cr(X))}}catch(ae){throw Ba(p),ae}finally{b.isBatchingLegacy=U}if(v!==null&&typeof v=="object"&&typeof v.then=="function"){var FA=v,CA=!1,JA={then:function(ae,At){CA=!0,FA.then(function(ct){Ba(p),an===0?ur(ct,ae,At):ae(ct)},function(ct){Ba(p),At(ct)})}};return!Qn&&typeof Promise<"u"&&Promise.resolve().then(function(){}).then(function(){CA||(Qn=!0,yA("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),JA}else{var oe=v;if(Ba(p),an===0){var Qe=b.current;Qe!==null&&(cr(Qe),b.current=null);var Ie={then:function(ae,At){b.current===null?(b.current=[],ur(oe,ae,At)):ae(oe)}};return Ie}else{var Je={then:function(ae,At){ae(oe)}};return Je}}}}function Ba(u){u!==an-1&&yA("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),an=u}function ur(u,p,U){{var v=b.current;if(v!==null)try{cr(v),nc(function(){v.length===0?(b.current=null,p(u)):ur(u,p,U)})}catch(X){U(X)}else p(u)}}var sr=!1;function cr(u){if(!sr){sr=!0;var p=0;try{for(;p.")}return p}function Bt(u,p){if(!(!u._store||u._store.validated||u.key!=null)){u._store.validated=!0;var U=ic(p);if(!tn[U]){tn[U]=!0;var v="";u&&u._owner&&u._owner!==PA.current&&(v=" It was passed a child from "+lA(u._owner.type)+"."),Da(u),yA('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',U,v),Da(null)}}}function $e(u,p){if(typeof u=="object"){if(at(u))for(var U=0;U ",X=" Did you accidentally export a JSX literal instead of a component?"):CA=typeof u,yA("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",CA,X)}var JA=YA.apply(this,arguments);if(JA==null)return JA;if(v)for(var oe=2;oe10&&pe("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."),v._updatedFibers.clear()}}}var cl=!1,or=null;function nc(u){if(or===null)try{var p=("require"+Math.random()).slice(0,7),U=B&&B[p];or=U.call(B,"timers").setImmediate}catch{or=function(X){cl===!1&&(cl=!0,typeof MessageChannel>"u"&&yA("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var FA=new MessageChannel;FA.port1.onmessage=X,FA.port2.postMessage(void 0)}}return or(u)}var an=0,Qn=!1;function hl(u){{var p=an;an++,Q.current===null&&(Q.current=[]);var U=Q.isBatchingLegacy,v;try{if(Q.isBatchingLegacy=!0,v=u(),!U&&Q.didScheduleLegacyUpdate){var X=Q.current;X!==null&&(Q.didScheduleLegacyUpdate=!1,cr(X))}}catch(ae){throw Ba(p),ae}finally{Q.isBatchingLegacy=U}if(v!==null&&typeof v=="object"&&typeof v.then=="function"){var FA=v,CA=!1,JA={then:function(ae,At){CA=!0,FA.then(function(ct){Ba(p),an===0?ur(ct,ae,At):ae(ct)},function(ct){Ba(p),At(ct)})}};return!Qn&&typeof Promise<"u"&&Promise.resolve().then(function(){}).then(function(){CA||(Qn=!0,yA("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),JA}else{var oe=v;if(Ba(p),an===0){var Qe=Q.current;Qe!==null&&(cr(Qe),Q.current=null);var Ie={then:function(ae,At){Q.current===null?(Q.current=[],ur(oe,ae,At)):ae(oe)}};return Ie}else{var Je={then:function(ae,At){ae(oe)}};return Je}}}}function Ba(u){u!==an-1&&yA("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),an=u}function ur(u,p,U){{var v=Q.current;if(v!==null)try{cr(v),nc(function(){v.length===0?(Q.current=null,p(u)):ur(u,p,U)})}catch(X){U(X)}else p(u)}}var sr=!1;function cr(u){if(!sr){sr=!0;var p=0;try{for(;p.")}return p}function Bt(u,p){if(!(
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */return function(){var M=$s(),Q=Symbol.for("react.element"),_=Symbol.for("react.portal"),MA=Symbol.for("react.fragment"),KA=Symbol.for("react.strict_mode"),V=Symbol.for("react.profiler"),s=Symbol.for("react.provider"),De=Symbol.for("react.context"),cA=Symbol.for("react.forward_ref"),dA=Symbol.for("react.suspense"),he=Symbol.for("react.suspense_list"),tA=Symbol.for("react.memo"),qA=Symbol.for("react.lazy"),iA=Symbol.for("react.offscreen"),pA=Symbol.iterator,aA="@@iterator";function Oe(y){if(y===null||typeof y!="object")return null;var W=pA&&y[pA]||y[aA];return typeof W=="function"?W:null}var fe=M.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;function jA(y){{for(var W=arguments.length,H=new Array(W>1?W-1:0),mA=1;mA1?W-1:0),mA=1;mA=1&&q>=0&&C[R]!==S[q];)q--;for(;R>=1&&q>=0;R--,q--)if(C[R]!==S[q]){if(R!==1||q!==1)do if(R--,q--,q<0||C[R]!==S[q]){var w=`
-`+C[R].replace(" at new "," at ");return y.displayName&&w.includes("")&&(w=w.replace("",y.displayName)),typeof y=="function"&&it.set(y,w),w}while(R>=1&&q>=0);break}}}finally{ye=!1,Xe.current=d,me(),Error.prepareStackTrace=ie}var G=y?y.displayName||y.name:"",Y=G?Ft(G):"";return typeof y=="function"&&it.set(y,Y),Y}function at(y,W,H){return ee(y,!1)}function nt(y){var W=y.prototype;return!!(W&&W.isReactComponent)}function rt(y,W,H){if(y==null)return"";if(typeof y=="function")return ee(y,nt(y));if(typeof y=="string")return Ft(y);switch(y){case dA:return Ft("Suspense");case he:return Ft("SuspenseList")}if(typeof y=="object")switch(y.$$typeof){case cA:return at(y.render);case tA:return rt(y.type,W,H);case qA:{var mA=y,ie=mA._payload,d=mA._init;try{return rt(d(ie),W,H)}catch{}}}return""}var et=Object.prototype.hasOwnProperty,hi={},B=fe.ReactDebugCurrentFrame;function z(y){if(y){var W=y._owner,H=rt(y.type,y._source,W?W.type:null);B.setExtraStackFrame(H)}else B.setExtraStackFrame(null)}function lA(y,W,H,mA,ie){{var d=Function.call.bind(et);for(var x in y)if(d(y,x)){var C=void 0;try{if(typeof y[x]!="function"){var S=Error((mA||"React class")+": "+H+" type `"+x+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof y[x]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw S.name="Invariant Violation",S}C=y[x](W,x,mA,H,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(R){C=R}C&&!(C instanceof Error)&&(z(ie),jA("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",mA||"React class",H,x,typeof C),z(null)),C instanceof Error&&!(C.message in hi)&&(hi[C.message]=!0,z(ie),jA("Failed %s type: %s",H,C.message),z(null))}}}var AA=Array.isArray;function RA(y){return AA(y)}function ZA(y){{var W=typeof Symbol=="function"&&Symbol.toStringTag,H=W&&y[Symbol.toStringTag]||y.constructor.name||"Object";return H}}function re(y){try{return UA(y),!1}catch{return!0}}function UA(y){return""+y}function GA(y){if(re(y))return jA("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",ZA(y)),UA(y)}var le=fe.ReactCurrentOwner,He={key:!0,ref:!0,__self:!0,__source:!0},pt,J;function gA(y){if(et.call(y,"ref")){var W=Object.getOwnPropertyDescriptor(y,"ref").get;if(W&&W.isReactWarning)return!1}return y.ref!==void 0}function YA(y){if(et.call(y,"key")){var W=Object.getOwnPropertyDescriptor(y,"key").get;if(W&&W.isReactWarning)return!1}return y.key!==void 0}function ge(y,W){typeof y.ref=="string"&&le.current}function Me(y,W){{var H=function(){pt||(pt=!0,jA("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",W))};H.isReactWarning=!0,Object.defineProperty(y,"key",{get:H,configurable:!0})}}function Ze(y,W){{var H=function(){J||(J=!0,jA("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",W))};H.isReactWarning=!0,Object.defineProperty(y,"ref",{get:H,configurable:!0})}}var Ye=function(y,W,H,mA,ie,d,x){var C={$$typeof:Q,type:y,key:W,ref:H,props:x,_owner:d};return C._store={},Object.defineProperty(C._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(C,"_self",{configurable:!1,enumerable:!1,writable:!1,value:mA}),Object.defineProperty(C,"_source",{configurable:!1,enumerable:!1,writable:!1,value:ie}),Object.freeze&&(Object.freeze(C.props),Object.freeze(C)),C};function Kt(y,W,H,mA,ie){{var d,x={},C=null,S=null;H!==void 0&&(GA(H),C=""+H),YA(W)&&(GA(W.key),C=""+W.key),gA(W)&&(S=W.ref,ge(W,ie));for(d in W)et.call(W,d)&&!He.hasOwnProperty(d)&&(x[d]=W[d]);if(y&&y.defaultProps){var R=y.defaultProps;for(d in R)x[d]===void 0&&(x[d]=R[d])}if(C||S){var q=typeof y=="function"?y.displayName||y.name||"Unknown":y;C&&Me(x,q),S&&Ze(x,q)}return Ye(y,C,S,ie,mA,le.current,x)}}var Ge=fe.ReactCurrentOwner,je=fe.ReactDebugCurrentFrame;function Ne(y){if(y){var W=y._owner,H=rt(y.type,y._source,W?W.type:null);je.setExtraStackFrame(H)}else je.setExtraStackFrame(null)}var Zi;Zi=!1;function wi(y){return typeof y=="object"&&y!==null&&y.$$typeof===Q}function Si(){{if(Ge.current){var y=O(Ge.current.type);if(y)return`
+`+C[R].replace(" at new "," at ");return y.displayName&&w.includes("")&&(w=w.replace("",y.displayName)),typeof y=="function"&&it.set(y,w),w}while(R>=1&&q>=0);break}}}finally{ye=!1,Xe.current=d,me(),Error.prepareStackTrace=ie}var G=y?y.displayName||y.name:"",Y=G?Ft(G):"";return typeof y=="function"&&it.set(y,Y),Y}function at(y,W,H){return ee(y,!1)}function nt(y){var W=y.prototype;return!!(W&&W.isReactComponent)}function rt(y,W,H){if(y==null)return"";if(typeof y=="function")return ee(y,nt(y));if(typeof y=="string")return Ft(y);switch(y){case dA:return Ft("Suspense");case he:return Ft("SuspenseList")}if(typeof y=="object")switch(y.$$typeof){case cA:return at(y.render);case tA:return rt(y.type,W,H);case qA:{var mA=y,ie=mA._payload,d=mA._init;try{return rt(d(ie),W,H)}catch{}}}return""}var et=Object.prototype.hasOwnProperty,hi={},M=fe.ReactDebugCurrentFrame;function z(y){if(y){var W=y._owner,H=rt(y.type,y._source,W?W.type:null);M.setExtraStackFrame(H)}else M.setExtraStackFrame(null)}function lA(y,W,H,mA,ie){{var d=Function.call.bind(et);for(var x in y)if(d(y,x)){var C=void 0;try{if(typeof y[x]!="function"){var S=Error((mA||"React class")+": "+H+" type `"+x+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof y[x]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw S.name="Invariant Violation",S}C=y[x](W,x,mA,H,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(R){C=R}C&&!(C instanceof Error)&&(z(ie),jA("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",mA||"React class",H,x,typeof C),z(null)),C instanceof Error&&!(C.message in hi)&&(hi[C.message]=!0,z(ie),jA("Failed %s type: %s",H,C.message),z(null))}}}var AA=Array.isArray;function RA(y){return AA(y)}function ZA(y){{var W=typeof Symbol=="function"&&Symbol.toStringTag,H=W&&y[Symbol.toStringTag]||y.constructor.name||"Object";return H}}function re(y){try{return UA(y),!1}catch{return!0}}function UA(y){return""+y}function GA(y){if(re(y))return jA("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",ZA(y)),UA(y)}var le=fe.ReactCurrentOwner,He={key:!0,ref:!0,__self:!0,__source:!0},pt,J;function gA(y){if(et.call(y,"ref")){var W=Object.getOwnPropertyDescriptor(y,"ref").get;if(W&&W.isReactWarning)return!1}return y.ref!==void 0}function YA(y){if(et.call(y,"key")){var W=Object.getOwnPropertyDescriptor(y,"key").get;if(W&&W.isReactWarning)return!1}return y.key!==void 0}function ge(y,W){typeof y.ref=="string"&&le.current}function Me(y,W){{var H=function(){pt||(pt=!0,jA("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",W))};H.isReactWarning=!0,Object.defineProperty(y,"key",{get:H,configurable:!0})}}function Ze(y,W){{var H=function(){J||(J=!0,jA("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",W))};H.isReactWarning=!0,Object.defineProperty(y,"ref",{get:H,configurable:!0})}}var Ye=function(y,W,H,mA,ie,d,x){var C={$$typeof:b,type:y,key:W,ref:H,props:x,_owner:d};return C._store={},Object.defineProperty(C._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(C,"_self",{configurable:!1,enumerable:!1,writable:!1,value:mA}),Object.defineProperty(C,"_source",{configurable:!1,enumerable:!1,writable:!1,value:ie}),Object.freeze&&(Object.freeze(C.props),Object.freeze(C)),C};function Kt(y,W,H,mA,ie){{var d,x={},C=null,S=null;H!==void 0&&(GA(H),C=""+H),YA(W)&&(GA(W.key),C=""+W.key),gA(W)&&(S=W.ref,ge(W,ie));for(d in W)et.call(W,d)&&!He.hasOwnProperty(d)&&(x[d]=W[d]);if(y&&y.defaultProps){var R=y.defaultProps;for(d in R)x[d]===void 0&&(x[d]=R[d])}if(C||S){var q=typeof y=="function"?y.displayName||y.name||"Unknown":y;C&&Me(x,q),S&&Ze(x,q)}return Ye(y,C,S,ie,mA,le.current,x)}}var Ge=fe.ReactCurrentOwner,je=fe.ReactDebugCurrentFrame;function Ne(y){if(y){var W=y._owner,H=rt(y.type,y._source,W?W.type:null);je.setExtraStackFrame(H)}else je.setExtraStackFrame(null)}var Zi;Zi=!1;function wi(y){return typeof y=="object"&&y!==null&&y.$$typeof===b}function Si(){{if(Ge.current){var y=O(Ge.current.type);if(y)return`
Check the render method of \``+y+"`."}return""}}function na(y){return""}var Mi={};function _a(y){{var W=Si();if(!W){var H=typeof y=="string"?y:y.displayName||y.name;H&&(W=`
-Check the top-level render call using <`+H+">.")}return W}}function Ea(y,W){{if(!y._store||y._store.validated||y.key!=null)return;y._store.validated=!0;var H=_a(W);if(Mi[H])return;Mi[H]=!0;var mA="";y&&y._owner&&y._owner!==Ge.current&&(mA=" It was passed a child from "+O(y._owner.type)+"."),Ne(y),jA('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',H,mA),Ne(null)}}function $a(y,W){{if(typeof y!="object")return;if(RA(y))for(var H=0;H ",C=" Did you accidentally export a JSX literal instead of a component?"):R=typeof y,jA("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",R,C)}var q=Kt(y,W,H,ie,d);if(q==null)return q;if(x){var w=W.children;if(w!==void 0)if(mA)if(RA(w)){for(var G=0;G0?"{key: someKey, "+wA.join(": ..., ")+": ...}":"{key: someKey}";if(!gt[Y+xA]){var vA=wA.length>0?"{"+wA.join(": ..., ")+": ...}":"{}";jA(`A props object containing a "key" prop is being spread into JSX:
+Check the top-level render call using <`+H+">.")}return W}}function Ea(y,W){{if(!y._store||y._store.validated||y.key!=null)return;y._store.validated=!0;var H=_a(W);if(Mi[H])return;Mi[H]=!0;var mA="";y&&y._owner&&y._owner!==Ge.current&&(mA=" It was passed a child from "+O(y._owner.type)+"."),Ne(y),jA('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',H,mA),Ne(null)}}function $a(y,W){{if(typeof y!="object")return;if(RA(y))for(var H=0;H ",C=" Did you accidentally export a JSX literal instead of a component?"):R=typeof y,jA("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",R,C)}var q=Kt(y,W,H,ie,d);if(q==null)return q;if(x){var w=W.children;if(w!==void 0)if(mA)if(RA(w)){for(var G=0;G0?"{key: someKey, "+wA.join(": ..., ")+": ...}":"{key: someKey}";if(!gt[Y+xA]){var vA=wA.length>0?"{"+wA.join(": ..., ")+": ...}":"{}";jA(`A props object containing a "key" prop is being spread into JSX:
let props = %s;
<%s {...props} />
React keys must be passed directly to JSX without using spread:
let props = %s;
- <%s key={someKey} {...props} />`,xA,Y,vA,Y),gt[Y+xA]=!0}}return y===MA?yt(q):An(q),q}}function Ai(y,W,H){return $t(y,W,H,!0)}function Ta(y,W,H){return $t(y,W,H,!1)}var Ra=Ta,La=Ai;ll.Fragment=MA,ll.jsx=Ra,ll.jsxs=La}(),ll}up.exports=Kw();var m=up.exports,mp={exports:{}},Ac={exports:{}},ec={},dp;function qw(){return dp||(dp=1,function(M){/**
+ <%s key={someKey} {...props} />`,xA,Y,vA,Y),gt[Y+xA]=!0}}return y===MA?yt(q):An(q),q}}function Ai(y,W,H){return $t(y,W,H,!0)}function Ta(y,W,H){return $t(y,W,H,!1)}var Ra=Ta,La=Ai;ll.Fragment=MA,ll.jsx=Ra,ll.jsxs=La}(),ll}up.exports=Kw();var m=up.exports,mp={exports:{}},Ac={exports:{}},ec={},dp;function qw(){return dp||(dp=1,function(B){/**
* @license React
* scheduler.development.js
*
@@ -57,7 +57,7 @@ React keys must be passed directly to JSX without using spread:
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var Q=!1,_=5;function MA(J,gA){var YA=J.length;J.push(gA),s(J,gA,YA)}function KA(J){return J.length===0?null:J[0]}function V(J){if(J.length===0)return null;var gA=J[0],YA=J.pop();return YA!==gA&&(J[0]=YA,De(J,YA,0)),gA}function s(J,gA,YA){for(var ge=YA;ge>0;){var Me=ge-1>>>1,Ze=J[Me];if(cA(Ze,gA)>0)J[Me]=gA,J[ge]=Ze,ge=Me;else return}}function De(J,gA,YA){for(var ge=YA,Me=J.length,Ze=Me>>>1;geYA&&(!J||B()));){var ge=TA.callback;if(typeof ge=="function"){TA.callback=null,O=TA.priorityLevel;var Me=TA.expirationTime<=YA,Ze=ge(Me);YA=M.unstable_now(),typeof Ze=="function"?TA.callback=Ze:TA===KA(HA)&&V(HA),qe(YA)}else V(HA);TA=KA(HA)}if(TA!==null)return!0;var Ye=KA(Ue);return Ye!==null&&GA(BA,Ye.startTime-YA),!1}function XA(J,gA){switch(J){case dA:case he:case tA:case qA:case iA:break;default:J=tA}var YA=O;O=J;try{return gA()}finally{O=YA}}function me(J){var gA;switch(O){case dA:case he:case tA:gA=tA;break;default:gA=O;break}var YA=O;O=gA;try{return J()}finally{O=YA}}function Xe(J){var gA=O;return function(){var YA=O;O=gA;try{return J.apply(this,arguments)}finally{O=YA}}}function Se(J,gA,YA){var ge=M.unstable_now(),Me;if(typeof YA=="object"&&YA!==null){var Ze=YA.delay;typeof Ze=="number"&&Ze>0?Me=ge+Ze:Me=ge}else Me=ge;var Ye;switch(J){case dA:Ye=Ae;break;case he:Ye=b;break;case iA:Ye=Ke;break;case qA:Ye=I;break;case tA:default:Ye=PA;break}var Kt=Me+Ye,Ge={id:We++,callback:gA,priorityLevel:J,startTime:Me,expirationTime:Kt,sortIndex:-1};return Me>ge?(Ge.sortIndex=Me,MA(Ue,Ge),KA(HA)===null&&Ge===KA(Ue)&&(pe?le():pe=!0,GA(BA,Me-ge))):(Ge.sortIndex=Kt,MA(HA,Ge),!$&&!te&&($=!0,UA(QA))),Ge}function Ft(){}function ye(){!$&&!te&&($=!0,UA(QA))}function it(){return KA(HA)}function Be(J){J.callback=null}function ee(){return O}var at=!1,nt=null,rt=-1,et=_,hi=-1;function B(){var J=M.unstable_now()-hi;return!(J125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported");return}J>0?et=Math.floor(1e3/J):et=_}var AA=function(){if(nt!==null){var J=M.unstable_now();hi=J;var gA=!0,YA=!0;try{YA=nt(gA,J)}finally{YA?RA():(at=!1,nt=null)}}else at=!1},RA;if(typeof _A=="function")RA=function(){_A(AA)};else if(typeof MessageChannel<"u"){var ZA=new MessageChannel,re=ZA.port2;ZA.port1.onmessage=AA,RA=function(){re.postMessage(null)}}else RA=function(){yA(AA,0)};function UA(J){nt=J,at||(at=!0,RA())}function GA(J,gA){rt=yA(function(){J(M.unstable_now())},gA)}function le(){hA(rt),rt=-1}var He=z,pt=null;M.unstable_IdlePriority=iA,M.unstable_ImmediatePriority=dA,M.unstable_LowPriority=qA,M.unstable_NormalPriority=tA,M.unstable_Profiling=pt,M.unstable_UserBlockingPriority=he,M.unstable_cancelCallback=Be,M.unstable_continueExecution=ye,M.unstable_forceFrameRate=lA,M.unstable_getCurrentPriorityLevel=ee,M.unstable_getFirstCallbackNode=it,M.unstable_next=me,M.unstable_pauseExecution=Ft,M.unstable_requestPaint=He,M.unstable_runWithPriority=XA,M.unstable_scheduleCallback=Se,M.unstable_shouldYield=B,M.unstable_wrapCallback=Xe,typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error)})()}(ec)),ec}var pp;function Nw(){return pp||(pp=1,Ac.exports=qw()),Ac.exports}var ci={},yp;function Ww(){if(yp)return ci;yp=1;/**
+ */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var b=!1,_=5;function MA(J,gA){var YA=J.length;J.push(gA),s(J,gA,YA)}function KA(J){return J.length===0?null:J[0]}function V(J){if(J.length===0)return null;var gA=J[0],YA=J.pop();return YA!==gA&&(J[0]=YA,De(J,YA,0)),gA}function s(J,gA,YA){for(var ge=YA;ge>0;){var Me=ge-1>>>1,Ze=J[Me];if(cA(Ze,gA)>0)J[Me]=gA,J[ge]=Ze,ge=Me;else return}}function De(J,gA,YA){for(var ge=YA,Me=J.length,Ze=Me>>>1;geYA&&(!J||M()));){var ge=TA.callback;if(typeof ge=="function"){TA.callback=null,O=TA.priorityLevel;var Me=TA.expirationTime<=YA,Ze=ge(Me);YA=B.unstable_now(),typeof Ze=="function"?TA.callback=Ze:TA===KA(HA)&&V(HA),qe(YA)}else V(HA);TA=KA(HA)}if(TA!==null)return!0;var Ye=KA(Ue);return Ye!==null&&GA(BA,Ye.startTime-YA),!1}function XA(J,gA){switch(J){case dA:case he:case tA:case qA:case iA:break;default:J=tA}var YA=O;O=J;try{return gA()}finally{O=YA}}function me(J){var gA;switch(O){case dA:case he:case tA:gA=tA;break;default:gA=O;break}var YA=O;O=gA;try{return J()}finally{O=YA}}function Xe(J){var gA=O;return function(){var YA=O;O=gA;try{return J.apply(this,arguments)}finally{O=YA}}}function Se(J,gA,YA){var ge=B.unstable_now(),Me;if(typeof YA=="object"&&YA!==null){var Ze=YA.delay;typeof Ze=="number"&&Ze>0?Me=ge+Ze:Me=ge}else Me=ge;var Ye;switch(J){case dA:Ye=Ae;break;case he:Ye=Q;break;case iA:Ye=Ke;break;case qA:Ye=I;break;case tA:default:Ye=PA;break}var Kt=Me+Ye,Ge={id:We++,callback:gA,priorityLevel:J,startTime:Me,expirationTime:Kt,sortIndex:-1};return Me>ge?(Ge.sortIndex=Me,MA(Ue,Ge),KA(HA)===null&&Ge===KA(Ue)&&(pe?le():pe=!0,GA(BA,Me-ge))):(Ge.sortIndex=Kt,MA(HA,Ge),!$&&!te&&($=!0,UA(QA))),Ge}function Ft(){}function ye(){!$&&!te&&($=!0,UA(QA))}function it(){return KA(HA)}function Be(J){J.callback=null}function ee(){return O}var at=!1,nt=null,rt=-1,et=_,hi=-1;function M(){var J=B.unstable_now()-hi;return!(J125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported");return}J>0?et=Math.floor(1e3/J):et=_}var AA=function(){if(nt!==null){var J=B.unstable_now();hi=J;var gA=!0,YA=!0;try{YA=nt(gA,J)}finally{YA?RA():(at=!1,nt=null)}}else at=!1},RA;if(typeof _A=="function")RA=function(){_A(AA)};else if(typeof MessageChannel<"u"){var ZA=new MessageChannel,re=ZA.port2;ZA.port1.onmessage=AA,RA=function(){re.postMessage(null)}}else RA=function(){yA(AA,0)};function UA(J){nt=J,at||(at=!0,RA())}function GA(J,gA){rt=yA(function(){J(B.unstable_now())},gA)}function le(){hA(rt),rt=-1}var He=z,pt=null;B.unstable_IdlePriority=iA,B.unstable_ImmediatePriority=dA,B.unstable_LowPriority=qA,B.unstable_NormalPriority=tA,B.unstable_Profiling=pt,B.unstable_UserBlockingPriority=he,B.unstable_cancelCallback=Be,B.unstable_continueExecution=ye,B.unstable_forceFrameRate=lA,B.unstable_getCurrentPriorityLevel=ee,B.unstable_getFirstCallbackNode=it,B.unstable_next=me,B.unstable_pauseExecution=Ft,B.unstable_requestPaint=He,B.unstable_runWithPriority=XA,B.unstable_scheduleCallback=Se,B.unstable_shouldYield=M,B.unstable_wrapCallback=Xe,typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error)})()}(ec)),ec}var pp;function Nw(){return pp||(pp=1,Ac.exports=qw()),Ac.exports}var ci={},yp;function Ww(){if(yp)return ci;yp=1;/**
* @license React
* react-dom.development.js
*
@@ -65,15 +65,15 @@ React keys must be passed directly to JSX without using spread:
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */return function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var M=$s(),Q=Nw(),_=M.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,MA=!1;function KA(A){MA=A}function V(A){if(!MA){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i1?e-1:0),i=1;i2&&(A[0]==="o"||A[0]==="O")&&(A[1]==="n"||A[1]==="N")}function Ye(A,e,t,i){if(t!==null&&t.type===RA)return!1;switch(typeof e){case"function":case"symbol":return!0;case"boolean":{if(i)return!1;if(t!==null)return!t.acceptsBooleans;var a=A.toLowerCase().slice(0,5);return a!=="data-"&&a!=="aria-"}default:return!1}}function Kt(A,e,t,i){if(e===null||typeof e>"u"||Ye(A,e,t,i))return!0;if(i)return!1;if(t!==null)switch(t.type){case UA:return!e;case GA:return e===!1;case le:return isNaN(e);case He:return isNaN(e)||e<1}return!1}function Ge(A){return Ne.hasOwnProperty(A)?Ne[A]:null}function je(A,e,t,i,a,n,r){this.acceptsBooleans=e===re||e===UA||e===GA,this.attributeName=i,this.attributeNamespace=a,this.mustUseProperty=t,this.propertyName=A,this.type=e,this.sanitizeURL=n,this.removeEmptyString=r}var Ne={},Zi=["children","dangerouslySetInnerHTML","defaultValue","defaultChecked","innerHTML","suppressContentEditableWarning","suppressHydrationWarning","style"];Zi.forEach(function(A){Ne[A]=new je(A,RA,!1,A,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(A){var e=A[0],t=A[1];Ne[e]=new je(e,ZA,!1,t,null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(A){Ne[A]=new je(A,re,!1,A.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(A){Ne[A]=new je(A,re,!1,A,null,!1,!1)}),["allowFullScreen","async","autoFocus","autoPlay","controls","default","defer","disabled","disablePictureInPicture","disableRemotePlayback","formNoValidate","hidden","loop","noModule","noValidate","open","playsInline","readOnly","required","reversed","scoped","seamless","itemScope"].forEach(function(A){Ne[A]=new je(A,UA,!1,A.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(A){Ne[A]=new je(A,UA,!0,A,null,!1,!1)}),["capture","download"].forEach(function(A){Ne[A]=new je(A,GA,!1,A,null,!1,!1)}),["cols","rows","size","span"].forEach(function(A){Ne[A]=new je(A,He,!1,A,null,!1,!1)}),["rowSpan","start"].forEach(function(A){Ne[A]=new je(A,le,!1,A.toLowerCase(),null,!1,!1)});var wi=/[\-\:]([a-z])/g,Si=function(A){return A[1].toUpperCase()};["accent-height","alignment-baseline","arabic-form","baseline-shift","cap-height","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","dominant-baseline","enable-background","fill-opacity","fill-rule","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","horiz-adv-x","horiz-origin-x","image-rendering","letter-spacing","lighting-color","marker-end","marker-mid","marker-start","overline-position","overline-thickness","paint-order","panose-1","pointer-events","rendering-intent","shape-rendering","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-decoration","text-rendering","underline-position","underline-thickness","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","vector-effect","vert-adv-y","vert-origin-x","vert-origin-y","word-spacing","writing-mode","xmlns:xlink","x-height"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,null,!1,!1)}),["xlink:actuate","xlink:arcrole","xlink:role","xlink:show","xlink:title","xlink:type"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(A){Ne[A]=new je(A,ZA,!1,A.toLowerCase(),null,!1,!1)});var na="xlinkHref";Ne[na]=new je("xlinkHref",ZA,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(A){Ne[A]=new je(A,ZA,!1,A.toLowerCase(),null,!0,!0)});var Mi=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i,_a=!1;function Ea(A){!_a&&Mi.test(A)&&(_a=!0,s("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.",JSON.stringify(A)))}function $a(A,e,t,i){if(i.mustUseProperty){var a=i.propertyName;return A[a]}else{et(t,e),i.sanitizeURL&&Ea(""+t);var n=i.attributeName,r=null;if(i.type===GA){if(A.hasAttribute(n)){var l=A.getAttribute(n);return l===""?!0:Kt(e,t,i,!1)?l:l===""+t?t:l}}else if(A.hasAttribute(n)){if(Kt(e,t,i,!1))return A.getAttribute(n);if(i.type===UA)return t;r=A.getAttribute(n)}return Kt(e,t,i,!1)?r===null?t:r:r===""+t?t:r}}function An(A,e,t,i){{if(!Me(e))return;if(!A.hasAttribute(e))return t===void 0?void 0:null;var a=A.getAttribute(e);return et(t,e),a===""+t?t:a}}function yt(A,e,t,i){var a=Ge(e);if(!Ze(e,a,i)){if(Kt(e,t,a,i)&&(t=null),i||a===null){if(Me(e)){var n=e;t===null?A.removeAttribute(n):(et(t,e),A.setAttribute(n,""+t))}return}var r=a.mustUseProperty;if(r){var l=a.propertyName;if(t===null){var o=a.type;A[l]=o===UA?!1:""}else A[l]=t;return}var c=a.attributeName,h=a.attributeNamespace;if(t===null)A.removeAttribute(c);else{var f=a.type,g;f===UA||f===GA&&t===!0?g="":(et(t,c),g=""+t,a.sanitizeURL&&Ea(g.toString())),h?A.setAttributeNS(h,c,g):A.setAttribute(c,g)}}}var gt=Symbol.for("react.element"),$t=Symbol.for("react.portal"),Ai=Symbol.for("react.fragment"),Ta=Symbol.for("react.strict_mode"),Ra=Symbol.for("react.profiler"),La=Symbol.for("react.provider"),y=Symbol.for("react.context"),W=Symbol.for("react.forward_ref"),H=Symbol.for("react.suspense"),mA=Symbol.for("react.suspense_list"),ie=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),x=Symbol.for("react.scope"),C=Symbol.for("react.debug_trace_mode"),S=Symbol.for("react.offscreen"),R=Symbol.for("react.legacy_hidden"),q=Symbol.for("react.cache"),w=Symbol.for("react.tracing_marker"),G=Symbol.iterator,Y="@@iterator";function wA(A){if(A===null||typeof A!="object")return null;var e=G&&A[G]||A[Y];return typeof e=="function"?e:null}var xA=Object.assign,vA=0,de,_e,ft,Ut,Ua,Qi,Dt;function ra(){}ra.__reactDisabledLog=!0;function qt(){{if(vA===0){de=console.log,_e=console.info,ft=console.warn,Ut=console.error,Ua=console.group,Qi=console.groupCollapsed,Dt=console.groupEnd;var A={configurable:!0,enumerable:!0,value:ra,writable:!0};Object.defineProperties(console,{info:A,log:A,warn:A,error:A,group:A,groupCollapsed:A,groupEnd:A})}vA++}}function Ve(){{if(vA--,vA===0){var A={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:xA({},A,{value:de}),info:xA({},A,{value:_e}),warn:xA({},A,{value:ft}),error:xA({},A,{value:Ut}),group:xA({},A,{value:Ua}),groupCollapsed:xA({},A,{value:Qi}),groupEnd:xA({},A,{value:Dt})})}vA<0&&s("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var ei=_.ReactCurrentDispatcher,la;function Gi(A,e,t){{if(la===void 0)try{throw Error()}catch(a){var i=a.stack.trim().match(/\n( *(at )?)/);la=i&&i[1]||""}return`
+ */return function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var B=$s(),b=Nw(),_=B.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,MA=!1;function KA(A){MA=A}function V(A){if(!MA){for(var e=arguments.length,t=new Array(e>1?e-1:0),i=1;i1?e-1:0),i=1;i2&&(A[0]==="o"||A[0]==="O")&&(A[1]==="n"||A[1]==="N")}function Ye(A,e,t,i){if(t!==null&&t.type===RA)return!1;switch(typeof e){case"function":case"symbol":return!0;case"boolean":{if(i)return!1;if(t!==null)return!t.acceptsBooleans;var a=A.toLowerCase().slice(0,5);return a!=="data-"&&a!=="aria-"}default:return!1}}function Kt(A,e,t,i){if(e===null||typeof e>"u"||Ye(A,e,t,i))return!0;if(i)return!1;if(t!==null)switch(t.type){case UA:return!e;case GA:return e===!1;case le:return isNaN(e);case He:return isNaN(e)||e<1}return!1}function Ge(A){return Ne.hasOwnProperty(A)?Ne[A]:null}function je(A,e,t,i,a,n,r){this.acceptsBooleans=e===re||e===UA||e===GA,this.attributeName=i,this.attributeNamespace=a,this.mustUseProperty=t,this.propertyName=A,this.type=e,this.sanitizeURL=n,this.removeEmptyString=r}var Ne={},Zi=["children","dangerouslySetInnerHTML","defaultValue","defaultChecked","innerHTML","suppressContentEditableWarning","suppressHydrationWarning","style"];Zi.forEach(function(A){Ne[A]=new je(A,RA,!1,A,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(A){var e=A[0],t=A[1];Ne[e]=new je(e,ZA,!1,t,null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(A){Ne[A]=new je(A,re,!1,A.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(A){Ne[A]=new je(A,re,!1,A,null,!1,!1)}),["allowFullScreen","async","autoFocus","autoPlay","controls","default","defer","disabled","disablePictureInPicture","disableRemotePlayback","formNoValidate","hidden","loop","noModule","noValidate","open","playsInline","readOnly","required","reversed","scoped","seamless","itemScope"].forEach(function(A){Ne[A]=new je(A,UA,!1,A.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(A){Ne[A]=new je(A,UA,!0,A,null,!1,!1)}),["capture","download"].forEach(function(A){Ne[A]=new je(A,GA,!1,A,null,!1,!1)}),["cols","rows","size","span"].forEach(function(A){Ne[A]=new je(A,He,!1,A,null,!1,!1)}),["rowSpan","start"].forEach(function(A){Ne[A]=new je(A,le,!1,A.toLowerCase(),null,!1,!1)});var wi=/[\-\:]([a-z])/g,Si=function(A){return A[1].toUpperCase()};["accent-height","alignment-baseline","arabic-form","baseline-shift","cap-height","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","dominant-baseline","enable-background","fill-opacity","fill-rule","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","horiz-adv-x","horiz-origin-x","image-rendering","letter-spacing","lighting-color","marker-end","marker-mid","marker-start","overline-position","overline-thickness","paint-order","panose-1","pointer-events","rendering-intent","shape-rendering","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-decoration","text-rendering","underline-position","underline-thickness","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","vector-effect","vert-adv-y","vert-origin-x","vert-origin-y","word-spacing","writing-mode","xmlns:xlink","x-height"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,null,!1,!1)}),["xlink:actuate","xlink:arcrole","xlink:role","xlink:show","xlink:title","xlink:type"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(A){var e=A.replace(wi,Si);Ne[e]=new je(e,ZA,!1,A,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(A){Ne[A]=new je(A,ZA,!1,A.toLowerCase(),null,!1,!1)});var na="xlinkHref";Ne[na]=new je("xlinkHref",ZA,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(A){Ne[A]=new je(A,ZA,!1,A.toLowerCase(),null,!0,!0)});var Mi=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i,_a=!1;function Ea(A){!_a&&Mi.test(A)&&(_a=!0,s("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.",JSON.stringify(A)))}function $a(A,e,t,i){if(i.mustUseProperty){var a=i.propertyName;return A[a]}else{et(t,e),i.sanitizeURL&&Ea(""+t);var n=i.attributeName,r=null;if(i.type===GA){if(A.hasAttribute(n)){var l=A.getAttribute(n);return l===""?!0:Kt(e,t,i,!1)?l:l===""+t?t:l}}else if(A.hasAttribute(n)){if(Kt(e,t,i,!1))return A.getAttribute(n);if(i.type===UA)return t;r=A.getAttribute(n)}return Kt(e,t,i,!1)?r===null?t:r:r===""+t?t:r}}function An(A,e,t,i){{if(!Me(e))return;if(!A.hasAttribute(e))return t===void 0?void 0:null;var a=A.getAttribute(e);return et(t,e),a===""+t?t:a}}function yt(A,e,t,i){var a=Ge(e);if(!Ze(e,a,i)){if(Kt(e,t,a,i)&&(t=null),i||a===null){if(Me(e)){var n=e;t===null?A.removeAttribute(n):(et(t,e),A.setAttribute(n,""+t))}return}var r=a.mustUseProperty;if(r){var l=a.propertyName;if(t===null){var o=a.type;A[l]=o===UA?!1:""}else A[l]=t;return}var c=a.attributeName,h=a.attributeNamespace;if(t===null)A.removeAttribute(c);else{var f=a.type,g;f===UA||f===GA&&t===!0?g="":(et(t,c),g=""+t,a.sanitizeURL&&Ea(g.toString())),h?A.setAttributeNS(h,c,g):A.setAttribute(c,g)}}}var gt=Symbol.for("react.element"),$t=Symbol.for("react.portal"),Ai=Symbol.for("react.fragment"),Ta=Symbol.for("react.strict_mode"),Ra=Symbol.for("react.profiler"),La=Symbol.for("react.provider"),y=Symbol.for("react.context"),W=Symbol.for("react.forward_ref"),H=Symbol.for("react.suspense"),mA=Symbol.for("react.suspense_list"),ie=Symbol.for("react.memo"),d=Symbol.for("react.lazy"),x=Symbol.for("react.scope"),C=Symbol.for("react.debug_trace_mode"),S=Symbol.for("react.offscreen"),R=Symbol.for("react.legacy_hidden"),q=Symbol.for("react.cache"),w=Symbol.for("react.tracing_marker"),G=Symbol.iterator,Y="@@iterator";function wA(A){if(A===null||typeof A!="object")return null;var e=G&&A[G]||A[Y];return typeof e=="function"?e:null}var xA=Object.assign,vA=0,de,_e,ft,Ut,Ua,Qi,Dt;function ra(){}ra.__reactDisabledLog=!0;function qt(){{if(vA===0){de=console.log,_e=console.info,ft=console.warn,Ut=console.error,Ua=console.group,Qi=console.groupCollapsed,Dt=console.groupEnd;var A={configurable:!0,enumerable:!0,value:ra,writable:!0};Object.defineProperties(console,{info:A,log:A,warn:A,error:A,group:A,groupCollapsed:A,groupEnd:A})}vA++}}function Ve(){{if(vA--,vA===0){var A={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:xA({},A,{value:de}),info:xA({},A,{value:_e}),warn:xA({},A,{value:ft}),error:xA({},A,{value:Ut}),group:xA({},A,{value:Ua}),groupCollapsed:xA({},A,{value:Qi}),groupEnd:xA({},A,{value:Dt})})}vA<0&&s("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var ei=_.ReactCurrentDispatcher,la;function Gi(A,e,t){{if(la===void 0)try{throw Error()}catch(a){var i=a.stack.trim().match(/\n( *(at )?)/);la=i&&i[1]||""}return`
`+la+A}}var oa=!1,Jt;{var mi=typeof WeakMap=="function"?WeakMap:Map;Jt=new mi}function ua(A,e){if(!A||oa)return"";{var t=Jt.get(A);if(t!==void 0)return t}var i;oa=!0;var a=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var n;n=ei.current,ei.current=null,qt();try{if(e){var r=function(){throw Error()};if(Object.defineProperty(r.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(r,[])}catch(D){i=D}Reflect.construct(A,[],r)}else{try{r.call()}catch(D){i=D}A.call(r.prototype)}}else{try{throw Error()}catch(D){i=D}A()}}catch(D){if(D&&i&&typeof D.stack=="string"){for(var l=D.stack.split(`
`),o=i.stack.split(`
`),c=l.length-1,h=o.length-1;c>=1&&h>=0&&l[c]!==o[h];)h--;for(;c>=1&&h>=0;c--,h--)if(l[c]!==o[h]){if(c!==1||h!==1)do if(c--,h--,h<0||l[c]!==o[h]){var f=`
-`+l[c].replace(" at new "," at ");return A.displayName&&f.includes("")&&(f=f.replace("",A.displayName)),typeof A=="function"&&Jt.set(A,f),f}while(c>=1&&h>=0);break}}}finally{oa=!1,ei.current=n,Ve(),Error.prepareStackTrace=a}var g=A?A.displayName||A.name:"",L=g?Gi(g):"";return typeof A=="function"&&Jt.set(A,L),L}function en(A,e,t){return ua(A,!0)}function ut(A,e,t){return ua(A,!1)}function Re(A){var e=A.prototype;return!!(e&&e.isReactComponent)}function bi(A,e,t){if(A==null)return"";if(typeof A=="function")return ua(A,Re(A));if(typeof A=="string")return Gi(A);switch(A){case H:return Gi("Suspense");case mA:return Gi("SuspenseList")}if(typeof A=="object")switch(A.$$typeof){case W:return ut(A.render);case ie:return bi(A.type,e,t);case d:{var i=A,a=i._payload,n=i._init;try{return bi(n(a),e,t)}catch{}}}return""}function Vi(A){switch(A._debugOwner&&A._debugOwner.type,A._debugSource,A.tag){case iA:return Gi(A.type);case Ke:return Gi("Lazy");case b:return Gi("Suspense");case We:return Gi("SuspenseList");case cA:case he:case I:return ut(A.type);case LA:return ut(A.type.render);case dA:return en(A.type);default:return""}}function zi(A){try{var e="",t=A;do e+=Vi(t),t=t.return;while(t);return e}catch(i){return`
+`+l[c].replace(" at new "," at ");return A.displayName&&f.includes("")&&(f=f.replace("",A.displayName)),typeof A=="function"&&Jt.set(A,f),f}while(c>=1&&h>=0);break}}}finally{oa=!1,ei.current=n,Ve(),Error.prepareStackTrace=a}var g=A?A.displayName||A.name:"",L=g?Gi(g):"";return typeof A=="function"&&Jt.set(A,L),L}function en(A,e,t){return ua(A,!0)}function ut(A,e,t){return ua(A,!1)}function Re(A){var e=A.prototype;return!!(e&&e.isReactComponent)}function bi(A,e,t){if(A==null)return"";if(typeof A=="function")return ua(A,Re(A));if(typeof A=="string")return Gi(A);switch(A){case H:return Gi("Suspense");case mA:return Gi("SuspenseList")}if(typeof A=="object")switch(A.$$typeof){case W:return ut(A.render);case ie:return bi(A.type,e,t);case d:{var i=A,a=i._payload,n=i._init;try{return bi(n(a),e,t)}catch{}}}return""}function Vi(A){switch(A._debugOwner&&A._debugOwner.type,A._debugSource,A.tag){case iA:return Gi(A.type);case Ke:return Gi("Lazy");case Q:return Gi("Suspense");case We:return Gi("SuspenseList");case cA:case he:case I:return ut(A.type);case LA:return ut(A.type.render);case dA:return en(A.type);default:return""}}function zi(A){try{var e="",t=A;do e+=Vi(t),t=t.return;while(t);return e}catch(i){return`
Error generating stack: `+i.message+`
-`+i.stack}}function ko(A,e,t){var i=A.displayName;if(i)return i;var a=e.displayName||e.name||"";return a!==""?t+"("+a+")":t}function ol(A){return A.displayName||"Context"}function xe(A){if(A==null)return null;if(typeof A.tag=="number"&&s("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),typeof A=="function")return A.displayName||A.name||null;if(typeof A=="string")return A;switch(A){case Ai:return"Fragment";case $t:return"Portal";case Ra:return"Profiler";case Ta:return"StrictMode";case H:return"Suspense";case mA:return"SuspenseList"}if(typeof A=="object")switch(A.$$typeof){case y:var e=A;return ol(e)+".Consumer";case La:var t=A;return ol(t._context)+".Provider";case W:return ko(A,A.render,"ForwardRef");case ie:var i=A.displayName||null;return i!==null?i:xe(A.type)||"Memo";case d:{var a=A,n=a._payload,r=a._init;try{return xe(r(n))}catch{return null}}}return null}function tc(A,e,t){var i=e.displayName||e.name||"";return A.displayName||(i!==""?t+"("+i+")":t)}function Da(A){return A.displayName||"Context"}function OA(A){var e=A.tag,t=A.type;switch(e){case $:return"Cache";case fe:var i=t;return Da(i)+".Consumer";case jA:var a=t;return Da(a._context)+".Provider";case Ue:return"DehydratedFragment";case LA:return tc(t,t.render,"ForwardRef");case aA:return"Fragment";case iA:return t;case qA:return"Portal";case tA:return"Root";case pA:return"Text";case Ke:return xe(t);case Oe:return t===Ta?"StrictMode":"Mode";case O:return"Offscreen";case Ae:return"Profiler";case TA:return"Scope";case b:return"Suspense";case We:return"SuspenseList";case pe:return"TracingMarker";case dA:case cA:case HA:case he:case PA:case I:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t;break}return null}var ul=_.ReactDebugCurrentFrame,ti=null,Mn=!1;function tn(){{if(ti===null)return null;var A=ti._debugOwner;if(A!==null&&typeof A<"u")return OA(A)}return null}function ic(){return ti===null?"":zi(ti)}function Bt(){ul.getCurrentStack=null,ti=null,Mn=!1}function $e(A){ul.getCurrentStack=A===null?null:ic,ti=A,Mn=!1}function Oo(){return ti}function vi(A){Mn=A}function ii(A){return""+A}function Ii(A){switch(typeof A){case"boolean":case"number":case"string":case"undefined":return A;case"object":return AA(A),A;default:return""}}var ac={button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0};function sl(A,e){ac[e.type]||e.onChange||e.onInput||e.readOnly||e.disabled||e.value==null||s("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."),e.onChange||e.readOnly||e.disabled||e.checked==null||s("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.")}function Xo(A){var e=A.type,t=A.nodeName;return t&&t.toLowerCase()==="input"&&(e==="checkbox"||e==="radio")}function cl(A){return A._valueTracker}function or(A){A._valueTracker=null}function nc(A){var e="";return A&&(Xo(A)?e=A.checked?"true":"false":e=A.value),e}function an(A){var e=Xo(A)?"checked":"value",t=Object.getOwnPropertyDescriptor(A.constructor.prototype,e);AA(A[e]);var i=""+A[e];if(!(A.hasOwnProperty(e)||typeof t>"u"||typeof t.get!="function"||typeof t.set!="function")){var a=t.get,n=t.set;Object.defineProperty(A,e,{configurable:!0,get:function(){return a.call(this)},set:function(l){AA(l),i=""+l,n.call(this,l)}}),Object.defineProperty(A,e,{enumerable:t.enumerable});var r={getValue:function(){return i},setValue:function(l){AA(l),i=""+l},stopTracking:function(){or(A),delete A[e]}};return r}}function Qn(A){cl(A)||(A._valueTracker=an(A))}function hl(A){if(!A)return!1;var e=cl(A);if(!e)return!0;var t=e.getValue(),i=nc(A);return i!==t?(e.setValue(i),!0):!1}function Ba(A){if(A=A||(typeof document<"u"?document:void 0),typeof A>"u")return null;try{return A.activeElement||A.body}catch{return A.body}}var ur=!1,sr=!1,cr=!1,Ho=!1;function Po(A){var e=A.type==="checkbox"||A.type==="radio";return e?A.checked!=null:A.value!=null}function ml(A,e){var t=A,i=e.checked,a=xA({},e,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:i??t._wrapperState.initialChecked});return a}function _o(A,e){sl("input",e),e.checked!==void 0&&e.defaultChecked!==void 0&&!sr&&(s("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",tn()||"A component",e.type),sr=!0),e.value!==void 0&&e.defaultValue!==void 0&&!ur&&(s("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",tn()||"A component",e.type),ur=!0);var t=A,i=e.defaultValue==null?"":e.defaultValue;t._wrapperState={initialChecked:e.checked!=null?e.checked:e.defaultChecked,initialValue:Ii(e.value!=null?e.value:i),controlled:Po(e)}}function u(A,e){var t=A,i=e.checked;i!=null&&yt(t,"checked",i,!1)}function p(A,e){var t=A;{var i=Po(e);!t._wrapperState.controlled&&i&&!Ho&&(s("A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),Ho=!0),t._wrapperState.controlled&&!i&&!cr&&(s("A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),cr=!0)}u(A,e);var a=Ii(e.value),n=e.type;if(a!=null)n==="number"?(a===0&&t.value===""||t.value!=a)&&(t.value=ii(a)):t.value!==ii(a)&&(t.value=ii(a));else if(n==="submit"||n==="reset"){t.removeAttribute("value");return}e.hasOwnProperty("value")?FA(t,e.type,a):e.hasOwnProperty("defaultValue")&&FA(t,e.type,Ii(e.defaultValue)),e.checked==null&&e.defaultChecked!=null&&(t.defaultChecked=!!e.defaultChecked)}function U(A,e,t){var i=A;if(e.hasOwnProperty("value")||e.hasOwnProperty("defaultValue")){var a=e.type,n=a==="submit"||a==="reset";if(n&&(e.value===void 0||e.value===null))return;var r=ii(i._wrapperState.initialValue);t||r!==i.value&&(i.value=r),i.defaultValue=r}var l=i.name;l!==""&&(i.name=""),i.defaultChecked=!i.defaultChecked,i.defaultChecked=!!i._wrapperState.initialChecked,l!==""&&(i.name=l)}function v(A,e){var t=A;p(t,e),X(t,e)}function X(A,e){var t=e.name;if(e.type==="radio"&&t!=null){for(var i=A;i.parentNode;)i=i.parentNode;et(t,"name");for(var a=i.querySelectorAll("input[name="+JSON.stringify(""+t)+'][type="radio"]'),n=0;n.")))}):e.dangerouslySetInnerHTML!=null&&(oe||(oe=!0,s("Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.")))),e.selected!=null&&!CA&&(s("Use the `defaultValue` or `value` props on instead of setting `selected` on ."),CA=!0)}function Ie(A,e){e.value!=null&&A.setAttribute("value",ii(Ii(e.value)))}var Je=Array.isArray;function ae(A){return Je(A)}var At;At=!1;function ct(){var A=tn();return A?`
+`+i.stack}}function ko(A,e,t){var i=A.displayName;if(i)return i;var a=e.displayName||e.name||"";return a!==""?t+"("+a+")":t}function ol(A){return A.displayName||"Context"}function xe(A){if(A==null)return null;if(typeof A.tag=="number"&&s("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),typeof A=="function")return A.displayName||A.name||null;if(typeof A=="string")return A;switch(A){case Ai:return"Fragment";case $t:return"Portal";case Ra:return"Profiler";case Ta:return"StrictMode";case H:return"Suspense";case mA:return"SuspenseList"}if(typeof A=="object")switch(A.$$typeof){case y:var e=A;return ol(e)+".Consumer";case La:var t=A;return ol(t._context)+".Provider";case W:return ko(A,A.render,"ForwardRef");case ie:var i=A.displayName||null;return i!==null?i:xe(A.type)||"Memo";case d:{var a=A,n=a._payload,r=a._init;try{return xe(r(n))}catch{return null}}}return null}function tc(A,e,t){var i=e.displayName||e.name||"";return A.displayName||(i!==""?t+"("+i+")":t)}function Da(A){return A.displayName||"Context"}function OA(A){var e=A.tag,t=A.type;switch(e){case $:return"Cache";case fe:var i=t;return Da(i)+".Consumer";case jA:var a=t;return Da(a._context)+".Provider";case Ue:return"DehydratedFragment";case LA:return tc(t,t.render,"ForwardRef");case aA:return"Fragment";case iA:return t;case qA:return"Portal";case tA:return"Root";case pA:return"Text";case Ke:return xe(t);case Oe:return t===Ta?"StrictMode":"Mode";case O:return"Offscreen";case Ae:return"Profiler";case TA:return"Scope";case Q:return"Suspense";case We:return"SuspenseList";case pe:return"TracingMarker";case dA:case cA:case HA:case he:case PA:case I:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t;break}return null}var ul=_.ReactDebugCurrentFrame,ti=null,Mn=!1;function tn(){{if(ti===null)return null;var A=ti._debugOwner;if(A!==null&&typeof A<"u")return OA(A)}return null}function ic(){return ti===null?"":zi(ti)}function Bt(){ul.getCurrentStack=null,ti=null,Mn=!1}function $e(A){ul.getCurrentStack=A===null?null:ic,ti=A,Mn=!1}function Oo(){return ti}function vi(A){Mn=A}function ii(A){return""+A}function Ii(A){switch(typeof A){case"boolean":case"number":case"string":case"undefined":return A;case"object":return AA(A),A;default:return""}}var ac={button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0};function sl(A,e){ac[e.type]||e.onChange||e.onInput||e.readOnly||e.disabled||e.value==null||s("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."),e.onChange||e.readOnly||e.disabled||e.checked==null||s("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.")}function Xo(A){var e=A.type,t=A.nodeName;return t&&t.toLowerCase()==="input"&&(e==="checkbox"||e==="radio")}function cl(A){return A._valueTracker}function or(A){A._valueTracker=null}function nc(A){var e="";return A&&(Xo(A)?e=A.checked?"true":"false":e=A.value),e}function an(A){var e=Xo(A)?"checked":"value",t=Object.getOwnPropertyDescriptor(A.constructor.prototype,e);AA(A[e]);var i=""+A[e];if(!(A.hasOwnProperty(e)||typeof t>"u"||typeof t.get!="function"||typeof t.set!="function")){var a=t.get,n=t.set;Object.defineProperty(A,e,{configurable:!0,get:function(){return a.call(this)},set:function(l){AA(l),i=""+l,n.call(this,l)}}),Object.defineProperty(A,e,{enumerable:t.enumerable});var r={getValue:function(){return i},setValue:function(l){AA(l),i=""+l},stopTracking:function(){or(A),delete A[e]}};return r}}function Qn(A){cl(A)||(A._valueTracker=an(A))}function hl(A){if(!A)return!1;var e=cl(A);if(!e)return!0;var t=e.getValue(),i=nc(A);return i!==t?(e.setValue(i),!0):!1}function Ba(A){if(A=A||(typeof document<"u"?document:void 0),typeof A>"u")return null;try{return A.activeElement||A.body}catch{return A.body}}var ur=!1,sr=!1,cr=!1,Ho=!1;function Po(A){var e=A.type==="checkbox"||A.type==="radio";return e?A.checked!=null:A.value!=null}function ml(A,e){var t=A,i=e.checked,a=xA({},e,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:i??t._wrapperState.initialChecked});return a}function _o(A,e){sl("input",e),e.checked!==void 0&&e.defaultChecked!==void 0&&!sr&&(s("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",tn()||"A component",e.type),sr=!0),e.value!==void 0&&e.defaultValue!==void 0&&!ur&&(s("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",tn()||"A component",e.type),ur=!0);var t=A,i=e.defaultValue==null?"":e.defaultValue;t._wrapperState={initialChecked:e.checked!=null?e.checked:e.defaultChecked,initialValue:Ii(e.value!=null?e.value:i),controlled:Po(e)}}function u(A,e){var t=A,i=e.checked;i!=null&&yt(t,"checked",i,!1)}function p(A,e){var t=A;{var i=Po(e);!t._wrapperState.controlled&&i&&!Ho&&(s("A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),Ho=!0),t._wrapperState.controlled&&!i&&!cr&&(s("A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),cr=!0)}u(A,e);var a=Ii(e.value),n=e.type;if(a!=null)n==="number"?(a===0&&t.value===""||t.value!=a)&&(t.value=ii(a)):t.value!==ii(a)&&(t.value=ii(a));else if(n==="submit"||n==="reset"){t.removeAttribute("value");return}e.hasOwnProperty("value")?FA(t,e.type,a):e.hasOwnProperty("defaultValue")&&FA(t,e.type,Ii(e.defaultValue)),e.checked==null&&e.defaultChecked!=null&&(t.defaultChecked=!!e.defaultChecked)}function U(A,e,t){var i=A;if(e.hasOwnProperty("value")||e.hasOwnProperty("defaultValue")){var a=e.type,n=a==="submit"||a==="reset";if(n&&(e.value===void 0||e.value===null))return;var r=ii(i._wrapperState.initialValue);t||r!==i.value&&(i.value=r),i.defaultValue=r}var l=i.name;l!==""&&(i.name=""),i.defaultChecked=!i.defaultChecked,i.defaultChecked=!!i._wrapperState.initialChecked,l!==""&&(i.name=l)}function v(A,e){var t=A;p(t,e),X(t,e)}function X(A,e){var t=e.name;if(e.type==="radio"&&t!=null){for(var i=A;i.parentNode;)i=i.parentNode;et(t,"name");for(var a=i.querySelectorAll("input[name="+JSON.stringify(""+t)+'][type="radio"]'),n=0;n.")))}):e.dangerouslySetInnerHTML!=null&&(oe||(oe=!0,s("Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.")))),e.selected!=null&&!CA&&(s("Use the `defaultValue` or `value` props on instead of setting `selected` on ."),CA=!0)}function Ie(A,e){e.value!=null&&A.setAttribute("value",ii(Ii(e.value)))}var Je=Array.isArray;function ae(A){return Je(A)}var At;At=!1;function ct(){var A=tn();return A?`
-Check the render method of \``+A+"`.":""}var bn=["value","defaultValue"];function dl(A){{sl("select",A);for(var e=0;e must be an array if `multiple` is true.%s",t,ct()):!A.multiple&&i&&s("The `%s` prop supplied to must be a scalar value if `multiple` is false.%s",t,ct())}}}}function Ma(A,e,t,i){var a=A.options;if(e){for(var n=t,r={},l=0;l.");var i=xA({},e,{value:void 0,defaultValue:void 0,children:ii(t._wrapperState.initialValue)});return i}function Lp(A,e){var t=A;sl("textarea",e),e.value!==void 0&&e.defaultValue!==void 0&&!Rp&&(s("%s contains a textarea with both value and defaultValue props. Textarea elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled textarea and remove one of these props. More info: https://reactjs.org/link/controlled-components",tn()||"A component"),Rp=!0);var i=e.value;if(i==null){var a=e.children,n=e.defaultValue;if(a!=null){s("Use the `defaultValue` or `value` props instead of setting children on