diff --git a/frontend-tools/video-js/src/VideoJS.jsx b/frontend-tools/video-js/src/VideoJS.jsx
index 97814232..8f462ea3 100644
--- a/frontend-tools/video-js/src/VideoJS.jsx
+++ b/frontend-tools/video-js/src/VideoJS.jsx
@@ -1,1558 +1,8 @@
-import React, { useEffect, useRef, useState, useMemo } from 'react';
-import videojs from 'video.js';
-
-// import './assets/css/mediacms-player.css';
-// import './assets/css/VideoPlayer.scss';
-import 'video.js/dist/video-js.css';
-
-// Define EndScreenOverlay outside the component to avoid re-definition
-const Component = videojs.getComponent('Component');
-
-class EndScreenOverlay extends Component {
- constructor(player, options) {
- // Store relatedVideos in options before calling super
- // so it's available during createEl()
- if (options && options.relatedVideos) {
- options._relatedVideos = options.relatedVideos;
- }
-
- super(player, options);
-
- // Now set the instance property after super() completes
- this.relatedVideos =
- options && options.relatedVideos ? options.relatedVideos : [];
-
- // console.log(
- // 'EndScreenOverlay created with',
- // this.relatedVideos.length,
- // 'related videos'
- // );
- }
-
- createEl() {
- // Get relatedVideos from options since createEl is called during super()
- const relatedVideos =
- this.options_ && this.options_._relatedVideos
- ? this.options_._relatedVideos
- : [];
-
- // console.log(
- // 'Creating end screen with',
- // relatedVideos.length,
- // 'related videos'
- // );
-
- const overlay = super.createEl('div', {
- className: 'vjs-end-screen-overlay',
- });
-
- // Create grid container
- const grid = videojs.dom.createEl('div', {
- className: 'vjs-related-videos-grid',
- });
-
- // Create video items
- if (
- relatedVideos &&
- Array.isArray(relatedVideos) &&
- relatedVideos.length > 0
- ) {
- relatedVideos.forEach((video) => {
- const videoItem = this.createVideoItem(video);
- grid.appendChild(videoItem);
- });
- } else {
- // Fallback message if no related videos
- const noVideos = videojs.dom.createEl('div', {
- className: 'vjs-no-related-videos',
- });
- noVideos.textContent = 'No related videos available';
- noVideos.style.color = 'white';
- noVideos.style.textAlign = 'center';
- grid.appendChild(noVideos);
- }
-
- overlay.appendChild(grid);
-
- return overlay;
- }
-
- createVideoItem(video) {
- const item = videojs.dom.createEl('div', {
- className: 'vjs-related-video-item',
- });
-
- const thumbnail = videojs.dom.createEl('img', {
- className: 'vjs-related-video-thumbnail',
- src: video.thumbnail,
- alt: video.title,
- });
-
- const overlay = videojs.dom.createEl('div', {
- className: 'vjs-related-video-overlay',
- });
-
- const title = videojs.dom.createEl('div', {
- className: 'vjs-related-video-title',
- });
- title.textContent = video.title;
-
- const author = videojs.dom.createEl('div', {
- className: 'vjs-related-video-author',
- });
- author.textContent = video.author;
-
- const views = videojs.dom.createEl('div', {
- className: 'vjs-related-video-views',
- });
- views.textContent = video.views;
-
- overlay.appendChild(title);
- overlay.appendChild(author);
- overlay.appendChild(views);
-
- item.appendChild(thumbnail);
- item.appendChild(overlay);
-
- // Add click handler
- item.addEventListener('click', () => {
- window.location.href = `/view?m=${video.id}`;
- });
-
- return item;
- }
-
- show() {
- this.el().style.display = 'flex';
- }
-
- hide() {
- this.el().style.display = 'none';
- }
-}
-
-// Register the component once
-videojs.registerComponent('EndScreenOverlay', EndScreenOverlay);
-
-// Enhanced Chapter Markers Component with continuous chapter display
-class ChapterMarkers extends Component {
- constructor(player, options) {
- super(player, options);
- this.on(player, 'loadedmetadata', this.updateChapterMarkers);
- this.on(player, 'texttrackchange', this.updateChapterMarkers);
- this.chaptersData = [];
- this.tooltip = null;
- this.isHovering = false;
- }
-
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-chapter-markers-track',
- });
-
- // Initialize tooltip as null - will be created when needed
- this.tooltip = null;
-
- return el;
- }
-
- updateChapterMarkers() {
- const player = this.player();
- const textTracks = player.textTracks();
- let chaptersTrack = null;
-
- // Find the chapters track
- for (let i = 0; i < textTracks.length; i++) {
- if (textTracks[i].kind === 'chapters') {
- chaptersTrack = textTracks[i];
- break;
- }
- }
-
- if (!chaptersTrack || !chaptersTrack.cues) {
- return;
- }
-
- // Store chapters data for tooltip lookup
- this.chaptersData = [];
- for (let i = 0; i < chaptersTrack.cues.length; i++) {
- const cue = chaptersTrack.cues[i];
- this.chaptersData.push({
- startTime: cue.startTime,
- endTime: cue.endTime,
- text: cue.text,
- });
- }
-
- // Clear existing markers
- this.el().innerHTML = '';
-
- const duration = player.duration();
- if (!duration || duration === Infinity) {
- return;
- }
-
- // Create markers for each chapter
- for (let i = 0; i < chaptersTrack.cues.length; i++) {
- const cue = chaptersTrack.cues[i];
- const marker = this.createMarker(cue, duration);
- this.el().appendChild(marker);
- }
-
- // Setup progress bar hover for continuous chapter display
- this.setupProgressBarHover();
- }
-
- setupProgressBarHover() {
- const progressControl = this.player()
- .getChild('controlBar')
- .getChild('progressControl');
- if (!progressControl) return;
-
- const seekBar = progressControl.getChild('seekBar');
- if (!seekBar) return;
-
- const seekBarEl = seekBar.el();
-
- // Ensure tooltip is properly created and add to seekBar if not already added
- if (!this.tooltip || !this.tooltip.nodeType) {
- // Recreate tooltip if it's not a proper DOM node
- this.tooltip = videojs.dom.createEl('div', {
- className: 'vjs-chapter-floating-tooltip',
- });
-
- // Style the floating tooltip
- Object.assign(this.tooltip.style, {
- position: 'absolute',
- background: 'rgba(0, 0, 0, 0.9)',
- color: 'white',
- padding: '8px 12px',
- borderRadius: '6px',
- fontSize: '12px',
- whiteSpace: 'nowrap',
- pointerEvents: 'none',
- zIndex: '1000',
- bottom: '45px',
- transform: 'translateX(-50%)',
- display: 'none',
- maxWidth: '250px',
- textAlign: 'center',
- border: '1px solid rgba(255, 255, 255, 0.2)',
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
- });
- }
-
- // Add tooltip to seekBar if not already added
- if (!seekBarEl.querySelector('.vjs-chapter-floating-tooltip')) {
- try {
- seekBarEl.appendChild(this.tooltip);
- } catch {
- // console.warn('Could not append chapter tooltip:', error);
- return;
- }
- }
-
- // Get the progress control element for larger hover area
- const progressControlEl = progressControl.el();
-
- // Remove existing listeners to prevent duplicates
- progressControlEl.removeEventListener(
- 'mouseenter',
- this.handleMouseEnter
- );
- progressControlEl.removeEventListener(
- 'mouseleave',
- this.handleMouseLeave
- );
- progressControlEl.removeEventListener(
- 'mousemove',
- this.handleMouseMove
- );
-
- // Bind methods to preserve context
- this.handleMouseEnter = () => {
- this.isHovering = true;
- this.tooltip.style.display = 'block';
- };
-
- this.handleMouseLeave = () => {
- this.isHovering = false;
- this.tooltip.style.display = 'none';
- };
-
- this.handleMouseMove = (e) => {
- if (!this.isHovering) return;
- this.updateChapterTooltip(e, seekBarEl, progressControlEl);
- };
-
- // Add event listeners to the entire progress control area (includes gray area above)
- progressControlEl.addEventListener('mouseenter', this.handleMouseEnter);
- progressControlEl.addEventListener('mouseleave', this.handleMouseLeave);
- progressControlEl.addEventListener('mousemove', this.handleMouseMove);
- }
-
- updateChapterTooltip(event, seekBarEl, progressControlEl) {
- if (!this.tooltip || !this.isHovering) return;
-
- const duration = this.player().duration();
- if (!duration) return;
-
- // Calculate time position based on mouse position relative to seekBar
- const seekBarRect = seekBarEl.getBoundingClientRect();
- const progressControlRect = progressControlEl.getBoundingClientRect();
-
- // Use seekBar for horizontal calculation but allow vertical tolerance
- const offsetX = event.clientX - seekBarRect.left;
- const percentage = Math.max(
- 0,
- Math.min(1, offsetX / seekBarRect.width)
- );
- const currentTime = percentage * duration;
-
- // Position tooltip relative to progress control area
- const tooltipOffsetX = event.clientX - progressControlRect.left;
-
- // Find current chapter
- const currentChapter = this.findChapterAtTime(currentTime);
-
- if (currentChapter) {
- // Format time for display
- const formatTime = (seconds) => {
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins}:${secs.toString().padStart(2, '0')}`;
- };
-
- const startTime = formatTime(currentChapter.startTime);
- const endTime = formatTime(currentChapter.endTime);
- const timeAtPosition = formatTime(currentTime);
-
- this.tooltip.innerHTML = `
-
${currentChapter.text}
- Chapter: ${startTime} - ${endTime}
- Position: ${timeAtPosition}
- `;
- } else {
- const timeAtPosition = this.formatTime(currentTime);
- this.tooltip.innerHTML = `
- No Chapter
- Position: ${timeAtPosition}
- `;
- }
-
- // Position tooltip relative to progress control container
- this.tooltip.style.left = `${tooltipOffsetX}px`;
- this.tooltip.style.display = 'block';
- }
-
- findChapterAtTime(time) {
- for (const chapter of this.chaptersData) {
- if (time >= chapter.startTime && time < chapter.endTime) {
- return chapter;
- }
- }
- return null;
- }
-
- formatTime(seconds) {
- const mins = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${mins}:${secs.toString().padStart(2, '0')}`;
- }
-
- createMarker(cue, duration) {
- const marker = videojs.dom.createEl('div', {
- className: 'vjs-chapter-marker',
- });
-
- // Calculate position as percentage
- const position = (cue.startTime / duration) * 100;
- marker.style.left = position + '%';
-
- // Create static tooltip for chapter start points
- const tooltip = videojs.dom.createEl('div', {
- className: 'vjs-chapter-marker-tooltip',
- });
- tooltip.textContent = cue.text;
- marker.appendChild(tooltip);
-
- // Add click handler to jump to chapter
- marker.addEventListener('click', (e) => {
- e.stopPropagation();
- this.player().currentTime(cue.startTime);
- });
-
- // Make marker interactive
- marker.style.pointerEvents = 'auto';
- marker.style.cursor = 'pointer';
-
- return marker;
- }
-
- dispose() {
- // Clean up event listeners
- const progressControl = this.player()
- .getChild('controlBar')
- ?.getChild('progressControl');
- if (progressControl) {
- const progressControlEl = progressControl.el();
- progressControlEl.removeEventListener(
- 'mouseenter',
- this.handleMouseEnter
- );
- progressControlEl.removeEventListener(
- 'mouseleave',
- this.handleMouseLeave
- );
- progressControlEl.removeEventListener(
- 'mousemove',
- this.handleMouseMove
- );
- }
-
- // Remove tooltip
- if (this.tooltip && this.tooltip.parentNode) {
- this.tooltip.parentNode.removeChild(this.tooltip);
- }
-
- super.dispose();
- }
-}
-
-// Register the chapter markers component
-videojs.registerComponent('ChapterMarkers', ChapterMarkers);
+import React from 'react';
+import { VideoJSPlayer } from './components';
function VideoJS() {
- const videoRef = useRef(null);
- const playerRef = useRef(null); // Track the player instance
- // const [chapters] = useState([]); // Track chapters for display
-
- // Safely access window.MEDIA_DATA with fallback using useMemo
- const mediaData = useMemo(
- () =>
- typeof window !== 'undefined' && window.MEDIA_DATA
- ? window.MEDIA_DATA
- : {
- data: {},
- siteUrl: '',
- },
- []
- );
- console.log('window.MEDIA_DATA hasNextLink', mediaData.hasNextLink);
-
- // Define chapters as JSON object
- // Note: The sample-chapters.vtt file is no longer needed as chapters are now loaded from this JSON
- const chaptersData = [
- { startTime: 0, endTime: 5, text: 'Start111' },
- { startTime: 5, endTime: 10, text: 'Introduction - EuroHPC' },
- { startTime: 10, endTime: 15, text: 'Planning - EuroHPC' },
- { startTime: 15, endTime: 20, text: 'Parcel Discounts - EuroHPC' },
- { startTime: 20, endTime: 25, text: 'Class Studies - EuroHPC' },
- { startTime: 25, endTime: 30, text: 'Sustainability - EuroHPC' },
- { startTime: 30, endTime: 35, text: 'Funding and Finance - EuroHPC' },
- { startTime: 35, endTime: 40, text: 'Virtual HPC Academy - EuroHPC' },
- { startTime: 40, endTime: 45, text: 'Wrapping up - EuroHPC' },
- ];
-
- // Get video data from mediaData
- const currentVideo = useMemo(
- () => ({
- id: mediaData.data?.friendly_token || 'default-video',
- title: mediaData.data?.title || 'Video',
- poster: mediaData.siteUrl + mediaData.data?.poster_url || '',
- sources: mediaData.data?.original_media_url
- ? [
- {
- src:
- mediaData.siteUrl +
- mediaData.data.original_media_url,
- type: 'video/mp4',
- },
- ]
- : [
- {
- src: 'https://vjs.zencdn.net/v/oceans.mp4',
- type: 'video/mp4',
- },
- ],
- }),
- [mediaData]
- );
-
- // Mock related videos data (would come from API)
- const [relatedVideos] = useState([
- {
- id: 'Otbc37Yj4',
- title: 'Amazing Ocean Depths',
- author: 'Marine Explorer',
- views: '2.1M views',
- thumbnail: 'https://picsum.photos/320/180?random=1',
- category: 'nature',
- },
- {
- id: 'Kt9m2Pv8x',
- title: 'Deep Sea Creatures',
- author: 'Aquatic Life',
- views: '854K views',
- thumbnail: 'https://picsum.photos/320/180?random=2',
- category: 'nature',
- },
- {
- id: 'Ln5q8Bw3r',
- title: 'Coral Reef Paradise',
- author: 'Ocean Films',
- views: '1.7M views',
- thumbnail: 'https://picsum.photos/320/180?random=3',
- category: 'nature',
- },
- {
- id: 'Mz4x7Cy9p',
- title: 'Underwater Adventure',
- author: 'Sea Documentaries',
- views: '3.2M views',
- thumbnail: 'https://picsum.photos/320/180?random=4',
- category: 'nature',
- },
- {
- id: 'Nx8v2Fk6w',
- title: 'Marine Wildlife',
- author: 'Nature Plus',
- views: '967K views',
- thumbnail: 'https://picsum.photos/320/180?random=5',
- category: 'nature',
- },
- {
- id: 'Py7t4Mn1q',
- title: 'Ocean Mysteries',
- author: 'Discovery Zone',
- views: '1.4M views',
- thumbnail: 'https://picsum.photos/320/180?random=6',
- category: 'nature',
- },
- {
- id: 'Qw5e8Rt2n',
- title: 'Whales and Dolphins',
- author: 'Ocean Planet',
- views: '2.8M views',
- thumbnail: 'https://picsum.photos/320/180?random=7',
- category: 'nature',
- },
- {
- id: 'Uv3k9Lp7m',
- title: 'Tropical Fish Paradise',
- author: 'Aquatic World',
- views: '1.2M views',
- thumbnail: 'https://picsum.photos/320/180?random=8',
- category: 'nature',
- },
- {
- id: 'Zx6c4Mn8b',
- title: 'Deep Ocean Exploration',
- author: 'Marine Science',
- views: '3.7M views',
- thumbnail: 'https://picsum.photos/320/180?random=9',
- category: 'nature',
- },
- ]);
-
- // Custom Next Video Button Component using modern Video.js API
- const Button = videojs.getComponent('Button');
-
- class NextVideoButton extends Button {
- constructor(player, options) {
- super(player, options);
- }
-
- createEl() {
- const button = super.createEl('button', {
- className: 'vjs-next-video-control vjs-control vjs-button',
- type: 'button',
- title: 'Next Video',
- 'aria-label': 'Next Video',
- });
-
- // Create the icon span using Video.js core icon
- const iconSpan = videojs.dom.createEl('span', {
- 'aria-hidden': 'true',
- });
-
- // Create SVG that matches Video.js icon dimensions
- iconSpan.innerHTML = `
-
- `;
-
- // Create control text span
- const controlTextSpan = videojs.dom.createEl('span', {
- className: 'vjs-control-text',
- });
- controlTextSpan.textContent = 'Next Video';
-
- // Append both spans to button
- button.appendChild(iconSpan);
- button.appendChild(controlTextSpan);
-
- return button;
- }
-
- handleClick() {
- this.player().trigger('nextVideo');
- }
- }
-
- // Register the component
- videojs.registerComponent('NextVideoButton', NextVideoButton);
-
- // Function to navigate to next video (disabled for single video)
- const goToNextVideo = () => {
- // console.log('Next video functionality disabled for single video mode');
- };
-
- useEffect(() => {
- // Only initialize if we don't already have a player and element exists
- if (videoRef.current && !playerRef.current) {
- // Check if element is already a Video.js player
- if (videoRef.current.player) {
- // console.log('Video.js already initialized on this element');
- return;
- }
-
- const timer = setTimeout(() => {
- // Double-check that we still don't have a player and element exists
- if (
- !playerRef.current &&
- videoRef.current &&
- !videoRef.current.player
- ) {
- playerRef.current = videojs(videoRef.current, {
- // ===== STANDARD