├── CloudXRLaunchOptions.txt ├── settings.gradle ├── app ├── src │ └── main │ │ ├── jni │ │ ├── Application.mk │ │ ├── log.h │ │ ├── Android.mk │ │ ├── jni.cpp │ │ ├── WaveCloudXRApp.h │ │ └── WaveCloudXRApp.cpp │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── jni_icon.png │ │ │ ├── purenative_icon.png │ │ │ └── singlebuffer_icon.png │ │ ├── mipmap-mdpi │ │ │ ├── jni_icon.png │ │ │ ├── purenative_icon.png │ │ │ └── singlebuffer_icon.png │ │ ├── mipmap-xhdpi │ │ │ ├── jni_icon.png │ │ │ ├── purenative_icon.png │ │ │ └── singlebuffer_icon.png │ │ ├── mipmap-xxhdpi │ │ │ ├── jni_icon.png │ │ │ ├── purenative_icon.png │ │ │ └── singlebuffer_icon.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── jni_icon.png │ │ │ ├── purenative_icon.png │ │ │ └── singlebuffer_icon.png │ │ ├── values-v21 │ │ │ └── styles.xml │ │ ├── values │ │ │ ├── styles.xml │ │ │ ├── dimens.xml │ │ │ └── strings.xml │ │ └── values-w820dp │ │ │ └── dimens.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── htc │ │ └── vr │ │ └── samples │ │ └── wavecloudxr │ │ └── MainActivity.java ├── libs │ └── NVIDIA CloudXR SDK 1.0 License.pdf ├── build_base.gradle ├── build.gradle └── build_sdk.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat ├── LICENSE └── gradlew /CloudXRLaunchOptions.txt: -------------------------------------------------------------------------------- 1 | -s 192.168.1.1 -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/jni/Application.mk: -------------------------------------------------------------------------------- 1 | APP_STL := c++_shared 2 | APP_CPPFLAGS := -std=c++11 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | app/.cxx/ 4 | app/build/ 5 | repo/ 6 | app/libs/CloudXR.aar 7 | local.properties 8 | app/libs/oboe-1.5.0.aar 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/jni_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/jni_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/jni_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/jni_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/jni_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/jni_icon.png -------------------------------------------------------------------------------- /app/libs/NVIDIA CloudXR SDK 1.0 License.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/libs/NVIDIA CloudXR SDK 1.0 License.pdf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/jni_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/jni_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/jni_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/jni_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/purenative_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/purenative_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/singlebuffer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-hdpi/singlebuffer_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/purenative_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/purenative_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/singlebuffer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-mdpi/singlebuffer_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/purenative_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/purenative_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/purenative_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/purenative_icon.png -------------------------------------------------------------------------------- /app/build_base.gradle: -------------------------------------------------------------------------------- 1 | android { 2 | compileSdkVersion 29 3 | 4 | defaultConfig { 5 | minSdkVersion 25 6 | targetSdkVersion 29 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/singlebuffer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xhdpi/singlebuffer_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/singlebuffer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxhdpi/singlebuffer_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/purenative_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/purenative_icon.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/singlebuffer_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ViveSoftware/Wave-CloudXR-Sample/HEAD/app/src/main/res/mipmap-xxxhdpi/singlebuffer_icon.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Wave CloudXR Sample Client 3 | Wave CloudXR Sample Client 4 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 30 19:16:42 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/jni/log.h: -------------------------------------------------------------------------------- 1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | 3 | #ifndef _LOG 4 | #define _LOG 5 | 6 | #ifndef LOG_TAG 7 | #define LOG_TAG "WaveCloudXRJNI" 8 | #endif 9 | 10 | #include 11 | 12 | #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) 13 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) 14 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) 15 | #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) 16 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) 17 | #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) 18 | 19 | #define LogD(tag, ...) __android_log_print(ANDROID_LOG_DEBUG, tag, __VA_ARGS__) 20 | #define LogE(tag, ...) __android_log_print(ANDROID_LOG_ERROR, tag, __VA_ARGS__) 21 | 22 | 23 | #endif // __LOG 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | org.gradle.daemon=true 10 | 11 | # Specifies the JVM arguments used for the daemon process. 12 | # The setting is particularly useful for tweaking memory settings. 13 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 14 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 15 | org.gradle.jvmargs=-Xmx1563M 16 | 17 | # When configured, Gradle will run in incubating parallel mode. 18 | # This option should only be used with decoupled projects. More details, visit 19 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 20 | # org.gradle.parallel=true 21 | 22 | org.gradle.configureondemand=true 23 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: "$projectDir/build_base.gradle" 3 | apply from: "$projectDir/build_sdk.gradle" 4 | 5 | android { 6 | buildToolsVersion '28.0.3' 7 | ndkVersion '21.4.7075529' 8 | defaultConfig { 9 | applicationId "com.htc.vr.samples.wavecloudxr" 10 | versionCode 4 11 | versionName "1.5" 12 | } 13 | 14 | signingConfigs { 15 | debug {} 16 | } 17 | 18 | buildTypes { 19 | debug { 20 | signingConfig signingConfigs.debug 21 | } 22 | release { 23 | signingConfig signingConfigs.debug 24 | } 25 | } 26 | 27 | flavorDimensions "version" 28 | productFlavors { 29 | 30 | bit64 { 31 | dimension "version" 32 | applicationId "com.htc.vr.samples.wavecloudxr" 33 | ndk { 34 | abiFilter 'arm64-v8a' 35 | } 36 | } 37 | } 38 | } 39 | 40 | repositories { 41 | flatDir{ 42 | dirs 'libs' 43 | } 44 | } 45 | 46 | dependencies { 47 | api 'com.htc.vr:wvr_client:+' 48 | implementation "com.nvidia.CloudXRClient:CloudXR@aar" 49 | implementation 'com.android.support:support-compat:28.0.0' 50 | implementation 'com.android.support:support-v4:28.0.0' 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/jni/Android.mk: -------------------------------------------------------------------------------- 1 | # ========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | LOCAL_PATH := $(call my-dir) 3 | 4 | VR_SDK_LIB := $(VR_SDK_ROOT)/jni/$(TARGET_ARCH_ABI) 5 | OBOE_SDK_LIB := $(OBOE_SDK_ROOT)/prefab/modules/oboe/libs/android.$(TARGET_ARCH_ABI) 6 | CLOUDXR_SDK_LIB := $(CLOUDXR_SDK_ROOT)/jni/$(TARGET_ARCH_ABI) 7 | 8 | include $(CLEAR_VARS) 9 | LOCAL_MODULE := wvr_api 10 | LOCAL_SRC_FILES := $(VR_SDK_LIB)/libwvr_api.so 11 | include $(PREBUILT_SHARED_LIBRARY) 12 | 13 | include $(CLEAR_VARS) 14 | LOCAL_MODULE := Oboe 15 | LOCAL_SRC_FILES := $(OBOE_SDK_LIB)/liboboe.so 16 | include $(PREBUILT_SHARED_LIBRARY) 17 | 18 | include $(CLEAR_VARS) 19 | LOCAL_MODULE := CloudXRClient 20 | LOCAL_SRC_FILES := $(CLOUDXR_SDK_LIB)/libCloudXRClient.so 21 | include $(PREBUILT_SHARED_LIBRARY) 22 | 23 | COMMON_INCLUDES := \ 24 | $(LOCAL_PATH)/include \ 25 | $(VR_SDK_ROOT)/include \ 26 | $(CLOUDXR_SDK_ROOT)/include \ 27 | $(OBOE_SDK_ROOT)/prefab/modules/oboe/include \ 28 | $(LOCAL_PATH) 29 | 30 | COMMON_FILES := \ 31 | WaveCloudXRApp.cpp \ 32 | jni.cpp 33 | 34 | #USE_CONTROLLER use device controller. 35 | #USE_CUSTOM_CONTROLLER use device emitter. 36 | 37 | include $(CLEAR_VARS) 38 | LOCAL_MODULE := WaveCloudXRJNI 39 | LOCAL_C_INCLUDES := $(COMMON_INCLUDES) 40 | LOCAL_SRC_FILES := $(COMMON_FILES) 41 | LOCAL_CPPFLAGS += -fexceptions 42 | LOCAL_LDLIBS := -llog -ljnigraphics -landroid -lEGL -lGLESv3 43 | LOCAL_SHARED_LIBRARIES := wvr_api Oboe CloudXRClient 44 | include $(BUILD_SHARED_LIBRARY) 45 | -------------------------------------------------------------------------------- /app/build_sdk.gradle: -------------------------------------------------------------------------------- 1 | apply from: "$projectDir/build_base.gradle" 2 | def jniDir = "src/main/jni" 3 | def aarFilename = "wvr_client" 4 | 5 | // unzip CloudXR SDK into build directory 6 | def cxrFile = file("${projectDir}/libs/CloudXR.aar") 7 | def CLOUDXR_SDK_ROOT = file("${buildDir}/CloudXR") 8 | if (cxrFile != null) { 9 | copy { 10 | println 'unzipping ' + cxrFile + ' into ' + CLOUDXR_SDK_ROOT 11 | from zipTree(cxrFile) 12 | into CLOUDXR_SDK_ROOT 13 | } 14 | } 15 | 16 | // unzip the Oboe sdk into build directory 17 | def oboeVersion = "1.5.0" 18 | def oboeFile = file("${projectDir}/libs/oboe-${oboeVersion}.aar") 19 | def OBOE_SDK_ROOT = file("${buildDir}/Oboe") 20 | if (oboeFile != null) { 21 | copy { 22 | println 'unzipping ' + oboeFile + ' into ' + OBOE_SDK_ROOT 23 | from zipTree(oboeFile) 24 | into OBOE_SDK_ROOT 25 | } 26 | } 27 | 28 | android { 29 | defaultConfig { 30 | externalNativeBuild { ndkBuild { 31 | def VR_SDK_ROOT = "${buildDir}/${aarFilename}" 32 | arguments "VR_SDK_ROOT=$VR_SDK_ROOT" 33 | arguments "OBOE_SDK_ROOT=$OBOE_SDK_ROOT" 34 | arguments "CLOUDXR_SDK_ROOT=$CLOUDXR_SDK_ROOT" 35 | }} 36 | ndk { 37 | abiFilters 'arm64-v8a' 38 | } 39 | } 40 | 41 | externalNativeBuild { 42 | ndkBuild { 43 | path "${jniDir}/Android.mk" 44 | } 45 | } 46 | 47 | buildTypes { 48 | release { 49 | externalNativeBuild { ndkBuild { 50 | arguments "NDK_DEBUG=0" 51 | }} 52 | } 53 | debug { 54 | externalNativeBuild { ndkBuild { 55 | arguments "NDK_DEBUG=1" 56 | }} 57 | } 58 | } 59 | } 60 | 61 | dependencies { 62 | compile 'com.htc.vr:wvr_client:+' 63 | } 64 | 65 | // unzip aar 66 | afterEvaluate { 67 | def unzipDir = file("${buildDir}/${aarFilename}") 68 | def artifacts = configurations.compile.resolvedConfiguration.resolvedArtifacts 69 | artifacts.find { it.name == aarFilename }?.with { aar -> 70 | copy { 71 | println 'unzip: ' + aar 72 | from zipTree(aar.file) 73 | into unzipDir 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/jni/jni.cpp: -------------------------------------------------------------------------------- 1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | bool gPaused = true; 10 | 11 | int main(int argc, char *argv[]) { 12 | 13 | WaveCloudXRApp *app = new WaveCloudXRApp(); 14 | if (!app->initVR()) { 15 | app->shutdownVR(); 16 | delete app; 17 | return 1; 18 | } 19 | 20 | if (!app->initGL()) { 21 | app->shutdownGL(); 22 | app->shutdownVR(); 23 | delete app; 24 | return 1; 25 | } 26 | 27 | if (!app->initCloudXR()) { 28 | app->shutdownGL(); 29 | app->shutdownVR(); 30 | app->shutdownCloudXR(); 31 | delete app; 32 | return 1; 33 | } 34 | 35 | app->beginPoseStream(); 36 | while (1) { 37 | if (!app->HandleCloudXRLifecycle(gPaused)) 38 | break; 39 | 40 | if (!app->handleInput()) 41 | break; 42 | 43 | if (!app->renderFrame()) 44 | break; 45 | 46 | // app->updatePose(); 47 | } 48 | app->stopPoseStream(); 49 | 50 | LOGE("Stop streaming."); 51 | LOGE("Shutting down components."); 52 | app->shutdownGL(); 53 | app->shutdownVR(); 54 | app->shutdownCloudXR(); 55 | 56 | delete app; 57 | return 0; 58 | } 59 | 60 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeInit(JNIEnv * env, jobject activityInstance, jobject assetManagerInstance) { 61 | LOGI("Register WVR main"); 62 | WVR_RegisterMain(main); 63 | } 64 | 65 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeOnPause(JNIEnv * env, jobject activityInstance) { 66 | gPaused = true; 67 | } 68 | 69 | extern "C" JNIEXPORT void JNICALL Java_com_htc_vr_samples_wavecloudxr_MainActivity_nativeOnResume(JNIEnv * env, jobject activityInstance) { 70 | gPaused = false; 71 | } 72 | 73 | 74 | jint JNI_OnLoad(JavaVM* vm, void* reserved) { 75 | 76 | return JNI_VERSION_1_6; 77 | } 78 | 79 | jint JNI_OnUnLoad(JavaVM* vm, void* reserved) { 80 | 81 | return 0; 82 | } 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Wave CloudXR Sample Client 3 | 4 | Demonstrate how to program with NVIDIA CloudXR SDK for VIVE Focus 3 and VIVE XR Elite headset. You can start to develop your own CloudXR application based on this sample client. 5 | 6 | Below are the instructions to build from source. Alternatively you can find a pre-built APK in the [Releases](https://github.com/ViveSoftware/Wave-CloudXR-Sample/releases) section. 7 | 8 | ## Requirements 9 | - HTC VIVE Focus 3 or VIVE XR Elite 10 | - [Wave Native SDK 4.3.0](https://developer.vive.com/resources/vive-wave/download/latest/) or later 11 | - [CloudXR SDK 4.0](https://developer.nvidia.com/nvidia-cloudxr-sdk) 12 | - [Google OBOE SDK 1.5.0](https://github.com/google/oboe/releases/tag/1.5.0) 13 | - Android development environment 14 | - Android Studio 4.0 or later 15 | - Android SDK 7.1.1 ‘Nougat’ (API level 25) or higher 16 | - Android build tools 28.0.3 17 | - Android NDK 21.4.7075529 18 | - OpenJDK 1.8n 19 | 20 | ## Build Instructions 21 | 1. Download [CloudXR SDK](https://developer.nvidia.com/nvidia-cloudxr-sdk) and [Google OBOE SDK 1.5.0](https://github.com/google/oboe/releases/tag/1.5.0). 22 | 2. Put ***CloudXR.aar*** and ***oboe-1.5.0.aar*** in ***[ProjectRoot]/app/libs*** 23 | 3. Download Wave SDK, extract the zip file and copy the ***repo*** folder to ***[ProjectRoot]***, alongside with ***app*** and ***gradle*** folders (paths can be modified in ***build_sdk.gradle***) 24 | 4. You are ready to build. 25 | 26 | ## Installation & Usage 27 | 1. Install CloudXR server on your PC. 28 | 2. Build Wave CloudXR Sample Client and install the apk to your headset 29 | 3. Modify the IP address in ***CloudXRLaunchOptions.txt*** and push it into ***/sdcard*** of your headset. 30 | - Please read [CloudXR Command-Line Options](https://docs.nvidia.com/cloudxr-sdk/usr_guide/cmd_line_options.html#command-line-options) for the format of ***CloudXRLaunchOptions.txt***) 31 | 5. Launch the apk to start streaming 32 | 33 | ## Notes 34 | * The application requires WRITE_EXTERNAL_STORAGE permission to proceed, for loading a config file from sdcard and writing CloudXR logs. 35 | * If RECORD_AUDIO permission is denied, microphone feature will be disabled. 36 | >The above permission requests will be prompted in-headset on first launch. To install it with permissions granted, use the *-g* flag with *adb install*. 37 | > `adb install -g client.apk` 38 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Wave CloudXR Sample client 3 | 4 | © 2021 HTC Corporation. All Rights Reserved. 5 | 6 | This document describes a contract between you and HTC Corporation (“HTC”) for the Works which refer to this software provided by HTC under the terms of this license. Please read it carefully before downloading or using this Work. If you do not agree to the terms of this license, please do not download or use this Work. 7 | 8 | This Work contains source code of CloudXR SDK provided by NVIDIA (https://developer.nvidia.com/nvidia-cloudxr-sdk) licensed under License Agreement for NVIDIA Software Development Kits. You must read and abide by restriction and other terms set for the in the NVIDIA license agreement. To review NVIDIA license agreement, look for file titled [NVIDIA CloudXR SDK 1.0 License.pdf] in NVIDIA CloudXR SDK distribution. 9 | 10 | Unless otherwise provided herein, the information contained in the Work is the exclusive property of HTC. 11 | 12 | HTC grants the users the rights to use, distribute and modify (if provided in a source code form) the Work within the scope of the legitimate development of software. The usage of the Work, with or without modification, is permitted provided that the following conditions are met: 13 | 14 | * The Work is distributed in a source code form must retain the above copyright notice, this list of conditions and the following disclaimer. 15 | * The Work is distributed in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distributions. 16 | * Neither HTC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 17 | * If you voluntarily provide HTC with any suggestions, enhancements, modifications, feature request or other feedback to the Work, you hereby grant HTC and its affiliates a perpetual, non-exclusive, worldwide, irrevocable license to use, reproduce, modify, license, sublicense (through multiple tiers of sublicensees), and distribute (through multiple tiers of distributors) it without the payment of any royalties or fees to you. HTC will use your feedback at its discretion. 18 | 19 | THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS IN THE WORK. 20 | 21 | IN NO EVENT SHALL HTC OR ITS SUPPLIERS BE LIABLE FOR ANY SPECIAL INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF INABILITY TO USE THE WORK, EVEN IF HTC HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/htc/vr/samples/wavecloudxr/MainActivity.java: -------------------------------------------------------------------------------- 1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | 3 | package com.htc.vr.samples.wavecloudxr; 4 | 5 | import com.htc.vr.sdk.VRActivity; 6 | 7 | import android.Manifest; 8 | import android.support.v4.app.ActivityCompat; 9 | import android.support.v4.content.ContextCompat; 10 | import android.content.pm.PackageInfo; 11 | import android.content.pm.PackageManager; 12 | import android.content.res.AssetManager; 13 | import android.os.Bundle; 14 | import android.util.Log; 15 | 16 | public class MainActivity extends VRActivity { 17 | private static final String TAG = "WaveCloudXRJAVA"; 18 | private final int PERMISSION_REQUEST_CODE = 1; 19 | private boolean mPermissionGranted = false; 20 | 21 | static { 22 | System.loadLibrary("WaveCloudXRJNI"); 23 | System.loadLibrary("CloudXRClient"); 24 | } 25 | public MainActivity() { 26 | } 27 | 28 | // Permission prompt is triggered by WVROverlayService. In order to use the service, WVR_Init must be called first, at native layer. 29 | // So the order is: JAVA onCreate() > Native WVR_Init() > JAVA requestPermission() > JAVA onRequestPermissionsResult > Native CloudXR Init 30 | @Override 31 | protected void onCreate(Bundle icicle) { 32 | super.onCreate(icicle); 33 | nativeInit(getResources().getAssets()); 34 | 35 | try { 36 | PackageManager pm = getPackageManager(); 37 | PackageInfo info = pm.getPackageInfo(getApplicationInfo().packageName, 0); 38 | Log.i(TAG, "Application Version: " + info.versionName + " code: " + info.versionCode); 39 | } catch (PackageManager.NameNotFoundException e) { 40 | e.printStackTrace(); 41 | } 42 | 43 | // Request storage permission for loading CloudXRLaunchOptions (required) & writing CloudXR log to sdcard 44 | // Request audio permission for microphone (optional) 45 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || 46 | ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { 47 | ActivityCompat.requestPermissions(this, new String[] { 48 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 49 | Manifest.permission.RECORD_AUDIO 50 | }, PERMISSION_REQUEST_CODE); 51 | Log.w(TAG, "Permission request sent."); 52 | } else { 53 | mPermissionGranted = true; 54 | OnPermission(); 55 | } 56 | } 57 | 58 | protected void OnPermission() { 59 | if (mPermissionGranted) { 60 | Log.e(TAG, "Permission granted."); 61 | } else { 62 | Log.e(TAG, "Failed to grant necessary permission. Aborting the program."); 63 | finish(); 64 | } 65 | } 66 | 67 | @Override 68 | protected void onResume() { 69 | super.onResume(); 70 | if (mPermissionGranted) nativeOnResume(); 71 | 72 | } 73 | 74 | @Override 75 | protected void onPause() { 76 | super.onPause(); 77 | if (mPermissionGranted) nativeOnPause(); 78 | } 79 | 80 | @Override 81 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 82 | if (requestCode == PERMISSION_REQUEST_CODE && grantResults != null && grantResults.length > 0) { 83 | // Write 84 | if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { 85 | Log.e(TAG, "Storage permission denied. Perm: "+ permissions[0] +" Result: " + grantResults[0]); 86 | mPermissionGranted = false; 87 | } else { 88 | mPermissionGranted = true; 89 | } 90 | 91 | // Audio 92 | if (grantResults[2] != PackageManager.PERMISSION_GRANTED) { 93 | Log.e(TAG, "Audio permission denied. Unable to use microphone during this streaming session. Perm: "+ permissions[1] +" Result: " + grantResults[1]); 94 | } 95 | } 96 | 97 | OnPermission(); 98 | } 99 | 100 | // JNI 101 | static native void nativeInit(AssetManager am); 102 | static native void nativeOnPause(); 103 | static native void nativeOnResume(); 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/jni/WaveCloudXRApp.h: -------------------------------------------------------------------------------- 1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | 3 | #pragma once 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "oboe/Oboe.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | class WaveCloudXRApp : public oboe::AudioStreamDataCallback 26 | { 27 | public: 28 | WaveCloudXRApp(); 29 | 30 | bool initVR(); 31 | bool initGL(); 32 | bool initCloudXR(); 33 | void shutdownVR(); 34 | void shutdownGL(); 35 | void shutdownCloudXR(); 36 | 37 | bool handleInput(); 38 | bool HandleCloudXRLifecycle(const bool pause); 39 | void beginPoseStream(); 40 | void stopPoseStream(); 41 | void updatePose(); 42 | bool renderFrame(); 43 | 44 | // Audio interface 45 | oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, 46 | void *audioData, int32_t numFrames) override; 47 | 48 | // CloudXR callback 49 | void GetTrackingState(cxrVRTrackingState* trackingState); 50 | void TriggerHaptic(const cxrHapticFeedback* haptic); 51 | cxrBool RenderAudio(const cxrAudioFrame*); 52 | // void HandleClientState(cxrClientState state, cxrStateReason reason); 53 | void HandleClientState(void* context, cxrClientState state, cxrError error); 54 | 55 | /* CloudXR interfaces */ 56 | /* 57 | * Send connection request to specified server IP 58 | * */ 59 | bool Connect(const bool async = true); 60 | protected: 61 | void Pause(); 62 | void Resume(); 63 | 64 | protected: 65 | uint16_t GetTouchInputIndex(const bool touched, const WVR_InputId wvrInputId); 66 | uint16_t GetPressInputIndex(const uint8_t hand, const bool pressed, const WVR_InputId wvrInputId); 67 | uint16_t GetAnalogInputIndex(const bool pressed, const WVR_InputId wvrInputId); 68 | void updateTime(); 69 | void processVREvent(const WVR_Event_t & event); 70 | 71 | void ReleaseFramebuffers(); 72 | void RecreateFramebuffer(const uint32_t width, const uint32_t height); 73 | 74 | GLuint CreateGLFramebuffer(const GLuint texId); 75 | 76 | // CloudXR function 77 | protected: 78 | bool LoadConfig(); 79 | bool InitAudio(); 80 | bool InitCallbacks(); 81 | bool InitDeviceDesc(); 82 | bool InitReceiver(); 83 | 84 | /* 85 | * Fetch video frame from CloudXR server 86 | */ 87 | bool UpdateFrame(); 88 | 89 | /* 90 | * Get device poses/inputs from WaveVR and update to CloudXR Server 91 | * */ 92 | bool UpdateHMDPose(const WVR_PoseState_t hmdPose); 93 | bool UpdateDevicePose(const WVR_DeviceType type, const WVR_PoseState_t ctrlPose); 94 | bool UpdateInput(const WVR_Event_t& event); 95 | bool UpdateAnalog(); 96 | 97 | /* 98 | * Render the video frame to the currently bound target surface 99 | */ 100 | bool Render(const uint32_t eye, WVR_TextureParams_t eyeTexture, const bool frameValid); 101 | 102 | void CheckStreamQuality(); 103 | private: 104 | 105 | // CloudXR 106 | cxrFramesLatched mFramesLatched{}; 107 | cxrReceiverHandle mReceiver= nullptr; 108 | cxrDeviceDesc mDeviceDesc{}; 109 | cxrClientCallbacks mClientCallbacks; 110 | cxrGraphicsContext mContext; 111 | CloudXR::ClientOptions mOptions; 112 | cxrConnectionDesc mConnectionDesc = {}; 113 | 114 | bool mStateDirty = false; 115 | cxrClientState mClientState = cxrClientState_ReadyToConnect; 116 | // cxrStateReason mClientStateReason = cxrStateReason_NoError; 117 | 118 | // Audio 119 | oboe::AudioStream* mPlaybackStream= nullptr; 120 | oboe::AudioStream* mRecordStream= nullptr; 121 | 122 | // Pose 123 | std::mutex mPoseMutex; 124 | std::thread *mPoseStream = nullptr; 125 | bool mExitPoseStream = false; 126 | cxrVRTrackingState mCXRPoseState; 127 | WVR_PoseState_t mHmdPose; 128 | WVR_PoseState_t mCtrlPoses[2]; 129 | 130 | // Input 131 | const uint8_t HAND_LEFT = 0; 132 | const uint8_t HAND_RIGHT = 1; 133 | const uint8_t IDX_TRIGGER = 0; 134 | const uint8_t IDX_GRIP = 1; 135 | const uint8_t IDX_THUMBSTICK = 2; 136 | cxrControllerHandle mControllers[2] = {}; 137 | bool mUpdateAnalogs[2][3] = {false};// [L|R][TRIGGER|GRIP|THUMBSTICK] 138 | // CXR Input event container 139 | cxrControllerEvent mCTLEvents[2][64] = {}; 140 | uint32_t mCTLEventCount[2] = {0}; 141 | 142 | uint32_t mLastButtons = 0; 143 | uint32_t mLastTouches = 0; 144 | WVR_AnalogState_t mLastAnalogs[3]; 145 | 146 | bool mIs6DoFHMD = false; 147 | bool mIs6DoFController[2] = {false, false}; 148 | 149 | bool mConnected; 150 | bool mPaused; 151 | bool mInited; 152 | 153 | // Render 154 | void* mLeftEyeQ; 155 | void* mRightEyeQ; 156 | 157 | std::vector mLeftEyeFBO; 158 | std::vector mRightEyeFBO; 159 | 160 | uint32_t mRenderWidth; 161 | uint32_t mRenderHeight; 162 | 163 | float mFrameInvalidTime = 0.0f; 164 | 165 | // Statistics 166 | float mTimeDiff; 167 | uint32_t mTimeAccumulator2S; // add in micro second. 168 | struct timeval mRtcTime; 169 | int mFrameCount = 0; 170 | float mFPS = 0; 171 | uint32_t mClockCount = 0; 172 | uint8_t mRetryConnCount = 0; 173 | const uint8_t mMaxRetryConnCount = 5; 174 | 175 | int mFramesUntilStats = 60; 176 | }; -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/jni/WaveCloudXRApp.cpp: -------------------------------------------------------------------------------- 1 | //========= Copyright 2016-2021, HTC Corporation. All rights reserved. =========== 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "CloudXRMatrixHelpers.h" 22 | #include "WaveCloudXRApp.h" 23 | 24 | // Return micro second. Should always positive because now is bigger. 25 | #define timeval_subtract(now, last) \ 26 | ((now.tv_sec - last.tv_sec) * 1000000LL + now.tv_usec - last.tv_usec) 27 | 28 | #define VR_MAX_CLOCKS 200 29 | #define LATCHFRAME_TIMEOUT_MS 100 // timeout to fetch a frame from CloudXR server 30 | #define FRAME_TIMEOUT_SECOND 10.0f // retry connection if no valid frame or connection timeout 31 | 32 | #define VERSION_CODE "v1.7" 33 | 34 | #define CASE(x) \ 35 | case x: \ 36 | return #x 37 | 38 | const char* ClientStateEnumToString(cxrClientState state) 39 | { 40 | switch (state) 41 | { 42 | CASE(cxrClientState_ReadyToConnect); 43 | CASE(cxrClientState_ConnectionAttemptInProgress); 44 | CASE(cxrClientState_ConnectionAttemptFailed); 45 | CASE(cxrClientState_StreamingSessionInProgress); 46 | CASE(cxrClientState_Disconnected); 47 | CASE(cxrClientState_Exiting); 48 | default: 49 | return ""; 50 | } 51 | } 52 | #undef CASE 53 | static constexpr int inputTouchLegacyCount = 21; 54 | 55 | static const char* inputsTouchLegacy[inputTouchLegacyCount] = 56 | { 57 | "/input/system/click", 58 | "/input/application_menu/click", 59 | "/input/trigger/click", // 2 60 | "/input/trigger/touch", // 3 61 | "/input/trigger/value", // 4 62 | "/input/grip/click", // 5 63 | "/input/grip/touch", // 6 64 | "/input/grip/value", // 7 65 | "/input/joystick/click", // 8 66 | "/input/joystick/touch", // 9 67 | "/input/joystick/x", // 10 68 | "/input/joystick/y", // 11 69 | "/input/a/click", // 12 70 | "/input/b/click", // 13 71 | "/input/x/click", // 14 72 | "/input/y/click", // 15 73 | "/input/a/touch", // 16 74 | "/input/b/touch", // 17 75 | "/input/x/touch", // 18 76 | "/input/y/touch", // 19 77 | "/input/thumb_rest/touch", 78 | }; 79 | 80 | cxrInputValueType inputValuesTouchLegacy[inputTouchLegacyCount] = 81 | { 82 | cxrInputValueType_boolean, //input/system/click 83 | cxrInputValueType_boolean, //input/application_menu/click 84 | cxrInputValueType_boolean, //input/trigger/click 85 | cxrInputValueType_boolean, //input/trigger/touch 86 | cxrInputValueType_float32, //input/trigger/value 87 | cxrInputValueType_boolean, //input/grip/click 88 | cxrInputValueType_boolean, //input/grip/touch 89 | cxrInputValueType_float32, //input/grip/value 90 | cxrInputValueType_boolean, //input/joystick/click 91 | cxrInputValueType_boolean, //input/joystick/touch 92 | cxrInputValueType_float32, //input/joystick/x 93 | cxrInputValueType_float32, //input/joystick/y 94 | cxrInputValueType_boolean, //input/a/click 95 | cxrInputValueType_boolean, //input/b/click 96 | cxrInputValueType_boolean, //input/x/click 97 | cxrInputValueType_boolean, //input/y/click 98 | cxrInputValueType_boolean, //input/a/touch 99 | cxrInputValueType_boolean, //input/b/touch 100 | cxrInputValueType_boolean, //input/x/touch 101 | cxrInputValueType_boolean, //input/y/touch 102 | cxrInputValueType_boolean, //input/thumb_rest/touch 103 | }; 104 | WaveCloudXRApp::WaveCloudXRApp() 105 | : mTimeDiff(0.0f) 106 | , mLeftEyeQ(nullptr) 107 | , mRightEyeQ(nullptr) 108 | , mReceiver(nullptr) 109 | , mPlaybackStream(nullptr) 110 | , mRecordStream(nullptr) 111 | , mClientState(cxrClientState_ReadyToConnect) 112 | , mRenderWidth(1720) 113 | , mRenderHeight(1720) 114 | , mConnected(false) 115 | , mInited(false) 116 | , mPaused(true) 117 | , mStateDirty(true) 118 | {} 119 | 120 | bool WaveCloudXRApp::initVR() { 121 | LOGI("Wave CloudXR Sample %s", VERSION_CODE); 122 | 123 | // Init WVR Runtime 124 | WVR_InitError eError = WVR_Init(WVR_AppType_VRContent); 125 | if (eError != WVR_InitError_None) { 126 | LOGE("Unable to init VR runtime: %s", WVR_GetInitErrorString(eError)); 127 | return false; 128 | } 129 | 130 | /* 131 | Controller mapping 132 | */ 133 | mIs6DoFHMD = false; 134 | if (WVR_NumDoF_6DoF==WVR_GetDegreeOfFreedom(WVR_DeviceType_HMD)) 135 | mIs6DoFHMD = true; 136 | 137 | mIs6DoFController[0] = (WVR_GetDegreeOfFreedom(WVR_DeviceType_Controller_Left)==WVR_NumDoF_6DoF); 138 | mIs6DoFController[1] = (WVR_GetDegreeOfFreedom(WVR_DeviceType_Controller_Right)==WVR_NumDoF_6DoF); 139 | LOGV("6DOF HMD = %d, Con1 = %d, Con2 = %d.", mIs6DoFHMD?1:0, mIs6DoFController[0]?1:0, mIs6DoFController[1]?1:0); 140 | 141 | WVR_SetPosePredictEnabled(WVR_DeviceType_HMD, true, true); 142 | WVR_SetPosePredictEnabled(WVR_DeviceType_Controller_Left, true, true); 143 | WVR_SetPosePredictEnabled(WVR_DeviceType_Controller_Right, true, true); 144 | 145 | WVR_SetArmModel(WVR_SimulationType_Auto); 146 | WVR_SetArmSticky(false); 147 | 148 | WVR_InputAttribute inputIdAndTypes[] = 149 | { 150 | {WVR_InputId_Alias1_System, WVR_InputType_Button, WVR_AnalogType_None}, 151 | {WVR_InputId_Alias1_Menu, WVR_InputType_Button, WVR_AnalogType_None}, 152 | {WVR_InputId_Alias1_Grip, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_1D}, 153 | {WVR_InputId_Alias1_DPad_Left, WVR_InputType_Button, WVR_AnalogType_None}, 154 | {WVR_InputId_Alias1_DPad_Up, WVR_InputType_Button, WVR_AnalogType_None}, 155 | {WVR_InputId_Alias1_DPad_Right, WVR_InputType_Button, WVR_AnalogType_None}, 156 | {WVR_InputId_Alias1_DPad_Down, WVR_InputType_Button, WVR_AnalogType_None}, 157 | {WVR_InputId_Alias1_Volume_Up, WVR_InputType_Button, WVR_AnalogType_None}, 158 | {WVR_InputId_Alias1_Volume_Down, WVR_InputType_Button, WVR_AnalogType_None}, 159 | {WVR_InputId_Alias1_Bumper, WVR_InputType_Button , WVR_AnalogType_None}, 160 | {WVR_InputId_Alias1_A, WVR_InputType_Button, WVR_AnalogType_None}, 161 | {WVR_InputId_Alias1_B, WVR_InputType_Button, WVR_AnalogType_None}, 162 | {WVR_InputId_Alias1_Back, WVR_InputType_Button, WVR_AnalogType_None}, 163 | {WVR_InputId_Alias1_Enter, WVR_InputType_Button, WVR_AnalogType_None}, 164 | {WVR_InputId_Alias1_Touchpad, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_2D}, 165 | {WVR_InputId_Alias1_Thumbstick, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_2D}, 166 | {WVR_InputId_Alias1_Trigger, WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog, WVR_AnalogType_1D} 167 | }; 168 | 169 | const size_t numAttribs = sizeof(inputIdAndTypes)/sizeof(*inputIdAndTypes); 170 | 171 | WVR_SetInputRequest(WVR_DeviceType_HMD, inputIdAndTypes, numAttribs); 172 | WVR_SetInputRequest(WVR_DeviceType_Controller_Left, inputIdAndTypes, numAttribs); 173 | WVR_SetInputRequest(WVR_DeviceType_Controller_Right, inputIdAndTypes, numAttribs); 174 | 175 | // Init Wave Render 176 | WVR_RenderInitParams_t param; 177 | param = { WVR_GraphicsApiType_OpenGL, WVR_RenderConfig_Default }; 178 | 179 | WVR_RenderError pError = WVR_RenderInit(¶m); 180 | if (pError != WVR_RenderError_None) { 181 | LOGE("Present init failed - Error[%d]", pError); 182 | } 183 | 184 | LOGI("initVR done"); 185 | return true; 186 | } 187 | 188 | bool WaveCloudXRApp::initGL() { 189 | 190 | gettimeofday(&mRtcTime, NULL); 191 | 192 | // Setup stereo render targets 193 | // WVR_GetRenderTargetSize(&mRenderWidth, &mRenderHeight); 194 | 195 | // Focus 3 panel resolution 196 | mRenderWidth = 2448; 197 | mRenderHeight = 2448; 198 | LOGD("Recommended size is %ux%u", mRenderWidth, mRenderHeight); 199 | if (mRenderWidth == 0 || mRenderHeight == 0) { 200 | LOGE("Please check server configure"); 201 | return false; 202 | } 203 | 204 | mLeftEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, mRenderWidth, mRenderHeight, 0); 205 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) { 206 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mLeftEyeQ, i).id); 207 | mLeftEyeFBO.push_back(fbo); 208 | } 209 | 210 | mRightEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, mRenderWidth, mRenderHeight, 0); 211 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) { 212 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mRightEyeQ, i).id); 213 | mRightEyeFBO.push_back(fbo); 214 | } 215 | 216 | return true; 217 | } 218 | 219 | GLuint WaveCloudXRApp::CreateGLFramebuffer(const GLuint texId) 220 | { 221 | GLuint fbo; 222 | glGenFramebuffers(1, &fbo); 223 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); 224 | glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, 225 | GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0); 226 | 227 | GLenum status = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); 228 | 229 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); 230 | 231 | if (status != GL_FRAMEBUFFER_COMPLETE) 232 | { 233 | LOGE("Incomplete frame buffer object (%d). Requested dimensions: %d x %d.", status, mRenderWidth, mRenderHeight); 234 | } 235 | return fbo; 236 | } 237 | 238 | void WaveCloudXRApp::shutdownGL() { 239 | 240 | ReleaseFramebuffers(); 241 | } 242 | 243 | void WaveCloudXRApp::shutdownVR() { 244 | WVR_Quit(); 245 | } 246 | 247 | void WaveCloudXRApp::shutdownCloudXR() { 248 | 249 | if (mPlaybackStream) 250 | { 251 | mPlaybackStream->close(); 252 | mPlaybackStream = nullptr; 253 | } 254 | 255 | if (mRecordStream) 256 | { 257 | mRecordStream->close(); 258 | mRecordStream = nullptr; 259 | } 260 | 261 | if (mReceiver) { 262 | cxrDestroyReceiver(mReceiver); 263 | mReceiver = nullptr; 264 | } 265 | 266 | mInited = false; 267 | LOGE("ShutdownCloudXR done"); 268 | } 269 | 270 | bool WaveCloudXRApp::HandleCloudXRLifecycle(const bool pause) 271 | { 272 | if (mPaused != pause) { 273 | if (pause) { 274 | Pause(); 275 | } else { 276 | Resume(); 277 | } 278 | } 279 | 280 | if (mStateDirty) { 281 | switch (mClientState) { 282 | case cxrClientState_ReadyToConnect: 283 | LOGI("Client is ready to connect."); 284 | Connect(); 285 | break; 286 | case cxrClientState_Disconnected: 287 | if (mRetryConnCount < mMaxRetryConnCount) { 288 | LOGE("Disconnected, reconnecting ... %d", mRetryConnCount); 289 | shutdownCloudXR(); 290 | if (initCloudXR()) { 291 | mRetryConnCount++; 292 | Connect(); 293 | } else { 294 | LOGE("Reinitialization failed, exiting app."); 295 | return false; 296 | } 297 | } else { 298 | LOGE("Unrecoverable disconnection, exiting app. "); 299 | return false; 300 | } 301 | break; 302 | case cxrClientState_Exiting: 303 | return false; 304 | 305 | default: 306 | break; 307 | } 308 | mStateDirty = false; 309 | } 310 | 311 | return true; 312 | } 313 | 314 | 315 | //----------------------------------------------------------------------------- 316 | // Purpose: Poll events. Quit application if return true. 317 | //----------------------------------------------------------------------------- 318 | bool WaveCloudXRApp::handleInput() { 319 | // Process WVR events 320 | WVR_Event_t event; 321 | while(WVR_PollEventQueue(&event)) { 322 | if(event.common.type == WVR_EventType_Quit) { 323 | shutdownCloudXR(); 324 | return false; 325 | } 326 | 327 | processVREvent(event); 328 | UpdateInput(event); 329 | } 330 | UpdateAnalog(); 331 | 332 | return true; 333 | } 334 | 335 | void WaveCloudXRApp::ReleaseFramebuffers() 336 | { 337 | if (mLeftEyeQ != 0) { 338 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) { 339 | glDeleteFramebuffers(1, &mLeftEyeFBO.at(i)); 340 | } 341 | WVR_ReleaseTextureQueue(mLeftEyeQ); 342 | } 343 | 344 | if (mRightEyeQ != 0) { 345 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) { 346 | glDeleteFramebuffers(1, &mRightEyeFBO.at(i)); 347 | } 348 | WVR_ReleaseTextureQueue(mRightEyeQ); 349 | } 350 | } 351 | 352 | void WaveCloudXRApp::RecreateFramebuffer(const uint32_t width, const uint32_t height) { 353 | 354 | if (mRenderWidth == width && mRenderHeight == height) 355 | return; 356 | 357 | ReleaseFramebuffers(); 358 | 359 | mLeftEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, width, height, 0); 360 | for (int i = 0; i < WVR_GetTextureQueueLength(mLeftEyeQ); i++) { 361 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mLeftEyeQ, i).id); 362 | mLeftEyeFBO.push_back(fbo); 363 | } 364 | 365 | mRightEyeQ = WVR_ObtainTextureQueue(WVR_TextureTarget_2D, WVR_TextureFormat_RGBA, WVR_TextureType_UnsignedByte, width, height, 0); 366 | for (int i = 0; i < WVR_GetTextureQueueLength(mRightEyeQ); i++) { 367 | GLuint fbo = CreateGLFramebuffer((GLuint)(size_t)WVR_GetTexture(mRightEyeQ, i).id); 368 | mRightEyeFBO.push_back(fbo); 369 | } 370 | 371 | mRenderWidth = width; 372 | mRenderHeight = height; 373 | LOGD("Recreated buffer %dx%d", mRenderWidth, mRenderHeight); 374 | } 375 | 376 | //----------------------------------------------------------------------------- 377 | // Purpose: Processes a single VR event 378 | //----------------------------------------------------------------------------- 379 | void WaveCloudXRApp::processVREvent(const WVR_Event_t & event) { 380 | switch(event.common.type) { 381 | case WVR_EventType_IpdChanged: 382 | { 383 | WVR_RenderProps_t props; 384 | bool ret = WVR_GetRenderProps(&props); 385 | float ipd = 0; 386 | if (ret) { 387 | mDeviceDesc.ipd = ipd; // used when re-init 388 | mCXRPoseState.hmd.ipd = ipd; // updated along with pose 389 | } 390 | LOGI("Receive WVR_EventType_IpdChanged = %d", ipd); 391 | } 392 | break; 393 | case WVR_EventType_RenderingToBePaused: 394 | case WVR_EventType_DeviceSuspend: 395 | { 396 | LOGE("Device %d suspended.", event.device.deviceType); 397 | mPaused = true; 398 | break; 399 | } 400 | 401 | case WVR_EventType_RenderingToBeResumed: 402 | case WVR_EventType_DeviceResume: 403 | { 404 | LOGE("WVR Device %d resumed.", event.device.deviceType); 405 | mPaused = false; 406 | break; 407 | } 408 | 409 | default: 410 | break; 411 | } 412 | } 413 | bool WaveCloudXRApp::renderFrame() { 414 | updateTime(); 415 | 416 | bool frameValid = UpdateFrame(); 417 | if (!frameValid) { 418 | // Exit program when no valid frame for too long 419 | mFrameInvalidTime += mTimeDiff; 420 | if (mFrameInvalidTime > FRAME_TIMEOUT_SECOND) { 421 | LOGD("No valid frame for %f seconds", mFrameInvalidTime); 422 | return false; 423 | } 424 | } else { 425 | mFrameInvalidTime = 0.0f; 426 | 427 | CheckStreamQuality(); 428 | } 429 | 430 | /* 431 | * Render & Submit 432 | * Left Eye 433 | * */ 434 | int32_t leftIdx = WVR_GetAvailableTextureIndex(mLeftEyeQ); 435 | GLuint fbo = mLeftEyeFBO.at(leftIdx); 436 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); 437 | 438 | WVR_TextureParams_t leftEyeTexture = WVR_GetTexture(mLeftEyeQ, leftIdx); 439 | 440 | glViewport(0, 0, mRenderWidth, mRenderHeight); 441 | glScissor(0, 0, mRenderWidth, mRenderHeight); 442 | WVR_PreRenderEye(WVR_Eye_Left, &leftEyeTexture); 443 | WVR_RenderMask(WVR_Eye_Left); 444 | 445 | glClear(GL_COLOR_BUFFER_BIT); 446 | Render(WVR_Eye_Left, leftEyeTexture, frameValid); 447 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); 448 | 449 | /* 450 | * Render & Submit 451 | * Right Eye 452 | * */ 453 | int32_t rightIdx = WVR_GetAvailableTextureIndex(mRightEyeQ); 454 | fbo = mRightEyeFBO.at(rightIdx); 455 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); 456 | WVR_TextureParams_t rightEyeTexture = WVR_GetTexture(mRightEyeQ, rightIdx); 457 | 458 | glViewport(0, 0, mRenderWidth, mRenderHeight); 459 | glScissor(0, 0, mRenderWidth, mRenderHeight); 460 | WVR_PreRenderEye(WVR_Eye_Right, &rightEyeTexture); 461 | WVR_RenderMask(WVR_Eye_Right); 462 | 463 | glClear(GL_COLOR_BUFFER_BIT); 464 | Render(WVR_Eye_Right, rightEyeTexture, frameValid); 465 | glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); 466 | 467 | // Clear 468 | { 469 | // We want to make sure the glFinish waits for the entire present to complete, not just the submission 470 | // of the command. So, we do a clear here right here so the glFinish will wait fully for the swap. 471 | glClearColor(0, 0, 0, 1); 472 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 473 | } 474 | 475 | usleep(1); // ? 476 | return true; 477 | } 478 | 479 | static uint updatePoseCount = 0; 480 | static uint getPoseCount = 0; 481 | void WaveCloudXRApp::updateTime() { 482 | // Process time variable. 483 | struct timeval now; 484 | gettimeofday(&now, NULL); 485 | 486 | mClockCount++; 487 | if (mRtcTime.tv_usec > now.tv_usec) 488 | mClockCount = 0; 489 | if (mClockCount >= VR_MAX_CLOCKS) 490 | mClockCount--; 491 | 492 | uint32_t timeDiff = timeval_subtract(now, mRtcTime); 493 | mTimeDiff = timeDiff / 1000000.0f; 494 | mTimeAccumulator2S += timeDiff; 495 | mRtcTime = now; 496 | mFrameCount++; 497 | if (mTimeAccumulator2S > 1000000) { 498 | mFPS = mFrameCount / (mTimeAccumulator2S / 1000000.0f); 499 | LOGI("FPS %2.0f, UpdatePose: %d, GetPose: %d", mFPS, updatePoseCount, getPoseCount); 500 | 501 | updatePoseCount = 0; 502 | getPoseCount = 0; 503 | mFrameCount = 0; 504 | mTimeAccumulator2S = 0; 505 | } 506 | } 507 | 508 | void WaveCloudXRApp::beginPoseStream() { 509 | if (mPoseStream == nullptr) { 510 | mPoseStream = new std::thread(&WaveCloudXRApp::updatePose, this); 511 | } 512 | } 513 | 514 | void WaveCloudXRApp::stopPoseStream() { 515 | if(mPoseStream!=nullptr) { 516 | mExitPoseStream = true; 517 | if(mPoseStream->joinable()) { 518 | mPoseStream->join(); 519 | delete mPoseStream; 520 | mPoseStream = nullptr; 521 | } 522 | } 523 | } 524 | // 1 sec = 1,000ms = 1,000,000,000ns 525 | void WaveCloudXRApp::updatePose() { 526 | // Update pose 250 per second by default 527 | int deno = (mDeviceDesc.posePollFreq == 0) ? 250 : mDeviceDesc.posePollFreq; 528 | long long int sleepNs = 1000000000 / deno; 529 | LOGI("PoseStream Update per %lldns", sleepNs); 530 | 531 | while (!mExitPoseStream) { 532 | 533 | while (mInited && mConnected) 534 | { 535 | std::lock_guard lock(mPoseMutex); 536 | { 537 | WVR_PoseOriginModel pom = WVR_PoseOriginModel_OriginOnGround; 538 | // Returns immediately with latest pose 539 | WVR_GetPoseState(WVR_DeviceType_HMD, pom, 0, &mHmdPose); 540 | UpdateHMDPose(mHmdPose); 541 | 542 | pom = mIs6DoFHMD ? WVR_PoseOriginModel_OriginOnGround 543 | : WVR_PoseOriginModel_OriginOnHead_3DoF; 544 | 545 | WVR_GetPoseState(WVR_DeviceType_Controller_Left, pom, 0, &mCtrlPoses[0]); 546 | UpdateDevicePose(WVR_DeviceType_Controller_Left, mCtrlPoses[0]); 547 | 548 | WVR_GetPoseState(WVR_DeviceType_Controller_Right, pom, 0, &mCtrlPoses[1]); 549 | UpdateDevicePose(WVR_DeviceType_Controller_Right, mCtrlPoses[1]); 550 | updatePoseCount++; 551 | } 552 | std::this_thread::sleep_for(std::chrono::nanoseconds(sleepNs)); 553 | } 554 | } 555 | 556 | LOGI("PoseStream end"); 557 | } 558 | 559 | /* CloudXR */ 560 | 561 | WVR_Matrix4f_t Convert(const cxrMatrix34& mtx) 562 | { 563 | WVR_Matrix4f_t out{}; 564 | memcpy(&out, &mtx, sizeof(mtx)); 565 | out.m[3][3] = 1.0f; 566 | 567 | return out; 568 | } 569 | 570 | cxrMatrix34 Convert(const WVR_Matrix4f_t& mtx) 571 | { 572 | cxrMatrix34 out{}; 573 | memcpy(&out, &mtx, sizeof(out)); 574 | 575 | return out; 576 | } 577 | 578 | cxrVector3 Convert(const WVR_Vector3f& vec) 579 | { 580 | return cxrVector3{vec.v[0], vec.v[1], vec.v[2]}; 581 | } 582 | 583 | WVR_Vector3f Convert(const cxrVector3& vec) 584 | { 585 | return WVR_Vector3f{vec.v[0], vec.v[1], vec.v[2]}; 586 | } 587 | 588 | // Make sure storage permission is granted before LoadConfig() and InitReceiver() 589 | // Make sure WaveVR is initialized before InitDeviceDesc() 590 | bool WaveCloudXRApp::initCloudXR() { 591 | 592 | if (mInited) { 593 | return true; 594 | } 595 | 596 | if (!LoadConfig()) return false; 597 | if (!InitCallbacks()) return false; 598 | if (!InitDeviceDesc()) return false; 599 | if (!InitReceiver()) return false; 600 | if (!InitAudio()) return false; 601 | 602 | mInited = true; 603 | LOGW("CloudXR initialization success"); 604 | return mInited; 605 | } 606 | 607 | bool WaveCloudXRApp::LoadConfig() { 608 | 609 | ParseStatus result = mOptions.ParseFile("/sdcard/CloudXRLaunchOptions.txt"); 610 | 611 | bool ret = false; 612 | switch(result) 613 | { 614 | case ParseStatus_Success: 615 | LOGI("Loaded server IP from config: %s", mOptions.mServerIP.c_str()); 616 | ret = true; 617 | break; 618 | case ParseStatus_FileNotFound: 619 | LOGE("Config file not found."); 620 | case ParseStatus_Fail: 621 | case ParseStatus_ExitRequested: 622 | case ParseStatus_BadVal: 623 | LOGE("Config file loading failed."); 624 | ret = false; 625 | break; 626 | default: 627 | break; 628 | } 629 | return ret; 630 | } 631 | 632 | bool WaveCloudXRApp::InitCallbacks() { 633 | 634 | mClientCallbacks.GetTrackingState = [](void* context, cxrVRTrackingState* trackingState) 635 | { 636 | return reinterpret_cast(context)->GetTrackingState(trackingState); 637 | }; 638 | mClientCallbacks.TriggerHaptic = [](void* context, const cxrHapticFeedback* haptic) 639 | { 640 | return reinterpret_cast(context)->TriggerHaptic(haptic); 641 | }; 642 | mClientCallbacks.RenderAudio = [](void* context, const cxrAudioFrame *audioFrame) 643 | { 644 | return reinterpret_cast(context)->RenderAudio(audioFrame); 645 | }; 646 | //mClientCallbacks.ReceiveUserData = [](void* context, const void* data, uint32_t size) 647 | //{ 648 | // return reinterpret_cast(context)->ReceiveUserData(data, size); 649 | //}; 650 | 651 | mClientCallbacks.UpdateClientState = [](void* context, cxrClientState state, cxrError error) 652 | { 653 | return reinterpret_cast(context)->HandleClientState(context, state, error); 654 | }; 655 | 656 | mClientCallbacks.LogMessage = [](void* context, cxrLogLevel level, cxrMessageCategory category, void* extra, const char* tag, const char* const messageText) 657 | { 658 | switch(level) { 659 | case cxrLogLevel::cxrLL_Verbose: 660 | //LOGV("[%s] %s", tag, messageText); // this opens up verbose decoder logging 661 | break; 662 | case cxrLogLevel::cxrLL_Info: 663 | LOGI("[%s] %s", tag, messageText); 664 | break; 665 | case cxrLogLevel::cxrLL_Debug: 666 | LOGD("[%s] %s", tag, messageText); 667 | break; 668 | case cxrLogLevel::cxrLL_Warning: 669 | LOGW("[%s] %s", tag, messageText); 670 | break; 671 | case cxrLogLevel::cxrLL_Critical: 672 | case cxrLogLevel::cxrLL_Error: 673 | LOGE("[%s] %s", tag, messageText); 674 | break; 675 | default: 676 | //LOGI("[%s] %s", tag, messageText); 677 | break; 678 | } 679 | }; 680 | 681 | mClientCallbacks.clientContext = this; 682 | 683 | return true; 684 | } 685 | 686 | bool WaveCloudXRApp::InitAudio() { 687 | 688 | if (mDeviceDesc.receiveAudio) 689 | { 690 | // Initialize audio playback 691 | oboe::AudioStreamBuilder playbackStreamBuilder; 692 | playbackStreamBuilder.setDirection(oboe::Direction::Output); 693 | playbackStreamBuilder.setPerformanceMode(oboe::PerformanceMode::LowLatency); 694 | playbackStreamBuilder.setSharingMode(oboe::SharingMode::Exclusive); 695 | playbackStreamBuilder.setFormat(oboe::AudioFormat::I16); 696 | playbackStreamBuilder.setChannelCount(oboe::ChannelCount::Stereo); 697 | playbackStreamBuilder.setSampleRate(CXR_AUDIO_SAMPLING_RATE); 698 | 699 | // TODO: proceed without audio? 700 | oboe::Result r = playbackStreamBuilder.openStream(&mPlaybackStream); 701 | if (r != oboe::Result::OK) { 702 | LOGE("Failed to open playback stream. Error: %s", oboe::convertToText(r)); 703 | return false; 704 | } 705 | 706 | int bufferSizeFrames = mPlaybackStream->getFramesPerBurst() * 2; 707 | r = mPlaybackStream->setBufferSizeInFrames(bufferSizeFrames); 708 | if (r != oboe::Result::OK) { 709 | LOGE("Failed to set playback stream buffer size to: %d. Error: %s", 710 | bufferSizeFrames, oboe::convertToText(r)); 711 | return false; 712 | } 713 | 714 | r = mPlaybackStream->start(); 715 | if (r != oboe::Result::OK) { 716 | LOGE("Failed to start playback stream. Error: %s", oboe::convertToText(r)); 717 | return false; 718 | } 719 | } 720 | 721 | if (mDeviceDesc.sendAudio) 722 | { 723 | // Initialize audio recording 724 | oboe::AudioStreamBuilder recordingStreamBuilder; 725 | recordingStreamBuilder.setDirection(oboe::Direction::Input); 726 | recordingStreamBuilder.setPerformanceMode(oboe::PerformanceMode::LowLatency); 727 | recordingStreamBuilder.setSharingMode(oboe::SharingMode::Exclusive); 728 | recordingStreamBuilder.setFormat(oboe::AudioFormat::I16); 729 | recordingStreamBuilder.setChannelCount(oboe::ChannelCount::Stereo); 730 | recordingStreamBuilder.setSampleRate(CXR_AUDIO_SAMPLING_RATE); 731 | recordingStreamBuilder.setInputPreset(oboe::InputPreset::VoiceCommunication); 732 | recordingStreamBuilder.setDataCallback(this); 733 | 734 | oboe::Result r = recordingStreamBuilder.openStream(&mRecordStream); 735 | if (r != oboe::Result::OK) { 736 | LOGE("Failed to open recording stream. Error: %s", oboe::convertToText(r)); 737 | LOGE("Continuing to run, without recording ability."); 738 | mDeviceDesc.sendAudio = false; 739 | } else { 740 | r = mRecordStream->start(); 741 | if (r != oboe::Result::OK) 742 | { 743 | LOGE("Failed to start recording stream. Error: %s", oboe::convertToText(r)); 744 | LOGE("Continuing to run, without recording ability."); 745 | mRecordStream->close(); 746 | mDeviceDesc.sendAudio = false; 747 | } 748 | } 749 | } 750 | 751 | return true; 752 | } 753 | 754 | bool WaveCloudXRApp::InitDeviceDesc() { 755 | WVR_RenderProps_t props; 756 | WVR_GetRenderProps(&props); 757 | 758 | mDeviceDesc.numVideoStreamDescs = CXR_NUM_VIDEO_STREAMS_XR; 759 | for (uint32_t i = 0; i < mDeviceDesc.numVideoStreamDescs; i++) { 760 | mDeviceDesc.videoStreamDescs[i].format = cxrClientSurfaceFormat_RGB; 761 | mDeviceDesc.videoStreamDescs[i].width = mRenderWidth; 762 | mDeviceDesc.videoStreamDescs[i].height = mRenderHeight; 763 | mDeviceDesc.videoStreamDescs[i].fps = props.refreshRate; 764 | mDeviceDesc.videoStreamDescs[i].maxBitrate = mOptions.mMaxVideoBitrate; 765 | } 766 | 767 | mDeviceDesc.stereoDisplay = true; 768 | mDeviceDesc.maxResFactor = mOptions.mMaxResFactor; 769 | 770 | mDeviceDesc.ipd = props.ipdMeter; 771 | mDeviceDesc.receiveAudio = mOptions.mReceiveAudio; 772 | mDeviceDesc.sendAudio = mOptions.mSendAudio; 773 | mDeviceDesc.posePollFreq = 0; 774 | 775 | mDeviceDesc.disablePosePrediction = false; 776 | mDeviceDesc.angularVelocityInDeviceSpace = true; 777 | mDeviceDesc.foveatedScaleFactor = (mOptions.mFoveation < 100) ? mOptions.mFoveation : 0; 778 | mDeviceDesc.disableVVSync = false; 779 | 780 | // Frustum 781 | float l,r,t,b; 782 | for (int i=0; i<2; ++i) { 783 | WVR_GetClippingPlaneBoundary((WVR_Eye)i, &l, &r, &t, &b); 784 | if(l < 0) l *= -1; 785 | if(r < 0) r *= -1; 786 | if(t < 0) t *= -1; 787 | if(b < 0) b *= -1; 788 | mDeviceDesc.proj[i][0] = -l; 789 | mDeviceDesc.proj[i][1] = r; 790 | mDeviceDesc.proj[i][2] = -b; 791 | mDeviceDesc.proj[i][3] = t; 792 | } 793 | 794 | mDeviceDesc.predOffset = 0.01f; 795 | 796 | // Set up server chaperone play area 797 | mDeviceDesc.chaperone.universe = cxrUniverseOrigin_Standing; 798 | mDeviceDesc.chaperone.origin.m[0][0] = mDeviceDesc.chaperone.origin.m[1][1] = mDeviceDesc.chaperone.origin.m[2][2] = 1; 799 | mDeviceDesc.chaperone.origin.m[0][1] = mDeviceDesc.chaperone.origin.m[0][2] = mDeviceDesc.chaperone.origin.m[0][3] = 0; 800 | mDeviceDesc.chaperone.origin.m[1][0] = mDeviceDesc.chaperone.origin.m[1][2] = mDeviceDesc.chaperone.origin.m[1][3] = 0; 801 | mDeviceDesc.chaperone.origin.m[2][0] = mDeviceDesc.chaperone.origin.m[2][1] = mDeviceDesc.chaperone.origin.m[2][3] = 0; 802 | 803 | WVR_Arena_t arena = WVR_GetArena(); 804 | if (arena.shape == WVR_ArenaShape_Round) 805 | { 806 | mDeviceDesc.chaperone.playArea.v[0] = arena.area.round.diameter; 807 | mDeviceDesc.chaperone.playArea.v[1] = arena.area.round.diameter; 808 | } else if (arena.shape == WVR_ArenaShape_Rectangle ) { 809 | mDeviceDesc.chaperone.playArea.v[0] = arena.area.rectangle.width; 810 | mDeviceDesc.chaperone.playArea.v[1] = arena.area.rectangle.length; 811 | } else { 812 | mDeviceDesc.chaperone.playArea.v[0] = 1.0f; 813 | mDeviceDesc.chaperone.playArea.v[1] = 1.0f; 814 | } 815 | 816 | LOGI("Device property stream#0: IPD: %f, FPS: %f, display %dx%d, play area %.2fx%.2f\n", 817 | mDeviceDesc.ipd, mDeviceDesc.videoStreamDescs[0].fps, 818 | mDeviceDesc.videoStreamDescs[0].width, mDeviceDesc.videoStreamDescs[0].height, 819 | mDeviceDesc.chaperone.playArea.v[0], mDeviceDesc.chaperone.playArea.v[1]); 820 | 821 | LOGI("Device property stream#1: IPD: %f, FPS: %f, display %dx%d, play area %.2fx%.2f\n", 822 | mDeviceDesc.ipd, mDeviceDesc.videoStreamDescs[1].fps, 823 | mDeviceDesc.videoStreamDescs[1].width, mDeviceDesc.videoStreamDescs[1].height, 824 | mDeviceDesc.chaperone.playArea.v[0], mDeviceDesc.chaperone.playArea.v[1]); 825 | return true; 826 | } 827 | 828 | bool WaveCloudXRApp::InitReceiver() { 829 | 830 | if (mReceiver) { 831 | LOGV("Receiver already created"); 832 | return true; 833 | } 834 | 835 | mContext.type = cxrGraphicsContext_GLES; 836 | mContext.egl.display = eglGetCurrentDisplay(); 837 | mContext.egl.context = eglGetCurrentContext(); 838 | if (mContext.egl.context == nullptr) { 839 | LOGV("eglContext invalid"); 840 | return false; 841 | } 842 | 843 | cxrReceiverDesc desc = { 0 }; 844 | desc.requestedVersion = CLOUDXR_VERSION_DWORD; 845 | desc.deviceDesc = mDeviceDesc; 846 | desc.clientCallbacks = mClientCallbacks; 847 | desc.shareContext = &mContext; 848 | desc.debugFlags = mOptions.mDebugFlags | cxrDebugFlags_OutputLinearRGBColor | cxrDebugFlags_EnableAImageReaderDecoder; 849 | desc.logMaxSizeKB = CLOUDXR_LOG_MAX_DEFAULT; 850 | desc.logMaxAgeDays = CLOUDXR_LOG_MAX_DEFAULT; 851 | strncpy(desc.appOutputPath, "sdcard/CloudXR/logs/", CXR_MAX_PATH - 1); // log file path 852 | desc.appOutputPath[CXR_MAX_PATH-1] = 0; // ensure null terminated if string was too long. 853 | cxrError err = cxrCreateReceiver(&desc, &mReceiver); 854 | if (err != cxrError_Success) 855 | { 856 | LOGE("Failed to create CloudXR receiver. Error %d, %s.", err, cxrErrorString(err)); 857 | return false; 858 | } 859 | 860 | LOGV("Receiver created!"); 861 | return true; 862 | } 863 | 864 | bool WaveCloudXRApp::Connect(const bool async) { 865 | 866 | if (mConnected) { 867 | LOGE("Already connected"); 868 | return true; 869 | } 870 | 871 | if (!mInited) { 872 | LOGE("CloudXR is not initialized."); 873 | return false; 874 | } 875 | 876 | if (mOptions.mServerIP.empty()) { 877 | LOGE("Server IP is not specified."); 878 | return false; 879 | } 880 | 881 | mConnectionDesc.async = async; 882 | mConnectionDesc.useL4S = mOptions.mUseL4S; // Low Latency, Low Loss, and Scalable Throughput 883 | mConnectionDesc.clientNetwork = mOptions.mClientNetwork; 884 | mConnectionDesc.topology = mOptions.mTopology; 885 | cxrError err = cxrConnect(mReceiver, mOptions.mServerIP.c_str(), &mConnectionDesc); 886 | std::string constr = async ? "Connection request sent" : "Connection" ; 887 | if (err != cxrError_Success) { 888 | LOGE("%s failed, %s. Error %d, %s.", 889 | constr.c_str(), 890 | mOptions.mServerIP.c_str(), (int) err, cxrErrorString(err)); 891 | shutdownCloudXR(); 892 | return false; 893 | } 894 | 895 | LOGV("%s success. %s", constr.c_str(), mOptions.mServerIP.c_str()); 896 | return true; 897 | } 898 | 899 | bool WaveCloudXRApp::UpdateFrame() { 900 | 901 | if (mPaused || !mInited) { 902 | return false; 903 | } 904 | 905 | // Fetch a video frame if available. 906 | bool frameValid = false; 907 | if (mReceiver) 908 | { 909 | if (mConnected) 910 | { 911 | cxrError frameErr = cxrLatchFrame(mReceiver, &mFramesLatched, 912 | cxrFrameMask_All, LATCHFRAME_TIMEOUT_MS); 913 | frameValid = (frameErr == cxrError_Success); 914 | if (!frameValid) 915 | { 916 | if (frameErr == cxrError_Frame_Not_Ready) 917 | LOGW("LatchFrame failed, frame not ready for %d ms", LATCHFRAME_TIMEOUT_MS); 918 | else if (frameErr == cxrError_Not_Connected) 919 | LOGW("LatchFrame failed, receiver no longer connected."); 920 | else 921 | LOGE("Error in LatchFrame [%0d] = %s", frameErr, cxrErrorString(frameErr)); 922 | } else { 923 | 924 | // CloudXR SDK 3.1.1: 925 | // If network condition is bad, e.g. bitrate usage down below ~5Mbps 926 | // Frames received will vary frequently in size (~5 times in 1 second) 927 | // and crash here due to no read/write protection 928 | 929 | // You can add mutex to protect buffer recreation process and potentially increase latency 930 | // or comment this function call to avoid crash caused by frequent size change 931 | // Not recreating buffer to match incoming frame size might result in blurry display. 932 | 933 | // CloudXR SDK 3.2: 934 | // Recreating frame buffer causes reconnection process failed after HMD idle/wakeup. 935 | // Reason: cxrStateReason_DeviceDescriptorMismatch. 936 | 937 | if ( mFramesLatched.frames[0].widthFinal != mRenderWidth || 938 | mFramesLatched.frames[0].heightFinal != mRenderHeight ) { 939 | /*LOGE("Receive frame %dx%d, buffer size %dx%d", 940 | mFramesLatched.frames[0].widthFinal, mFramesLatched.frames[0].heightFinal, 941 | mRenderWidth, mRenderHeight);*/ 942 | //RecreateFramebuffer(mFramesLatched.frames[0].widthFinal, mFramesLatched.frames[0].heightFinal); 943 | } 944 | } 945 | } 946 | } 947 | return frameValid; 948 | } 949 | 950 | bool WaveCloudXRApp::UpdateHMDPose(const WVR_PoseState_t hmdPose) { 951 | 952 | if (mPaused || !mInited) { 953 | return false; 954 | } 955 | 956 | { 957 | if (mDeviceDesc.ipd != 0.0f) 958 | { 959 | // is this flag supposed to be cleared after CXR gets it? 960 | mCXRPoseState.hmd.flags = cxrHmdTrackingFlags_HasIPD; 961 | mCXRPoseState.hmd.ipd = mDeviceDesc.ipd; 962 | } 963 | 964 | mCXRPoseState.hmd.pose.poseIsValid = hmdPose.isValidPose; //cxrTrue; 965 | mCXRPoseState.hmd.pose.deviceIsConnected = cxrTrue; 966 | mCXRPoseState.hmd.pose.trackingResult = cxrTrackingResult_Running_OK; 967 | 968 | cxrMatrix34 mat = Convert(mHmdPose.poseMatrix); 969 | cxrMatrixToVecQuat(&mat, &mCXRPoseState.hmd.pose.position, &mCXRPoseState.hmd.pose.rotation); 970 | mCXRPoseState.hmd.pose.velocity = Convert(hmdPose.velocity); 971 | mCXRPoseState.hmd.pose.angularVelocity = Convert(hmdPose.angularVelocity); 972 | 973 | mHmdPose = hmdPose; 974 | } 975 | return true; 976 | } 977 | 978 | bool WaveCloudXRApp::UpdateDevicePose(const WVR_DeviceType type, const WVR_PoseState_t ctrlPose) { 979 | 980 | if (mPaused || !mInited) { 981 | return false; 982 | } 983 | 984 | { 985 | size_t idx = type == WVR_DeviceType_Controller_Left ? 0 : 1; 986 | if (!WVR_IsDeviceConnected(type)) 987 | { 988 | mCXRPoseState.controller[idx].pose.poseIsValid = cxrFalse; 989 | mCXRPoseState.controller[idx].pose.deviceIsConnected = cxrFalse; 990 | } else { 991 | mCXRPoseState.controller[idx].pose.poseIsValid = ctrlPose.isValidPose; //cxrTrue; 992 | mCXRPoseState.controller[idx].pose.deviceIsConnected = cxrTrue; 993 | mCXRPoseState.controller[idx].pose.trackingResult = cxrTrackingResult_Running_OK; 994 | 995 | cxrMatrix34 mat = Convert(ctrlPose.poseMatrix); 996 | cxrMatrixToVecQuat(&mat, &mCXRPoseState.controller[idx].pose.position, &mCXRPoseState.controller[idx].pose.rotation); 997 | mCXRPoseState.controller[idx].pose.velocity = Convert(ctrlPose.velocity); 998 | mCXRPoseState.controller[idx].pose.angularVelocity = Convert(ctrlPose.angularVelocity); 999 | } 1000 | } 1001 | return true; 1002 | } 1003 | 1004 | bool WaveCloudXRApp::UpdateAnalog() 1005 | { 1006 | if (mPaused || !mInited) { 1007 | return false; 1008 | } 1009 | 1010 | if (!mConnected) { 1011 | return false; 1012 | } 1013 | 1014 | for(size_t hand = HAND_LEFT; hand <= HAND_RIGHT; ++hand) { 1015 | 1016 | WVR_DeviceType ctl = (hand == HAND_LEFT) ? 1017 | WVR_DeviceType_Controller_Left : WVR_DeviceType_Controller_Right; 1018 | 1019 | if(!WVR_IsDeviceConnected(ctl)) { 1020 | continue; 1021 | } 1022 | 1023 | uint32_t inputType = WVR_InputType_Button | WVR_InputType_Touch | WVR_InputType_Analog; 1024 | uint32_t buttons = 0; 1025 | uint32_t touches = 0; 1026 | WVR_AnalogState_t analogState[3]; 1027 | uint32_t analogCount = (uint32_t)WVR_GetInputTypeCount(ctl, WVR_InputType_Analog); 1028 | if (!WVR_GetInputDeviceState(ctl, inputType, &buttons, &touches, analogState, analogCount)) { 1029 | continue; 1030 | } 1031 | int stateCount = sizeof(analogState) / sizeof(WVR_AnalogState_t); 1032 | 1033 | for(size_t buttonType = IDX_TRIGGER; buttonType <= IDX_THUMBSTICK; ++buttonType) { 1034 | if (mUpdateAnalogs[hand][buttonType]) { 1035 | for(int i = 0; i < stateCount; i++) { 1036 | if(analogState[i].id == WVR_InputId_Alias1_Trigger || 1037 | analogState[i].id == WVR_InputId_Alias1_Grip) { 1038 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]]; 1039 | e.inputValue.valueType = cxrInputValueType_float32; 1040 | // e.clientTimeNS = event.device.common.timestamp; 1041 | e.clientInputIndex = GetAnalogInputIndex(true, analogState[i].id); 1042 | e.inputValue.vF32 = analogState[i].axis.x; 1043 | mCTLEventCount[hand]++; 1044 | 1045 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, Analog %f, Timestamp %lu", 1046 | hand == HAND_LEFT ? "LEFT" : "RIGHT", analogState[i].id, 1047 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vF32, e.clientTimeNS);*/ 1048 | } else if (analogState[i].id == WVR_InputId_Alias1_Thumbstick) { 1049 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]]; 1050 | e.inputValue.valueType = cxrInputValueType_float32; 1051 | // e.clientTimeNS = event.device.common.timestamp; 1052 | e.clientInputIndex = 10; // "/input/joystick/x" 1053 | e.inputValue.vF32 = analogState[i].axis.x; 1054 | mCTLEventCount[hand]++; 1055 | 1056 | cxrControllerEvent &e2 = mCTLEvents[hand][mCTLEventCount[hand]]; 1057 | e2.inputValue.valueType = cxrInputValueType_float32; 1058 | // e.clientTimeNS = event.device.common.timestamp; 1059 | e2.clientInputIndex = 11; // "/input/joystick/y" 1060 | e2.inputValue.vF32 = analogState[i].axis.y; 1061 | mCTLEventCount[hand]++; 1062 | 1063 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, Analog (%f, %f), Timestamp %lu", 1064 | hand == HAND_LEFT ? "LEFT" : "RIGHT", analogState[i].id, 1065 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vF32, e2.inputValue.vF32 , e.clientTimeNS);*/ 1066 | } 1067 | } 1068 | 1069 | } 1070 | } 1071 | 1072 | if (mCTLEventCount[hand] > 0) { 1073 | cxrError err = cxrFireControllerEvents(mReceiver, mControllers[hand], mCTLEvents[hand], mCTLEventCount[hand]); 1074 | if (err != cxrError_Success) 1075 | { 1076 | LOGE("[UpdateInput] cxrFireControllerEvents failed: %s", cxrErrorString(err)); 1077 | 1078 | } 1079 | } 1080 | mCTLEventCount[hand] = 0; 1081 | } 1082 | 1083 | 1084 | return true; 1085 | } 1086 | 1087 | bool WaveCloudXRApp::Render(const uint32_t eye, WVR_TextureParams_t eyeTexture, const bool frameValid) { 1088 | 1089 | if (frameValid) { 1090 | // Submit frame with pose that render this frame 1091 | auto& framePose = mFramesLatched.poseMatrix; 1092 | WVR_Matrix4f_t headMatrix = Convert(framePose); 1093 | WVR_ConvertMatrixQuaternion(&headMatrix, &mHmdPose.rawPose.rotation, true); 1094 | 1095 | cxrBool result = cxrBlitFrame(mReceiver, &mFramesLatched, 1 << eye); 1096 | 1097 | WVR_SubmitExtend ext = WVR_SubmitExtend_Default; 1098 | WVR_SubmitFrame((WVR_Eye)eye, &eyeTexture, &mHmdPose, ext); 1099 | 1100 | if (eye == (uint32_t)WVR_Eye_Right && mReceiver && mConnected) { 1101 | cxrReleaseFrame(mReceiver, &mFramesLatched); 1102 | } 1103 | 1104 | } 1105 | // Render grey color gradient when frame invalid for whatever reason 1106 | else { 1107 | static const float ping = 0.0f; 1108 | static const float pong = 0.4f; 1109 | static const float step = 0.0025f; 1110 | static float direction = 1.0f; 1111 | static float color = ping; 1112 | 1113 | color += direction * step; 1114 | if (color > pong || color < ping) direction *= -1.0f; 1115 | 1116 | // LOGD("loading %f", color); 1117 | glClearColor(color, color, color, 1.0f); 1118 | glClear(GL_COLOR_BUFFER_BIT); 1119 | 1120 | WVR_SubmitExtend ext = WVR_SubmitExtend_Default; 1121 | WVR_SubmitFrame((WVR_Eye)eye, &eyeTexture, &mHmdPose, ext); 1122 | } 1123 | 1124 | return true; 1125 | } 1126 | 1127 | // Fire 1 input event once at a time 1128 | // checkme: update all button states at fixed freq and fire all button event at once 1129 | bool WaveCloudXRApp::UpdateInput(const WVR_Event_t& event) 1130 | { 1131 | if (mPaused || !mInited) { 1132 | return false; 1133 | } 1134 | 1135 | if (!mConnected) { 1136 | return false; 1137 | } 1138 | 1139 | // filter out non-controller mCTLEvents 1140 | /*if (event.common.type < WVR_EventType_ButtonPressed || event.common.type > WVR_EventType_UpToDownSwipe) { 1141 | return false; 1142 | }*/ 1143 | 1144 | WVR_DeviceType ctl = event.device.deviceType; 1145 | if (ctl != WVR_DeviceType_Controller_Left && ctl != WVR_DeviceType_Controller_Right) { 1146 | return false; 1147 | } 1148 | 1149 | uint8_t hand = (ctl == WVR_DeviceType_Controller_Left) ? HAND_LEFT : HAND_RIGHT; 1150 | // Create CXR controller handle 1151 | if (mControllers[hand] == nullptr) { 1152 | if (!WVR_IsDeviceConnected(ctl)) { 1153 | // device disconnected but have input event incoming? 1154 | // continue; 1155 | return false; 1156 | } 1157 | 1158 | cxrControllerDesc desc = {}; 1159 | desc.id = ctl; 1160 | desc.role = (hand == HAND_LEFT) ? 1161 | "cxr://input/hand/left" : "cxr://input/hand/right"; 1162 | desc.controllerName = "Oculus Touch"; 1163 | //desc.controllerName = "vive_focus3_controller"; // CXR server does not recognize this name 1164 | // desc.controllerName = "VIVE FOCUS 3 Controller"; 1165 | desc.inputCount = inputTouchLegacyCount; 1166 | desc.inputPaths = inputsTouchLegacy; 1167 | desc.inputValueTypes = inputValuesTouchLegacy; 1168 | cxrError e = cxrAddController(mReceiver, &desc, &mControllers[hand]); 1169 | if (e!=cxrError_Success) 1170 | { 1171 | LOGE("[UpdateInput] Error adding controller: %s", cxrErrorString(e)); 1172 | //continue; 1173 | return false; 1174 | } 1175 | LOGE("[UpdateInput] Added controller %s, %s", desc.controllerName, desc.role); 1176 | } else { 1177 | // Controller handle exist but device is actually disconnected 1178 | if (!WVR_IsDeviceConnected(ctl)) { 1179 | LOGE("[UpdateInput] Device %d is disconnected.", ctl); 1180 | // destroy controller handle 1181 | } 1182 | } 1183 | 1184 | // button 1185 | cxrControllerEvent &e = mCTLEvents[hand][mCTLEventCount[hand]]; 1186 | bool updateAnalog = true; 1187 | switch (event.common.type) { 1188 | case WVR_EventType_TouchTapped:{ 1189 | e.inputValue.vBool = cxrTrue; 1190 | e.clientInputIndex = GetTouchInputIndex(true, event.input.inputId); 1191 | break; 1192 | } 1193 | case WVR_EventType_TouchUntapped:{ 1194 | e.inputValue.vBool = cxrFalse; 1195 | e.clientInputIndex = GetTouchInputIndex(false, event.input.inputId); 1196 | break; 1197 | } 1198 | case WVR_EventType_ButtonPressed:{ 1199 | e.inputValue.vBool = cxrTrue; 1200 | e.clientInputIndex = GetPressInputIndex(hand, true, event.input.inputId); 1201 | break; 1202 | } 1203 | case WVR_EventType_ButtonUnpressed:{ 1204 | e.inputValue.vBool = cxrFalse; 1205 | e.clientInputIndex = GetPressInputIndex(hand, false, event.input.inputId); 1206 | break; 1207 | } 1208 | default: 1209 | updateAnalog = false; 1210 | break; 1211 | } 1212 | 1213 | if (e.clientInputIndex >= inputTouchLegacyCount) { 1214 | // skip unbinded input 1215 | LOGE("[UpdateInput] skip unbinded input %s, WVRInputId %d, CXRInputIndex %d", 1216 | hand == HAND_LEFT ? "LEFT" : "RIGHT", event.input.inputId, 1217 | e.clientInputIndex); 1218 | return false; 1219 | } 1220 | 1221 | e.clientTimeNS = event.device.common.timestamp; 1222 | e.inputValue.valueType = cxrInputValueType_boolean; 1223 | mCTLEventCount[hand]++; 1224 | 1225 | // analog flag 1226 | switch(event.input.inputId) { 1227 | case WVR_InputId_Alias1_Trigger: 1228 | mUpdateAnalogs[hand][IDX_TRIGGER] = updateAnalog; 1229 | break; 1230 | case WVR_InputId_Alias1_Grip: 1231 | mUpdateAnalogs[hand][IDX_GRIP] = updateAnalog; 1232 | break; 1233 | case WVR_InputId_Alias1_Thumbstick: 1234 | mUpdateAnalogs[hand][IDX_THUMBSTICK] = updateAnalog; 1235 | break; 1236 | default: 1237 | break; 1238 | } 1239 | 1240 | if (mCTLEventCount[hand] > 0) { 1241 | cxrError err = cxrFireControllerEvents(mReceiver, mControllers[hand], mCTLEvents[hand], mCTLEventCount[hand]); 1242 | /*LOGE("[UpdateInput] %s, WVRInputId %d, %s, %d, Timestamp %lu", 1243 | hand == HAND_LEFT ? "LEFT" : "RIGHT", event.input.inputId, 1244 | inputsTouchLegacy[e.clientInputIndex], e.inputValue.vBool, e.clientTimeNS);*/ 1245 | if (err != cxrError_Success) 1246 | { 1247 | LOGE("[UpdateInput] cxrFireControllerEvents failed: %s", cxrErrorString(err)); 1248 | // TODO: how to handle UNUSUAL API errors? might just return up. 1249 | } 1250 | } 1251 | mCTLEventCount[hand] = 0; 1252 | 1253 | return true; 1254 | } 1255 | 1256 | /* 1257 | * CloudXR callbacks 1258 | * 1259 | * */ 1260 | oboe::DataCallbackResult WaveCloudXRApp::onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) { 1261 | cxrAudioFrame recordedFrame{}; 1262 | recordedFrame.streamBuffer = (int16_t*)audioData; 1263 | recordedFrame.streamSizeBytes = numFrames * CXR_AUDIO_CHANNEL_COUNT * CXR_AUDIO_SAMPLE_SIZE; 1264 | cxrSendAudio(mReceiver, &recordedFrame); 1265 | 1266 | return oboe::DataCallbackResult::Continue; 1267 | } 1268 | 1269 | void WaveCloudXRApp::GetTrackingState(cxrVRTrackingState *trackingState) { 1270 | 1271 | if (mPaused || !mConnected || nullptr == trackingState) 1272 | return; 1273 | 1274 | // std::lock_guard lock(mPoseMutex); 1275 | *trackingState = mCXRPoseState; 1276 | 1277 | getPoseCount++; 1278 | } 1279 | 1280 | cxrBool WaveCloudXRApp::RenderAudio(const cxrAudioFrame *audioFrame) { 1281 | if (!mPlaybackStream || !mInited || !mConnected) 1282 | { 1283 | return cxrFalse; 1284 | } 1285 | 1286 | const uint32_t timeout = audioFrame->streamSizeBytes / CXR_AUDIO_BYTES_PER_MS; 1287 | const uint32_t numFrames = timeout * CXR_AUDIO_SAMPLING_RATE / 1000; 1288 | uint32_t timeoutMS = 4*timeout; // WAR for oboe timing issue on Focus+. 1289 | mPlaybackStream->write(audioFrame->streamBuffer, numFrames, timeoutMS * oboe::kNanosPerMillisecond); 1290 | 1291 | return cxrTrue; 1292 | } 1293 | 1294 | void WaveCloudXRApp::TriggerHaptic(const cxrHapticFeedback *haptic) { 1295 | 1296 | if (!mConnected || !mInited) 1297 | return; 1298 | 1299 | // Apply haptic feedback 1300 | if (haptic->seconds <= 0) 1301 | return; 1302 | 1303 | // deviceID is not necessary VR controller, can be gamepad or anything 1304 | // need to map this ID to WVR Device 1305 | WVR_TriggerVibration(haptic->deviceID == 0 ? 1306 | WVR_DeviceType_Controller_Left : WVR_DeviceType_Controller_Right, 1307 | WVR_InputId_Max, static_cast(haptic->seconds*1000000), 1, 1308 | WVR_Intensity_Normal); 1309 | } 1310 | 1311 | void WaveCloudXRApp::Pause() { 1312 | if (!mPaused) { 1313 | LOGW("Receive pause"); 1314 | mPaused = true; 1315 | shutdownCloudXR(); 1316 | } else { 1317 | // already paused, skip 1318 | } 1319 | } 1320 | 1321 | void WaveCloudXRApp::Resume() { 1322 | if (mPaused) { 1323 | LOGW("Receive resume"); 1324 | mPaused = false; 1325 | } else { 1326 | // already resumed 1327 | } 1328 | } 1329 | 1330 | // This is called from CloudXR thread 1331 | void WaveCloudXRApp::HandleClientState(void* context, cxrClientState state, cxrError error) { 1332 | switch (state) 1333 | { 1334 | case cxrClientState_ConnectionAttemptInProgress: 1335 | LOGW("Connection attempt in progress."); 1336 | mConnected = false; 1337 | break; 1338 | case cxrClientState_StreamingSessionInProgress: 1339 | LOGW("Connection attempt succeeded."); 1340 | mConnected = true; 1341 | break; 1342 | case cxrClientState_ConnectionAttemptFailed: 1343 | LOGE("Connection attempt failed with error: %s", cxrErrorString(error)); 1344 | state = cxrClientState_Disconnected; // retry connection 1345 | mConnected = false; 1346 | break; 1347 | case cxrClientState_Disconnected: 1348 | LOGE("Server disconnected with error: [%s]", cxrErrorString(error)); 1349 | mConnected = false; 1350 | break; 1351 | default: 1352 | LOGW("Client state updated: %s to %s, reason: %s", ClientStateEnumToString(mClientState), ClientStateEnumToString(state), cxrErrorString(error)); 1353 | break; 1354 | } 1355 | 1356 | if (mClientState != state) { 1357 | mClientState = state; 1358 | 1359 | mStateDirty = true; 1360 | } 1361 | } 1362 | 1363 | uint16_t WaveCloudXRApp::GetTouchInputIndex(const bool touched, const WVR_InputId wvrInputId) { 1364 | 1365 | uint16_t ret = 999; 1366 | switch(wvrInputId) { 1367 | case WVR_InputId_Alias1_Thumbstick: 1368 | ret = 9; // "/input/joystick/touch" 1369 | break; 1370 | case WVR_InputId_Alias1_Trigger: 1371 | ret = 3; // "/input/trigger/touch" 1372 | break; 1373 | case WVR_InputId_Alias1_Grip: 1374 | ret = 6; // "/input/grip/touch" 1375 | break; 1376 | case WVR_InputId_Alias1_A: 1377 | ret = 16; // "/input/a/touch" 1378 | break; 1379 | case WVR_InputId_Alias1_B: 1380 | ret = 17; // "/input/b/touch" 1381 | break; 1382 | case WVR_InputId_Alias1_X: 1383 | ret = 18; // "/input/x/touch" 1384 | break; 1385 | case WVR_InputId_Alias1_Y: 1386 | ret = 19; // "/input/y/touch" 1387 | break; 1388 | //case WVR_InputId_Alias1_Menu: 1389 | //case WVR_InputId_Alias1_System: 1390 | default: 1391 | break; 1392 | } 1393 | 1394 | return ret; 1395 | } 1396 | 1397 | uint16_t WaveCloudXRApp::GetPressInputIndex(const uint8_t hand, const bool pressed, const WVR_InputId wvrInputId) { 1398 | 1399 | uint16_t ret = 999; 1400 | switch(wvrInputId) { 1401 | case WVR_InputId_Alias1_Thumbstick: 1402 | ret = 8; // "/input/joystick/click" 1403 | break; 1404 | case WVR_InputId_Alias1_Trigger: 1405 | ret = 2; // "/input/trigger/click" 1406 | break; 1407 | case WVR_InputId_Alias1_Grip: 1408 | ret = 5; // "/input/grip/click" 1409 | break; 1410 | // WVR only sends A/B 1411 | case WVR_InputId_Alias1_A: 1412 | if (hand == HAND_RIGHT) ret = 12; // "/input/a/click" 1413 | if (hand == HAND_LEFT) ret = 14; // "/input/x/click" 1414 | break; 1415 | case WVR_InputId_Alias1_B: 1416 | if (hand == HAND_RIGHT) ret = 13; // "/input/b/click" 1417 | if (hand == HAND_LEFT) ret = 15; // "/input/y/click" 1418 | break; 1419 | case WVR_InputId_Alias1_X: 1420 | ret = 14; // "/input/x/click" 1421 | break; 1422 | case WVR_InputId_Alias1_Y: 1423 | ret = 15; // "/input/y/click" 1424 | break; 1425 | case WVR_InputId_Alias1_Menu: 1426 | // ret = 1; // "/input/application_menu/click" 1427 | ret = 0; // "/input/system/click" 1428 | break; 1429 | case WVR_InputId_Alias1_System: 1430 | ret = 0; // "/input/system/click" 1431 | break; 1432 | default: 1433 | break; 1434 | } 1435 | return ret; 1436 | } 1437 | 1438 | uint16_t WaveCloudXRApp::GetAnalogInputIndex(const bool pressed, const WVR_InputId wvrInputId) { 1439 | 1440 | uint16_t ret = 999; 1441 | switch(wvrInputId) { 1442 | /*case WVR_InputId_Alias1_Thumbstick: 1443 | ret = 10; // "/input/joystick/x" 1444 | ret = 11; // "/input/joystick/y" 1445 | break;*/ 1446 | case WVR_InputId_Alias1_Trigger: 1447 | ret = 4; // "/input/trigger/click" 1448 | break; 1449 | case WVR_InputId_Alias1_Grip: 1450 | ret = 7; // "/input/grip/click" 1451 | break; 1452 | default: 1453 | break; 1454 | } 1455 | return ret; 1456 | } 1457 | 1458 | void WaveCloudXRApp::CheckStreamQuality() { 1459 | 1460 | // Log connection stats every 3 seconds 1461 | const int STATS_INTERVAL_SEC = 3; 1462 | mFramesUntilStats--; 1463 | cxrConnectionStats mStats = {}; 1464 | if (mFramesUntilStats <= 0 && 1465 | cxrGetConnectionStats(mReceiver, &mStats) == cxrError_Success) 1466 | { 1467 | // Capture the key connection statistics 1468 | char statsString[64] = { 0 }; 1469 | snprintf(statsString, 64, "FPS: %6.1f Bitrate (kbps): %5d Latency (ms): %3d", mStats.framesPerSecond, mStats.bandwidthUtilizationKbps, mStats.roundTripDelayMs); 1470 | 1471 | // Turn the connection quality into a visual representation along the lines of a signal strength bar 1472 | char qualityString[64] = { 0 }; 1473 | snprintf(qualityString, 64, "Connection quality: [%s]", 1474 | mStats.quality == cxrConnectionQuality_Bad ? "Bad" : 1475 | mStats.quality == cxrConnectionQuality_Poor ? "Poor" : 1476 | mStats.quality == cxrConnectionQuality_Fair ? "Fair" : 1477 | mStats.quality == cxrConnectionQuality_Good ? "Good" : 1478 | mStats.quality == cxrConnectionQuality_Excellent ? "Excellent" : "Invalid"); 1479 | 1480 | // There could be multiple reasons for low quality however we show only the most impactful to the end user here 1481 | char reasonString[64] = { 0 }; 1482 | if (mStats.quality <= cxrConnectionQuality_Fair) 1483 | { 1484 | if (mStats.qualityReasons == cxrConnectionQualityReason_EstimatingQuality) 1485 | { 1486 | snprintf(reasonString, 64, "Reason: Estimating quality"); 1487 | } 1488 | else if (mStats.qualityReasons & cxrConnectionQualityReason_HighLatency) 1489 | { 1490 | snprintf(reasonString, 64, "Reason: High Latency (ms): %3d", mStats.roundTripDelayMs); 1491 | } 1492 | else if (mStats.qualityReasons & cxrConnectionQualityReason_LowBandwidth) 1493 | { 1494 | snprintf(reasonString, 64, "Reason: Low Bandwidth (kbps): %5d", mStats.bandwidthAvailableKbps); 1495 | } 1496 | else if (mStats.qualityReasons & cxrConnectionQualityReason_HighPacketLoss) 1497 | { 1498 | if (mStats.totalPacketsLost == 0) 1499 | { 1500 | snprintf(reasonString, 64, "Reason: High Packet Loss (Recoverable)"); 1501 | } 1502 | else 1503 | { 1504 | snprintf(reasonString, 64, "Reason: High Packet Loss (%%): %3.1f", 100.0f * mStats.totalPacketsLost / mStats.totalPacketsReceived); 1505 | } 1506 | } 1507 | } 1508 | 1509 | LOGI("%s %s %s", statsString, qualityString, reasonString); 1510 | mFramesUntilStats = (int)mStats.framesPerSecond * STATS_INTERVAL_SEC; 1511 | } 1512 | } 1513 | 1514 | --------------------------------------------------------------------------------