V2A (Video to ANSI) is a video format optimized for terminal playback using Unicode Quadrant Block characters with 24-bit true color.
Quadrant blocks are solid rectangular Unicode characters (U+2596 to U+259F) that fully occupy the character cell. Combined with ANSI true color (24-bit RGB), each quadrant displays its actual pixel color:
- Clean, sharp edges
- Full coverage of character space
- True color per pixel (24-bit)
Unicode Range: U+2596 to U+259F (10 characters)
Quadrant layout (2×2 grid):
┌───┬───┐
│ TL│ TR│
├───┼───┤
│ BL│ BR│
└───┴───┘
V2AFrame {
width: u16, // Number of quadrant cells horizontally
height: u16, // Number of quadrant cells vertically
pixel_cells: Vec<Cell> // Per-cell data
}
Cell {
quadrant_index: u8, // 0-9, which quadrant character to use
colors: [RGB; 4] // 4 RGB colors, one per quadrant position
}
RGB {
r: u8, // Red (0-255)
g: u8, // Green (0-255)
b: u8, // Blue (0-255)
}
Each 2×2 pixel block from source video maps to 1 Quadrant cell:
[px1][px2] → TL, TR
[px3][px4] → BL, BR
The quadrant_index determines which quadrants are filled (show fg color) vs empty (show bg color).
Filled quadrants show the average color of their respective pixels. Empty quadrants show the background color.
For each 2×2 pixel block:
- Read 4 source pixels (RGB)
- Store the 4 RGB colors directly in the cell
- Determine quadrant_index based on luminance threshold:
- If pixel luminance > block average: quadrant is filled
- If pixel luminance <= block average: quadrant is empty
- Calculate fill/empty colors:
- Filled quadrants: average color of filled pixels
- Empty quadrants: average color of empty pixels (or transparent)
Foreground (24-bit RGB):
\x1b[38;2;R;G;Bm
Background (24-bit RGB):
\x1b[48;2;R;G;Bm
Reset:
\x1b[0m
Example:
\x1b[38;2;255;128;0m\x1b[48;2;0;0;255m▚\x1b[0m
┌─────────────────────────────────────┐
│ Header (32 bytes) │
├─────────────────────────────────────┤
│ Audio Data (N bytes) │
├─────────────────────────────────────┤
│ Frame 1 (gzip compressed) │
├─────────────────────────────────────┤
│ Frame 2 (gzip compressed) │
├─────────────────────────────────────┤
│ ... │
├─────────────────────────────────────┤
│ Frame N (gzip compressed) │
└─────────────────────────────────────┘
| Offset | Type | Description |
|---|---|---|
| 0-3 | bytes | Magic: "V2A\0" |
| 4-5 | u16 | Version: 2 |
| 6-9 | u32 | Frame count |
| 10-13 | u32 | Original video width |
| 14-17 | u32 | Original video height |
| 18-21 | float | FPS |
| 22-29 | u64 | Audio data size |
| 30-31 | bytes | Padding |
Raw audio bytes immediately following the header. Size is specified in audio_data_size.
Each frame is individually gzip compressed.
| Offset | Type | Description |
|---|---|---|
| 0-1 | u16 | Frame width (cells) |
| 2-3 | u16 | Frame height (cells) |
| 4+ | bytes | pixel_cells as 13-byte tuples |
| Offset | Size | Description |
|---|---|---|
| 0 | 1 | quadrant_index (0-9) |
| 1 | 3 | TL RGB (r,g,b) |
| 4 | 3 | TR RGB (r,g,b) |
| 7 | 3 | BL RGB (r,g,b) |
| 10 | 3 | BR RGB (r,g,b) |
The player uses a prefetch buffer to prevent I/O lag:
| Setting | Value |
|---|---|
| Buffer size | 300 frames |
| Prefetch thread | Background thread continuously reads frames |
| Buffer type | Thread-safe queue |
| Sync mechanism | Condition variable for producer/consumer |
[Disk] → [Prefetch Thread] → [Buffer] → [Playback Thread] → [Terminal]
Characters indexed 0-9, mapped to Unicode U+2596-U+259F:
Index 0: U+2596 (▖) - Lower-left
Index 1: U+2597 (▗) - Lower-right
Index 2: U+2598 (▘) - Upper-left
Index 3: U+2599 (▙) - Three quadrants (UL+LL+LR)
Index 4: U+259A (▚) - Diagonal (UL+LR)
Index 5: U+259B (▛) - Three quadrants (UL+LL+UR)
Index 6: U+259C (▜) - Three quadrants (UL+UR+LR)
Index 7: U+259D (▝) - Upper-right
Index 8: U+259E (▞) - Diagonal (UR+LL)
Index 9: U+259F (▟) - Three quadrants (UR+LL+LR)
- Get quadrant_index and 4 RGB colors
- Calculate foreground color (average of filled quadrants)
- Calculate background color (average of empty quadrants)
- Output:
\x1b[38;2;R;G;Bm\x1b[48;2;R;G;Bm<quadrant_char>\x1b[0m
scaled_width = floor(term_cols * 2 * scale)
scaled_height = floor(term_rows * 2 * scale)
When paused:
- Video playback freezes (no frames advance)
- Audio playback freezes
- Internal timing pauses
When resumed:
- Video continues from exact pause point
- Audio continues from exact pause point
- Timing compensates for pause duration
Format Version: 2