GoVA is a minimal, frame-accurate video annotation tool powered by a Go backend and a lightweight Vanilla JS frontend. It uses an MJPEG stream generated locally by ffmpeg to ensure the backend maintains absolute playback authority.
- Go: 1.26+
- ffmpeg / ffprobe: 8.0.1+ (must be available in your system
PATH)
- Backend-Driven Playback: The Go backend determines the precise frame position. The browser acts merely as a thin display layer.
- Anywhere in filesystem: Load video files by passing an absolute path from the CLI or via the web UI.
- Responsive Controls: Seek via frame number or timeline slider, step accurately by individual frames (-1 / +1), and dynamic framerate overrides (1 to 240 fps).
- Canvas-Aligned Timeline: The play-position bar is rendered directly below the video and always uses the full video viewport width (landscape canvas width) for finer positioning.
- Live Drag Scrubbing: Dragging the timeline sends immediate backend seek updates while the mouse is moving; no wait for mouse-button release.
- Pause-Only Frame Input Editing: During playback, the frame number field is display-only. Entering a target frame number is enabled only while paused.
- Latest-Frame Stream Delivery: The MJPEG broadcaster drops stale queued frames per subscriber so pause/seek/step actions show the intended frame immediately instead of an older buffered frame.
- Frame Watermark Toggle: UI button toggles an in-video frame-number watermark (
Frame N) for easier verification during seek/step debugging. - Paused Single-Frame Rebroadcast: Seek/step actions while paused rebroadcast the resolved frame shortly after the first emit to reduce stale first-frame artifacts in MJPEG consumers.
- Bidirectional Playback: Play forward or backward from the backend, including reverse-play button support in the UI.
- High-Performance Scrubbing: A bidirectional LRU frame cache ensures instantaneous stepping and scrubbing without launching new decoder processes per frame.
- Smooth Reverse Playback: Reverse playback uses a chunk-aligned triple-buffer prefetch queue in the background (including across start/end wrap), so chunk decoding happens ahead of time instead of stalling the playback loop at cache boundaries.
- Directional Idle Prefetch: While paused (including after load and slider seeks), prefetch starts in the expected play direction first, then fills the opposite direction after a short delay so playback can resume smoothly either way.
- Direction-Scoped Active Prefetch: While actively playing, prefetch work is constrained to the active playback direction to avoid wasting decode budget.
- Infinite Loop Playback: Playback wraps automatically at boundaries (end -> start, start -> end) instead of stopping.
- Graceful Frame-Skipping: If playback is configured higher than system decoding capabilities (e.g., 240 fps), the backend automatically drops MJPEG broadcasts while maintaining precise logical frame advancement (render one, skip three).
- Prefetch works in fixed chunks (default cache
3000-> three chunks of1000frames). - Chunk addressing is circular, so prefetch naturally crosses the start/end boundary.
- While paused (including immediately after load and after slider seeks):
- First, prefetch in the expected direction (
PlayDirection) to reduce start latency. - If still paused after a short delay, prefetch one chunk in the opposite direction so either play direction is ready.
- First, prefetch in the expected direction (
- While playing, prefetch is constrained to the active play direction only.
-
Start the server:
go run .Available CLI arguments:
go run . -file /absolute/path/to/video.mkv -port 8080 -cache 3000-file: Load a video immediately on startup.-port: The HTTP server port (default: 8080).-cache: Number of decoded MJPEG frames to keep in memory for instantaneous scrubbing and backward playback (default: 3000, typically split into three 1000-frame reverse-prefetch chunks).
-
Open your browser to
http://localhost:8080. -
Hotkeys:
Space: Play/PauseRight Arrow: Step Forward 1 frameLeft Arrow: Step Backward 1 frame
- Run all tests (including integration tests):
go test ./... - Skip integration tests:
go test -short ./... - Integration tests generate a temporary synthetic video via
ffmpegand verify:- seek after pause shows the requested frame (no stale decoder frame),
- pause+seek flushes buffered stream frames to the newest frame,
- first prev-step after pause shows the actual previous frame.
main.go: Handles HTTP routing, WebSocket/SSE endpoints, and the MJPEG stream.player.go: The core playback engine controlling an externalffmpegprocess. It loops over precise time tickers, reads raw JPEGs via pipe fromffmpeg, tracks frame positions, and handles dynamic seeking and scaling.static/: Pure HTML, JS, and CSS. No build step required.