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>
19 KiB
Tasks: VideoWall
Input: Design documents from /specs/001-video-wall/
Prerequisites: plan.md (required), spec.md (required), research.md, data-model.md, contracts/
Tests: Not explicitly requested — test tasks omitted.
Organization: Tasks grouped by user story. US2 (server generation) comes before US1 (mobile view) because the mobile app depends on the API endpoints existing.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
Path Conventions
- Backend (ImageApi):
src/atC:\Users\ccord\RustroverProjects\ImageApi - Frontend (SynologyFileViewer):
app/,components/,hooks/atC:\Users\ccord\development\SynologyFileViewer
Phase 1: Setup (Shared Infrastructure)
Purpose: Database migration, new environment variable, shared types
- T001 Create Diesel migration for
video_preview_clipstable: rundiesel migration generate create_video_preview_clips, writeup.sqlwith table definition (id, file_path UNIQUE, status DEFAULT 'pending', duration_seconds, file_size_bytes, error_message, created_at, updated_at) and indexes (idx_preview_clips_file_path, idx_preview_clips_status), writedown.sqlwith DROP TABLE. Seedata-model.mdfor full schema. - T002 Run migration and regenerate schema: execute
diesel migration runthendiesel print-schema > src/database/schema.rsto add thevideo_preview_clipstable tosrc/database/schema.rs - T003 Add
PREVIEW_CLIPS_DIRECTORYenvironment variable: read it insrc/main.rsstartup (alongside existingGIFS_DIRECTORY), create the directory if it doesn't exist, and add it toAppStateor pass it where needed. Follow the pattern used forGIFS_DIRECTORYandTHUMBNAILS.
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Diesel model, DAO, and request/response types that all user stories depend on
CRITICAL: No user story work can begin until this phase is complete
- T004 [P] Add
VideoPreviewClipDiesel model struct insrc/database/models.rswith fields matching thevideo_preview_clipsschema table (Queryable, Insertable derives). Add aNewVideoPreviewClipstruct for inserts. - T005 [P] Add
PreviewClipRequestandPreviewStatusRequest/PreviewStatusResponsetypes insrc/data/mod.rs.PreviewClipRequesthaspath: String.PreviewStatusRequesthaspaths: Vec<String>.PreviewStatusResponsehaspreviews: Vec<PreviewStatusItem>where each item haspath,status,preview_url: Option<String>. All with Serialize/Deserialize derives. - T006 Create
PreviewDaotrait andSqlitePreviewDaoimplementation insrc/database/preview_dao.rs. Methods:insert_preview(file_path, status) -> Result,update_status(file_path, status, duration_seconds?, file_size_bytes?, error_message?) -> Result,get_preview(file_path) -> Result<Option<VideoPreviewClip>>,get_previews_batch(file_paths: &[String]) -> Result<Vec<VideoPreviewClip>>,get_by_status(status) -> Result<Vec<VideoPreviewClip>>. Follow theExifDao/SqliteExifDaopattern withArc<Mutex<SqliteConnection>>and OpenTelemetry tracing spans. - T007 Register
preview_daomodule insrc/database/mod.rsand addPreviewDaoto the database module exports. WireSqlitePreviewDaointoAppStateinsrc/state.rsfollowing the existing DAO pattern (e.g., howExifDaois added).
Checkpoint: Foundation ready — DAO, models, and types available for all stories
Phase 3: User Story 2 - Server Generates Preview Clips (Priority: P1) MVP
Goal: Backend can generate 480p MP4 preview clips (10 equally spaced 1-second segments) and serve them via API endpoints with on-demand generation and batch status checking.
Independent Test: Request GET /video/preview?path=<video> for any video — should return an MP4 file of at most 10 seconds. Request POST /video/preview/status with video paths — should return status for each.
Implementation for User Story 2
- T008 [P] [US2] Add
generate_preview_clip()function insrc/video/ffmpeg.rs. Takes input video path, output MP4 path, and video duration. Uses ffprobe to get duration (existing pattern). Calculates interval =duration / 10(or fewer for short videos per FR-009). Builds ffmpeg command with: video filterselect='lt(mod(t,{interval}),1)',setpts=N/FRAME_RATE/TB,scale=-2:480, audio filteraselect='lt(mod(t,{interval}),1)',asetpts=N/SR/TB, codec H.264 CRF 28 preset veryfast, AAC audio. Output path uses.mp4extension. Creates parent directories for output. ReturnsResult<(f64, u64)>with (duration_seconds, file_size_bytes). Seeresearch.mdR1 for full ffmpeg strategy. - T009 [P] [US2] Create
PreviewClipGeneratoractor insrc/video/actors.rs. Struct holdsArc<Semaphore>(limit 2 concurrent), preview clips directory path, base path, andArc<dyn PreviewDao>. HandlesGeneratePreviewMessage { video_path: String }: acquires semaphore permit, updates DB status toprocessing, callsgenerate_preview_clip(), updates DB tocompletewith duration/size on success orfailedwith error on failure. Follow thePlaylistGeneratoractor pattern withtokio::spawnfor async processing. - T010 [US2] Add
PreviewClipGeneratoractor toAppStateinsrc/state.rs. Initialize it during server startup insrc/main.rswith thePREVIEW_CLIPS_DIRECTORY,BASE_PATH, and preview DAO reference. Start the actor withPreviewClipGenerator::new(...).start(). - T011 [US2] Implement
GET /video/previewhandler insrc/main.rs. Validate path withis_valid_full_path(). Check preview DAO for status: ifcomplete→ serve MP4 file withNamedFile::open()(200); ifprocessing→ return 202 JSON; ifpending/not found → insert/update record aspending, sendGeneratePreviewMessageto actor, return 202 JSON; iffailed→ return 500 with error. Seecontracts/api-endpoints.mdfor full response contract. - T012 [US2] Implement
POST /video/preview/statushandler insrc/main.rs. AcceptPreviewStatusRequestJSON body. Callpreview_dao.get_previews_batch()for all paths. Map results: for each path, return status andpreview_url(only whencomplete). Paths not in DB get statusnot_found. Limit to 200 paths per request. ReturnPreviewStatusResponseJSON. - T013 [US2] Register both new endpoints in route configuration in
src/main.rs. Addweb::resource("/video/preview").route(web::get().to(get_video_preview))andweb::resource("/video/preview/status").route(web::post().to(get_preview_status)). Both require authentication (Claims extraction). - T014 [US2] Handle short videos (< 10 seconds) in
generate_preview_clip()insrc/video/ffmpeg.rs. When duration < 10s, calculate segment count asfloor(duration)and interval asduration / segment_count. When duration < 1s, use the entire video as the preview (just transcode to 480p MP4). Add this logic to the interval calculation in T008.
Checkpoint: Backend fully functional — preview clips can be generated, cached, and served via API
Phase 4: User Story 1 - Browse Videos as a Visual Wall (Priority: P1) MVP
Goal: Mobile app displays a responsive 2-3 column grid of simultaneously looping, muted video previews with long-press audio and tap-to-navigate.
Independent Test: Navigate to a folder with videos in the app, switch to VideoWall view, confirm grid displays with playing previews. Long-press to hear audio. Tap to open full video.
Implementation for User Story 1
- T015 [P] [US1] Create
useVideoWallhook inhooks/useVideoWall.ts(SynologyFileViewer). Accepts array ofGridItem[](video items only, filtered from current files context). CallsPOST /video/preview/statuswith video paths on mount to get availability. Returns{ previewStatuses: Map<string, PreviewStatus>, focusedVideoPath: string | null, setFocusedVideo: (path) => void, refreshStatuses: () => void }. UsesauthenticatedFetch()from auth hook. Polls status every 5 seconds for any items still inpending/processingstate, stops polling when all arecompleteorfailed. - T016 [P] [US1] Create
VideoWallItemcomponent incomponents/VideoWallItem.tsx(SynologyFileViewer). Renders anexpo-videoVideoViewfor a single preview clip. Props:videoPath: string, previewStatus: string, isFocused: boolean, onTap: () => void, onLongPress: () => void, isVisible: boolean. WhenpreviewStatus === 'complete': createuseVideoPlayerwith source URL${baseUrl}/video/preview?path=${videoPath}and auth headers, setplayer.loop = true,player.muted = !isFocused. WhenisVisibleis true →player.play(), false →player.pause(). When status is not complete: show placeholder (thumbnail image from existing/image?path=&size=thumbendpoint with a loading indicator overlay). Whenfailed: show error icon overlay. Aspect ratio 16:9 withnativeControls={false}. - T017 [US1] Create VideoWall view in
app/(app)/grid/video-wall.tsx(SynologyFileViewer). UseFlatListwithnumColumnscalculated asMath.floor(dimensions.width / 180)(targeting 2-3 columns). Get video items fromFilesContext— filterallItemsorfilteredItemsto only include video extensions (use same detection as existingisVideo()check). Pass items touseVideoWallhook. UseviewabilityConfigwithviewAreaCoveragePercentThreshold: 50andonViewableItemsChangedcallback to track visible items, passingisVisibleto eachVideoWallItem. ImplementkeyExtractorusing video path. Add scroll-to-top FAB button following existing grid pattern. - T018 [US1] Add
video-wallroute to stack navigator inapp/(app)/grid/_layout.tsx(SynologyFileViewer). Add<Stack.Screen name="video-wall" options={{ title: "Video Wall" }} />to the existing Stack navigator. - T019 [US1] Add navigation entry point to switch to VideoWall from the grid view. In
app/(app)/grid/[path].tsx(SynologyFileViewer), add a header button (e.g., a grid/video icon from@expo/vector-icons) that callsrouter.push("/grid/video-wall"). Only show the button when the current folder contains at least one video file. - T020 [US1] Implement long-press audio-on-focus behavior. In
VideoWallItem, wrap the VideoView in aPressablewithonLongPresscallingonLongPressprop. Invideo-wall.tsx, whenonLongPressfires for an item: callsetFocusedVideo(path)if different from current, orsetFocusedVideo(null)to toggle off. TheisFocusedprop drivesplayer.mutedinVideoWallItem— when focused, unmute; all others stay muted. - T021 [US1] Implement tap-to-navigate to full video player. In
VideoWallItem, theonTapprop triggers navigation. Invideo-wall.tsx, theonTaphandler sets thecurrentIndexinFilesContextto the tapped video's index and callsrouter.push("/grid/viewer/video")following the existing pattern from[path].tsxgrid item press.
Checkpoint: Full VideoWall experience works for folder browsing with simultaneous playback, audio-on-focus, and tap-to-view
Phase 5: User Story 3 - VideoWall from Search Results (Priority: P2)
Goal: VideoWall works with search/filter results, showing only matching videos.
Independent Test: Perform a search with filters that returns videos, switch to VideoWall, confirm only matching videos appear.
Implementation for User Story 3
- T022 [US3] Ensure VideoWall uses
filteredItemswhen available. Inapp/(app)/grid/video-wall.tsx(SynologyFileViewer), check iffilteredItemsfromFilesContextis non-empty — if so, usefilteredItemsfiltered to videos only; otherwise useallItemsfiltered to videos. This should already work if T017 reads from the context correctly, but verify the logic handles both folder browsing and search result modes. - T023 [US3] Add VideoWall toggle from search results. In
app/(app)/search.tsx(SynologyFileViewer), add a button (same icon as T019) that navigates to/grid/video-wallwhen search results contain at least one video. ThefilteredItemsinFilesContextshould already be populated by the search, so VideoWall will pick them up automatically.
Checkpoint: VideoWall works with both folder navigation and search/filter results
Phase 6: User Story 4 - Background Preview Generation (Priority: P3)
Goal: Preview clips are generated proactively during file watching so most are ready before users open VideoWall.
Independent Test: Add new video files to a monitored folder, wait for file watcher scan cycle, confirm preview clips appear in PREVIEW_CLIPS_DIRECTORY without any user request.
Implementation for User Story 4
- T024 [US4] Extend
process_new_files()insrc/main.rsto detect videos missing preview clips. After the existing EXIF batch query, add a batch query viapreview_dao.get_previews_batch()for all discovered video paths. Collect videos that have no record or havefailedstatus (for retry). - T025 [US4] Queue preview generation for new/unprocessed videos in
process_new_files()insrc/main.rs. For each video missing a preview, insert apendingrecord viapreview_dao.insert_preview()(skip if already exists), then sendGeneratePreviewMessageto thePreviewClipGeneratoractor. Follow the existing pattern of sendingQueueVideosMessagetoVideoPlaylistManager. - T026 [US4] Add preview clip directory creation to startup scan in
src/main.rs. During the initial startup thumbnail generation phase, also check for videos missing preview clips and queue them for generation (same logic as T024/T025 but for the initial full scan). Ensure thePREVIEW_CLIPS_DIRECTORYis created at startup if it doesn't exist.
Checkpoint: New videos automatically get preview clips generated during file watcher scans
Phase 7: Polish & Cross-Cutting Concerns
Purpose: Error handling, loading states, observability
- T027 [P] Add loading/placeholder state for pending previews in
components/VideoWallItem.tsx(SynologyFileViewer). Show the existing thumbnail from/image?path=&size=thumbwith a semi-transparent overlay and a loading spinner when preview status ispendingorprocessing. - T028 [P] Add error state for failed previews in
components/VideoWallItem.tsx(SynologyFileViewer). Show the existing thumbnail with an error icon overlay and optional "Retry" text when preview status isfailed. - T029 [P] Add OpenTelemetry tracing spans for preview generation in
src/video/actors.rsandsrc/main.rsendpoints. Follow the existing pattern ofglobal_tracer().start("preview_clip_generate")with status and duration attributes. - T030 Verify cargo build and cargo clippy pass with all backend changes. Fix any warnings or errors.
- T031 Run quickstart.md validation: test both API endpoints manually with curl, verify preview clip file is generated in correct directory structure, confirm mobile app connects and displays VideoWall.
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — start immediately
- Foundational (Phase 2): Depends on Phase 1 (migration must run first)
- US2 - Server Generation (Phase 3): Depends on Phase 2 (needs DAO, models, types)
- US1 - Mobile VideoWall (Phase 4): Depends on Phase 3 (needs API endpoints to exist)
- US3 - Search Results (Phase 5): Depends on Phase 4 (extends VideoWall view)
- US4 - Background Generation (Phase 6): Depends on Phase 3 only (backend only, no mobile dependency)
- Polish (Phase 7): Depends on Phases 4 and 6
User Story Dependencies
- US2 (P1): Can start after Foundational — no other story dependencies
- US1 (P1): Depends on US2 (needs preview API endpoints)
- US3 (P2): Depends on US1 (extends the VideoWall view)
- US4 (P3): Depends on US2 only (extends file watcher with preview generation; independent of mobile app)
Within Each User Story
- Models/types before services/DAO
- DAO before actors
- Actors before endpoints
- Backend endpoints before mobile app views
- Core view before navigation integration
Parallel Opportunities
Phase 2: T004, T005 can run in parallel (different files) Phase 3: T008, T009 can run in parallel (ffmpeg.rs vs actors.rs) Phase 4: T015, T016 can run in parallel (hook vs component, different files) Phase 6: T024, T025 are sequential (same file) but Phase 6 can run in parallel with Phase 4/5 Phase 7: T027, T028, T029 can all run in parallel (different files)
Parallel Example: User Story 2
# Launch parallelizable tasks together:
Task T008: "Add generate_preview_clip() function in src/video/ffmpeg.rs"
Task T009: "Create PreviewClipGenerator actor in src/video/actors.rs"
# Then sequential tasks (depend on T008+T009):
Task T010: "Add PreviewClipGenerator to AppState in src/state.rs"
Task T011: "Implement GET /video/preview handler in src/main.rs"
Task T012: "Implement POST /video/preview/status handler in src/main.rs"
Task T013: "Register endpoints in route configuration in src/main.rs"
Task T014: "Handle short videos in generate_preview_clip() in src/video/ffmpeg.rs"
Parallel Example: User Story 1
# Launch parallelizable tasks together:
Task T015: "Create useVideoWall hook in hooks/useVideoWall.ts"
Task T016: "Create VideoWallItem component in components/VideoWallItem.tsx"
# Then sequential tasks (depend on T015+T016):
Task T017: "Create VideoWall view in app/(app)/grid/video-wall.tsx"
Task T018: "Add video-wall route to stack navigator"
Task T019: "Add navigation entry point from grid view"
Task T020: "Implement long-press audio-on-focus"
Task T021: "Implement tap-to-navigate to full video player"
Implementation Strategy
MVP First (US2 + US1)
- Complete Phase 1: Setup (migration, env var)
- Complete Phase 2: Foundational (model, DAO, types)
- Complete Phase 3: US2 — Server generates preview clips
- STOP and VALIDATE: Test API with curl per quickstart.md
- Complete Phase 4: US1 — Mobile VideoWall view
- STOP and VALIDATE: Test end-to-end on device
- Deploy/demo — this is the MVP!
Incremental Delivery
- Setup + Foundational → Foundation ready
- US2 (Server Generation) → Backend API working (testable with curl)
- US1 (Mobile VideoWall) → Full end-to-end MVP (testable on device)
- US3 (Search Results) → Extended browsing from search (incremental value)
- US4 (Background Generation) → Performance enhancement (clips pre-generated)
- Polish → Error states, tracing, validation
Note on US4 Parallelism
US4 (Background Generation) only depends on US2 (backend), not on the mobile app. It can be developed in parallel with US1 by a second developer, or deferred to after MVP is validated.
Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story
- Backend work is in
C:\Users\ccord\RustroverProjects\ImageApi - Frontend work is in
C:\Users\ccord\development\SynologyFileViewer - Commit after each task or logical group
- Stop at any checkpoint to validate story independently