Fix video player bugs and improve overlay behavior
Fixed issues: 1. Duration now displays correctly using Plyr's time tracking instead of YouTube API 2. Chapter jump functionality now works using Plyr's currentTime property 3. Overlays now properly avoid Plyr controls - only show when paused/ended 4. Hidden YouTube's native play button with CSS Key changes: - Use Plyr's native time tracking (player.currentTime) instead of YouTube API - Jump to time uses Plyr's seek (player.currentTime = time, then play) - Top overlay only appears when paused/ended (not when playing) - Paused/ended overlays start at bottom: 80px to avoid Plyr controls - Added CSS to hide .ytp-large-play-button and .html5-video-player background - Moved time tracking to onReady callback to ensure player is initialized - Removed dependencies on chapters from useEffect to prevent re-initialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
iv_load_policy: 3,
|
||||
modestbranding: 1,
|
||||
controls: 0,
|
||||
customControls: true,
|
||||
},
|
||||
controls: [
|
||||
'play-large',
|
||||
@@ -145,6 +146,37 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
// Create YouTube Player instance
|
||||
const ytPlayer = new window.YT.Player(iframe, {
|
||||
events: {
|
||||
onReady: () => {
|
||||
// Set up time tracking for chapters using Plyr's time
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (playerRef.current) {
|
||||
const currentTime = playerRef.current.currentTime;
|
||||
if (onTimeUpdate && currentTime > 0) {
|
||||
onTimeUpdate(currentTime);
|
||||
}
|
||||
|
||||
// Find current chapter
|
||||
if (chapters.length > 0) {
|
||||
let index = chapters.findIndex((chapter, i) => {
|
||||
const nextChapter = chapters[i + 1];
|
||||
return currentTime >= chapter.time && (!nextChapter || currentTime < nextChapter.time);
|
||||
});
|
||||
|
||||
// If before first chapter, no active chapter
|
||||
if (index === -1 && currentTime < chapters[0].time) {
|
||||
index = -1;
|
||||
}
|
||||
|
||||
if (index !== currentChapterIndex) {
|
||||
setCurrentChapterIndex(index);
|
||||
if (index >= 0 && onChapterChange) {
|
||||
onChapterChange(chapters[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
onStateChange: (event: any) => {
|
||||
const state = event.data;
|
||||
setYtPlayerState(state);
|
||||
@@ -160,36 +192,6 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
});
|
||||
|
||||
ytPlayerRef.current = ytPlayer;
|
||||
|
||||
// Set up time tracking for chapters
|
||||
intervalRef.current = setInterval(() => {
|
||||
if (ytPlayer && ytPlayer.getCurrentTime) {
|
||||
const currentTime = ytPlayer.getCurrentTime();
|
||||
if (onTimeUpdate) {
|
||||
onTimeUpdate(currentTime);
|
||||
}
|
||||
|
||||
// Find current chapter
|
||||
if (chapters.length > 0) {
|
||||
let index = chapters.findIndex((chapter, i) => {
|
||||
const nextChapter = chapters[i + 1];
|
||||
return currentTime >= chapter.time && (!nextChapter || currentTime < nextChapter.time);
|
||||
});
|
||||
|
||||
// If before first chapter, no active chapter
|
||||
if (index === -1 && currentTime < chapters[0].time) {
|
||||
index = -1;
|
||||
}
|
||||
|
||||
if (index !== currentChapterIndex) {
|
||||
setCurrentChapterIndex(index);
|
||||
if (index >= 0 && onChapterChange) {
|
||||
onChapterChange(chapters[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Initialize YouTube player when API is ready
|
||||
@@ -213,6 +215,13 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
.plyr__progress__buffer {
|
||||
color: ${accentColor}40 !important;
|
||||
}
|
||||
/* Hide YouTube's native play button */
|
||||
.ytp-large-play-button {
|
||||
display: none !important;
|
||||
}
|
||||
.html5-video-player {
|
||||
background: #000;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
@@ -230,7 +239,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
}
|
||||
player.destroy();
|
||||
};
|
||||
}, [isYouTube, ytApiReady, accentColor, chapters, currentChapterIndex, onChapterChange, onTimeUpdate]);
|
||||
}, [isYouTube, ytApiReady, accentColor]);
|
||||
|
||||
// Prevent context menu and unwanted interactions
|
||||
const preventAction = (e: React.MouseEvent) => {
|
||||
@@ -240,19 +249,13 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
|
||||
// Jump to specific time
|
||||
const jumpToTime = (time: number) => {
|
||||
if (ytPlayerRef.current) {
|
||||
ytPlayerRef.current.seekTo(time, true);
|
||||
ytPlayerRef.current.playVideo();
|
||||
} else if (playerRef.current) {
|
||||
if (playerRef.current) {
|
||||
playerRef.current.currentTime = time;
|
||||
playerRef.current.play();
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentTime = () => {
|
||||
if (ytPlayerRef.current) {
|
||||
return ytPlayerRef.current.getCurrentTime() || 0;
|
||||
}
|
||||
return playerRef.current ? playerRef.current.currentTime : 0;
|
||||
};
|
||||
|
||||
@@ -288,22 +291,24 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
|
||||
{/* --- Strategic Overlays to Block YouTube UI --- */}
|
||||
|
||||
{/* 1. Block "Copy Link" and Title on top (always on) */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '60px',
|
||||
zIndex: 5,
|
||||
backgroundColor: 'transparent',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onContextMenu={preventAction}
|
||||
onClick={preventAction}
|
||||
title="Educational content - external links disabled"
|
||||
/>
|
||||
{/* 1. Block "Copy Link" and Title on top (only when paused or not started) */}
|
||||
{(ytPlayerState === -1 || ytPlayerState === 5 || ytPlayerState === 2 || ytPlayerState === 0) && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '60px',
|
||||
zIndex: 5,
|
||||
backgroundColor: 'transparent',
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
onContextMenu={preventAction}
|
||||
onClick={preventAction}
|
||||
title="Educational content - external links disabled"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 2. Block "YouTube" logo - Bottom right (always on) */}
|
||||
<div
|
||||
@@ -322,8 +327,8 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
title="Educational content - external links disabled"
|
||||
/>
|
||||
|
||||
{/* 3. Block "Watch on YouTube" - Bottom left (before video starts) */}
|
||||
{(ytPlayerState === -1 || ytPlayerState === 5) && (
|
||||
{/* 3. Block "Watch on YouTube" - Bottom left (before video starts or paused) */}
|
||||
{(ytPlayerState === -1 || ytPlayerState === 5 || ytPlayerState === 2) && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -341,15 +346,15 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 4. Block "More Videos" overlay - When paused */}
|
||||
{/* 4. Block "More Videos" overlay - When paused (below the controls area) */}
|
||||
{ytPlayerState === 2 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '15%',
|
||||
bottom: '80px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '30%',
|
||||
height: 'calc(100% - 140px)',
|
||||
zIndex: 6,
|
||||
backgroundColor: 'transparent',
|
||||
pointerEvents: 'auto',
|
||||
@@ -360,15 +365,15 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 5. Block related videos overlay - When video ends */}
|
||||
{/* 5. Block related videos overlay - When video ends (below the controls area) */}
|
||||
{ytPlayerState === 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '13%',
|
||||
bottom: '80px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '87%',
|
||||
height: 'calc(100% - 140px)',
|
||||
zIndex: 7,
|
||||
backgroundColor: 'transparent',
|
||||
pointerEvents: 'auto',
|
||||
|
||||
Reference in New Issue
Block a user