39948-vm/frontend/docs/video-hooks-module.md
2026-07-03 16:11:24 +02:00

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.