Skip to content

Commit 0ec701c

Browse files
Merge pull request #503 from CodebilityDev/applicants/onboarding/videoplayer-buffering-issues
Fixed Video Player Buffering Issues
2 parents 6322598 + 8d78da2 commit 0ec701c

1 file changed

Lines changed: 159 additions & 24 deletions

File tree

‎apps/codebility/app/applicant/onboarding/_components/VideoPlayer.tsx‎

Lines changed: 159 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ export default function VideoPlayer({
2525
const [hasWatched, setHasWatched] = useState(false);
2626
const [currentTime, setCurrentTime] = useState(0);
2727
const [duration, setDuration] = useState(0);
28+
const [isBuffering, setIsBuffering] = useState(false);
29+
const [error, setError] = useState<string | null>(null);
30+
const [loadProgress, setLoadProgress] = useState(0);
2831

29-
// Use ref to track if we've already saved progress
3032
const progressSavedRef = useRef(false);
3133

3234
const saveVideoProgress = async () => {
@@ -35,16 +37,20 @@ export default function VideoPlayer({
3537

3638
progressSavedRef.current = true;
3739

38-
await updateVideoProgress({
39-
applicantId,
40-
videoNumber,
41-
watchedDuration: video.currentTime,
42-
totalDuration: video.duration,
43-
completed: true,
44-
});
45-
46-
// Notify parent component to refresh progress state (to unlock Next button)
47-
onVideoComplete();
40+
try {
41+
await updateVideoProgress({
42+
applicantId,
43+
videoNumber,
44+
watchedDuration: video.currentTime,
45+
totalDuration: video.duration,
46+
completed: true,
47+
});
48+
onVideoComplete();
49+
} catch (err) {
50+
console.error("Failed to save progress:", err);
51+
// Reset so user can try again
52+
progressSavedRef.current = false;
53+
}
4854
};
4955

5056
useEffect(() => {
@@ -56,41 +62,105 @@ export default function VideoPlayer({
5662
const percentWatched = (video.currentTime / video.duration) * 100;
5763
setProgress(percentWatched);
5864

59-
// Mark as watched if they've seen 98% or more (to unlock Next button)
60-
// But don't auto-advance - user must click Next manually
6165
if (percentWatched >= 98 && !progressSavedRef.current) {
6266
setHasWatched(true);
63-
// Save progress in background without triggering navigation
6467
saveVideoProgress();
6568
}
6669
};
6770

6871
const handleLoadedMetadata = () => {
6972
setDuration(video.duration);
73+
setError(null);
74+
};
75+
76+
const handlePlay = () => {
77+
setIsPlaying(true);
78+
setIsBuffering(false);
7079
};
7180

72-
const handlePlay = () => setIsPlaying(true);
7381
const handlePause = () => setIsPlaying(false);
7482

83+
// NEW: Buffering detection
84+
const handleWaiting = () => {
85+
setIsBuffering(true);
86+
};
87+
88+
const handleCanPlay = () => {
89+
setIsBuffering(false);
90+
};
91+
92+
const handlePlaying = () => {
93+
setIsBuffering(false);
94+
};
95+
96+
// NEW: Error handling
97+
const handleError = () => {
98+
setError("Failed to load video. Please refresh the page or try again later.");
99+
setIsBuffering(false);
100+
};
101+
102+
// NEW: Track loading progress
103+
const handleProgress = () => {
104+
if (video.buffered.length > 0) {
105+
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
106+
const loadPercent = (bufferedEnd / video.duration) * 100;
107+
setLoadProgress(loadPercent);
108+
}
109+
};
110+
111+
// NEW: Stalled detection (when download has stopped)
112+
const handleStalled = () => {
113+
console.warn("Video download stalled");
114+
setIsBuffering(true);
115+
};
116+
117+
const handleSuspend = () => {
118+
// Browser stopped fetching media data
119+
console.log("Video loading suspended");
120+
};
121+
75122
video.addEventListener("timeupdate", handleTimeUpdate);
76123
video.addEventListener("loadedmetadata", handleLoadedMetadata);
77124
video.addEventListener("play", handlePlay);
78125
video.addEventListener("pause", handlePause);
126+
video.addEventListener("waiting", handleWaiting);
127+
video.addEventListener("canplay", handleCanPlay);
128+
video.addEventListener("playing", handlePlaying);
129+
video.addEventListener("error", handleError);
130+
video.addEventListener("progress", handleProgress);
131+
video.addEventListener("stalled", handleStalled);
132+
video.addEventListener("suspend", handleSuspend);
79133

80134
return () => {
81135
video.removeEventListener("timeupdate", handleTimeUpdate);
82136
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
83137
video.removeEventListener("play", handlePlay);
84138
video.removeEventListener("pause", handlePause);
139+
video.removeEventListener("waiting", handleWaiting);
140+
video.removeEventListener("canplay", handleCanPlay);
141+
video.removeEventListener("playing", handlePlaying);
142+
video.removeEventListener("error", handleError);
143+
video.removeEventListener("progress", handleProgress);
144+
video.removeEventListener("stalled", handleStalled);
145+
video.removeEventListener("suspend", handleSuspend);
85146
};
86-
}, []); // Empty dependency array - only set up listeners once
147+
}, []);
87148

88149
const formatTime = (seconds: number) => {
150+
if (!isFinite(seconds)) return "0:00";
89151
const mins = Math.floor(seconds / 60);
90152
const secs = Math.floor(seconds % 60);
91153
return `${mins}:${secs.toString().padStart(2, "0")}`;
92154
};
93155

156+
const handleRetry = () => {
157+
setError(null);
158+
setIsBuffering(false);
159+
if (videoRef.current) {
160+
videoRef.current.load();
161+
}
162+
};
163+
94164
if (!canWatch) {
95165
return (
96166
<div className="flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-700 bg-gray-900/20 p-12 text-center">
@@ -123,11 +193,56 @@ export default function VideoPlayer({
123193
className="h-full w-full"
124194
controls
125195
controlsList="nodownload"
126-
onContextMenu={(e) => e.preventDefault()} // Disable right-click
196+
onContextMenu={(e) => e.preventDefault()}
197+
preload="auto" // NEW: Start loading immediately
198+
playsInline // NEW: Better mobile support
127199
>
128200
<source src={videoUrl} type="video/mp4" />
129201
Your browser does not support the video tag.
130202
</video>
203+
204+
{/* NEW: Buffering overlay */}
205+
{isBuffering && !error && (
206+
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
207+
<div className="flex flex-col items-center gap-3">
208+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-600 border-t-customTeal"></div>
209+
<p className="text-sm text-white">Loading video...</p>
210+
{loadProgress > 0 && loadProgress < 100 && (
211+
<p className="text-xs text-gray-400">
212+
{Math.round(loadProgress)}% buffered
213+
</p>
214+
)}
215+
</div>
216+
</div>
217+
)}
218+
219+
{/* NEW: Error overlay */}
220+
{error && (
221+
<div className="absolute inset-0 flex items-center justify-center bg-black/80">
222+
<div className="flex flex-col items-center gap-4 px-6 text-center">
223+
<svg
224+
className="h-12 w-12 text-red-500"
225+
fill="none"
226+
viewBox="0 0 24 24"
227+
stroke="currentColor"
228+
>
229+
<path
230+
strokeLinecap="round"
231+
strokeLinejoin="round"
232+
strokeWidth={2}
233+
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
234+
/>
235+
</svg>
236+
<div>
237+
<h3 className="text-lg font-semibold text-white">Video Error</h3>
238+
<p className="mt-2 text-sm text-gray-300">{error}</p>
239+
</div>
240+
<Button onClick={handleRetry} className="mt-2">
241+
Retry
242+
</Button>
243+
</div>
244+
</div>
245+
)}
131246
</div>
132247

133248
{/* Progress Bar */}
@@ -138,22 +253,42 @@ export default function VideoPlayer({
138253
{formatTime(currentTime)} / {formatTime(duration)}
139254
</span>
140255
</div>
256+
257+
{/* Watch Progress */}
141258
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-800">
142259
<div
143260
className="h-full bg-gradient-to-r from-customTeal via-customBlue-100 to-customViolet-100 transition-all duration-300"
144261
style={{ width: `${progress}%` }}
145262
/>
146263
</div>
264+
265+
{/* NEW: Buffer Progress (shows how much has loaded) */}
266+
{loadProgress > 0 && loadProgress < 100 && (
267+
<div className="space-y-1">
268+
<span className="text-xs text-gray-500">
269+
Buffered: {Math.round(loadProgress)}%
270+
</span>
271+
<div className="h-1 w-full overflow-hidden rounded-full bg-gray-800">
272+
<div
273+
className="h-full bg-gray-600 transition-all duration-300"
274+
style={{ width: `${loadProgress}%` }}
275+
/>
276+
</div>
277+
</div>
278+
)}
147279
</div>
148280

149-
{/* Status Indicator */}
150-
{hasWatched && (
281+
{/* Status Indicators */}
282+
{isBuffering && !error && (
283+
<div className="flex items-center justify-center gap-2 rounded-lg bg-yellow-900/20 border border-yellow-500 p-3 text-yellow-400">
284+
<div className="h-4 w-4 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
285+
<span className="text-sm font-medium">Buffering video...</span>
286+
</div>
287+
)}
288+
289+
{hasWatched && !error && (
151290
<div className="flex items-center justify-center gap-2 rounded-lg bg-green-900/20 border border-green-500 p-3 text-green-400">
152-
<svg
153-
className="h-5 w-5"
154-
fill="currentColor"
155-
viewBox="0 0 20 20"
156-
>
291+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
157292
<path
158293
fillRule="evenodd"
159294
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"

0 commit comments

Comments
 (0)