496 lines
14 KiB
Markdown
496 lines
14 KiB
Markdown
# 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** - `useTransitionPlayback` uses `useVideoBlobUrl`, `useVideoTimeouts`
|
|
- **UI element video player** - `useVideoPlayer` uses `useVideoPlaybackCore`
|
|
- **Background video** - `useBackgroundVideoPlayback` uses 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.
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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 `waiting` and `canplay` events
|
|
- Computes buffered percentage for progress display
|
|
- Handles edge cases like `readyState` checks
|
|
|
|
---
|
|
|
|
### useVideoBlobUrl
|
|
|
|
**File:** `useVideoBlobUrl.ts` (~150 LOC)
|
|
|
|
**Purpose:** Resolves video URLs to blob URLs from preload cache for instant playback.
|
|
|
|
```typescript
|
|
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:**
|
|
1. `getReadyBlob(storageKey)` → Create fresh blob URL (avoids decoder issues)
|
|
2. `getReadyBlobUrl(storageKey)` → In-memory blob URL (O(1) lookup)
|
|
3. `getReadyBlobUrl(videoUrl)` → Fallback lookup by resolved URL
|
|
4. `getCachedBlobUrl()` → Cache API/IndexedDB lookup
|
|
5. Return original `videoUrl` → Network fetch
|
|
|
|
**Usage:**
|
|
```typescript
|
|
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`.
|
|
|
|
```typescript
|
|
interface UseVideoFirstFrameOptions {
|
|
videoRef: RefObject<HTMLVideoElement | null>;
|
|
onFirstFrame?: () => void;
|
|
enabled?: boolean;
|
|
}
|
|
|
|
interface UseVideoFirstFrameResult {
|
|
isFirstFrameRendered: boolean;
|
|
reset: () => void;
|
|
}
|
|
```
|
|
|
|
**Key Features:**
|
|
- Uses `requestVideoFrameCallback` API (Safari 15.4+, Chrome 83+)
|
|
- Falls back to `playing` event 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.
|
|
|
|
```typescript
|
|
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:**
|
|
1. First error → Mark presigned URL failed, retry with proxy URL
|
|
2. Second error → Retry with fresh presigned URL request
|
|
3. Third error → Call `onRecoveryFailed`
|
|
|
|
---
|
|
|
|
### useVideoTimeouts
|
|
|
|
**File:** `useVideoTimeouts.ts` (~80 LOC)
|
|
|
|
**Purpose:** Manages timeout IDs for video playback operations with automatic cleanup.
|
|
|
|
```typescript
|
|
interface UseVideoTimeoutsResult {
|
|
setLoadTimeout: (callback: () => void, ms: number) => void;
|
|
setPlayTimeout: (callback: () => void, ms: number) => void;
|
|
setFinishTimeout: (callback: () => void, ms: number) => void;
|
|
clearAllTimeouts: () => void;
|
|
}
|
|
```
|
|
|
|
**Usage:**
|
|
```typescript
|
|
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`.
|
|
|
|
```typescript
|
|
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 resolution
|
|
- `useVideoBufferingState` - Buffering tracking
|
|
- `useVideoFirstFrame` - Ready detection
|
|
- `useVideoErrorRecovery` - Error handling
|
|
- `useVideoEventManager` - Event listeners
|
|
|
|
---
|
|
|
|
### useVideoPlayer
|
|
|
|
**File:** `useVideoPlayer.ts` (~200 LOC)
|
|
|
|
**Purpose:** Complete video player hook for UI elements (VideoPlayerElement). Wraps `useVideoPlaybackCore` with UI-specific features.
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
const { resolvedUrl } = useVideoBlobUrl({
|
|
videoUrl,
|
|
enabled: !!videoUrl && isActive, // Only resolve when needed
|
|
});
|
|
```
|
|
|
|
### 3. Callback Refs
|
|
|
|
Expensive operations use refs to avoid re-renders:
|
|
```typescript
|
|
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:
|
|
```typescript
|
|
useEffect(() => {
|
|
// Setup event listeners
|
|
return () => {
|
|
// Cleanup
|
|
timeoutIdsRef.current.forEach(clearTimeout);
|
|
timeoutIdsRef.current.clear();
|
|
};
|
|
}, []);
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Hooks Module](./hooks-module.md) - Parent hooks documentation
|
|
- [Navigation & Smooth Transitions](./navigation-smooth-transitions.md) - Transition playback integration
|
|
- [Assets Preloading](../../documentation/assets-preloading.md) - Blob URL caching
|
|
- [Video Playback](../../documentation/video-playback.md) - 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.
|