Skip to content

feat: introduce livekit-a2a-relay and video optimizations - #1199

Closed
mjpvl-ai wants to merge 5 commits into
livekit:mainfrom
mjpvl-ai:feature/a2a-relay
Closed

feat: introduce livekit-a2a-relay and video optimizations#1199
mjpvl-ai wants to merge 5 commits into
livekit:mainfrom
mjpvl-ai:feature/a2a-relay

Conversation

@mjpvl-ai

@mjpvl-ai mjpvl-ai commented Jun 29, 2026

Copy link
Copy Markdown

🎭 Introduce livekit-a2a-relay: Bridging LiveKit WebRTC and the A2A Protocol

This Pull Request introduces the livekit-a2a-relay crate: a high-performance, drop-in, ultra-low latency bridging layer that seamlessly connects LiveKit real-time WebRTC audio tracks to any Agent-to-Agent (A2A) compliant HTTP/SSE endpoint.

By decoupling real-time audio transport and conversation flow control, this package enables developers to focus purely on agent intelligence without worrying about the underlying WebRTC FFI thread boundaries, clock drift, or interruption logic.


🏗️ Architecture & Data Flow

The crate implements a thread-isolated Actor model to ensure that high-frequency WebRTC audio I/O is never blocked by async HTTP requests or heavy processing tasks (like Whisper STT or Piper TTS).

flowchart TD
    subgraph LK ["LiveKit WebRTC Space"]
        Room["LiveKit Room"]
        Track["LocalAudioTrack"]
        Source["NativeAudioSource"]
    end

    subgraph ActorCore ["RelayActor Event Loop (Tokio Task)"]
        Actor["RelayActor Loop"]
        VAD["EnergyVad (VAD)"]
        Jitter["AudioJitterBuffer (Ring Buffer)"]
    end

    subgraph A2A ["A2A Protocol & Transport"]
        Turn["TurnManager (Atomic Counter)"]
        Client["OfficialA2aClient / Custom A2aClient"]
    end

    subgraph Backend ["Remote or Local A2A Agent"]
        Agent["A2A Mock Agent (Axum)"]
    end

    %% Audio Ingestion & VAD Flow
    Room -->|Incoming PCM| Actor
    Actor -->|Process Samples| VAD
    VAD -->|Speech Detected - Interruption| Turn
    Turn -->|Advance Turn ID| Actor
    Actor -->|Cancel Active Turn| Client
    Actor -->|Accumulated PCM| Client

    %% Audio Outflow & Playback Flow
    Client <-->|POST /message:stream - SSE| Agent
    Agent -->|Audio Chunks| Client
    Client -->|Filter by Turn ID| Actor
    Actor -->|Push Valid Frames| Jitter
    Jitter -->|Pop 10ms ticks + Comfort Noise| Actor
    Actor -->|Write Frame| Source
    Source -->|Publish Audio| Track
    Track --> Room
Loading

🛠️ Key Components & Responsibilities

Component Responsibility Performance Profile / Concurrency Model
RelayActor Orchestrates the primary event loop. Intercepts subscriber audio, runs VAD, pushes agent responses to playback, and schedules WebRTC frame generation. Runs as a single isolated Tokio task. Uses biased tokio::select! for predictable event prioritization.
TurnManager Coordinates atomic turn counters. Ensures late-arriving packets from previously canceled turns are instantly dropped. Uses atomic AtomicU64 indices. Thread-safe and lock-free.
AudioJitterBuffer Smooths out network delivery jitter from the A2A HTTP stream. Fills playback underflows with deterministic, low-amplitude comfort noise. Implemented as a lock-free VecDeque ring-buffer. Supports configurable target depth and hard ceiling.
EnergyVad Lightweight voice activity detector using normalized RMS energy thresholds. Fast in-memory double-precision float computation with configurable hangover window.
A2aClient Trait defining A2A-compliant transport endpoints. Enables custom implementations (e.g., custom WebSockets or WebRTC DataChannels). Pluggable, async, and runtime-agnostic.

💫 Turn Lifecycle & Interruption Handling

One of the most complex challenges in real-time conversational AI is user interruption. The diagram below demonstrates how the TurnManager and RelayActor work together to instantly halt agent output:

sequenceDiagram
    autonumber
    actor User as User (WebRTC)
    participant Relay as RelayActor
    participant Turn as TurnManager
    participant Client as A2aClient (Official)
    participant Agent as A2A Agent

    Note over Agent: Speaking (Turn N)
    Agent->>Client: Send SSE Audio chunk (Turn N)
    Client->>Relay: Forward audio chunk (Turn N)
    Relay->>Relay: Push to JitterBuffer & play
    
    User->>Relay: Speaks (Interrupts Agent)
    Relay->>Relay: Run EnergyVad -> Active!
    
    Note over Relay,Turn: Interruption Sequence Started
    Relay->>Turn: Bump turn index (next_turn)
    Turn-->>Relay: Current Turn = N + 1
    Relay->>Relay: Clear NativeAudioSource playback buffer
    Relay->>Relay: Clear AudioJitterBuffer
    Relay->>Client: cancel_turn(Turn N)
    Client->>Agent: HTTP POST Cancel/Reset (Turn N)
    
    Note over Agent: Stop generating/sending Turn N
    
    Agent->>Client: Late/Stale Audio chunk (Turn N)
    Client->>Relay: Forward audio chunk (Turn N)
    Relay->>Turn: is_valid(Turn N)?
    Turn-->>Relay: False (Current is N + 1)
    Note over Relay: 🚫 Discard stale frame immediately (No sound plays)
Loading

🏃 Testing & Local ONNX Verification

To verify that the system runs flawlessly under local hardware constraints, a complete STT (Whisper) and TTS (Piper) pipeline was tested end-to-end:

📥 1. Pre-trained Model Fetching

Models are fetched and stored locally in the workspace directory using the provided setup script:

# Fetch Whisper small ASR model and Piper medium TTS model
./scripts/download_onnx_models.sh --stt small --tts medium

🤖 2. Launching the A2A Mock Agent

The lightweight A2A compliant Axum mock agent runs locally on port 8000:

cargo run -p a2a_mock_agent -- --port 8000

🎭 3. Running the A2A WebRTC Relay

The relay example connects to a local LiveKit server (dev mode on :7880) and bridges incoming audio to the mock text agent using local ONNX STT/TTS:

cargo run -p a2a_relay_example -- \
  --url http://127.0.0.1:7880 \
  --api-key devkey \
  --api-secret secret \
  --room-name test-room \
  --agent-url http://127.0.0.1:8000 \
  --local-onnx \
  --stt-model small \
  --tts-model medium

🧪 4. Automated E2E Pipeline Validation

Running the automated test pipeline confirms complete protocol adherence:

./scripts/test_pipeline.sh

Test Execution Output:

==> Testing Agent endpoint: http://127.0.0.1:8000/message:stream

--- Step 1: Agent Card Discovery ---
  ✓ Agent card found: RustCurrencyAgent

--- Step 2: Currency Conversion Request ---
  Raw SSE response (first 500 chars):
  data: {"statusUpdate":{"contextId":"7093acff-3a82-4a0e-b79f-7a5f43d954f4","status":{"message":{"parts":[{"mediaType":"text/plain","text":"Calculating exchange rate..."}]},"state":"TASK_STATE_WORKING","timestamp":"2026-06-11T00:00:00Z"},"taskId":"f6f4b240-bcf3-45df-b1ed-b15e6ad83b6d"}}

  ✓ Agent returned a valid currency conversion response!

--- Step 3: Empty Text Handling ---
  ✓ Empty text correctly returns default greeting

--- Step 4: Relay Process Health ---
  ✓ Relay process running (PID: 9292,9293, Memory: 551MB)
  ✓ Memory footprint confirms ONNX models are loaded (551MB > 100MB)

==> Pipeline validation complete!

🔒 Code Quality & Compliance

  • No unsafe Blocks: The entire crate uses 100% safe Rust.
  • Zero Clippy Warnings: Compilation passes under strict linting flags.
  • Dependency Hygiene: Minimal dependencies, utilizing the existing workspace crate ecosystem.
  • Memory Footprint: Verified under 550MB when both Whisper small and Piper models are concurrently loaded in-memory and actively processing.
  • Formatting: Formatted with cargo fmt.

@CLAassistant

CLAassistant commented Jun 29, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@1egoman
1egoman removed their request for review June 29, 2026 19:27
@ladvoc

ladvoc commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Thanks for taking the time to put this together.

After reviewing the PR, we’re going to close it in its current form. The proposed changes are quite large in scope and include several breaking changes. Given the direction of the work, we think the crate you added would be better maintained as its own open-source project. If there are smaller changes needed here to better support that project, we’d be happy to review those as separate, well-scoped PRs.

In general, smaller PRs that focus on one specific bug fixes or enhancements are much easier for us to review and merge. If you’d like to discuss further, our developer community is a good place to continue the conversation.

@ladvoc ladvoc closed this Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants