LIV has two parallel applications for managing game streaming on Quest:
Hub (liv-control-center) |
Control (lck-control) |
|
|---|---|---|
| Stage | Production (Quest Store, low reviews) | Prototype (new architecture) |
| Stack | Rust + Tauri + Leptos (WASM) | Kotlin + Jetpack Compose + Hilt |
| Streaming | App captures screen & encodes | Game encodes directly from render pipeline |
| Communication | Async via backend server | Synchronous IPC via AIDL |
| Destinations | Single target | Multi-destination |
| UE5 Plugin | LCKStreaming (HTTP/JSON-RPC) |
LCKControl (AIDL/JNI) |
graph TB
subgraph Quest Headset
subgraph "Hub App (Tauri + Leptos WASM)"
UI_H["Leptos UI<br/>(WASM)"]
Core_H["Rust Core<br/>(Tauri Backend)"]
Encoder_H["MediaCodec<br/>H.264 + AAC"]
RTMP_H["minirtmp<br/>(RTMP Client)"]
Capture["ScreenCaptureService<br/>(MediaProjection)"]
end
subgraph "UE5 Game"
Plugin_S["LCKStreaming Plugin"]
API_Client["HTTP/JSON-RPC Client"]
end
end
subgraph "Cloud Server"
Backend_H["Hub Backend<br/>(api.obi.gg)"]
end
subgraph "Streaming Platforms"
YT["YouTube Live"]
TW["Twitch"]
end
UI_H <-->|Tauri IPC| Core_H
Core_H -->|JSON-RPC 2.0<br/>HTTPS + Cert Pinning| Backend_H
Plugin_S -->|JSON-RPC 2.0<br/>HTTPS| Backend_H
Backend_H -->|"Device Pairing<br/>(async polling)"| Plugin_S
Core_H --> Capture
Capture --> Encoder_H
Encoder_H --> RTMP_H
RTMP_H -->|RTMP| YT
RTMP_H -->|RTMP| TW
style Backend_H fill:#f96,stroke:#333
style Capture fill:#ff9,stroke:#333
style Encoder_H fill:#ff9,stroke:#333
Key: The Hub app captures the screen, encodes it, and streams. The game and hub communicate indirectly through the backend server.
graph TB
subgraph Quest Headset
subgraph "Control App (Kotlin + Compose)"
UI_C["Compose UI"]
VM["ViewModels + Repos"]
Service["LckControlService<br/>(Foreground + AIDL)"]
DB["Room DB<br/>(Local Cache)"]
end
subgraph "UE5 Game"
Plugin_C["LCKControl Plugin"]
JNI["JNI Bridge"]
SDK["lck-control-sdk<br/>(AAR)"]
Encoder_C["LCK Encoder<br/>(H.264 + AAC)"]
RTMP_C1["RTMP Sink 1"]
RTMP_C2["RTMP Sink 2"]
RTMP_CN["RTMP Sink N"]
end
end
subgraph "Self-Hosted Server"
Backend_C["Control Backend<br/>(Node.js + Fastify)"]
SQLite["SQLite DB"]
end
subgraph "Streaming Platforms"
YT2["YouTube Live"]
TW2["Twitch"]
Manual["Custom RTMP"]
end
UI_C <--> VM
VM <-->|REST API<br/>JWT Auth| Backend_C
VM <--> DB
VM <--> Service
Plugin_C --> JNI
JNI --> SDK
SDK <-->|"AIDL IPC<br/>(Bound Service)"| Service
Backend_C <--> SQLite
Backend_C -->|"OAuth + RTMP<br/>Resolution"| YT2
Backend_C -->|"OAuth + RTMP<br/>Resolution"| TW2
Encoder_C --> RTMP_C1
Encoder_C --> RTMP_C2
Encoder_C --> RTMP_CN
RTMP_C1 -->|RTMP| YT2
RTMP_C2 -->|RTMP| TW2
RTMP_CN -->|RTMP| Manual
style Service fill:#9f9,stroke:#333
style SDK fill:#9f9,stroke:#333
style Encoder_C fill:#9cf,stroke:#333
Key: The game encodes directly from its render pipeline and streams to multiple destinations. The companion app provides stream configuration via direct IPC.
sequenceDiagram
participant Game as UE5 Game<br/>(LCKStreaming)
participant Server as Hub Backend<br/>(api.obi.gg)
participant Hub as Hub App<br/>(Tauri)
participant Platform as YouTube/Twitch
Note over Game,Hub: Device Pairing (one-time)
Game->>Server: create_device_login_attempt()
Server-->>Game: 6-digit pairing code
Game->>Game: Display code to user
Hub->>Server: pair_device(code)
Server-->>Hub: Device paired
Note over Game,Hub: Stream Setup
Hub->>Server: get_user_profile()
Server-->>Hub: Streaming target + RTMP URL
Hub->>Hub: Start screen capture
Hub->>Hub: Encode H.264 + AAC
Hub->>Platform: RTMP stream
Note over Game,Server: Game has no direct<br/>connection to Hub
Game->>Server: Poll for updates (2.5s)
Server-->>Game: Current state
sequenceDiagram
participant Game as UE5 Game<br/>(LCKControl + JNI)
participant App as Control App<br/>(AIDL Service)
participant Server as Control Backend<br/>(Fastify)
participant Platform as YouTube/Twitch
Note over Game,App: Service Binding (direct)
Game->>App: bindService() via AIDL
App-->>Game: ILckControlService binder
Game->>App: registerAsClient("MyGame", pkg)
App-->>Game: clientId
Note over Game,Platform: Stream Lifecycle
Game->>App: getStreamPlans()
App-->>Game: List<StreamPlan>
Game->>App: prepareStreamPlan(planId)
App->>Server: POST /streams/plans/{id}/prepare
Server->>Platform: Create broadcast + get RTMP URLs
Platform-->>Server: RTMP URLs + stream keys
Server-->>App: PrepareResponse
App-->>Game: StreamPlan (with RTMP data)
Game->>Game: Encode from render pipeline
Game->>Platform: RTMP stream (dest 1)
Game->>Platform: RTMP stream (dest 2)
Game->>App: startStreamPlan(planId)
App->>Server: POST /streams/plans/{id}/start
Server->>Platform: Transition broadcast to LIVE
| Component | Hub | Control |
|---|---|---|
| Language | Rust (95%) + Kotlin (JNI) | Kotlin (100%) |
| UI Framework | Leptos 0.8.2 (Rust WASM) | Jetpack Compose (2024.09 BOM) |
| App Framework | Tauri v2.6.2 | Native Android |
| Styling | TailwindCSS v4 | Material Design 3 |
| State Mgmt | Leptos reactive signals | StateFlow + collectAsStateWithLifecycle |
| DI | None (manual wiring) | Hilt 2.59.2 |
| Navigation | Leptos Router | Compose Navigation 2.8.4 |
| Local Storage | Platform credential store | Room 2.8.4 + EncryptedSharedPreferences |
| HTTP Client | reqwest (rustls TLS) | Retrofit 2.11.0 + OkHttp 4.12.0 |
| JSON | serde_json | Moshi 1.15.1 |
| Auth SDK | Meta Horizon Platform SDK 77.0.1 | Meta Horizon Platform SDK 77.0.1 |
| Crash Reporting | Sentry (Android SDK bridge) | None |
| Component | Hub Backend | Control Backend |
|---|---|---|
| Hosting | Cloud (api.obi.gg) |
Self-hosted (Docker on NAS, port 3100) |
| Protocol | JSON-RPC 2.0 | REST (JSON) |
| Stack | Unknown (external) | Node.js 20 + Fastify 5 + TypeScript 5.7 |
| Database | Unknown | SQLite (Prisma 6.4 ORM) |
| Auth | JWT (via JSON-RPC response headers) | JWT HS256 (jose 6.0) |
| Token Security | Unknown | AES-256-GCM encryption + SHA256 hashing |
| OAuth | Server handles YouTube/Twitch | Server handles YouTube/Twitch |
| Rate Limiting | Unknown | 100 req/min (Fastify plugin) |
| Deployment | Managed cloud | Docker + docker-compose |
| Component | LCKStreaming (Hub) | LCKControl (Companion) |
|---|---|---|
| Communication | HTTP/JSON-RPC 2.0 | AIDL via JNI |
| Transport | HTTPS (cross-network) | Local IPC (same device) |
| Auth Flow | Device code (6-digit) + polling | Direct service binding |
| Token Storage | EncryptedSharedPreferences | None (companion owns tokens) |
| RTMP Sinks | 1 (single destination) | N (multi-destination) |
| Blocking Model | Async HTTP callbacks | Synchronous JNI calls |
| Platform Support | Cross-platform capable | Android only |
| Latency | Network round-trip (100ms+) | IPC (~1ms) |
| Offline Capable | No (requires server) | Partial (companion has local cache) |
graph LR
subgraph "UE5 Game Process"
Render["Game Renderer<br/>(GPU)"]
end
subgraph "Android OS"
FB["Framebuffer /<br/>Display Compositor"]
MP["MediaProjection<br/>(Screen Capture API)"]
end
subgraph "Hub App Process"
VD["VirtualDisplay"]
MC_V["MediaCodec<br/>(H.264 Encoder)"]
MC_A["MediaCodec<br/>(AAC Encoder)"]
AR["AudioRecord<br/>(System Audio)"]
MR["minirtmp<br/>(RTMP Client)"]
end
subgraph "CDN"
RTMP["YouTube / Twitch<br/>RTMP Ingest"]
end
Render --> FB
FB --> MP
MP --> VD
VD --> MC_V
AR --> MC_A
MC_V --> MR
MC_A --> MR
MR --> RTMP
style FB fill:#fbb,stroke:#333
style MP fill:#fbb,stroke:#333
Problems:
graph LR
subgraph "UE5 Game Process"
Render["Game Renderer<br/>(GPU)"]
SCC["SceneCaptureComponent2D<br/>(Render Target)"]
ENC["LCK Encoder<br/>(H.264 + AAC)"]
S1["RTMP Sink 1<br/>(YouTube)"]
S2["RTMP Sink 2<br/>(Twitch)"]
S3["RTMP Sink 3<br/>(Custom)"]
end
subgraph "CDN"
YT["YouTube RTMP"]
TW["Twitch RTMP"]
CU["Custom RTMP"]
end
Render --> SCC
SCC --> ENC
ENC --> S1
ENC --> S2
ENC --> S3
S1 --> YT
S2 --> TW
S3 --> CU
style SCC fill:#bfb,stroke:#333
style ENC fill:#bfb,stroke:#333
Advantages:
| Feature | Hub | Control | Notes |
|---|---|---|---|
| Meta/Quest Login | Yes | Yes | Both use Horizon Platform SDK 77.0.1 |
| YouTube OAuth | Yes | Yes | Both server-side token exchange |
| Twitch OAuth | Yes | Yes | Both server-side token exchange |
| Multi-Destination Streaming | No (1) | Yes (N) | Major difference |
| Stream Plans | No | Yes | Control has full lifecycle management |
| Direct Game Encoding | No | Yes | Control encodes from render pipeline |
| Screen Capture Streaming | Yes | No | Hub captures and re-encodes |
| Custom RTMP Targets | Yes | Yes | Both support manual RTMP |
| Game Client Management | Yes (pairing) | Yes (AIDL) | Different mechanisms |
| IGDB Game Database | Yes | No | Hub has game cover art |
| Watermark | Yes | No | Hub has overlay support |
| Subscription Model | Yes | No | Hub has paid tier |
| Sentry Crash Reporting | Yes | No | Hub has telemetry |
| Certificate Pinning | Yes | No | Hub has SPKI pinning |
| Offline Caching | No | Yes | Control has Room DB |
| Background Token Refresh | Unknown | Yes | Control backend has scheduler |
| CI/CD Pipeline | Yes (Jenkins) | Partial (deploy.ps1) | Hub has full CI |
| Desktop Support | Yes | No | Tauri supports desktop |
| Cross-Platform | Yes (Desktop + Android) | No (Android only) | Hub has wider reach |
stateDiagram-v2
[*] --> Idle
Idle --> LoggingIn: StartLogin()
LoggingIn --> WaitingForCode: create_device_login_attempt
WaitingForCode --> Polling: Display 6-digit code
Polling --> Authenticated: check_device_login_attempt<br/>(every 2.5s)
Polling --> Polling: Not yet paired
Authenticated --> FetchingProfile: get_user_profile
FetchingProfile --> Ready: Got RTMP target
Ready --> Streaming: StartStreaming()
Streaming --> Ready: StopStreaming()
Ready --> Idle: Logout()
note right of Polling
User must manually enter
code in Hub app or website
end note
Architecture:
ULCKStreamingSubsystem — GameInstance subsystem, owns API client + RTMP sinkFLCKStreamingApiClient — HTTP client, JSON-RPC 2.0, cert pinningFLCKRtmpSink / FLCKRtmpClient — Single RTMP connection via librtmpstateDiagram-v2
[*] --> Disconnected
Disconnected --> Connecting: ConnectToCompanionApp()
Connecting --> Connected: AIDL service bound<br/>(poll every 1s)
Connected --> HasPlans: GetStreamPlans()
HasPlans --> Prepared: PrepareStreamPlan(planId)<br/>→ RTMP URLs resolved
Prepared --> Streaming: StartStreamPlan(planId)<br/>+ Attach N RTMP sinks
Streaming --> Prepared: EndStreamPlan(planId)
Connected --> Disconnected: DisconnectFromCompanionApp()
note right of Connected
Direct AIDL binding,
no pairing code needed
end note
note right of Streaming
Multiple RTMP sinks active
simultaneously
end note
Architecture:
ULCKControlSubsystem — GameInstance subsystem, owns JNI bridge + multiple RTMP sinksLCKControlAndroid.cpp — ~700 lines of JNI bindings to LckControlClient (AAR)FLCKRtmpSink instances — one per stream destinationBoth plugins share:
ILCKStreamingFeature — Common interface (StartLogin, StartStreaming, StopStreaming, etc.)ILCKEncoderFactory — Encoder creationULCKRecorderSubsystem — Encoder lifecycle managementFLCKRtmpSink / FLCKRtmpClient — RTMP transport layerapi.obi.gg)graph TB
subgraph "Cloud (Managed)"
API_H["Hub Backend API"]
DB_H["Database<br/>(Unknown)"]
IGDB["IGDB API<br/>(Game Metadata)"]
end
Hub["Hub App"] -->|"JSON-RPC 2.0<br/>POST /api/rpc"| API_H
Game_S["LCKStreaming<br/>Plugin"] -->|"JSON-RPC 2.0<br/>POST /api/rpc"| API_H
API_H --> DB_H
API_H --> IGDB
style API_H fill:#f96,stroke:#333
Known RPC Methods:
LoginUser, RefreshUser — AuthListMyStreamingTargets, CreateStreamingTarget, UpdateStreamingTarget, DeleteStreamingTarget — TargetsPairDevice, UnpairDevice, GetConnectedGames — Device managementStartStreaming, StopStreaming — Stream eventsSearchIgdbGames — Game metadataCreateOauthConnectIntent, GetOauthConnectIntent — OAuthlck-control-backend)graph TB
subgraph "Self-Hosted (Docker on NAS)"
API_C["Fastify 5 API<br/>(TypeScript)"]
Prisma["Prisma 6.4 ORM"]
SQLite["SQLite DB"]
Scheduler["Token Refresh<br/>Scheduler (10min)"]
end
App["Control App"] -->|"REST API<br/>JWT Bearer Auth"| API_C
API_C --> Prisma --> SQLite
Scheduler --> API_C
API_C -->|OAuth| Google["Google OAuth"]
API_C -->|OAuth| Twitch_API["Twitch OAuth"]
API_C -->|Nonce Validate| Meta_Graph["Meta Graph API"]
API_C -->|Live API| YT_API["YouTube Live API"]
API_C -->|Helix API| TW_API["Twitch Helix API"]
style API_C fill:#9cf,stroke:#333
REST Endpoints:
| Group | Endpoints |
|---|---|
| Auth | POST /auth/meta/callback, POST /auth/refresh, GET /auth/me, POST /auth/logout |
| Providers | GET /providers/accounts, GET /providers/{yt|tw}/auth-url, POST /providers/{yt|tw}/callback, DELETE /providers/:serviceId |
| Streams | GET /streams/plans, POST /streams/plans, GET /streams/plans/:id, DELETE /streams/plans/:id |
| Lifecycle | POST /streams/plans/:id/prepare, POST /streams/plans/:id/start, POST /streams/plans/:id/end |
graph LR
subgraph "Data Ownership"
direction TB
Server_H["Hub Backend<br/>(owns ALL data)"]
end
Hub_App["Hub App<br/>(thin client)"] <-->|"All state via<br/>JSON-RPC"| Server_H
Game_H["UE5 Game<br/>(paired device)"] <-->|"All state via<br/>JSON-RPC"| Server_H
YT_H["YouTube API"] <--> Server_H
TW_H["Twitch API"] <--> Server_H
style Server_H fill:#f96,stroke:#333
graph LR
subgraph "Data Ownership"
direction TB
Server_C["Control Backend<br/>(tokens, plans,<br/>OAuth)"]
App_C["Control App<br/>(local cache,<br/>session tokens)"]
Game_C["UE5 Game<br/>(RTMP streams)"]
end
App_C <-->|REST API| Server_C
Game_C <-->|"AIDL IPC<br/>(stream plans,<br/>RTMP config)"| App_C
Server_C <--> YT_C["YouTube API"]
Server_C <--> TW_C["Twitch API"]
Game_C -->|"RTMP<br/>(direct)"| CDN["YouTube / Twitch<br/>RTMP Ingest"]
style App_C fill:#9f9,stroke:#333
style Game_C fill:#9cf,stroke:#333
The Control architecture is fundamentally superior for game streaming because:
gantt
title Unification Roadmap
dateFormat YYYY-MM-DD
axisFormat %b %Y
section Phase 1: Production Readiness
CI/CD pipeline (Jenkins/GH Actions) :p1a, 2026-03-01, 14d
Sentry crash reporting :p1b, 2026-03-01, 7d
Backend deploy to cloud :p1c, 2026-03-08, 7d
Certificate pinning (OkHttp) :p1d, 2026-03-08, 3d
section Phase 2: Feature Parity
IGDB game metadata integration :p2a, 2026-03-15, 7d
Watermark / overlay support in encoder :p2b, 2026-03-15, 10d
Subscription model + paywall :p2c, 2026-03-22, 14d
section Phase 3: Hub Migration
Add fallback screen-capture mode :p3a, 2026-04-05, 14d
Port device pairing (for non-integrated games) :p3b, 2026-04-05, 10d
Migrate Hub users to Control :p3c, 2026-04-19, 14d
Deprecate Hub app :p3d, 2026-05-03, 7d
section Phase 4: Polish
Desktop companion (optional) :p4a, 2026-05-10, 21d
Advanced stream analytics :p4b, 2026-05-10, 14d
Store listing + marketing :p4c, 2026-05-24, 7d
graph TB
subgraph "Unified App"
direction TB
A["Control App Architecture<br/>(Kotlin + Compose + Hilt)"]
B["Control Backend<br/>(Fastify + Prisma + SQLite)"]
C["LCKControl Plugin<br/>(AIDL + multi-destination)"]
D["Stream Plan System<br/>(DRAFT → READY → LIVE → ENDED)"]
end
subgraph "Adopt from Hub"
E["Sentry Crash Reporting"]
F["IGDB Game Database"]
G["Certificate Pinning"]
H["Jenkins CI/CD"]
I["Watermark Renderer"]
J["Screen Capture Fallback"]
end
subgraph "Discard"
K["Rust/Tauri/Leptos Stack"]
L["JSON-RPC 2.0 Protocol"]
M["minirtmp (Rust RTMP)"]
N["Device Code Pairing<br/>(replaced by AIDL)"]
O["Single-Destination Limit"]
end
E --> A
F --> B
G --> A
H --> A
I --> C
J --> A
style A fill:#9f9,stroke:#333
style B fill:#9cf,stroke:#333
style C fill:#9f9,stroke:#333
style D fill:#9f9,stroke:#333
style K fill:#fbb,stroke:#333
style L fill:#fbb,stroke:#333
style M fill:#fbb,stroke:#333
style N fill:#fbb,stroke:#333
style O fill:#fbb,stroke:#333
To maintain the Hub's "works with any game" advantage, add a fallback path:
graph TB
Start["Game Launches"] --> Check{"LCKControl Plugin<br/>integrated?"}
Check -->|Yes| AIDL["AIDL IPC Path<br/>(direct encode,<br/>multi-destination)"]
Check -->|No| Capture["Screen Capture Path<br/>(MediaProjection,<br/>single destination)"]
AIDL --> Stream["Stream to Platforms"]
Capture --> Stream
style AIDL fill:#9f9,stroke:#333
style Capture fill:#ff9,stroke:#333
This gives the unified app both modes:
| Risk | Impact | Mitigation |
|---|---|---|
| Hub users lose access during migration | High | Run both apps in parallel during transition, provide migration guide |
| AIDL only works on Android (no desktop) | Medium | Screen capture fallback for desktop; evaluate PCVR needs later |
| Self-hosted backend scalability | Medium | Move to managed cloud (Railway, Fly.io) before store launch |
| Synchronous JNI blocking causes ANR | Medium | Add timeout handling, move to async callback pattern |
| No subscription model in Control | Low | Implement before store launch using existing Hub billing logic |
| Losing crash telemetry | Low | Add Sentry SDK early in Phase 1 |
quadrantChart
title Streaming Quality vs Maintenance Complexity
x-axis Low Maintenance --> High Maintenance
y-axis Low Quality --> High Quality
quadrant-1 Ideal
quadrant-2 Overengineered
quadrant-3 Avoid
quadrant-4 Quick & Dirty
Control App: [0.35, 0.85]
Hub App: [0.75, 0.45]
Unified - Recommended: [0.45, 0.90]
Recommendation: The Control App architecture with adopted Hub features provides the best path forward — higher streaming quality with a more maintainable stack. The Hub's Rust/Tauri/Leptos stack adds significant complexity without proportional benefits for an Android-focused product.
Document generated 2026-02-26. Based on analysis of liv-control-center, lck-control, lck-control-backend, and LCKGame codebases.