A fully immersive 2D/3D video player for the Meta Quest 3 that streams straight from your own NAS over SMB, NFS, FTP, SFTP, 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/) β OpenXR session, swapchains and the render loop on top of Meta'sSampleXrFramework(OVRFW). Vulkan is the default backend, with an OpenGL ES path kept as a fallback. 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 and HLS (including AES-128 encrypted segments) |
| 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 |
| 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.
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.
Manual dependency
The Meta OpenXR Mobile SDK cannot be downloaded automatically β it requires accepting a license on Meta's portal. Extract it into sdk/meta-openxr-sdk/ so that sdk/meta-openxr-sdk/Samples/SampleXrFramework/ and sdk/meta-openxr-sdk/OpenXR/ exist. That folder is gitignored; every 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 checks for the Meta SDK):
./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.
The unified script cross-compiles Rust, copies the resulting .so files into jniLibs, and then runs Gradle:
./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.
Useful when you are iterating on a single layer and do not want to rebuild everything.
- Compile the Rust workspace (must be
cargo ndk, not plaincargo buildβcore,audioandbridgeneed the Android NDK toolchain):
cd rust
cargo ndk -t aarch64-linux-android -P 26 -o ../app/src/main/jniLibs build --release
cd ..- Copy the FFmpeg shared libraries next to them:
cp ffmpeg-android-maker/build/ffmpeg/arm64-v8a/lib/*.so app/src/main/jniLibs/arm64-v8a/- Build the Android app (this also triggers the CMake build of the native C++ layer):
./gradlew assembleDebug- Install 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.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 -- -D warnings
# Kotlin JVM unit tests + lint
./gradlew testDebugUnitTest
./gradlew ktlintCheckNetwork protocol integration tests run against real SMB/HTTP/HTTPS/FTP/SFTP servers 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.
Contributions are welcome β start with CONTRIBUTING.md.