Chore: rename api->camera-management-api and web->management-dashboard-web-app; update compose, ignore, README references
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Video Streaming Hooks - Index
|
||||
*
|
||||
* Centralized export for all video streaming hooks.
|
||||
* This makes it easy to import hooks from a single location.
|
||||
*/
|
||||
|
||||
export { useVideoList, type UseVideoListReturn } from './useVideoList';
|
||||
export { useVideoPlayer, type UseVideoPlayerReturn, type VideoPlayerState } from './useVideoPlayer';
|
||||
export { useVideoInfo, type UseVideoInfoReturn } from './useVideoInfo';
|
||||
|
||||
// Re-export types that are commonly used with hooks
|
||||
export type {
|
||||
VideoListFilters,
|
||||
VideoListSortOptions,
|
||||
} from '../types';
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* useVideoInfo Hook
|
||||
*
|
||||
* Custom React hook for fetching and managing video metadata and streaming information.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { videoApiService } from '../services/videoApi';
|
||||
import {
|
||||
type VideoInfoResponse,
|
||||
type VideoStreamingInfo,
|
||||
type VideoError,
|
||||
type LoadingState
|
||||
} from '../types';
|
||||
|
||||
export interface UseVideoInfoReturn {
|
||||
videoInfo: VideoInfoResponse | null;
|
||||
streamingInfo: VideoStreamingInfo | null;
|
||||
loading: LoadingState;
|
||||
error: VideoError | null;
|
||||
refetch: () => Promise<void>;
|
||||
clearCache: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
interface UseVideoInfoOptions {
|
||||
autoFetch?: boolean;
|
||||
cacheKey?: string;
|
||||
}
|
||||
|
||||
export function useVideoInfo(
|
||||
fileId: string | null,
|
||||
options: UseVideoInfoOptions = {}
|
||||
) {
|
||||
const { autoFetch = true, cacheKey = 'default' } = options;
|
||||
|
||||
// State
|
||||
const [videoInfo, setVideoInfo] = useState<VideoInfoResponse | null>(null);
|
||||
const [streamingInfo, setStreamingInfo] = useState<VideoStreamingInfo | null>(null);
|
||||
const [loading, setLoading] = useState<LoadingState>('idle');
|
||||
const [error, setError] = useState<VideoError | null>(null);
|
||||
|
||||
// Refs for cleanup and caching
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const cacheRef = useRef<Map<string, {
|
||||
videoInfo: VideoInfoResponse;
|
||||
streamingInfo: VideoStreamingInfo;
|
||||
timestamp: number;
|
||||
}>>(new Map());
|
||||
|
||||
const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
/**
|
||||
* Check if cached data is still valid
|
||||
*/
|
||||
const isCacheValid = useCallback((timestamp: number): boolean => {
|
||||
return Date.now() - timestamp < CACHE_DURATION;
|
||||
}, [CACHE_DURATION]);
|
||||
|
||||
/**
|
||||
* Fetch video information
|
||||
*/
|
||||
const fetchVideoInfo = useCallback(async (id: string): Promise<void> => {
|
||||
// Cancel any ongoing request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
try {
|
||||
setLoading('loading');
|
||||
setError(null);
|
||||
|
||||
// Check cache first
|
||||
const key = `${cacheKey}_${id}`;
|
||||
const cached = cacheRef.current.get(key);
|
||||
|
||||
if (cached && isCacheValid(cached.timestamp)) {
|
||||
setVideoInfo(cached.videoInfo);
|
||||
setStreamingInfo(cached.streamingInfo);
|
||||
setLoading('success');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch both video info and streaming info in parallel
|
||||
const [videoInfoResponse, streamingInfoResponse] = await Promise.all([
|
||||
videoApiService.getVideoInfo(id),
|
||||
videoApiService.getStreamingInfo(id)
|
||||
]);
|
||||
|
||||
// Check if request was aborted
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cacheRef.current.set(key, {
|
||||
videoInfo: videoInfoResponse,
|
||||
streamingInfo: streamingInfoResponse,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Update state
|
||||
setVideoInfo(videoInfoResponse);
|
||||
setStreamingInfo(streamingInfoResponse);
|
||||
setLoading('success');
|
||||
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoError: VideoError = err instanceof Error
|
||||
? { code: 'FETCH_ERROR', message: err.message, details: err }
|
||||
: { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred' };
|
||||
|
||||
setError(videoError);
|
||||
setLoading('error');
|
||||
} finally {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, [cacheKey, isCacheValid]);
|
||||
|
||||
/**
|
||||
* Refetch video information
|
||||
*/
|
||||
const refetch = useCallback(async (): Promise<void> => {
|
||||
if (!fileId) return;
|
||||
await fetchVideoInfo(fileId);
|
||||
}, [fileId, fetchVideoInfo]);
|
||||
|
||||
/**
|
||||
* Clear cache for current video
|
||||
*/
|
||||
const clearCache = useCallback((): void => {
|
||||
if (!fileId) return;
|
||||
const key = `${cacheKey}_${fileId}`;
|
||||
cacheRef.current.delete(key);
|
||||
}, [fileId, cacheKey]);
|
||||
|
||||
/**
|
||||
* Reset state
|
||||
*/
|
||||
const reset = useCallback((): void => {
|
||||
setVideoInfo(null);
|
||||
setStreamingInfo(null);
|
||||
setLoading('idle');
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
// Auto-fetch when fileId changes
|
||||
useEffect(() => {
|
||||
if (fileId && autoFetch) {
|
||||
fetchVideoInfo(fileId);
|
||||
} else if (!fileId) {
|
||||
reset();
|
||||
}
|
||||
|
||||
// Cleanup on unmount or fileId change
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [fileId, autoFetch, fetchVideoInfo, reset]);
|
||||
|
||||
// Cleanup cache periodically
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
for (const [key, value] of cacheRef.current.entries()) {
|
||||
if (!isCacheValid(value.timestamp)) {
|
||||
cacheRef.current.delete(key);
|
||||
}
|
||||
}
|
||||
}, CACHE_DURATION);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isCacheValid, CACHE_DURATION]);
|
||||
|
||||
return {
|
||||
videoInfo,
|
||||
streamingInfo,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
clearCache,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* useVideoList Hook
|
||||
*
|
||||
* Custom React hook for managing video list state, fetching, filtering, and pagination.
|
||||
* Provides a clean interface for components to interact with video data.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { videoApiService } from '../services/videoApi';
|
||||
import {
|
||||
type VideoFile,
|
||||
type VideoListParams,
|
||||
type VideoError,
|
||||
type LoadingState,
|
||||
type VideoListFilters,
|
||||
type VideoListSortOptions
|
||||
} from '../types';
|
||||
|
||||
export interface UseVideoListReturn {
|
||||
videos: VideoFile[];
|
||||
totalCount: number;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
loading: LoadingState;
|
||||
error: VideoError | null;
|
||||
refetch: () => Promise<void>;
|
||||
loadMore: () => Promise<void>;
|
||||
hasMore: boolean;
|
||||
goToPage: (page: number) => Promise<void>;
|
||||
nextPage: () => Promise<void>;
|
||||
previousPage: () => Promise<void>;
|
||||
updateFilters: (filters: VideoListFilters) => void;
|
||||
updateSort: (sortOptions: VideoListSortOptions) => void;
|
||||
clearCache: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
import { filterVideos, sortVideos } from '../utils/videoUtils';
|
||||
|
||||
interface UseVideoListOptions {
|
||||
initialParams?: VideoListParams;
|
||||
autoFetch?: boolean;
|
||||
cacheKey?: string;
|
||||
}
|
||||
|
||||
export function useVideoList(options: UseVideoListOptions = {}) {
|
||||
const {
|
||||
initialParams = {},
|
||||
autoFetch = true,
|
||||
cacheKey = 'default'
|
||||
} = options;
|
||||
|
||||
// State
|
||||
const [videos, setVideos] = useState<VideoFile[]>([]);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [loading, setLoading] = useState<LoadingState>('idle');
|
||||
const [error, setError] = useState<VideoError | null>(null);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [currentParams, setCurrentParams] = useState<VideoListParams>(initialParams);
|
||||
|
||||
// Refs for cleanup and caching
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/**
|
||||
* Fetch videos from API
|
||||
*/
|
||||
const fetchVideos = useCallback(async (
|
||||
params: VideoListParams = initialParams,
|
||||
append: boolean = false
|
||||
): Promise<void> => {
|
||||
// Cancel any ongoing request
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
try {
|
||||
setLoading('loading');
|
||||
setError(null);
|
||||
|
||||
// Fetch from API
|
||||
const response = await videoApiService.getVideos(params);
|
||||
|
||||
// Check if request was aborted
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update state
|
||||
setVideos(append ? prev => [...prev, ...response.videos] : response.videos);
|
||||
setTotalCount(response.total_count);
|
||||
|
||||
// Update pagination state
|
||||
if (response.page && response.total_pages) {
|
||||
setCurrentPage(response.page);
|
||||
setTotalPages(response.total_pages);
|
||||
setHasMore(response.has_next || false);
|
||||
} else {
|
||||
// Fallback for offset-based pagination
|
||||
setHasMore(response.videos.length === (params.limit || 50));
|
||||
}
|
||||
|
||||
setLoading('success');
|
||||
|
||||
} catch (err) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoError: VideoError = err instanceof Error
|
||||
? { code: 'FETCH_ERROR', message: err.message, details: err }
|
||||
: { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred' };
|
||||
|
||||
setError(videoError);
|
||||
setLoading('error');
|
||||
} finally {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}, [initialParams]);
|
||||
|
||||
/**
|
||||
* Refetch videos with current page
|
||||
*/
|
||||
const refetch = useCallback(async (): Promise<void> => {
|
||||
const currentParams = {
|
||||
...initialParams,
|
||||
page: currentPage,
|
||||
limit: initialParams.limit || 20,
|
||||
};
|
||||
await fetchVideos(currentParams, false);
|
||||
}, [fetchVideos, initialParams, currentPage]);
|
||||
|
||||
/**
|
||||
* Load more videos (pagination) - for backward compatibility
|
||||
*/
|
||||
const loadMore = useCallback(async (): Promise<void> => {
|
||||
if (!hasMore || loading === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = videos.length;
|
||||
const params = { ...initialParams, offset };
|
||||
await fetchVideos(params, true);
|
||||
}, [hasMore, loading, videos.length, initialParams, fetchVideos]);
|
||||
|
||||
/**
|
||||
* Go to specific page
|
||||
*/
|
||||
const goToPage = useCallback(async (page: number): Promise<void> => {
|
||||
if (page < 1 || (totalPages > 0 && page > totalPages) || loading === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = { ...currentParams, page, limit: currentParams.limit || 20 };
|
||||
setCurrentParams(params);
|
||||
await fetchVideos(params, false);
|
||||
}, [currentParams, totalPages, loading, fetchVideos]);
|
||||
|
||||
/**
|
||||
* Go to next page
|
||||
*/
|
||||
const nextPage = useCallback(async (): Promise<void> => {
|
||||
if (currentPage < totalPages) {
|
||||
await goToPage(currentPage + 1);
|
||||
}
|
||||
}, [currentPage, totalPages, goToPage]);
|
||||
|
||||
/**
|
||||
* Go to previous page
|
||||
*/
|
||||
const previousPage = useCallback(async (): Promise<void> => {
|
||||
if (currentPage > 1) {
|
||||
await goToPage(currentPage - 1);
|
||||
}
|
||||
}, [currentPage, goToPage]);
|
||||
|
||||
/**
|
||||
* Update filters and refetch
|
||||
*/
|
||||
const updateFilters = useCallback((filters: VideoListFilters): void => {
|
||||
const newParams: VideoListParams = {
|
||||
...initialParams,
|
||||
camera_name: filters.cameraName,
|
||||
start_date: filters.dateRange?.start,
|
||||
end_date: filters.dateRange?.end,
|
||||
page: 1, // Reset to first page when filters change
|
||||
limit: initialParams.limit || 20,
|
||||
};
|
||||
|
||||
setCurrentParams(newParams);
|
||||
fetchVideos(newParams, false);
|
||||
}, [initialParams, fetchVideos]);
|
||||
|
||||
/**
|
||||
* Update sort options and refetch
|
||||
*/
|
||||
const updateSort = useCallback((sortOptions: VideoListSortOptions): void => {
|
||||
// Since the API doesn't support sorting, we'll sort locally
|
||||
setVideos(prev => sortVideos(prev, sortOptions.field, sortOptions.direction));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Clear cache (placeholder for future caching implementation)
|
||||
*/
|
||||
const clearCache = useCallback((): void => {
|
||||
// TODO: Implement cache clearing when caching is added
|
||||
console.log('Cache cleared');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Reset to initial state
|
||||
*/
|
||||
const reset = useCallback((): void => {
|
||||
setVideos([]);
|
||||
setTotalCount(0);
|
||||
setCurrentPage(1);
|
||||
setTotalPages(0);
|
||||
setLoading('idle');
|
||||
setError(null);
|
||||
setHasMore(true);
|
||||
}, []);
|
||||
|
||||
// Auto-fetch on mount only
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchVideos(initialParams, false);
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []); // Empty dependency array - only run once on mount
|
||||
|
||||
return {
|
||||
videos,
|
||||
totalCount,
|
||||
currentPage,
|
||||
totalPages,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
hasMore,
|
||||
// Pagination methods
|
||||
goToPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
// Additional utility methods
|
||||
updateFilters,
|
||||
updateSort,
|
||||
clearCache,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user