14 KiB
Video Hooks Module
Overview
The Video Hooks module provides 8 primitive hooks for video playback management. These hooks follow a composition pattern - smaller primitive hooks are combined to build complex video playback scenarios.
Location: frontend/src/hooks/video/
Total Files: 9 TypeScript files (8 hooks + 1 index file)
Architecture Pattern: Primitive hooks that can be composed for different use cases:
- Transition video playback -
useTransitionPlaybackusesuseVideoBlobUrl,useVideoTimeouts - UI element video player -
useVideoPlayerusesuseVideoPlaybackCore - Background video -
useBackgroundVideoPlaybackuses primitives directly
Hook Hierarchy
┌─────────────────────────────┐
│ Application Hooks │
│ (useTransitionPlayback, │
│ useBackgroundVideoPlayback)│
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Composite Hook │
│ useVideoPlaybackCore │
│ useVideoPlayer │
└──────────────┬──────────────┘
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
┌───────▼───────┐ ┌─────────────▼─────────────┐ ┌─────────▼─────────┐
│ useVideoBlobUrl│ │ useVideoBufferingState │ │ useVideoFirstFrame│
│ URL resolution │ │ Buffering detection │ │ First frame detect│
└───────────────┘ └───────────────────────────┘ └───────────────────┘
│ │ │
┌───────▼───────┐ ┌─────────────▼─────────────┐ ┌─────────▼─────────┐
│useVideoEvent │ │ useVideoErrorRecovery │ │ useVideoTimeouts │
│Manager │ │ Error handling + retry │ │ Timeout management│
└───────────────┘ └───────────────────────────┘ └───────────────────┘
Primitive Hooks
useVideoEventManager
File: useVideoEventManager.ts (~90 LOC)
Purpose: Centralizes video element event listener management with automatic cleanup.
type VideoEventType = 'play' | 'pause' | 'ended' | 'timeupdate' | 'waiting' |
'canplay' | 'playing' | 'loadedmetadata' | 'error' | 'progress';
interface UseVideoEventManagerOptions {
videoRef: RefObject<HTMLVideoElement | null>;
handlers: Partial<Record<VideoEventType, () => void>>;
enabled?: boolean;
}
const useVideoEventManager = (options: UseVideoEventManagerOptions): void;
Usage:
useVideoEventManager({
videoRef,
handlers: {
playing: () => setIsPlaying(true),
ended: () => setIsPlaying(false),
waiting: () => setIsBuffering(true),
canplay: () => setIsBuffering(false),
},
enabled: !!videoUrl,
});
useVideoBufferingState
File: useVideoBufferingState.ts (~180 LOC)
Purpose: Tracks video buffering state with debouncing to avoid UI flicker.
interface UseVideoBufferingStateOptions {
videoRef: RefObject<HTMLVideoElement | null>;
onBufferingChange?: (isBuffering: boolean) => void;
debounceMs?: number; // Default: 100ms
}
interface UseVideoBufferingStateResult {
isBuffering: boolean;
isWaitingForData: boolean;
bufferedRanges: TimeRanges | null;
bufferedPercent: number;
}
Key Features:
- Debounced state changes to prevent rapid UI updates
- Tracks
waitingandcanplayevents - Computes buffered percentage for progress display
- Handles edge cases like
readyStatechecks
useVideoBlobUrl
File: useVideoBlobUrl.ts (~150 LOC)
Purpose: Resolves video URLs to blob URLs from preload cache for instant playback.
interface PreloadCacheProvider {
getReadyBlobUrl?: (url: string) => string | null;
getReadyBlob?: (url: string) => Blob | null;
getCachedBlobUrl?: (url: string) => Promise<string | null>;
}
interface UseVideoBlobUrlOptions {
videoUrl: string | undefined;
storageKey?: string;
preloadCache?: PreloadCacheProvider;
enabled?: boolean;
}
interface UseVideoBlobUrlResult {
resolvedUrl: string | null;
isFromCache: boolean;
isResolving: boolean;
}
Resolution Priority:
getReadyBlob(storageKey)→ Create fresh blob URL (avoids decoder issues)getReadyBlobUrl(storageKey)→ In-memory blob URL (O(1) lookup)getReadyBlobUrl(videoUrl)→ Fallback lookup by resolved URLgetCachedBlobUrl()→ Cache API/IndexedDB lookup- Return original
videoUrl→ Network fetch
Usage:
const { resolvedUrl, isFromCache } = useVideoBlobUrl({
videoUrl: transition.videoUrl,
storageKey: transition.storageKey,
preloadCache: preloadOrchestrator,
enabled: !!transition,
});
useVideoFirstFrame
File: useVideoFirstFrame.ts (~130 LOC)
Purpose: Detects when the first video frame has been rendered using requestVideoFrameCallback.
interface UseVideoFirstFrameOptions {
videoRef: RefObject<HTMLVideoElement | null>;
onFirstFrame?: () => void;
enabled?: boolean;
}
interface UseVideoFirstFrameResult {
isFirstFrameRendered: boolean;
reset: () => void;
}
Key Features:
- Uses
requestVideoFrameCallbackAPI (Safari 15.4+, Chrome 83+) - Falls back to
playingevent for older browsers - Provides
reset()for reuse across video changes
useVideoErrorRecovery
File: useVideoErrorRecovery.ts (~160 LOC)
Purpose: Handles video errors with automatic retry and presigned URL fallback.
interface UseVideoErrorRecoveryOptions {
videoRef: RefObject<HTMLVideoElement | null>;
currentUrl: string | null;
storageKey?: string;
maxRetries?: number; // Default: 2
onRecoveryAttempt?: (newUrl: string) => void;
onRecoveryFailed?: (error: string) => void;
}
interface UseVideoErrorRecoveryResult {
errorCount: number;
lastError: string | null;
isRecovering: boolean;
currentResolvedUrl: string | null;
}
Recovery Strategy:
- First error → Mark presigned URL failed, retry with proxy URL
- Second error → Retry with fresh presigned URL request
- Third error → Call
onRecoveryFailed
useVideoTimeouts
File: useVideoTimeouts.ts (~80 LOC)
Purpose: Manages timeout IDs for video playback operations with automatic cleanup.
interface UseVideoTimeoutsResult {
setLoadTimeout: (callback: () => void, ms: number) => void;
setPlayTimeout: (callback: () => void, ms: number) => void;
setFinishTimeout: (callback: () => void, ms: number) => void;
clearAllTimeouts: () => void;
}
Usage:
const timeouts = useVideoTimeouts();
// Set load timeout
timeouts.setLoadTimeout(() => {
console.warn('Video load timed out');
onError?.('load-timeout');
}, 10000);
// Clear on success
video.oncanplay = () => timeouts.clearAllTimeouts();
Composite Hooks
useVideoPlaybackCore
File: useVideoPlaybackCore.ts (~280 LOC)
Purpose: Core video playback logic combining multiple primitives. Used by useVideoPlayer.
interface UseVideoPlaybackCoreOptions {
videoRef: RefObject<HTMLVideoElement | null>;
videoUrl: string | undefined;
storageKey?: string;
autoplay?: boolean;
loop?: boolean;
muted?: boolean;
startTime?: number | null;
endTime?: number | null;
preloadCache?: PreloadCacheProvider;
onReady?: () => void;
onEnded?: () => void;
onError?: (error: string) => void;
}
interface UseVideoPlaybackCoreResult {
// State
isReady: boolean;
isPlaying: boolean;
isBuffering: boolean;
isEnded: boolean;
currentTime: number;
duration: number;
// Resolved URL
resolvedUrl: string | null;
isFromCache: boolean;
// Controls
play: () => Promise<void>;
pause: () => void;
seek: (time: number) => void;
reset: () => void;
}
Composes:
useVideoBlobUrl- URL resolutionuseVideoBufferingState- Buffering trackinguseVideoFirstFrame- Ready detectionuseVideoErrorRecovery- Error handlinguseVideoEventManager- Event listeners
useVideoPlayer
File: useVideoPlayer.ts (~200 LOC)
Purpose: Complete video player hook for UI elements (VideoPlayerElement). Wraps useVideoPlaybackCore with UI-specific features.
interface UseVideoPlayerOptions extends UseVideoPlaybackCoreOptions {
// Inherited from UseVideoPlaybackCoreOptions
showControls?: boolean;
posterUrl?: string;
onFirstPlay?: () => void;
}
interface UseVideoPlayerResult extends UseVideoPlaybackCoreResult {
// Additional UI state
hasPlayedOnce: boolean;
showPoster: boolean;
progress: number; // 0-100
// UI Controls
togglePlay: () => void;
toggleMute: () => void;
setVolume: (volume: number) => void;
}
Usage in VideoPlayerElement:
const player = useVideoPlayer({
videoRef,
videoUrl: element.videoUrl,
storageKey: element.videoStoragePath,
autoplay: element.autoplay ?? false,
loop: element.loop ?? false,
muted: element.muted ?? true,
preloadCache: preloadOrchestrator,
onReady: () => console.log('Video ready'),
});
return (
<div>
<video ref={videoRef} src={player.resolvedUrl} />
{player.showPoster && <img src={posterUrl} />}
<button onClick={player.togglePlay}>
{player.isPlaying ? 'Pause' : 'Play'}
</button>
<progress value={player.progress} max={100} />
</div>
);
Index Exports
File: index.ts (~60 LOC)
// Primitive hooks
export { useVideoEventManager } from './useVideoEventManager';
export { useVideoBufferingState } from './useVideoBufferingState';
export { useVideoBlobUrl } from './useVideoBlobUrl';
export { useVideoFirstFrame } from './useVideoFirstFrame';
export { useVideoErrorRecovery } from './useVideoErrorRecovery';
export { useVideoTimeouts } from './useVideoTimeouts';
// Composite hooks
export { useVideoPlaybackCore } from './useVideoPlaybackCore';
export { useVideoPlayer } from './useVideoPlayer';
// Types
export type { PreloadCacheProvider } from './useVideoBlobUrl';
Integration with useTransitionPlayback
The useTransitionPlayback hook uses video primitives for transition video playback:
// useTransitionPlayback.ts
import { useVideoBlobUrl, useVideoTimeouts, type PreloadCacheProvider } from './video';
export function useTransitionPlayback(options) {
const { transition, videoRef, preloadCache } = options;
// Resolve blob URL from cache
const { resolvedUrl, isFromCache } = useVideoBlobUrl({
videoUrl: transition?.videoUrl,
storageKey: transition?.storageKey,
preloadCache,
enabled: !!transition,
});
// For back navigation, also resolve reverse video
const { resolvedUrl: reverseUrl } = useVideoBlobUrl({
videoUrl: transition?.reverseVideoUrl,
storageKey: transition?.reverseStorageKey,
preloadCache,
enabled: transition?.isBack && !!transition?.reverseVideoUrl,
});
// Manage timeouts
const timeouts = useVideoTimeouts();
// ... playback logic
}
Design Patterns
1. Primitive Composition
Each primitive hook handles one concern:
- Event management →
useVideoEventManager - Buffering state →
useVideoBufferingState - URL resolution →
useVideoBlobUrl
Composite hooks combine primitives:
function useVideoPlaybackCore(options) {
const { resolvedUrl } = useVideoBlobUrl(options);
const { isBuffering } = useVideoBufferingState({ videoRef });
const { isFirstFrameRendered } = useVideoFirstFrame({ videoRef });
// ... combine state
}
2. Enabled Pattern
All hooks accept an enabled prop to conditionally activate:
const { resolvedUrl } = useVideoBlobUrl({
videoUrl,
enabled: !!videoUrl && isActive, // Only resolve when needed
});
3. Callback Refs
Expensive operations use refs to avoid re-renders:
const timeoutIdsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
const setLoadTimeout = useCallback((cb, ms) => {
const id = setTimeout(cb, ms);
timeoutIdsRef.current.set('load', id);
}, []);
4. Cleanup on Unmount
All hooks properly cleanup:
useEffect(() => {
// Setup event listeners
return () => {
// Cleanup
timeoutIdsRef.current.forEach(clearTimeout);
timeoutIdsRef.current.clear();
};
}, []);
Related Documentation
- Hooks Module - Parent hooks documentation
- Navigation & Smooth Transitions - Transition playback integration
- Assets Preloading - Blob URL caching
- Video Playback - Background video implementation
Summary
| Hook | Type | LOC | Purpose |
|---|---|---|---|
useVideoEventManager |
Primitive | ~90 | Event listener management |
useVideoBufferingState |
Primitive | ~180 | Buffering state tracking |
useVideoBlobUrl |
Primitive | ~150 | URL resolution from cache |
useVideoFirstFrame |
Primitive | ~130 | First frame detection |
useVideoErrorRecovery |
Primitive | ~160 | Error handling + retry |
useVideoTimeouts |
Primitive | ~80 | Timeout management |
useVideoPlaybackCore |
Composite | ~280 | Core playback logic |
useVideoPlayer |
Composite | ~200 | UI video player |
| Total | ~1,270 |
The video hooks module provides reusable primitives that can be composed for any video playback scenario - from transition videos to background videos to UI element players.