Files
ImageApi/specs/001-video-wall/research.md
Cameron 19c099360e Add VideoWall feature: server-side preview clip generation and mobile grid view
Backend (Rust/Actix-web):
- Add video_preview_clips table and PreviewDao for tracking preview generation
- Add ffmpeg preview clip generator: 10 equally-spaced 1s segments at 480p with CUDA NVENC auto-detection
- Add PreviewClipGenerator actor with semaphore-limited concurrent processing
- Add GET /video/preview and POST /video/preview/status endpoints
- Extend file watcher to detect and queue previews for new videos
- Use relative paths consistently for DB storage (matching EXIF convention)

Frontend (React Native/Expo):
- Add VideoWall grid view with 2-3 column layout of looping preview clips
- Add VideoWallItem component with ActiveVideoPlayer sub-component for lifecycle management
- Add useVideoWall hook for batch status polling with 5s refresh
- Add navigation button in grid header (visible when videos exist)
- Use TextureView surface type to fix Android z-ordering issues
- Optimize memory: players only mount while visible via FlatList windowSize
- Configure ExoPlayer buffer options and caching for short clips
- Tap to toggle audio focus, long press to open in full viewer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:40:17 -05:00

6.0 KiB

Research: VideoWall

R1: FFmpeg Preview Clip Generation Strategy

Decision: Use ffmpeg's select filter with segment-based extraction, extending the existing OverviewVideo pattern in src/video/ffmpeg.rs.

Rationale: The codebase already has a nearly identical pattern at src/video/ffmpeg.rs using select='lt(mod(t,{interval}),1)' which selects 1-second frames at evenly spaced intervals across the video duration. The existing pattern outputs GIF; we adapt it to output MP4 at 480p with audio.

Approach:

  1. Use ffprobe to get video duration (existing get_video_duration() pattern)
  2. Calculate interval: duration / 10 (or fewer segments for short videos)
  3. Use ffmpeg with:
    • Video filter: select='lt(mod(t,{interval}),1)',setpts=N/FRAME_RATE/TB,scale=-2:480
    • Audio filter: aselect='lt(mod(t,{interval}),1)',asetpts=N/SR/TB
    • Output: MP4 with H.264 video + AAC audio
    • CRF 28 (lower quality acceptable for previews, reduces file size)
    • Preset: veryfast (matches existing HLS transcoding pattern)

Alternatives considered:

  • Generating separate segment files and concatenating: More complex, no benefit over select filter
  • Using GIF output: Rejected per clarification — MP4 is 5-10x smaller with better quality
  • Stream copy (no transcode): Not possible since we're extracting non-contiguous segments

R2: Preview Clip Storage and Caching

Decision: Store preview clips on filesystem in a dedicated PREVIEW_CLIPS_DIRECTORY mirroring the source directory structure (same pattern as THUMBNAILS and GIFS_DIRECTORY).

Rationale: The project already uses this directory-mirroring pattern for thumbnails and GIF previews. It's simple, requires no database for file lookup (path is deterministic), and integrates naturally with the existing file watcher cleanup logic.

Storage path formula: {PREVIEW_CLIPS_DIRECTORY}/{relative_path_from_BASE_PATH}.mp4

  • Example: Video at BASE_PATH/2024/vacation.mov → Preview at PREVIEW_CLIPS_DIRECTORY/2024/vacation.mp4

Alternatives considered:

  • Database BLOBs: Too large, not suited for binary video files
  • Content-addressed storage (hash-based): Unnecessary complexity for single-user system
  • Flat directory with UUID names: Loses the intuitive mapping that thumbnails/GIFs use

R3: Preview Generation Status Tracking

Decision: Track generation status in SQLite via a new video_preview_clips table with Diesel ORM, following the existing DAO pattern.

Rationale: The batch status endpoint (FR-004) needs to efficiently check which previews are ready for a list of video paths. A database table is the right tool — it supports batch queries (existing get_exif_batch() pattern), survives restarts, and tracks failure states. The file watcher already uses batch DB queries to detect unprocessed files.

Status values: pending, processing, complete, failed

Alternatives considered:

  • Filesystem-only (check if .mp4 exists): Cannot track processing or failed states; race conditions on concurrent requests
  • In-memory HashMap: Lost on restart; doesn't support batch queries efficiently across actor boundaries

R4: Concurrent Generation Limits

Decision: Use Arc<Semaphore> with a limit of 2 concurrent ffmpeg preview generation processes, matching the existing PlaylistGenerator pattern.

Rationale: The PlaylistGenerator actor in src/video/actors.rs already uses this exact pattern to limit concurrent ffmpeg processes. Preview generation is CPU-intensive (transcoding), so limiting concurrency prevents server overload. The semaphore pattern is proven in this codebase.

Alternatives considered:

  • Unbounded concurrency: Would overwhelm the server with many simultaneous ffmpeg processes
  • Queue with single worker: Too slow for batch generation; 2 concurrent is a good balance
  • Sharing the existing PlaylistGenerator semaphore: Would cause HLS generation and preview generation to compete for the same slots; better to keep them independent

R5: Mobile App Video Playback Strategy

Decision: Use expo-video VideoView components inside FlatList items, with muted autoplay and viewport-based pause/resume.

Rationale: The app already uses expo-video (v3.0.15) for the single video player in viewer/video.tsx. The library supports multiple simultaneous players, loop mode, and programmatic mute/unmute. FlatList's viewabilityConfig callback can be used to pause/resume players based on viewport visibility.

Key configuration per cell:

  • player.loop = true
  • player.muted = true (default)
  • player.play() when visible, player.pause() when offscreen
  • nativeControls={false} (no controls needed in grid)

Audio-on-focus: On long-press, unmute the pressed player and mute all others. Track the "focused" player ID in hook state.

Alternatives considered:

  • HLS streaming for previews: Overkill for <10s clips; direct MP4 download is simpler and faster
  • Animated GIF display via Image component: Rejected per clarification — MP4 with expo-video is better
  • WebView-based player: Poor performance, no native gesture integration

R6: API Endpoint Design

Decision: Two new endpoints — one to serve preview clips, one for batch status checking.

Rationale:

  • GET /video/preview?path=... serves the MP4 file directly (or triggers on-demand generation and returns 202 Accepted). Follows the pattern of GET /image?path=... for serving files.
  • POST /video/preview/status accepts a JSON body with an array of video paths and returns their preview generation status. This allows the mobile app to efficiently determine which previews are ready in a single request (batch pattern from get_exif_batch()).

Alternatives considered:

  • Single endpoint that blocks until generation completes: Bad UX — generation takes up to 30s
  • WebSocket for real-time status: Overkill for this use case; polling with batch status is simpler
  • Including preview URL in the existing /photos response: Would couple the photo listing endpoint to preview generation; better to keep separate