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:
dwindown
2026-01-01 01:51:47 +07:00
parent b2a5d2fca6
commit a1acbd9395

View File

@@ -113,6 +113,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
iv_load_policy: 3, iv_load_policy: 3,
modestbranding: 1, modestbranding: 1,
controls: 0, controls: 0,
customControls: true,
}, },
controls: [ controls: [
'play-large', 'play-large',
@@ -145,6 +146,37 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
// Create YouTube Player instance // Create YouTube Player instance
const ytPlayer = new window.YT.Player(iframe, { const ytPlayer = new window.YT.Player(iframe, {
events: { 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) => { onStateChange: (event: any) => {
const state = event.data; const state = event.data;
setYtPlayerState(state); setYtPlayerState(state);
@@ -160,36 +192,6 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
}); });
ytPlayerRef.current = ytPlayer; 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 // Initialize YouTube player when API is ready
@@ -213,6 +215,13 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
.plyr__progress__buffer { .plyr__progress__buffer {
color: ${accentColor}40 !important; 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); document.head.appendChild(style);
@@ -230,7 +239,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
} }
player.destroy(); player.destroy();
}; };
}, [isYouTube, ytApiReady, accentColor, chapters, currentChapterIndex, onChapterChange, onTimeUpdate]); }, [isYouTube, ytApiReady, accentColor]);
// Prevent context menu and unwanted interactions // Prevent context menu and unwanted interactions
const preventAction = (e: React.MouseEvent) => { const preventAction = (e: React.MouseEvent) => {
@@ -240,19 +249,13 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
// Jump to specific time // Jump to specific time
const jumpToTime = (time: number) => { const jumpToTime = (time: number) => {
if (ytPlayerRef.current) { if (playerRef.current) {
ytPlayerRef.current.seekTo(time, true);
ytPlayerRef.current.playVideo();
} else if (playerRef.current) {
playerRef.current.currentTime = time; playerRef.current.currentTime = time;
playerRef.current.play(); playerRef.current.play();
} }
}; };
const getCurrentTime = () => { const getCurrentTime = () => {
if (ytPlayerRef.current) {
return ytPlayerRef.current.getCurrentTime() || 0;
}
return playerRef.current ? playerRef.current.currentTime : 0; return playerRef.current ? playerRef.current.currentTime : 0;
}; };
@@ -288,22 +291,24 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
{/* --- Strategic Overlays to Block YouTube UI --- */} {/* --- Strategic Overlays to Block YouTube UI --- */}
{/* 1. Block "Copy Link" and Title on top (always on) */} {/* 1. Block "Copy Link" and Title on top (only when paused or not started) */}
<div {(ytPlayerState === -1 || ytPlayerState === 5 || ytPlayerState === 2 || ytPlayerState === 0) && (
style={{ <div
position: 'absolute', style={{
top: 0, position: 'absolute',
left: 0, top: 0,
right: 0, left: 0,
height: '60px', right: 0,
zIndex: 5, height: '60px',
backgroundColor: 'transparent', zIndex: 5,
pointerEvents: 'auto', backgroundColor: 'transparent',
}} pointerEvents: 'auto',
onContextMenu={preventAction} }}
onClick={preventAction} onContextMenu={preventAction}
title="Educational content - external links disabled" onClick={preventAction}
/> title="Educational content - external links disabled"
/>
)}
{/* 2. Block "YouTube" logo - Bottom right (always on) */} {/* 2. Block "YouTube" logo - Bottom right (always on) */}
<div <div
@@ -322,8 +327,8 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
title="Educational content - external links disabled" title="Educational content - external links disabled"
/> />
{/* 3. Block "Watch on YouTube" - Bottom left (before video starts) */} {/* 3. Block "Watch on YouTube" - Bottom left (before video starts or paused) */}
{(ytPlayerState === -1 || ytPlayerState === 5) && ( {(ytPlayerState === -1 || ytPlayerState === 5 || ytPlayerState === 2) && (
<div <div
style={{ style={{
position: 'absolute', 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 && ( {ytPlayerState === 2 && (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: '15%', bottom: '80px',
left: 0, left: 0,
right: 0, right: 0,
height: '30%', height: 'calc(100% - 140px)',
zIndex: 6, zIndex: 6,
backgroundColor: 'transparent', backgroundColor: 'transparent',
pointerEvents: 'auto', 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 && ( {ytPlayerState === 0 && (
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: '13%', bottom: '80px',
left: 0, left: 0,
right: 0, right: 0,
height: '87%', height: 'calc(100% - 140px)',
zIndex: 7, zIndex: 7,
backgroundColor: 'transparent', backgroundColor: 'transparent',
pointerEvents: 'auto', pointerEvents: 'auto',