MV Wall Sync is a local multi-screen MV playback experiment. It plays one video across multiple browser player windows by treating the video as a virtual canvas and assigning each connected player a rectangular viewport inside that canvas.
It is a small tool, not a polished video-wall product. The interesting part is the model: players can join and leave dynamically, and the controller can arrange any number of surface rectangles over the source video.
- Virtual video canvas generated from the selected video metadata.
- Draggable and resizable surface rectangles, with aspect-ratio-preserving resize.
- Dynamic player registration through WebSocket.
- Player viewport reporting, so the controller can size a surface from the real browser window ratio.
- Backend-authoritative project state and playback state.
- Unified scheduled playback using server epoch time plus client clock-offset sampling.
- Player readiness tracking before unified playback.
- Online/offline player status.
- Single audio-player assignment; non-audio players stay muted to avoid doubled sound.
- Uploaded video mode and backend-local file mode.
- Backend video serving with byte-range support and ETag.
- OpenAPI schema generation for frontend types.
The repository follows a Rust backend plus Vite frontend layout:
.
├── src/ # Rust actix-web backend
│ ├── protocol.rs # API/WebSocket DTOs and OpenAPI schemas
│ ├── state.rs # Authoritative project and playback state
│ └── routes/ # API, video, OpenAPI, and frontend routes
├── frontend-panel/ # React/Vite/Tailwind controller and player UI
│ ├── src/core/ # Frontend project/layout/sync helpers
│ ├── src/services/ # API access and generated OpenAPI types
│ └── src/ui/ # Controller and player screens
└── tests/ # OpenAPI generation test
The backend is the authority. The controller and players keep local UI state, but playback, project updates, player registration, media readiness, and online/offline state flow through the Rust server.
BroadcastChannel exists only as a same-browser fallback when the backend WebSocket is unavailable.
- Rust toolchain compatible with the
Cargo.tomlrust-version. - Bun, used as the frontend package runner.
ffprobeavailable inPATHwhen using local video path mode.
Install frontend dependencies:
cd frontend-panel
bun installRun the Rust backend:
cargo runBy default the backend listens on 127.0.0.1:8788.
You can override the backend host and port:
MV_WALL_SERVER_HOST=0.0.0.0 cargo run
MV_WALL_SERVER_PORT=8790 cargo run
MV_WALL_SERVER_HOST=0.0.0.0 MV_WALL_SERVER_PORT=8790 cargo runRun the Vite frontend in another terminal:
cd frontend-panel
bun run devThe Vite dev server listens on 127.0.0.1:8787 and proxies /api and WebSocket traffic to 127.0.0.1:8788.
Open:
- Controller:
http://127.0.0.1:8787/ - Player:
http://127.0.0.1:8787/?view=player
Build the frontend:
cd frontend-panel
bun run buildThen run the backend:
cargo runThe backend serves the built frontend from frontend-panel/dist:
- Frontend:
http://127.0.0.1:8788/ - API state:
http://127.0.0.1:8788/api/v1/state - Video stream:
http://127.0.0.1:8788/api/v1/video - WebSocket:
ws://127.0.0.1:8788/api/v1/ws
- Open the controller.
- Select a video.
- Open one or more player windows.
- Put each player on the physical display that should render that crop.
- Assign players to surface rectangles in the controller.
- Drag or resize the rectangles over the virtual video canvas.
- Wait until all assigned players are online and media-ready.
- Click unified play.
During playback, the controller progress bar shows the backend-authoritative playback position. Clicking or dragging the progress bar sends a seek request to the backend.
The controller can upload a video to the backend. Uploaded video data is stored under:
data/video/
The backend serves the uploaded video at:
/api/v1/video
The controller can also tell the backend to use an absolute local file path. The backend probes metadata with ffprobe and serves the file directly through the same /api/v1/video endpoint.
This mode is useful when the controller, backend, and players are on the same machine, or when the backend machine has direct access to the media file.
Playback is backend-authoritative:
- The controller sends transport requests such as
play,pause,restart, orseek. - The backend updates
WallProject.playback. - Players follow the broadcast project playback state.
- Scheduled playback uses server epoch timestamps.
- Clients periodically sample server time and estimate
serverClockOffsetMs.
When a new player joins during playback, it loads the current video, seeks to the backend-authoritative time, and attempts to play. If the browser blocks audible autoplay, the player shows an unlock button.
Only one player should output audio. The selected audio player is stored in project.audioPlayerId.
Non-audio players are muted so that multiple displays do not produce overlapping sound. This matters because even small playback drift between audible browsers is very obvious.
Rust protocol types are the source of truth. Frontend API/project types should come from generated OpenAPI types, not handwritten DTO copies.
Generate OpenAPI and frontend types:
cargo test --features openapi generate_openapi
cd frontend-panel
bun run generate-apiOpenAPI is served in debug builds with the openapi feature:
cargo run --features openapiThen open:
http://127.0.0.1:8788/api-docs/openapi.json
Recommended checks:
cargo fmt --all --check
cargo clippy --all-targets --all-features
cargo test video::tests
cd frontend-panel
bun run check
bun run buildFor a broader backend check:
cargo check
cargo check --features openapi- The server is local-first and binds to
127.0.0.1by default. - The frontend dev server uses Vite proxying; production-style use is served by the Rust backend after
bun run build. - Browser autoplay policy still applies. Muted players can usually autoplay; the selected audio player may require one user click.
- Player synchronization depends on reasonable local clock-offset sampling and browser media behavior. The backend is authoritative, but the browser still controls actual decode/playback timing.
ffprobeis required only for backend-local path mode.
MIT License