343 lines
8.6 KiB
TypeScript
Executable File
343 lines
8.6 KiB
TypeScript
Executable File
/**
|
|
* useVideoPlayer Hook
|
|
*
|
|
* Custom React hook for managing video player state and controls.
|
|
* Provides a comprehensive interface for video playback functionality.
|
|
*/
|
|
|
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
|
|
// Video player state interface
|
|
export interface VideoPlayerState {
|
|
isPlaying: boolean;
|
|
currentTime: number;
|
|
duration: number;
|
|
volume: number;
|
|
isMuted: boolean;
|
|
isFullscreen: boolean;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface UseVideoPlayerReturn {
|
|
state: VideoPlayerState;
|
|
actions: {
|
|
play: () => void;
|
|
pause: () => void;
|
|
togglePlay: () => void;
|
|
seek: (time: number) => void;
|
|
setVolume: (volume: number) => void;
|
|
toggleMute: () => void;
|
|
toggleFullscreen: () => void;
|
|
skip: (seconds: number) => void;
|
|
setPlaybackRate: (rate: number) => void;
|
|
reset: () => void;
|
|
};
|
|
ref: React.RefObject<HTMLVideoElement>;
|
|
}
|
|
|
|
interface UseVideoPlayerOptions {
|
|
autoPlay?: boolean;
|
|
loop?: boolean;
|
|
muted?: boolean;
|
|
volume?: number;
|
|
onPlay?: () => void;
|
|
onPause?: () => void;
|
|
onEnded?: () => void;
|
|
onError?: (error: string) => void;
|
|
onTimeUpdate?: (currentTime: number) => void;
|
|
onDurationChange?: (duration: number) => void;
|
|
}
|
|
|
|
export function useVideoPlayer(options: UseVideoPlayerOptions = {}) {
|
|
const {
|
|
autoPlay = false,
|
|
loop = false,
|
|
muted = false,
|
|
volume = 1,
|
|
onPlay,
|
|
onPause,
|
|
onEnded,
|
|
onError,
|
|
onTimeUpdate,
|
|
onDurationChange,
|
|
} = options;
|
|
|
|
// Video element ref
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
|
// Player state
|
|
const [state, setState] = useState<VideoPlayerState>({
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
duration: 0,
|
|
volume: volume,
|
|
isMuted: muted,
|
|
isFullscreen: false,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
/**
|
|
* Update state helper
|
|
*/
|
|
const updateState = useCallback((updates: Partial<VideoPlayerState>) => {
|
|
setState(prev => ({ ...prev, ...updates }));
|
|
}, []);
|
|
|
|
/**
|
|
* Play video
|
|
*/
|
|
const play = useCallback(async () => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
try {
|
|
updateState({ isLoading: true, error: null });
|
|
await video.play();
|
|
updateState({ isPlaying: true, isLoading: false });
|
|
onPlay?.();
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Failed to play video';
|
|
updateState({ isLoading: false, error: errorMessage });
|
|
onError?.(errorMessage);
|
|
}
|
|
}, [updateState, onPlay, onError]);
|
|
|
|
/**
|
|
* Pause video
|
|
*/
|
|
const pause = useCallback(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.pause();
|
|
updateState({ isPlaying: false });
|
|
onPause?.();
|
|
}, [updateState, onPause]);
|
|
|
|
/**
|
|
* Toggle play/pause
|
|
*/
|
|
const togglePlay = useCallback(() => {
|
|
if (state.isPlaying) {
|
|
pause();
|
|
} else {
|
|
play();
|
|
}
|
|
}, [state.isPlaying, play, pause]);
|
|
|
|
/**
|
|
* Seek to specific time
|
|
*/
|
|
const seek = useCallback((time: number) => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.currentTime = Math.max(0, Math.min(time, video.duration || 0));
|
|
}, []);
|
|
|
|
/**
|
|
* Set volume (0-1)
|
|
*/
|
|
const setVolume = useCallback((newVolume: number) => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
const clampedVolume = Math.max(0, Math.min(1, newVolume));
|
|
video.volume = clampedVolume;
|
|
updateState({ volume: clampedVolume });
|
|
}, [updateState]);
|
|
|
|
/**
|
|
* Toggle mute
|
|
*/
|
|
const toggleMute = useCallback(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.muted = !video.muted;
|
|
updateState({ isMuted: video.muted });
|
|
}, [updateState]);
|
|
|
|
/**
|
|
* Enter/exit fullscreen
|
|
*/
|
|
const toggleFullscreen = useCallback(async () => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
try {
|
|
if (!document.fullscreenElement) {
|
|
await video.requestFullscreen();
|
|
updateState({ isFullscreen: true });
|
|
} else {
|
|
await document.exitFullscreen();
|
|
updateState({ isFullscreen: false });
|
|
}
|
|
} catch (error) {
|
|
console.warn('Fullscreen not supported or failed:', error);
|
|
}
|
|
}, [updateState]);
|
|
|
|
/**
|
|
* Skip forward/backward
|
|
*/
|
|
const skip = useCallback((seconds: number) => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
const newTime = video.currentTime + seconds;
|
|
seek(newTime);
|
|
}, [seek]);
|
|
|
|
/**
|
|
* Set playback rate
|
|
*/
|
|
const setPlaybackRate = useCallback((rate: number) => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.playbackRate = Math.max(0.25, Math.min(4, rate));
|
|
}, []);
|
|
|
|
/**
|
|
* Reset video to beginning
|
|
*/
|
|
const reset = useCallback(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
video.currentTime = 0;
|
|
pause();
|
|
}, [pause]);
|
|
|
|
// Event handlers
|
|
useEffect(() => {
|
|
const video = videoRef.current;
|
|
if (!video) return;
|
|
|
|
const handleLoadStart = () => {
|
|
updateState({ isLoading: true, error: null });
|
|
|
|
// Set a timeout to detect if loading takes too long
|
|
const loadTimeout = setTimeout(() => {
|
|
if (video && video.readyState < 2) { // HAVE_CURRENT_DATA
|
|
updateState({
|
|
isLoading: false,
|
|
error: 'Video loading timeout. The video may not be accessible or there may be a network issue.'
|
|
});
|
|
}
|
|
}, 30000); // 30 second timeout
|
|
|
|
// Store timeout ID to clear it later
|
|
(video as any)._loadTimeout = loadTimeout;
|
|
};
|
|
|
|
const handleLoadedData = () => {
|
|
updateState({ isLoading: false });
|
|
|
|
// Clear the loading timeout
|
|
if ((video as any)._loadTimeout) {
|
|
clearTimeout((video as any)._loadTimeout);
|
|
(video as any)._loadTimeout = null;
|
|
}
|
|
};
|
|
|
|
const handleTimeUpdate = () => {
|
|
updateState({ currentTime: video.currentTime });
|
|
onTimeUpdate?.(video.currentTime);
|
|
};
|
|
|
|
const handleDurationChange = () => {
|
|
updateState({ duration: video.duration });
|
|
onDurationChange?.(video.duration);
|
|
};
|
|
|
|
const handlePlay = () => {
|
|
updateState({ isPlaying: true });
|
|
};
|
|
|
|
const handlePause = () => {
|
|
updateState({ isPlaying: false });
|
|
};
|
|
|
|
const handleEnded = () => {
|
|
updateState({ isPlaying: false });
|
|
onEnded?.();
|
|
};
|
|
|
|
const handleError = () => {
|
|
const errorMessage = video.error?.message || 'Video playback error';
|
|
updateState({ isLoading: false, error: errorMessage, isPlaying: false });
|
|
onError?.(errorMessage);
|
|
|
|
// Clear the loading timeout
|
|
if ((video as any)._loadTimeout) {
|
|
clearTimeout((video as any)._loadTimeout);
|
|
(video as any)._loadTimeout = null;
|
|
}
|
|
};
|
|
|
|
const handleVolumeChange = () => {
|
|
updateState({
|
|
volume: video.volume,
|
|
isMuted: video.muted
|
|
});
|
|
};
|
|
|
|
const handleFullscreenChange = () => {
|
|
updateState({ isFullscreen: !!document.fullscreenElement });
|
|
};
|
|
|
|
// Add event listeners
|
|
video.addEventListener('loadstart', handleLoadStart);
|
|
video.addEventListener('loadeddata', handleLoadedData);
|
|
video.addEventListener('timeupdate', handleTimeUpdate);
|
|
video.addEventListener('durationchange', handleDurationChange);
|
|
video.addEventListener('play', handlePlay);
|
|
video.addEventListener('pause', handlePause);
|
|
video.addEventListener('ended', handleEnded);
|
|
video.addEventListener('error', handleError);
|
|
video.addEventListener('volumechange', handleVolumeChange);
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
|
|
// Set initial properties
|
|
video.autoplay = autoPlay;
|
|
video.loop = loop;
|
|
video.muted = muted;
|
|
video.volume = volume;
|
|
|
|
// Cleanup
|
|
return () => {
|
|
video.removeEventListener('loadstart', handleLoadStart);
|
|
video.removeEventListener('loadeddata', handleLoadedData);
|
|
video.removeEventListener('timeupdate', handleTimeUpdate);
|
|
video.removeEventListener('durationchange', handleDurationChange);
|
|
video.removeEventListener('play', handlePlay);
|
|
video.removeEventListener('pause', handlePause);
|
|
video.removeEventListener('ended', handleEnded);
|
|
video.removeEventListener('error', handleError);
|
|
video.removeEventListener('volumechange', handleVolumeChange);
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
};
|
|
}, [autoPlay, loop, muted, volume, updateState, onTimeUpdate, onDurationChange, onEnded, onError]);
|
|
|
|
return {
|
|
state,
|
|
actions: {
|
|
play,
|
|
pause,
|
|
togglePlay,
|
|
seek,
|
|
setVolume,
|
|
toggleMute,
|
|
toggleFullscreen,
|
|
skip,
|
|
setPlaybackRate,
|
|
reset,
|
|
},
|
|
ref: videoRef,
|
|
};
|
|
}
|