Two React Native apps for the MOQ (Media over QUIC) protocol: a publisher (camera/mic capture and publish) and a subscriber (subscribe and playback). Built as a monorepo with a shared Rust core.
apps/
publisher/ - React Native publisher app
subscriber/ - React Native subscriber/player app
packages/
moq-core/ - Rust crate: QUIC transport, MOQ protocol, media packaging
moq-native/ - UniFFI-generated TypeScript + JSI TurboModule bindings
moq-media/ - Platform native modules: camera capture, hardware codecs (Kotlin/Swift)
moq-ui/ - Shared React Native components and Zustand state stores
| Layer | Technology | What It Does |
|---|---|---|
| moq-core | Rust, Quinn, tokio, UniFFI | QUIC transport, MOQ protocol (draft-16/14), CMAF/MI packaging, NAL parsing, jitter buffer, A/V sync |
| moq-native | uniffi-bindgen-react-native | Auto-generated TypeScript + JSI bridge from Rust to RN |
| moq-media | Kotlin (MediaCodec), Swift (VideoToolbox) | Camera/mic capture, hardware H.264/Opus/AAC encode/decode |
| moq-ui | React Native, Zustand, TypeScript | Connection panel, stats overlay, shared hooks and stores |
Publisher: Camera/Mic -> Platform hardware encode -> Rust (MOQ packaging) -> QUIC -> Relay
Subscriber: Relay -> QUIC -> Rust (MOQ depackaging) -> Platform hardware decode -> Video/Audio output
Media data stays on the native side. JS handles control plane only.
- MOQ Transport draft-16 (primary) with draft-14 fallback
- MoQ Media Interop (draft-cenzano-moq-media-interop-03)
- CMAF/fMP4 packaging
- Codecs: H.264 (video), Opus (audio primary), AAC-LC (audio fallback)
- Adaptive quality with 720p30 default
- Node.js 18+
- Yarn 4+
- Rust toolchain (rustup) with Android targets
- Android SDK 34+ and NDK 27+ (for Android builds)
- Xcode 15+ (for iOS builds)
- cargo-ndk (
cargo install cargo-ndk)
# Clone and install
git clone <repo-url> moq-rn
cd moq-rn
corepack enable
yarn install
# Build Rust core (verify compilation)
cd packages/moq-core
cargo build
cargo test
cd ../..The Rust core must be cross-compiled for each target platform using uniffi-bindgen-react-native. This generates the native shared library, TypeScript bindings, and C++ JSI turbo module glue.
# Add Android cross-compilation targets
rustup target add x86_64-linux-android # emulator
rustup target add aarch64-linux-android # physical device
# Set environment (use absolute paths)
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/27.1.12297006
cd packages/moq-native
# Build for physical device (arm64)
yarn ubrn build android --config ubrn.config.yaml --and-generate -t arm64-v8a
# Or build for emulator (x86_64)
yarn ubrn build android --config ubrn.config.yaml --and-generate -t x86_64Important: The ubrn build replaces the jniLibs directory each time, so you can only target one ABI per build. Match the ABI to your target:
| Target | ABI | ndk.abiFilters in build.gradle |
|---|---|---|
| Physical device | arm64-v8a |
abiFilters "arm64-v8a" |
| x86_64 emulator | x86_64 |
abiFilters "x86_64" |
If switching targets, update packages/moq-native/android/build.gradle ndk { abiFilters } to match.
The build produces:
android/src/main/jniLibs/<arch>/libmoq_core.so-- the Rust shared librarysrc/generated/-- TypeScript bindingscpp/generated/-- C++ JNI bridge code
If clang-format fails (cosmetic only), the bindings are still generated. Format manually with /usr/bin/clang-format -i cpp/generated/*.cpp cpp/generated/*.hpp.
# Add iOS cross-compilation targets
rustup target add aarch64-apple-ios # physical device
rustup target add aarch64-apple-ios-sim # simulator (Apple Silicon)
rustup target add x86_64-apple-ios # simulator (Intel)
cd packages/moq-native
# Build for physical device only
yarn ubrn build ios --config ubrn.config.yaml --and-generate --targets aarch64-apple-ios
# Or build for simulator only (Apple Silicon)
yarn ubrn build ios --config ubrn.config.yaml --and-generate --targets aarch64-apple-ios-sim
# Or build for all targets (creates universal xcframework)
yarn ubrn build ios --config ubrn.config.yaml --and-generateImportant: Unlike Android, iOS builds create an xcframework that can contain multiple architectures. Match the target to your build environment:
| Target | Architecture | Use Case |
|---|---|---|
| Physical device | aarch64-apple-ios |
Running on iPhone/iPad |
| Simulator (Apple Silicon) | aarch64-apple-ios-sim |
Running on M1/M2/M3 Mac simulator |
| Simulator (Intel) | x86_64-apple-ios |
Running on Intel Mac simulator |
The build produces:
MoqRnMoqNativeFramework.xcframework-- Universal Rust framework for iOSsrc/generated/-- TypeScript bindingscpp/generated/-- C++ JSI bridge code
After building the Rust library, install CocoaPods dependencies:
cd apps/publisher/ios
pod install
cd ../../..For the subscriber app, replace publisher with subscriber in the path above.
If clang-format fails (cosmetic only), the bindings are still generated. Format manually with /usr/bin/clang-format -i cpp/generated/*.cpp cpp/generated/*.hpp.
Post-build fix required: The generated C++ code uses namespace as a parameter name, which is a C++ reserved keyword. After running ubrn build ios --and-generate, fix it with:
sed -i '' 's/RustBuffer namespace,/RustBuffer namespace_,/g; s/RustBuffer namespace)/RustBuffer namespace_)/g' cpp/generated/moq_core.cppThis must be reapplied each time you regenerate bindings. This is a known uniffi-bindgen-react-native issue.
# Run publisher app
yarn publisher:start # Metro bundler
yarn publisher:android # Android build
yarn publisher:ios # iOS build
# Run subscriber app
yarn subscriber:start
yarn subscriber:android
yarn subscriber:ioscd packages/moq-core && cargo testConnect the device via USB with developer mode and USB debugging enabled. Verify it appears:
adb devices -l
adb shell getprop ro.product.cpu.abi # should show arm64-v8aEnsure the Rust library was built for arm64-v8a (see "Building Rust UniFFI Bindings" above) and build.gradle has abiFilters "arm64-v8a".
Each app needs two terminals -- one for Metro bundler and one for the build:
# Terminal 1: Start Metro bundler
yarn subscriber:start
# Terminal 2: Build and install on device
yarn subscriber:androidFor the publisher app, replace subscriber with publisher in the commands above.
Ensure the Android SDK tools are on your PATH:
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$PATHAdd these to ~/.bashrc or ~/.profile to make them permanent.
The x86_64 emulator requires KVM hardware acceleration. Check with kvm-ok or ls /dev/kvm. If unavailable, use a physical device instead.
List available emulators and start one:
emulator -list-avds
emulator -avd <avd-name> &Wait for the emulator to fully boot. Ensure the Rust library was built for x86_64 and build.gradle has abiFilters "x86_64", then run the app as above.
On the emulator the publisher falls back to a test pattern generator since no camera is available. Audio capture uses the emulator's virtual microphone.
Connect the device via USB and ensure it's trusted by your Mac. Verify it appears in Xcode's device list or via:
xcrun xctrace list devicesEnsure the Rust library was built for iOS (see "Building for iOS" above) and pods are installed.
Each app needs two terminals -- one for Metro bundler and one for the build:
# Terminal 1: Start Metro bundler
yarn publisher:start
# Terminal 2: Build and install on device
yarn publisher:iosFor the subscriber app, replace publisher with subscriber in the commands above.
Ensure Xcode is installed and a simulator is available:
xcrun simctl list devices availableBuild and run the Rust library for simulator targets (aarch64-apple-ios-sim), then run the app as above.
On the simulator the publisher falls back to a test pattern generator since no camera is available. Audio capture uses the simulator's virtual microphone.
End-to-end testing requires a MOQ relay running locally or remotely:
- Start the relay
- Launch the publisher app, connect to the relay, and start publishing
- Launch the subscriber app, connect to the same relay, fetch tracks, and subscribe
Core implementation complete:
- Monorepo scaffolding (yarn workspaces, RN 0.84)
- Rust core: wire format, protocol messages, QUIC transport, session management
- Rust core: H.264 NAL parsing, MoQ Media Interop, CMAF/fMP4, catalog
- Rust core: jitter buffer, A/V sync timing
- Rust core: draft-14 fallback support
- UniFFI API surface and binding package
- UniFFI cross-compilation and binding generation (ubrn build android)
- Platform modules: Android (MediaCodec) + iOS (VideoToolbox)
- Shared UI: Zustand stores, ConnectionPanel, StatsOverlay
- WebTransport transport layer (default, with raw QUIC fallback)
- PUBLISH/SUBSCRIBE message encoding (draft-16)
- Publisher session: track management, SubgroupHeader streams, frame sending
- Subscriber session: subscribe, receive loop, frame dispatch with cancellation
- Expanded UniFFI API: publish, subscribe, catalog functions
- Android: test pattern generator + Camera2 capture with fallback
- iOS: test pattern generator + AVCaptureSession capture with fallback
- Audio capture: Android (AudioRecord + AAC) + iOS (AVAudioEngine + AAC)
- Native video views: VideoPreview + VideoPlaybackView (Android/iOS)
- React Native hooks: useMoqSession, usePublisher, useSubscriber
- Publisher app screen wired with capture and publish controls
- Subscriber app screen wired with track selection and playback
- On-device testing and optimization
- moq-dev/moq - Reference Rust + TS implementation
- cloudflare/moq-rs - Production Rust implementation
- uniffi-bindgen-react-native - Rust to RN bridge