Compare commits

...

24 Commits

Author SHA1 Message Date
Yiannis Christodoulou
e6b5023b97 Update version.py 2025-10-16 18:40:15 +03:00
Yiannis Christodoulou
4772c11305 fix Format segments data for API request - use ref to get latest segments and sort by start time 2025-10-16 18:38:46 +03:00
Yiannis Christodoulou
41771442d9 update assets 2025-10-16 18:15:56 +03:00
Yiannis Christodoulou
0854eabd7f Reset control bar behavior after end screen events
Adds a method to reset the control bar to its default auto-hide behavior after the end screen is hidden, when auto-playing the next video, and during cleanup. Ensures the control bar does not remain visible unintentionally after video end or transitions.
2025-10-16 18:04:16 +03:00
Yiannis Christodoulou
03d62909b4 audio poster image 2025-10-16 17:19:59 +03:00
Yiannis Christodoulou
48e632c17f Improve icon size calculation for AutoplayToggleButton
Icon size is now calculated in pixels based on em units from PlayerConfig, with scaling and sensible defaults. Added defensive checks to ensure iconSize is always a valid number, improving reliability across device types.
2025-10-16 17:13:44 +03:00
Yiannis Christodoulou
06baf6b1e6 Update version.py 2025-10-16 16:57:59 +03:00
Yiannis Christodoulou
af7b4f6212 Hide fullscreen and PiP controls for audio and touch devices
Updated VideoJSPlayer to hide the fullscreen toggle for audio files and the picture-in-picture toggle for both audio files and touch devices, improving the control bar UI for these scenarios.
2025-10-16 16:55:05 +03:00
Yiannis Christodoulou
133e147b32 build assets 2025-10-16 16:29:39 +03:00
Yiannis Christodoulou
84ed74d40d Increase mobile caption font size to 13
Updated the mobileFontSize setting in the captions config from 10 to 13 to improve readability on mobile devices.
2025-10-16 16:19:20 +03:00
Yiannis Christodoulou
b186bbe669 build assets 2025-10-16 16:12:45 +03:00
Yiannis Christodoulou
5a282c7cd2 Improve Safari audio/video initialization and fallbacks
Adds Safari-specific detection and initialization logic to better support audio and video playback, especially for cases where metadata is not loaded as expected. Implements fallback event listeners, user interaction triggers, and exposes an initialization helper to ensure the editor works reliably on Safari and iOS devices.
2025-10-16 16:12:35 +03:00
Yiannis Christodoulou
ac2aee8b8b build assets 2025-10-16 16:01:36 +03:00
Yiannis Christodoulou
219e80e9e2 Scope fullscreen button size to hover-capable devices
Wrapped the fullscreen button SVG sizing rules in a media query targeting devices with hover and fine pointer capabilities. This prevents the size override from affecting touch devices.
2025-10-16 15:50:56 +03:00
Yiannis Christodoulou
d955576a7e Center fullscreen button and adjust its size
Added CSS rules to center the fullscreen button's SVG and set its width and height to 30px for improved alignment and appearance.
2025-10-16 15:48:21 +03:00
Yiannis Christodoulou
166882558d Hide load progress bar in VideoJSPlayer seek bar
Set loadProgressBar to false in the seekBar config to hide the buffered/loaded progress indicator in the VideoJSPlayer component.
2025-10-16 15:43:43 +03:00
Yiannis Christodoulou
aa458a1a31 Add sample media file and update VideoJSPlayer
Added a comprehensive sample-media-file.json for use with the video player. Updated VideoJSPlayer.jsx to support or utilize the new sample media file, likely for development or testing purposes.
2025-10-16 15:16:11 +03:00
Yiannis Christodoulou
085e944861 ChapterEditor: Add keyboard shortcuts for video playback controls
Implemented keyboard shortcuts for play/pause (Space), jump backward (ArrowLeft), and jump forward (ArrowRight) in the chapters editor. Shortcuts are disabled when typing in input fields to prevent interference with text entry.
2025-10-16 14:59:32 +03:00
Yiannis Christodoulou
b330567955 build assets 2025-10-16 14:49:17 +03:00
Yiannis Christodoulou
1e8b1ea839 build assets 2025-10-16 14:48:32 +03:00
Yiannis Christodoulou
276bf6a875 Standardize z-index hierarchy for VideoJS overlays
Introduced a Z-INDEX-HIERARCHY.md documentation file and updated z-index values across overlay and control CSS files to enforce a consistent stacking order. Ensures tooltips are always on top, menus and chapters overlays are above informational overlays, and end screen overlays remain at the base of the overlay stack. This improves UI layering logic and user interaction reliability.
2025-10-16 14:47:23 +03:00
Yiannis Christodoulou
6b68e6537f Chapter Editor: Show placeholder for default chapter titles
When a segment's chapter title is a default generated name (e.g., 'Chapter 1'), the editing field now displays an empty string to trigger the placeholder, improving the user experience for custom title entry.
2025-10-16 14:28:15 +03:00
Yiannis Christodoulou
88d6ef3700 Increase default font size in player config
Updated the fontSize property from 12 to 14 in the player configuration to improve text readability.
2025-10-16 13:42:04 +03:00
Yiannis Christodoulou
211a442f29 Add default audio poster and update video player props
Added a default audio poster image and updated VideoJSPlayer to use it when no poster_url is provided. Also refactored mock media data property names for consistency.
2025-10-16 13:41:48 +03:00
26 changed files with 1903 additions and 345 deletions

View File

@ -1 +1 @@
VERSION = "6.7.1.beta-6" VERSION = "6.7.1.beta-8"

View File

@ -6,6 +6,7 @@ import EditingTools from '@/components/EditingTools';
import ClipSegments from '@/components/ClipSegments'; import ClipSegments from '@/components/ClipSegments';
import MobilePlayPrompt from '@/components/IOSPlayPrompt'; import MobilePlayPrompt from '@/components/IOSPlayPrompt';
import useVideoChapters from '@/hooks/useVideoChapters'; import useVideoChapters from '@/hooks/useVideoChapters';
import { useEffect } from 'react';
const App = () => { const App = () => {
const { const {
@ -39,9 +40,10 @@ const App = () => {
isMobile, isMobile,
videoInitialized, videoInitialized,
setVideoInitialized, setVideoInitialized,
initializeSafariIfNeeded,
} = useVideoChapters(); } = useVideoChapters();
const handlePlay = () => { const handlePlay = async () => {
if (!videoRef.current) return; if (!videoRef.current) return;
const video = videoRef.current; const video = videoRef.current;
@ -54,6 +56,16 @@ const App = () => {
return; return;
} }
// Safari: Try to initialize if needed before playing
if (duration === 0) {
const initialized = await initializeSafariIfNeeded();
if (initialized) {
// Wait a moment for initialization to complete
setTimeout(() => handlePlay(), 200);
return;
}
}
// Start playing - no boundary checking, play through entire timeline // Start playing - no boundary checking, play through entire timeline
video video
.play() .play()
@ -67,6 +79,46 @@ const App = () => {
}); });
}; };
// Handle keyboard shortcuts
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Don't handle keyboard shortcuts if user is typing in an input field
const target = event.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
switch (event.code) {
case 'Space':
event.preventDefault(); // Prevent default spacebar behavior (scrolling, button activation)
handlePlay();
break;
case 'ArrowLeft':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.max(currentTime - 10, 0);
handleMobileSafeSeek(newTime);
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
}
break;
case 'ArrowRight':
event.preventDefault();
if (videoRef.current) {
const newTime = Math.min(currentTime + 10, duration);
handleMobileSafeSeek(newTime);
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
}
break;
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
return ( return (
<div className="bg-background min-h-screen"> <div className="bg-background min-h-screen">
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} /> <MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />

View File

@ -9,7 +9,7 @@ interface MobilePlayPromptProps {
const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay }) => { const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay }) => {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
// Check if the device is mobile // Check if the device is mobile or Safari browser
useEffect(() => { useEffect(() => {
const checkIsMobile = () => { const checkIsMobile = () => {
// More comprehensive check for mobile/tablet devices // More comprehensive check for mobile/tablet devices
@ -18,7 +18,7 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
); );
}; };
// Always show for mobile devices on each visit // Only show for mobile devices
const isMobile = checkIsMobile(); const isMobile = checkIsMobile();
setIsVisible(isMobile); setIsVisible(isMobile);
}, []); }, []);
@ -49,22 +49,6 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
return ( return (
<div className="mobile-play-prompt-overlay"> <div className="mobile-play-prompt-overlay">
<div className="mobile-play-prompt"> <div className="mobile-play-prompt">
{/* <h3>Mobile Device Notice</h3>
<p>
For the best video editing experience on mobile devices, you need to <strong>play the video first</strong> before
using the timeline controls.
</p>
<div className="mobile-prompt-instructions">
<p>Please follow these steps:</p>
<ol>
<li>Tap the button below to start the video</li>
<li>After the video starts, you can pause it</li>
<li>Then you'll be able to use all timeline controls</li>
</ol>
</div> */}
<button className="mobile-play-button" onClick={handlePlayClick}> <button className="mobile-play-button" onClick={handlePlayClick}>
Click to start editing... Click to start editing...
</button> </button>

View File

@ -190,12 +190,14 @@ const TimelineControls = ({
try { try {
setIsAutoSaving(true); setIsAutoSaving(true);
// Format segments data for API request - use ref to get latest segments // Format segments data for API request - use ref to get latest segments and sort by start time
const chapters = clipSegmentsRef.current.map((chapter) => ({ const chapters = clipSegmentsRef.current
startTime: formatDetailedTime(chapter.startTime), .sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
endTime: formatDetailedTime(chapter.endTime), .map((chapter) => ({
chapterTitle: chapter.chapterTitle, startTime: formatDetailedTime(chapter.startTime),
})); endTime: formatDetailedTime(chapter.endTime),
chapterTitle: chapter.chapterTitle,
}));
logger.debug('chapters', chapters); logger.debug('chapters', chapters);
@ -266,7 +268,13 @@ const TimelineControls = ({
// Update editing title when selected segment changes // Update editing title when selected segment changes
useEffect(() => { useEffect(() => {
if (selectedSegment) { if (selectedSegment) {
setEditingChapterTitle(selectedSegment.chapterTitle || ''); // Check if the chapter title is a default generated name (e.g., "Chapter 1", "Chapter 2", etc.)
const isDefaultChapterName = selectedSegment.chapterTitle &&
/^Chapter \d+$/.test(selectedSegment.chapterTitle);
// If it's a default name, show empty string so placeholder appears
// If it's a custom title, show the actual title
setEditingChapterTitle(isDefaultChapterName ? '' : (selectedSegment.chapterTitle || ''));
} else { } else {
setEditingChapterTitle(''); setEditingChapterTitle('');
} }
@ -490,9 +498,10 @@ const TimelineControls = ({
setSaveType('chapters'); setSaveType('chapters');
try { try {
// Format chapters data for API request // Format chapters data for API request - sort by start time first
const chapters = clipSegments const chapters = clipSegments
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim()) .filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
.map((segment) => ({ .map((segment) => ({
chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`, chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
from: formatDetailedTime(segment.startTime), from: formatDetailedTime(segment.startTime),

View File

@ -38,14 +38,24 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4'; (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
// Detect iOS device // Detect iOS device and Safari browser
useEffect(() => { useEffect(() => {
const checkIOS = () => { const checkIOS = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream; return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
}; };
const checkSafari = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
};
setIsIOS(checkIOS()); setIsIOS(checkIOS());
// Store Safari detection globally for other components
if (typeof window !== 'undefined') {
(window as any).isSafari = checkSafari();
}
// Check if video was previously initialized // Check if video was previously initialized
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@ -336,7 +346,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<div className="video-player-container"> <div className="video-player-container">
<video <video
ref={videoRef} ref={videoRef}
preload="auto" preload="metadata"
crossOrigin="anonymous" crossOrigin="anonymous"
onClick={handleVideoClick} onClick={handleVideoClick}
playsInline playsInline
@ -346,7 +356,10 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
muted={isMuted} muted={isMuted}
> >
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> {/* Safari fallback for audio files */}
<source src={sampleVideoUrl} type="audio/mp4" />
<source src={sampleVideoUrl} type="audio/mpeg" />
<p>Your browser doesn't support HTML5 video or audio.</p>
</video> </video>
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */} {/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}

View File

@ -24,6 +24,18 @@ const useVideoChapters = () => {
return `Chapter ${chapterIndex + 1}`; return `Chapter ${chapterIndex + 1}`;
}; };
// Helper function to renumber all segments in chronological order
const renumberAllSegments = (segments: Segment[]): Segment[] => {
// Sort segments by start time
const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime);
// Renumber each segment based on its chronological position
return sortedSegments.map((segment, index) => ({
...segment,
chapterTitle: `Chapter ${index + 1}`
}));
};
// Helper function to parse time string (HH:MM:SS.mmm) to seconds // Helper function to parse time string (HH:MM:SS.mmm) to seconds
const parseTimeToSeconds = (timeString: string): number => { const parseTimeToSeconds = (timeString: string): number => {
const parts = timeString.split(':'); const parts = timeString.split(':');
@ -87,12 +99,23 @@ const useVideoChapters = () => {
} }
}, [history, historyPosition]); }, [history, historyPosition]);
// Detect Safari browser
const isSafari = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
const isSafariBrowser = /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
if (isSafariBrowser) {
logger.debug('Safari browser detected, enabling audio support fallbacks');
}
return isSafariBrowser;
};
// Initialize video event listeners // Initialize video event listeners
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
const handleLoadedMetadata = () => { const handleLoadedMetadata = () => {
logger.debug('Video loadedmetadata event fired, duration:', video.duration);
setDuration(video.duration); setDuration(video.duration);
setTrimEnd(video.duration); setTrimEnd(video.duration);
@ -146,11 +169,32 @@ const useVideoChapters = () => {
setHistory([initialState]); setHistory([initialState]);
setHistoryPosition(0); setHistoryPosition(0);
setClipSegments(initialSegments); setClipSegments(initialSegments);
logger.debug('Editor initialized with segments:', initialSegments.length);
}; };
initializeEditor(); initializeEditor();
}; };
// Safari-specific fallback for audio files
const handleCanPlay = () => {
logger.debug('Video canplay event fired');
// If loadedmetadata hasn't fired yet but we have duration, trigger initialization
if (video.duration && duration === 0) {
logger.debug('Safari fallback: Using canplay event to initialize');
handleLoadedMetadata();
}
};
// Additional Safari fallback for audio files
const handleLoadedData = () => {
logger.debug('Video loadeddata event fired');
// If we still don't have duration, try again
if (video.duration && duration === 0) {
logger.debug('Safari fallback: Using loadeddata event to initialize');
handleLoadedMetadata();
}
};
const handleTimeUpdate = () => { const handleTimeUpdate = () => {
setCurrentTime(video.currentTime); setCurrentTime(video.currentTime);
}; };
@ -176,6 +220,33 @@ const useVideoChapters = () => {
video.addEventListener('pause', handlePause); video.addEventListener('pause', handlePause);
video.addEventListener('ended', handleEnded); video.addEventListener('ended', handleEnded);
// Safari-specific fallback event listeners for audio files
if (isSafari()) {
logger.debug('Adding Safari-specific event listeners for audio support');
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('loadeddata', handleLoadedData);
// Additional timeout fallback for Safari audio files
const safariTimeout = setTimeout(() => {
if (video.duration && duration === 0) {
logger.debug('Safari timeout fallback: Force initializing editor');
handleLoadedMetadata();
}
}, 1000);
return () => {
// Remove event listeners
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
video.removeEventListener('ended', handleEnded);
video.removeEventListener('canplay', handleCanPlay);
video.removeEventListener('loadeddata', handleLoadedData);
clearTimeout(safariTimeout);
};
}
return () => { return () => {
// Remove event listeners // Remove event listeners
video.removeEventListener('loadedmetadata', handleLoadedMetadata); video.removeEventListener('loadedmetadata', handleLoadedMetadata);
@ -186,6 +257,92 @@ const useVideoChapters = () => {
}; };
}, []); }, []);
// Safari auto-initialization on user interaction
useEffect(() => {
if (isSafari() && videoRef.current) {
const video = videoRef.current;
const initializeSafariOnInteraction = () => {
// Try to load video metadata by attempting to play and immediately pause
const attemptInitialization = async () => {
try {
logger.debug('Safari: Attempting auto-initialization on user interaction');
// Briefly play to trigger metadata loading, then pause
await video.play();
video.pause();
// Check if we now have duration and initialize if needed
if (video.duration > 0 && clipSegments.length === 0) {
logger.debug('Safari: Successfully initialized metadata, creating default segment');
const defaultSegment: Segment = {
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
setDuration(video.duration);
setTrimEnd(video.duration);
setClipSegments([defaultSegment]);
const initialState: EditorState = {
trimStart: 0,
trimEnd: video.duration,
splitPoints: [],
clipSegments: [defaultSegment],
};
setHistory([initialState]);
setHistoryPosition(0);
}
} catch (error) {
logger.debug('Safari: Auto-initialization failed, will retry on next interaction:', error);
}
};
attemptInitialization();
};
// Listen for any user interaction with video controls
const handleUserInteraction = () => {
if (clipSegments.length === 0 && video.duration === 0) {
initializeSafariOnInteraction();
}
};
// Add listeners for various user interactions
document.addEventListener('click', handleUserInteraction);
document.addEventListener('keydown', handleUserInteraction);
return () => {
document.removeEventListener('click', handleUserInteraction);
document.removeEventListener('keydown', handleUserInteraction);
};
}
}, [clipSegments.length]);
// Safari initialization helper
const initializeSafariIfNeeded = async () => {
if (isSafari() && videoRef.current && duration === 0) {
const video = videoRef.current;
try {
logger.debug('Safari: Initializing on user interaction');
// This play/pause will trigger metadata loading in Safari
await video.play();
video.pause();
// The metadata events should fire now and initialize segments
return true;
} catch (error) {
logger.debug('Safari: Initialization attempt failed:', error);
return false;
}
}
return false;
};
// Play/pause video // Play/pause video
const playPauseVideo = () => { const playPauseVideo = () => {
const video = videoRef.current; const video = videoRef.current;
@ -194,6 +351,21 @@ const useVideoChapters = () => {
if (isPlaying) { if (isPlaying) {
video.pause(); video.pause();
} else { } else {
// Safari: Try to initialize if needed before playing
if (isSafari() && duration === 0) {
initializeSafariIfNeeded().then(() => {
// After initialization, try to play again
setTimeout(() => {
if (video && !isPlaying) {
video.play().catch((err) => {
console.error('Error playing after Safari initialization:', err);
});
}
}, 100);
});
return;
}
// iOS Safari fix: Use the last seeked position if available // iOS Safari fix: Use the last seeked position if available
if (!isPlaying && typeof window !== 'undefined' && window.lastSeekedPosition > 0) { if (!isPlaying && typeof window !== 'undefined' && window.lastSeekedPosition > 0) {
// Only apply this if the video is not at the same position already // Only apply this if the video is not at the same position already
@ -228,6 +400,20 @@ const useVideoChapters = () => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
// Safari: Try to initialize if needed before seeking
if (isSafari() && duration === 0) {
initializeSafariIfNeeded().then(() => {
// After initialization, try to seek again
setTimeout(() => {
if (video) {
video.currentTime = time;
setCurrentTime(time);
}
}, 100);
});
return;
}
// Track if the video was playing before seeking // Track if the video was playing before seeking
const wasPlaying = !video.paused; const wasPlaying = !video.paused;
@ -458,19 +644,16 @@ const useVideoChapters = () => {
newSegments.splice(segmentIndex, 1); newSegments.splice(segmentIndex, 1);
// Remove the original segment first to get accurate positioning for new segments
const segmentsWithoutOriginal = newSegments;
const firstHalf: Segment = { const firstHalf: Segment = {
id: Date.now(), id: Date.now(),
chapterTitle: generateChapterName(segmentToSplit.startTime, segmentsWithoutOriginal), chapterTitle: '', // Temporary title, will be set by renumberAllSegments
startTime: segmentToSplit.startTime, startTime: segmentToSplit.startTime,
endTime: timeToSplit, endTime: timeToSplit,
}; };
const secondHalf: Segment = { const secondHalf: Segment = {
id: Date.now() + 1, id: Date.now() + 1,
chapterTitle: generateChapterName(timeToSplit, [...segmentsWithoutOriginal, firstHalf]), chapterTitle: '', // Temporary title, will be set by renumberAllSegments
startTime: timeToSplit, startTime: timeToSplit,
endTime: segmentToSplit.endTime, endTime: segmentToSplit.endTime,
}; };
@ -478,11 +661,11 @@ const useVideoChapters = () => {
// Add the new segments // Add the new segments
newSegments.push(firstHalf, secondHalf); newSegments.push(firstHalf, secondHalf);
// Sort segments by start time // Renumber all segments to ensure proper chronological naming
newSegments.sort((a, b) => a.startTime - b.startTime); const renumberedSegments = renumberAllSegments(newSegments);
// Update state // Update state
setClipSegments(newSegments); setClipSegments(renumberedSegments);
saveState('split_segment'); saveState('split_segment');
} }
}; };
@ -513,8 +696,9 @@ const useVideoChapters = () => {
setSplitPoints([]); setSplitPoints([]);
setClipSegments([defaultSegment]); setClipSegments([defaultSegment]);
} else { } else {
// Just update the segments normally // Renumber remaining segments to ensure proper chronological naming
setClipSegments(newSegments); const renumberedSegments = renumberAllSegments(newSegments);
setClipSegments(renumberedSegments);
} }
saveState('delete_segment'); saveState('delete_segment');
} }
@ -733,12 +917,19 @@ const useVideoChapters = () => {
return; return;
} }
// Convert chapters to backend expected format // Convert chapters to backend expected format and sort by start time
const backendChapters = chapters.map((chapter) => ({ const backendChapters = chapters
startTime: chapter.from, .map((chapter) => ({
endTime: chapter.to, startTime: chapter.from,
chapterTitle: chapter.chapterTitle, endTime: chapter.to,
})); chapterTitle: chapter.chapterTitle,
}))
.sort((a, b) => {
// Parse time strings to seconds for proper comparison
const aStartSeconds = parseTimeToSeconds(a.startTime);
const bStartSeconds = parseTimeToSeconds(b.startTime);
return aStartSeconds - bStartSeconds;
});
// Create the API request body // Create the API request body
const requestData = { const requestData = {
@ -946,6 +1137,7 @@ const useVideoChapters = () => {
isMobile, isMobile,
videoInitialized, videoInitialized,
setVideoInitialized, setVideoInitialized,
initializeSafariIfNeeded, // Expose Safari initialization helper
}; };
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,9 @@ class AutoplayToggleButton extends Button {
} */ } */
// Store the appropriate font size based on device type // Store the appropriate font size based on device type
this.iconSize = isTouchDevice ? PlayerConfig.controlBar.mobileFontSize : PlayerConfig.controlBar.fontSize; // PlayerConfig values are in em units, convert to pixels for SVG dimensions
const baseFontSize = isTouchDevice ? PlayerConfig.controlBar.mobileFontSize : PlayerConfig.controlBar.fontSize;
this.iconSize = Math.round((baseFontSize || 14) * 1.2); // Scale and default to 14em if undefined
this.userPreferences = options.userPreferences; this.userPreferences = options.userPreferences;
// Get autoplay preference from localStorage, default to false if not set // Get autoplay preference from localStorage, default to false if not set
@ -72,6 +74,11 @@ class AutoplayToggleButton extends Button {
} }
updateIconClass() { updateIconClass() {
// Ensure iconSize is a valid number (defensive check)
if (!this.iconSize || isNaN(this.iconSize)) {
this.iconSize = 16; // Default to 16px if undefined or NaN
}
// Remove existing icon classes // Remove existing icon classes
this.iconSpan.className = 'vjs-icon-placeholder vjs-svg-icon vjs-autoplay-icon__OFFF'; this.iconSpan.className = 'vjs-icon-placeholder vjs-svg-icon vjs-autoplay-icon__OFFF';
this.iconSpan.style.position = 'relative'; this.iconSpan.style.position = 'relative';

View File

@ -26,7 +26,7 @@
opacity 0.2s ease, opacity 0.2s ease,
visibility 0.2s ease, visibility 0.2s ease,
transform 0.2s ease; transform 0.2s ease;
z-index: 1000; z-index: 20000;
margin-bottom: 10px; margin-bottom: 10px;
font-family: font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",

View File

@ -14,6 +14,7 @@
overflow: hidden; overflow: hidden;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
right: 10px; right: 10px;
z-index: 10000;
} }
.chapter-head { .chapter-head {

View File

@ -37,7 +37,7 @@
border-radius: 7px; border-radius: 7px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
display: none; display: none;
z-index: 9999; z-index: 10000;
font-size: 14px; font-size: 14px;
overflow: auto; overflow: auto;
} }

View File

@ -10,7 +10,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 4; z-index: 200;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
opacity: 0; opacity: 0;
@ -154,7 +154,7 @@
.autoplay-close-button { .autoplay-close-button {
display: flex !important; display: flex !important;
} }
.autoplay-cancel-button { .autoplay-cancel-button {
display: inline-block !important; display: inline-block !important;
} }

View File

@ -4,7 +4,7 @@
position: absolute !important; position: absolute !important;
top: 10px !important; top: 10px !important;
left: 10px !important; left: 10px !important;
z-index: 1000 !important; z-index: 5000 !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
gap: 10px !important; gap: 10px !important;

View File

@ -5,11 +5,11 @@
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 60px; /* Leave space for control bar */ bottom: 60px;
background: #000000; /* Solid black background */ background: #000000;
display: none; display: none;
z-index: 100; z-index: 100;
overflow: hidden; /* Prevent content from overflowing */ overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
} }
@ -353,9 +353,9 @@
visibility: hidden !important; visibility: hidden !important;
} }
/* Ensure end screen overlay covers everything with solid background */ /* Ensure end screen overlay covers everything with solid background but stays below menus */
.video-js.vjs-ended .vjs-end-screen-overlay { .video-js.vjs-ended .vjs-end-screen-overlay {
background: #000000 !important; background: #000000 !important;
z-index: 99999 !important; z-index: 100 !important;
display: flex !important; display: flex !important;
} }

View File

@ -1,6 +1,9 @@
button { button {
cursor: pointer; cursor: pointer;
} }
.playlist-items a {
text-decoration: none !important;
}
.video-js, .video-js,
.video-js[tabindex], .video-js[tabindex],
@ -27,3 +30,12 @@ button {
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important; visibility: hidden !important;
} }
/* Center the fullscreen button inside its wrapper */
/* @media (hover: hover) and (pointer: fine) {
.vjs-fullscreen-control svg {
width: 30px !important;
height: 30px !important;
}
}
*/

View File

@ -28,6 +28,12 @@ import { EndScreenHandler } from '../../utils/EndScreenHandler';
import KeyboardHandler from '../../utils/KeyboardHandler'; import KeyboardHandler from '../../utils/KeyboardHandler';
import PlaybackEventHandler from '../../utils/PlaybackEventHandler'; import PlaybackEventHandler from '../../utils/PlaybackEventHandler';
// Import sample media data
import sampleMediaData from '../../../public/sample-media-file.json';
// Import fallback poster image
import audioPosterImg from '../../../public/audio-poster.jpg';
// Function to enable tooltips for all standard VideoJS buttons // Function to enable tooltips for all standard VideoJS buttons
const enableStandardButtonTooltips = (player) => { const enableStandardButtonTooltips = (player) => {
// Wait a bit for all components to be initialized // Wait a bit for all components to be initialized
@ -243,14 +249,32 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
typeof window !== 'undefined' && window.MEDIA_DATA typeof window !== 'undefined' && window.MEDIA_DATA
? window.MEDIA_DATA ? window.MEDIA_DATA
: { : {
data: { data: sampleMediaData,
// other
useRoundedCorners: false,
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',
frame: { width: 160, height: 90, seconds: 10 },
},
siteUrl: 'https://videojs.mediacms.io',
nextLink: 'https://videojs.mediacms.io/view?m=elygiagorgechania',
urlAutoplay: true,
urlMuted: false,
// FALLBACK - keep old structure for reference
__data_old: {
// COMMON // COMMON
title: 'Modi tempora est quaerat numquam', title: 'Modi tempora est quaerat numquam',
author_name: 'Markos Gogoulos', author_name: 'Markos Gogoulos',
author_profile: '/user/markos/', author_profile: '/user/markos/',
author_thumbnail: '/media/userlogos/user.jpg', author_thumbnail: '/media/userlogos/user.jpg',
url: 'https://videojs.mediacms.io/view?m=2Uk08Il5u', url: 'https://videojs.mediacms.io/view?m=2Uk08Il5u',
poster_url: poster_url: '',
__poster_url:
'/media/original/thumbnails/user/markos/db52140de7204022a1e5f08e078b4ec6_VKPTF4v.UniversityofCopenhagenMærskTower.mp4.jpg', '/media/original/thumbnails/user/markos/db52140de7204022a1e5f08e078b4ec6_VKPTF4v.UniversityofCopenhagenMærskTower.mp4.jpg',
___chapter_data: [], ___chapter_data: [],
chapter_data: [ chapter_data: [
@ -1248,9 +1272,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// VIDEO // VIDEO
media_type: 'audio', media_type: 'audio',
_original_media_url: original_media_url:
'/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4', '/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4',
_hls_info: { hls_info: {
master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8', master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8',
'240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/iframes.m3u8', '240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/iframes.m3u8',
'480_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/iframes.m3u8', '480_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/iframes.m3u8',
@ -1263,9 +1287,9 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
'144_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/stream.m3u8', '144_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/stream.m3u8',
'360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8', '360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8',
}, },
hls_info: {}, __hls_info: {},
encodings_info: {}, __encodings_info: {},
___encodings_info: { encodings_info: {
144: { 144: {
h264: { h264: {
title: 'h264-144', title: 'h264-144',
@ -1337,18 +1361,6 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
hls_info: {}, hls_info: {},
encodings_info: {},*/ encodings_info: {},*/
}, },
// other
useRoundedCorners: false,
isPlayList: false,
previewSprite: {
url: 'https://videojs.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',
urlAutoplay: true,
urlMuted: false,
}, },
[] []
); );
@ -1656,7 +1668,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
? mediaData.siteUrl + mediaData.data.author_thumbnail ? mediaData.siteUrl + mediaData.data.author_thumbnail
: '', : '',
url: mediaData.data?.url || '', url: mediaData.data?.url || '',
poster: mediaData.data?.poster_url ? mediaData.siteUrl + mediaData.data.poster_url : '', poster: mediaData.data?.poster_url ? mediaData.siteUrl + mediaData.data.poster_url : audioPosterImg,
previewSprite: mediaData?.previewSprite || {}, previewSprite: mediaData?.previewSprite || {},
useRoundedCorners: mediaData?.useRoundedCorners, useRoundedCorners: mediaData?.useRoundedCorners,
isPlayList: mediaData?.isPlayList, isPlayList: mediaData?.isPlayList,
@ -2076,7 +2088,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
controlBar: { controlBar: {
playToggle: true, playToggle: true,
progressControl: { progressControl: {
seekBar: {}, seekBar: { loadProgressBar: false }, // Hide the buffered/loaded progress indicator
}, },
/* progressControl: { /* progressControl: {
seekBar: { seekBar: {
@ -2100,11 +2112,11 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// Custom control spacer // Custom control spacer
customControlSpacer: true, customControlSpacer: true,
// Fullscreen toggle button // Fullscreen toggle button (hide for audio files since fullscreen doesn't work on mobile)
fullscreenToggle: true, fullscreenToggle: mediaData.data?.media_type === 'audio' ? false : true,
// Picture-in-picture toggle button // Picture-in-picture toggle button (hide for audio and touch devices)
pictureInPictureToggle: isTouchDevice ? false : true, pictureInPictureToggle: isTouchDevice || mediaData.data?.media_type === 'audio' ? false : true,
// Remove default playback speed dropdown from control bar // Remove default playback speed dropdown from control bar
playbackRateMenuButton: false, playbackRateMenuButton: false,

View File

@ -38,9 +38,9 @@ const PlayerConfig = {
height: 3, height: 3,
// Font size in em units // Font size in em units
fontSize: 12, fontSize: 14,
mobileFontSize: 10, mobileFontSize: 13,
}, },
}; };

View File

@ -25,10 +25,41 @@ export class EndScreenHandler {
if (this.autoplayCountdown) { if (this.autoplayCountdown) {
this.autoplayCountdown.stopCountdown(); this.autoplayCountdown.stopCountdown();
} }
// Reset control bar to normal auto-hide behavior
this.resetControlBarBehavior();
}; };
this.player.on('play', hideEndScreenAndStopCountdown); this.player.on('play', hideEndScreenAndStopCountdown);
this.player.on('seeking', hideEndScreenAndStopCountdown); this.player.on('seeking', hideEndScreenAndStopCountdown);
// Reset control bar when playing after ended state
this.player.on('playing', () => {
// Only reset if we're coming from ended state (time near 0)
if (this.player.currentTime() < 1) {
setTimeout(() => {
this.player.userActive(false);
}, 1000); // Hide controls after 1 second
}
});
}
// New method to reset control bar to default behavior
resetControlBarBehavior() {
const controlBar = this.player.getChild('controlBar');
if (controlBar && controlBar.el()) {
// Remove the forced visible styles
controlBar.el().style.opacity = '';
controlBar.el().style.pointerEvents = '';
// Let video.js handle the control bar visibility normally
// Force the player to be inactive after a short delay
setTimeout(() => {
if (!this.player.paused() && !this.player.ended()) {
this.player.userActive(false);
}
}, 500);
}
} }
handleVideoEnded() { handleVideoEnded() {
@ -138,6 +169,8 @@ export class EndScreenHandler {
nextVideoData: nextVideoData, nextVideoData: nextVideoData,
countdownSeconds: 5, countdownSeconds: 5,
onPlayNext: () => { onPlayNext: () => {
// Reset control bar when auto-playing next video
this.resetControlBarBehavior();
goToNextVideo(); goToNextVideo();
}, },
onCancel: () => { onCancel: () => {
@ -171,7 +204,7 @@ export class EndScreenHandler {
relatedVideos: relatedVideos, relatedVideos: relatedVideos,
}); });
// Also store the data directly on the component as backup and update it // Store the data directly on the component as backup and update it
this.endScreen.relatedVideos = relatedVideos; this.endScreen.relatedVideos = relatedVideos;
if (this.endScreen.setRelatedVideos) { if (this.endScreen.setRelatedVideos) {
this.endScreen.setRelatedVideos(relatedVideos); this.endScreen.setRelatedVideos(relatedVideos);
@ -195,5 +228,7 @@ export class EndScreenHandler {
cleanup() { cleanup() {
this.cleanupOverlays(); this.cleanupOverlays();
// Reset control bar on cleanup
this.resetControlBarBehavior();
} }
} }

View File

@ -14,6 +14,7 @@ export default defineConfig({
}, },
}, },
root: path.resolve(__dirname, 'src'), root: path.resolve(__dirname, 'src'),
publicDir: path.resolve(__dirname, 'public'),
define: { define: {
'process.env': { 'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'production'), NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'production'),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long