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

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 - 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.

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 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.

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:

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 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.

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.

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 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.

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();
  };
}, []);


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.