Skip to main content
🤖AI-generated documentation curatedAI Generated
This page was drafted by an AI assistant and may contain inaccuracies.
About content generation types
🤖
AI GeneratedPage drafted entirely by AI from codebase or prompt instructions.
(e.g., docs generated from codebase analysis)
← this page
✋→🤖
AI TransformattedHuman provided raw material; AI restructured it into a different format.
(e.g., livestream → blog post, meeting notes → docs)
Human GeneratedPage written entirely by a human author.
(e.g., hand-written tutorial)
More info about content generation types ↗

Frontend Component Architecture

The frontend is a React 19 + TypeScript application rendered inside Electron (or a browser for development). It uses HashRouter for routing, react-resizable-panels for layout, and Redux Toolkit for state management.

Entry Point & Provider Hierarchy

The app boots from main.tsx and nests providers in a deliberate order. Order matters — inner providers can depend on outer ones.

main.tsx
React.StrictMode
App (src/app/App.tsx)
Provider (Redux store) ← must be outermost: everything reads Redux
ServerContextProvider ← WebSocket, frames, keypoints, logs
AppContent (src/app/AppContent.tsx)
HashRouter ← routing depends on ServerContext
AutoUpdateProvider ← Electron auto-updater state
PlaybackProvider ← loaded videos, timestamps, sources
BasePanelLayout ← the three-panel layout
BaseContentRouter ← route → page mapping

Routing Design

Why HashRouter

The app uses HashRouter (URLs look like /#/streaming) instead of BrowserRouter (/streaming). Reason: Electron loads the app via file:// protocol. A BrowserRouter would try to request /streaming from the filesystem, which doesn't work. HashRouter keeps everything client-side.

Route Table

Defined in src/layout/content/BaseContentRouter.tsx:

PathPagePurpose
/Redirect to /streamingRoot
/streamingStreamingViewPageLive camera grid + optional 3D viewport
/browseRedirect to /playbackLegacy redirect
/playbackPlaybackPageSynced multi-video playback + 3D skeleton viewer
/active-recordingActiveRecordingPagePipeline stage status + processing controls
*Redirect to /streamingCatch-all

Navigation happens three ways:

  1. MainNavTabs — Segmented control in the header bar. Uses react-router-dom's useNavigate() to switch routes.
  2. Programmatic — Buttons within pages call navigate() directly (e.g., "Go to Playback" from the recording complete dialog).
  3. Electron native menususeMenuActions hook bridges Electron's native menu bar actions to React-Redux, calling navigate() for menu items like "Switch to Playback."

Layout System

BasePanelLayout

The top-level layout component (src/layout/BasePanelLayout.tsx) uses react-resizable-panels to create a three-panel design:

  • Top panel (87% default): Sidebar on the left, main content on the right, divided by a vertical resize handle.
  • Bottom panel (13% default, starts collapsed): Console area with framerate viewer and log terminal.
  • Both sidebars are collapsible — they shrink to a narrow strip that can be clicked to re-expand.

The left sidebar (SidePanelContent) is a vertical stack of collapsible sections. Sections are conditionally visible depending on which tab is active:

SectionVisible on tab
Camera Config TreeStreaming only
Calibration ModuleAll tabs
Recording PathStreaming only
Recording ControlStreaming only
Process MocapPlayback, Active Recording
Mocap PanelAll tabs
Recording BrowserPlayback, Active Recording

Bottom Console

The bottom panel (BottomPanelContent) shows different content depending on the active tab:

  • Streaming / Active Recording tabs: Framerate viewer (D3 charts) on the left, log terminal on the right
  • Playback tab: Log terminal fills the space

Page Components

StreamingViewPage

The main workspace. Shows live camera feeds in a responsive grid (CameraViewsGrid) with optional 3D viewport (ThreeJsCanvas). The camera grid uses react-grid-layout for drag-to-reorder and resize. Each camera tile is a <canvas> element rendered by the CanvasManager service — not a <video> element.

PlaybackPage

Synced multi-video playback with a companion 3D skeleton viewport. The SyncedVideoPlayer component loads recordings and plays them with frame-perfect synchronization using a rAF-based drift-correction loop. A RecordingBrowser section in the sidebar lets you browse and select recordings.

ActiveRecordingPage

Shows the currently active recording's status: which pipeline stages are complete, what data is available, and controls for processing (calibrate, process mocap, export to Blender).

Component Patterns

Context vs Redux vs Refs — The Decision Tree

This is the most important architectural decision in the frontend. Putting data in the wrong place causes bugs that are hard to diagnose.

MechanismUse whenDon't use when
ReduxConfig that survives navigation; anything persisted to localStorage; cross-cutting UI state that multiple unrelated components readRapidly-changing data (60fps); data only needed by one subtree
React ContextShared state for a specific subtree (playback, auto-update); avoiding prop drilling through 3+ levelsGlobal state that many slices need (use Redux); hot-path data (use refs)
useRefData that changes at frame rate: camera frames, keypoints, rigid body poses, overlay dataState that should trigger re-renders when it changes

Subscription Pattern

3D data (keypoints, rigid bodies) uses a Set-based subscription pattern in ServerContextProvider. Consumers register a callback via subscribeToKeypointsRaw(), subscribeToKeypointsFiltered(), or subscribeToRigidBodies(). When new data arrives on the WebSocket, the rAF loop iterates the subscriber Set and calls each callback. This avoids React re-renders entirely — the Three.js scene updates directly.

rAF-Driven Processing Loop

The ServerContextProvider runs a requestAnimationFrame loop rather than processing WebSocket messages as they arrive. Why:

  • WebSocket messages can storm — dozens of binary frames per tick from multiple cameras
  • Promise-based processing would starve the main thread — each await yields, and a storm of messages means you never finish processing one before the next arrives
  • rAF gives you one tick per frame — you process all queued messages, then render, then wait for the next frame

The loop structure:

  1. Ack the latest frame number (tells the backend it can pipeline the next batch)
  2. Dispatch any buffered JSON payloads (keypoints, overlays, rigid bodies, logs, progress)
  3. Decode binary image frames asynchronously (never blocks the loop)
  4. Measure frontend framerate from decoded frame inter-arrival times

Anti-Patterns & Pitfalls

Don't put frame data in Redux

Frame payloads arrive 30+ times per second per camera. Dispatching each one to Redux would cause render storms. Use refs in ServerContextProvider instead.

Don't read from Redux in the rAF loop

The rAF loop runs at display refresh rate. Reading from Redux selectors inside it creates garbage collection pressure and can cause stale reads. Cache what you need before the loop starts.

Don't add a new state path without deciding: Context, Redux, or ref?

Every new piece of state needs a conscious decision. The rule of thumb:

  • Survives navigation? → Redux
  • Only needed by a subtree? → Context
  • Changes at frame rate? → ref

ServerContextProvider: current state, not end state

At ~700 lines, ServerContextProvider.tsx is larger than it should be. Its responsibilities (WebSocket, frame processing, canvas management, overlay compositing) are tightly coupled — they share a single WebSocket connection and a single rAF loop — which is why they've grown together. This is accepted as the current state, but it's not the target architecture. The next obvious step is to break it into focused pieces (connection management, frame pipeline, overlay system) with clear interfaces. That refactor is planned but deferred — for now, the monolith works.

Directory Map

src/
├── app/ Entry points
│ ├── App.tsx Provider + ServerContextProvider wrapper
│ ├── AppContent.tsx HashRouter + providers + BasePanelLayout + BaseContentRouter
│ └── App.css Top-level app styles

├── layout/ Panel layout and routing
│ ├── BasePanelLayout.tsx Three-panel resizable layout
│ └── content/
│ ├── BaseContentRouter.tsx Route definitions
│ ├── SidePanelContent.tsx Left sidebar with section stack
│ └── BottomPanelContent.tsx Bottom console (framerate + logs)

├── pages/ Top-level page components
│ ├── StreamingViewPage.tsx Live camera grid + 3D viewport
│ ├── PlaybackPage.tsx Synced video + 3D skeleton viewer
│ └── ActiveRecordingPage.tsx Pipeline status + processing controls

├── components/ Shared and domain components
│ ├── camera-views/ Camera grid display
│ ├── control-panels/ Sidebar panels (camera-config-panel, mocap-control-panel, recording-info-panel, realtime-panel, server-connection)
│ ├── framerate-viewer/ D3 framerate charts (timeseries, histogram, statistics)
│ ├── languages/ LanguageSwitcher + custom flag icons
│ ├── mocap-setup/ Mocap setup wizard (blender settings, detector settings, 3D reconstruction, processing directory, setup modal)
│ ├── playback/ PlaybackContext, SyncedVideoPlayer, RecordingBrowser, PlaybackControls, RecordingBrowserSection, ZoomableVideoTile, usePlaybackController
│ ├── pipeline-progress/ PipelineProgressPanel, PipelineProgressBar, PipelineProgressSnackbar, PipelineGroupCard, calibration-progress/, realtime/
│ ├── viewport3d/ Three.js scene, renderers, hooks, workers
│ ├── log-terminal/ Scrollable server log viewer
│ ├── ui-components/ Design-system components (Button, Header, Footer, Toggle, SegmentedControl, etc.)
│ └── common/ ErrorBoundary, DirectoryStatusPanel, PresetPicker, CollapsibleSidebarSection, CalibrationTomlPicker, RecordingStatusPanel

├── services/ Backend communication
│ ├── server/
│ │ ├── ServerContextProvider.tsx WebSocket lifecycle, rAF loop, frame processing, overlays
│ │ ├── server-context.ts ServerContext interface + useServer hook
│ │ └── server-helpers/ canvas-manager, frame-processor, image-overlay, log-store, websocket-connection, console-log-bridge, framerate-store, offscreen-renderer.worker, tracked-object-definition, websocket-message-types
│ └── electron-ipc/ tRPC proxy over Electron IPC

├── store/ Redux state management
│ ├── store.ts configureStore (13 slices, 2 middleware)
│ ├── hooks.ts useAppDispatch, useAppSelector (typed)
│ ├── types.ts Shared store types
│ ├── persistence.ts localStorage load/save helpers
│ ├── persistence-listener.ts Auto-persist middleware (300ms debounce)
│ ├── camera-config-listener.ts Auto-apply camera config middleware (350ms debounce)
│ └── slices/ One directory per slice (13 total, includes theme slice)

├── styles/ CSS utility classes and design tokens
│ ├── App.css Master import file + utility classes
│ ├── color.css Design tokens (CSS custom properties)
│ ├── icons.css Icon class definitions
│ ├── animation.css Keyframe animations
│ └── (domain-specific .css files)

├── hooks/ Custom React hooks
├── i18n/ Internationalization (i18next, 41 locales)
├── types/ Shared TypeScript types
├── utils/ Pure utility functions
├── constants/ URLs, external links
└── assets/ SVG icons (92 icons)