Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .husky/post-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-checkout "$@"
3 changes: 3 additions & 0 deletions .husky/post-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-commit "$@"
3 changes: 3 additions & 0 deletions .husky/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs post-merge "$@"
3 changes: 3 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs pre-push "$@"
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/app/components/video/AdvancedVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
MessageSquare,
} from 'lucide-react';
import { useVideoPlayer } from '../../hooks/useVideoPlayer';
import { useVideoLazyLoad } from '../../hooks/useVideoLazyLoad';
import { PlaybackControls } from './PlaybackControls';
import { VideoNotes } from './VideoNotes';
import { VideoBookmarks } from './VideoBookmarks';
Expand Down Expand Up @@ -62,6 +63,12 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTapTime, setLastTapTime] = useState(0);

const { isInViewport, isLoaded } = useVideoLazyLoad(videoRef, {
enabled: true,
threshold: 0.1,
rootMargin: '50px',
});

const [autoQuality, setAutoQuality] = useState(true);
const initialQualityValue = qualities?.[0]?.value ?? '';
const [selectedQualityValue, setSelectedQualityValue] = useState<string>(initialQualityValue);
Expand Down Expand Up @@ -365,8 +372,9 @@ export function AdvancedVideoPlayer(props: AdvancedVideoPlayerProps) {

<video
ref={videoRef}
src={activeSrc}
src={isLoaded ? activeSrc : undefined}
poster={poster}
preload="metadata"
className="w-full h-full object-contain"
onDoubleClick={toggleFullscreen}
onLoadedMetadata={() => {
Expand Down
10 changes: 9 additions & 1 deletion src/app/components/video/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { BookmarkManager } from './BookmarkManager';
import { TranscriptView } from './TranscriptView';
import { NotesTaker } from './NotesTaker';
import { useVideoPlayer } from '../../hooks/useVideoPlayer';
import { useVideoLazyLoad } from '../../hooks/useVideoLazyLoad';

interface VideoPlayerProps {
src: string;
Expand Down Expand Up @@ -51,6 +52,12 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTapTime, setLastTapTime] = useState(0);

const { isInViewport, isLoaded } = useVideoLazyLoad(videoRef, {
enabled: true,
threshold: 0.1,
rootMargin: '50px',
});

const {
isPlaying,
currentTime,
Expand Down Expand Up @@ -375,8 +382,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
{/* Video Element */}
<video
ref={videoRef}
src={src}
src={isLoaded ? src : undefined}
poster={poster}
preload="metadata"
className="w-full h-full object-contain"
onDoubleClick={toggleFullscreen}
aria-label="Course video content"
Expand Down
87 changes: 87 additions & 0 deletions src/app/hooks/useVideoLazyLoad.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client';

import { RefObject, useEffect, useState, useCallback } from 'react';

interface UseVideoLazyLoadProps {
enabled?: boolean;
threshold?: number | number[];
rootMargin?: string;
}

/**
* Hook for implementing lazy loading of video elements using Intersection Observer
* Prevents loading entire video until it's visible in the viewport
*/
export const useVideoLazyLoad = (
videoRef: RefObject<HTMLVideoElement>,
options: UseVideoLazyLoadProps = {},
) => {
const {
enabled = true,
threshold = 0.1,
rootMargin = '50px',
} = options;

const [isInViewport, setIsInViewport] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);

useEffect(() => {
if (!enabled || !videoRef.current) return;

// If already intersecting initially, mark as loaded
const video = videoRef.current;
if (!isLoaded && isInViewport) {
// Start loading by setting event listeners
setIsLoaded(true);
}

}, [enabled, isInViewport, isLoaded]);

useEffect(() => {
if (!enabled || !videoRef.current) return;

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInViewport(true);
// Mark as loaded when visible
if (!isLoaded) {
setIsLoaded(true);
}
} else {
setIsInViewport(false);
}
});
},
{
threshold,
rootMargin,
},
);

const videoElement = videoRef.current;
if (videoElement) {
observer.observe(videoElement);
}

return () => {
if (videoElement) {
observer.unobserve(videoElement);
}
observer.disconnect();
};
}, [enabled, threshold, rootMargin, isLoaded]);

const resetLazyLoad = useCallback(() => {
setIsLoaded(false);
setIsInViewport(false);
}, []);

return {
isInViewport,
isLoaded,
resetLazyLoad,
shouldLoadVideo: isLoaded && !enabled === false ? true : enabled ? isLoaded : true,
};
};
Loading