Live Onchain Video Environment
LOVE is a decentralized live streaming platform built on Celestia's data availability layer. It captures video and audio from your webcam/microphone, encodes them using H.264 video compression, multiplexes them into 2MB data chunks (~8 seconds of A/V), and submits them as blobs to the Celestia blockchain. Viewers can then fetch these blobs and play back the stream in real-time with synchronized audio and video.
- Live Streaming: Real-time video capture from webcam with configurable resolution and framerate
- H.264 Video Compression: Efficient video encoding using ffmpeg for optimal streaming
- Audio Support: Synchronized audio capture from microphone (16-bit PCM, configurable sample rate)
- Local Preview: Optional local preview window for monitoring your stream
- On-chain Storage: Stream data is stored as Celestia blobs with automatic gas estimation
- A/V Sync: Timestamp-based synchronization ensures proper audio/video playback
- Background Prefetching: Viewer fetches blobs in background for smooth playback
- Decentralized: No central server - streams go directly to the blockchain
- Censorship Resistant: Once on-chain, streams cannot be removed
- Pluggable Codec: Interface-based design allows swapping encoding implementations
-
Go 1.21+
-
ffmpeg (required for H.264 encoding/decoding)
# macOS brew install ffmpeg # Ubuntu/Debian apt install ffmpeg
-
OpenCV 4.x with GoCV bindings
# macOS brew install opencv # Ubuntu/Debian apt install libopencv-dev
-
Audio libraries (Linux only)
# Ubuntu/Debian (ALSA) apt install libasound2-dev -
Celestia light node running locally (or remote node access)
-
Auth token for Celestia node
git clone https://github.com/vgonkivs/love.git
cd love
make buildcelestia light auth admin --p2p.network <network># Stream with local preview
make stream token=<auth_token>
# Stream with custom settings
make stream token=<auth_token> fps=15 width=640 height=480
# View a stream
make view token=<auth_token> namespace=<hex> start_height=<height>
# Show help
make help# Basic streaming
./love stream -token <auth_token>
# Custom settings
./love stream -width 1920 -height 1080 -fps 30 -bitrate 4M -samplerate 48000 -token <auth_token>
# View a stream
./love view -namespace <namespace_hex> -height <start_height> -token <auth_token>Press ESC to stop streaming or exit viewer.
| Variable | Default | Description |
|---|---|---|
token |
Celestia auth token (required) | |
node |
http://localhost:26658 | Celestia node URL |
camera |
0 | Camera device ID |
width |
1280 | Video width (pixels) |
height |
720 | Video height (pixels) |
fps |
30 | Frames per second |
namespace |
Stream namespace hex (required for view) | |
start_height |
Start block height (required for view) |
┌─────────────────────────────────────────────────────────────────────────────┐
│ STREAMING │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Capturer │ │
│ │ 1. Send entrypoint blob (metadata) │ │
│ │ 2. Initialize devices: │ │
│ │ ┌──────────┐ │ │
│ │ │ Webcam │──┐ │ │
│ │ └──────────┘ │ ┌──────────────┐ ┌─────────────┐ │ │
│ │ ├───▶│ Encoder │───▶│ Preview │ │ │
│ │ ┌──────────┐ │ │ (H.264) │ │ Window │ │ │
│ │ │ Mic │──┘ └──────────────┘ └─────────────┘ │ │
│ │ └──────────┘ │ │ │
│ │ ▼ │ │
│ │ 2MB Blobs │ │
│ │ 3. Send stream end blob │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Streamer │ │
│ │ - Random namespace │ │
│ │ - Submit to Celestia│ │
│ └──────────┬───────────┘ │
│ │ │
└─────────────────────────────────────────┼────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ Celestia Network │
│ │
│ Blobs stored in │
│ namespace at │
│ sequential heights │
└──────────┬───────────┘
│
┌─────────────────────────────────────────┼────────────────────────────────────┐
│ VIEWING │ │
├─────────────────────────────────────────┼────────────────────────────────────┤
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ Viewer │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌──────────────┐ ┌────────────────────┐ │ │
│ │ │ Background │───▶│ Decoder │───▶│ Display (GoCV) │ │ │
│ │ │ Blob Fetcher │ │ (H.264) │ └────────────────────┘ │ │
│ │ │ (prefetching) │ │ │ ┌────────────────────┐ │ │
│ │ └─────────────────┘ │ │───▶│ Audio Player │ │ │
│ │ └──────────────┘ │ (malgo) │ │ │
│ │ └────────────────────┘ │ │
│ │ A/V Sync: Video paced by timestamps, audio plays at sample rate │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
LOVE uses a pluggable codec architecture. The current implementation is H264Encoder/Decoder using ffmpeg, but the interface allows for alternative implementations:
// Encoder encodes video and audio frames for streaming
type Encoder interface {
EncodeVideo(frame gocv.Mat, timestamp time.Duration, sequence uint32) ([]byte, error)
EncodeAudio(samples []byte, timestamp time.Duration, sequence uint32) ([]byte, error)
CreateEntrypoint(sampleRate int, channels int, fps int) []byte
CreateStreamEnd(totalDuration time.Duration, totalFrames uint32) []byte
}
// Decoder decodes multiplexed video and audio frames
type Decoder interface {
Decode(data []byte) (*DecodedFrame, int)
ParseEntrypoint(data []byte) (sampleRate int, channels int, fps int, valid bool)
}Each video/audio frame is prefixed with a header:
┌───────────┬───────────┬─────────────────┬──────────────┐
│ Marker │ Size │ Timestamp │ Sequence │
│ 4 bytes │ 4 bytes │ 8 bytes │ 4 bytes │
├───────────┼───────────┼─────────────────┼──────────────┤
│ "H264" or │ Payload │ Nanoseconds │ Frame │
│ "AUDF" │ length │ since start │ number │
└───────────┴───────────┴─────────────────┴──────────────┘
- H264: H.264 encoded video frame (may contain multiple NAL units: SPS, PPS, IDR, P-frames)
- AUDF: Audio frame (16-bit PCM samples)
Frames are accumulated into 2MB blobs (~8 seconds of A/V at 2Mbps video + 128kbps audio):
┌─────────────────────────────────────────────────────────┐
│ 2MB Blob │
├─────────────────────────────────────────────────────────┤
│ [Header][H.264 Data][Header][PCM Data][Header][H.264]...│
└─────────────────────────────────────────────────────────┘
The Capturer sends an entrypoint blob first (before camera initialization) with stream metadata:
┌───────────┬─────────────┬──────────┬─────────┬───────┬───────┬────────┐
│ Marker │ Sample Rate │ Channels │ FPS │ Codec │ Width │ Height │
│ 4 bytes │ 4 bytes │ 1 byte │ 1 byte │1 byte │2 bytes│2 bytes │
├───────────┼─────────────┼──────────┼─────────┼───────┼───────┼────────┤
│ "ENTR" │ 44100 │ 1 │ 30 │ 1 │ 1280 │ 720 │
└───────────┴─────────────┴──────────┴─────────┴───────┴───────┴────────┘
Codec: 0 = JPEG (legacy), 1 = H.264
When the stream ends gracefully (ESC or Ctrl+C), the Capturer sends a stream end notification:
┌───────────┬─────────────────────┬──────────────┐
│ Marker │ Total Duration │ Total Frames │
│ 4 bytes │ 8 bytes │ 4 bytes │
├───────────┼─────────────────────┼──────────────┤
│ "ENDS" │ Nanoseconds │ Count │
└───────────┴─────────────────────┴──────────────┘
This allows viewers to distinguish between "stream ended gracefully" vs "stream stopped unexpectedly".
| Option | Default | Description |
|---|---|---|
-camera |
0 | Camera device ID |
-width |
1280 | Video width (pixels) |
-height |
720 | Video height (pixels) |
-fps |
30 | Frames per second |
-bitrate |
2M | H.264 bitrate (e.g., 2M, 4M) |
-samplerate |
44100 | Audio sample rate (Hz) |
-node |
http://localhost:26658 | Celestia node URL |
-token |
Auth token (required) |
| Option | Default | Description |
|---|---|---|
-namespace |
Stream namespace hex (required) | |
-height |
Start block height (required) | |
-node |
http://localhost:26658 | Celestia node URL |
-token |
Auth token (required) |
- Entrypoint: Capturer sends entrypoint blob with stream metadata (sample rate, channels, fps, dimensions, codec)
- Initialize: Capturer opens webcam (GoCV) and microphone (malgo)
- Preview: Frames are displayed in local preview window (optional)
- Encode: Video frames are H.264 encoded via ffmpeg (SPS/PPS/IDR combined), audio is 16-bit PCM
- Multiplex: Frames are tagged with H264/AUDF markers and timestamps
- Chunk: Data is accumulated into 2MB buffers inside Capturer (~8 seconds of A/V)
- Submit: Blobs are submitted to Celestia via Streamer with automatic gas estimation
- Stream End: When stopping gracefully, Capturer sends stream end blob with total duration and frame count
- Connect: Viewer connects to Celestia node
- Find Entrypoint: Locate the ENTR blob with stream parameters and codec type
- Create Decoder: Initialize H.264 decoder based on codec identifier
- Background Fetch: Goroutine prefetches blobs at sequential block heights into a buffered channel
- Decode: Parse frame headers, decode H.264 video via ffmpeg, extract PCM audio
- A/V Sync: Video is paced by timestamps, audio plays at native sample rate through malgo
- Display: Show video in window, play audio through speakers
- SPS/PPS Caching: Decoder caches parameter sets for mid-stream joining
love/
├── main.go # CLI entry point
├── Makefile # Build and run commands
├── cmd/
│ └── chaintest/ # H.264 encode/decode chain test app
│ └── main.go
├── lib/
│ ├── capture/ # Video + audio capture with embedded encoder
│ │ ├── capture.go # Capturer implementation
│ │ └── config.go # Capture configuration
│ ├── codec/ # Encoding/decoding interfaces and implementations
│ │ ├── interface.go # Encoder/Decoder interfaces
│ │ ├── jpeg.go # JPEGCodec implementation (legacy)
│ │ ├── h264_encoder.go # H.264 encoder using ffmpeg
│ │ ├── h264_decoder.go # H.264 decoder using ffmpeg
│ │ ├── codec.go # Shared constants and helpers
│ │ └── decoder.go # Frame decoding utilities
│ ├── streamer/ # Celestia blob submission
│ │ ├── streamer.go # Streamer implementation
│ │ └── config.go # Streamer configuration
│ └── viewer/ # Blob fetching + playback with embedded decoder
│ ├── viewer.go # Viewer implementation (background fetcher + A/V sync)
│ └── config.go # Viewer configuration
- Ensure ffmpeg is installed:
ffmpeg -version - Check if OpenCV/GoCV is properly installed
- Linux: Install ALSA dev libraries:
apt install libasound2-dev - Check microphone permissions
- This happens when joining mid-stream before a keyframe
- Wait for the next keyframe or restart from an earlier height
- Ensure you're using a fresh recording (old recordings may have sync issues)
- The viewer uses timestamp-based sync - video paced by timestamps, audio at native rate
MIT
Go live with LOVE