A fully immersive 2D/3D video player for the Meta Quest 3 that streams straight from your own NAS over SMB, NFS, FTP, SFTP, WebDAV, DLNA or HTTP(S).
English Β· PortuguΓͺs (BR)
tucaVR (from Tamarutaca, shortened to tuca) is an open source immersive media player for the Meta Quest 3 / Quest 3s. It runs as a 100% VR application β a NativeActivity with com.oculus.vr.mode = vr_only, no classic flat Android UI β so the file browser, the network screens and the playback controls are all rendered as panels floating in 3D space.
The project is built on three languages, each doing what it is best at:
graph LR
K["<b>Kotlin</b><br/>app shell, UI panels,<br/>credentials, history, i18n"]
C["<b>C++</b><br/>OpenXR session, Vulkan/GLES<br/>render loop, controller input"]
R["<b>Rust</b><br/>demux, HW decode, audio,<br/>network protocols"]
K -- "JNI" --> C
C -- "C ABI" --> R
- Kotlin (
app/) β Android shell and the UI, drawn as plain AndroidViews inside anandroid.app.Presentationon aVirtualDisplay, then projected as textures onto 3D quads by the native layer. Also handles encrypted credential storage, Room-backed playback history and localization. - C++ (
native/, C++20) β OpenXR session, swapchains and the render loop. Vulkan is the default backend (independent of OVRFW), with an OpenGL ES path kept as a frozen fallback built on Meta'sSampleXrFramework(OVRFW). Decoded frames arrive asAHardwareBuffers and are bound zero-copy as external textures. - Rust (
rust/) β cross-compiled toaarch64-linux-android. Demuxing withffmpeg-next, hardware decoding throughndk::MediaCodec, audio output via Oboe, and every network protocol client written in pure Rust (no native TLS/SSH libraries, to keep cross-compilation sane).
Kotlin never calls Rust directly: it talks to C++ over JNI, and C++ is the only consumer of the Rust bridge crate's flat extern "C" API.
| Area | Support |
|---|---|
| Video | H.264, H.265/HEVC and AV1 with hardware decode (MediaCodec); containers handled by FFmpeg |
| 3D / VR | Side-by-Side and Over/Under (half & full), 360Β° mono and stereo, VR180 β with automatic format detection |
| Audio | Oboe output with A/V sync, spatial audio, per-track selection |
| Subtitles | External subtitle files with automatic charset detection (chardetng) |
| Streaming | HTTP(S) direct URLs, HLS (including AES-128 encrypted segments) and DASH |
| Rendering | Vulkan by default, OpenGL ES available as a fallback backend |
| Protocol | Notes |
|---|---|
| SMB / CIFS | Pure-Rust SMB2/3 client with auto-reconnect for flaky Wi-Fi |
| NFS | Browse and play from NFS exports |
| FTP | Pure-Rust blocking client |
| SFTP | Over SSH via russh β no libssh2/OpenSSL dependency |
| WebDAV | Over HTTP(S) with RFC 4918 PROPFIND XML directory parsing |
| DLNA / UPnP | Device description + DIDL-Lite browsing |
| HTTP / HTTPS | Direct URLs, with rustls for TLS |
| Discovery | Automatic mDNS / DNS-SD server discovery on the local network |
| Local files | On-device storage browsing with thumbnails and folder previews |
Remote reads go through a prefetching, chunked reader that issues concurrent range requests over a single session, so seeking on a NAS share does not stall the render loop.
An in-app web browser (GeckoView) rendered on its own panel, photo and 360Β°/3D photo viewing, hand tracking, passthrough, selectable 3D environments, downloads and playlists.
Playback history with "continue watching" and resume prompts, generated thumbnails for local and network files, encrypted credential storage (EncryptedSharedPreferences), saved servers, sorting and filtering in the file browser, thermal monitoring, and a fully localized UI (English, Portuguese-BR, and Spanish; see docs/i18n.md for translation contribution guidelines).
The project was born so I could reach my own files on my own NAS, over any network protocol, without having to pay 20 dollars for it β and as a hands-on performance experiment in Rust.
Hardware
- A Meta Quest 3 or Quest 3s headset with developer mode enabled (how to enable it). The app targets
arm64-v8aonly and requires 6DoF head tracking.
Toolchain
| Tool | Version | Download |
|---|---|---|
| Git | any | git-scm.com |
| JDK (Temurin) | 17 | adoptium.net |
| Android SDK Platform | API 34 (min API 26) | developer.android.com |
| Android NDK | 26.3.11579264 | developer.android.com |
| CMake | 3.22.1 | cmake.org |
| Rust (rustup) | stable | rustup.rs |
| cargo-ndk | latest | github.com/bbqsrc/cargo-ndk |
| Android Platform Tools (adb) | latest | developer.android.com |
| Docker + Compose | optional β only for the network protocol integration tests | docs.docker.com |
Gradle is not in that list on purpose β the repository ships the Gradle Wrapper (8.7), so
./gradlewbootstraps it for you.
Meta OpenXR SDK
./scripts/setup-deps.sh downloads the latest public Meta OpenXR SDK GitHub release into sdk/meta-openxr-sdk/ if it is not already present. That folder is gitignored, so each machine needs its own copy.
- Clone the repository:
git clone https://github.com/bgluis/tucaVR.git- Enter the project directory:
cd tucaVR- Prepare the external dependencies (clones
ffmpeg-android-makerand downloads the Meta SDK if missing):
./scripts/setup-deps.sh- Cross-compile FFmpeg for
arm64-v8a(one time only β it takes several minutes):
cd ffmpeg-android-maker
export ANDROID_SDK_HOME=$ANDROID_HOME
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.3.11579264
./ffmpeg-android-maker.sh --target-abis=arm64-v8a --android-api-level=26
cd ..From here you can build in one of two ways. Step 4 is only needed in host mode (see Method B); Docker mode compiles FFmpeg inside its image.
The unified script runs Gradle, whose buildRust task (scripts/build-rust.sh) cross-compiles Rust and copies the resulting .so files into jniLibs before the native build and APK assembly:
./scripts/build.sh # Rust + C++ + Gradle
# or, equivalently:
make build # same as scripts/build.sh
make deploy # build + adb install on the connected headsetThe APK lands in app/build/outputs/apk/debug/app-debug.apk.
APK assembly automatically runs the Gradle buildRust task (scripts/build-rust.sh), so Android
Studio's Build > Make Project and ./gradlew assembleDebug include current
Rust, C++, and Kotlin changes.
The build system automatically selects between Docker and Host Machine for compiling Rust & FFmpeg:
- Docker Mode (Default when host Rust toolchain is absent): No Rust,
cargo-ndk, orffmpeg-android-makerinstallation is needed on your host machineβonly Docker (or Podman) is required. - Host Machine Mode: To compile natively on your host machine without Docker, install:
- Rust with
aarch64-linux-androidtarget (rustup target add aarch64-linux-android). cargo-ndk(cargo install cargo-ndk).- Android NDK 26.3 (
26.3.11579264). - FFmpeg binaries in
ffmpeg-android-maker(./scripts/setup-deps.shfollowed by./ffmpeg-android-maker.sh --target-abis=arm64-v8a --android-api-level=26).
- Rust with
- Overriding Build Mode: Set
-PrustBuildMode=docker,-PrustBuildMode=host, or-PrustBuildMode=skipon the command line or ingradle.properties, or exportRUST_BUILD_MODE.
To build manually from a terminal:
./gradlew assembleDebugInstall it on the headset:
adb install -r app/build/outputs/apk/debug/app-debug.apkTo build against the OpenGL ES fallback backend instead of Vulkan:
./gradlew assembleDebug -PvrplayerGraphicsApi=GLES
scripts/build-rust.sh sets these for you; you only need them when running the Rust build by hand:
| Variable | Purpose | Default used by the script |
|---|---|---|
ANDROID_NDK_HOME |
Path to NDK 26.3.11579264 | $ANDROID_HOME/ndk/26.3.11579264 |
ANDROID_NDK_ROOT |
Same path β some tools read this one | mirrors ANDROID_NDK_HOME |
PKG_CONFIG_ALLOW_CROSS |
Lets pkg-config resolve the cross-compiled FFmpeg |
1 |
PKG_CONFIG_PATH |
Where the cross-compiled FFmpeg .pc files live |
ffmpeg-android-maker/build/ffmpeg/arm64-v8a/lib/pkgconfig |
BINDGEN_EXTRA_CLANG_ARGS |
Include paths and sysroot for the FFmpeg bindings | NDK sysroot + FFmpeg headers |
rust/core, rust/audio and rust/bridge do not compile on a normal host β they pull in ndk-sys/oboe-sys, which expect the Android NDK toolchain. That is exactly why pure logic (A/V sync, resample math, playback clamps) was extracted into the dependency-free media-logic crate: so it can be tested on a laptop. See docs/TESTING-PLAN.md for the full rationale.
# Rust unit tests (host-testable crates only)
cd rust && cargo test -p protocols -p media-logic
# Rust lint (CI runs this with -D warnings)
cd rust && cargo clippy -p protocols -p media-logic --all-targets --all-features -- -D warnings
# Kotlin JVM unit tests + lint
./gradlew testDebugUnitTest
./gradlew ktlintCheckNetwork protocol integration tests run against real SMB/HTTP/HTTPS/FTP/SFTP/WebDAV servers (plus generated DASH fixtures) in Docker β no headset required:
./scripts/test-network-protocols.sh # spins containers up, runs, tears down
./scripts/test-network-protocols.sh --keep # leaves them running for debuggingAnything that needs actual OpenXR rendering, controller haptics or hardware MediaCodec decode has no automated coverage and must be verified on a physical Quest 3. For the on-device debug HUD, forcing screen modes over adb and the optional Vulkan validation layers, see docs/DEBUGGING.md.
Released under the MIT License.
Contributions are welcome β start with CONTRIBUTING.md.