diff --git a/examples/android/apks/anbox-native-stream-example_1.8.1.apk b/examples/android/apks/anbox-native-stream-example_1.8.1.apk
new file mode 100644
index 0000000..3b49a0e
Binary files /dev/null and b/examples/android/apks/anbox-native-stream-example_1.8.1.apk differ
diff --git a/examples/android/native_streaming/.gitignore b/examples/android/native_streaming/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/examples/android/native_streaming/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/examples/android/native_streaming/.idea/.name b/examples/android/native_streaming/.idea/.name
new file mode 100644
index 0000000..98b5519
--- /dev/null
+++ b/examples/android/native_streaming/.idea/.name
@@ -0,0 +1 @@
+Anbox Streaming Native Example
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/codeStyles/Project.xml b/examples/android/native_streaming/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..663459a
--- /dev/null
+++ b/examples/android/native_streaming/.idea/codeStyles/Project.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/codeStyles/codeStyleConfig.xml b/examples/android/native_streaming/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/examples/android/native_streaming/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/gradle.xml b/examples/android/native_streaming/.idea/gradle.xml
new file mode 100644
index 0000000..ac6b0ae
--- /dev/null
+++ b/examples/android/native_streaming/.idea/gradle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/jarRepositories.xml b/examples/android/native_streaming/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/examples/android/native_streaming/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/misc.xml b/examples/android/native_streaming/.idea/misc.xml
new file mode 100644
index 0000000..7bfef59
--- /dev/null
+++ b/examples/android/native_streaming/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/runConfigurations.xml b/examples/android/native_streaming/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/examples/android/native_streaming/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/.idea/vcs.xml b/examples/android/native_streaming/.idea/vcs.xml
new file mode 100644
index 0000000..4fce1d8
--- /dev/null
+++ b/examples/android/native_streaming/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/README.md b/examples/android/native_streaming/README.md
new file mode 100644
index 0000000..bd4d16a
--- /dev/null
+++ b/examples/android/native_streaming/README.md
@@ -0,0 +1,17 @@
+# Anbox Stream SDK - Android Example
+
+This example demonstrates how to use the native Anbox Stream SDK in an Android
+application. The application itself is kept simple and mainly concentrates on showing the
+integration with the Anbox Stream SDK itself.
+
+## Configure Project
+
+The example project can be build with Android Studio but needs to be configured first to
+know where to find the Anbox Stream SDK header and binary files. For that add the
+following to your local.properties file
+
+anbox-stream-sdk.dir=/path/to/unpacked/anbox-streaming-sdk
+
+This will tell the build where to look for the header and binary files.
+
+Afterwards you can build and run the Android application with Android Studio as usual.
diff --git a/examples/android/native_streaming/app/.gitignore b/examples/android/native_streaming/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/examples/android/native_streaming/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/build.gradle b/examples/android/native_streaming/app/build.gradle
new file mode 100644
index 0000000..42e1eac
--- /dev/null
+++ b/examples/android/native_streaming/app/build.gradle
@@ -0,0 +1,58 @@
+apply plugin: 'com.android.application'
+
+Properties localProps = new Properties()
+localProps.load(project.rootProject.file("local.properties").newDataInputStream())
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.0"
+
+ defaultConfig {
+ applicationId "com.canonical.anbox.streaming.sdk.native_example"
+ minSdkVersion 16
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ cppFlags "-std=c++14"
+ arguments "-DANBOX_STREAM_SDK_DIR=" + localProps.getProperty('anbox-stream-sdk.dir')
+ }
+ }
+
+ ndk {
+ abiFilters 'arm64-v8a', 'x86_64'
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path "src/main/cpp/CMakeLists.txt"
+ version "3.10.2"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation group: 'org.glassfish.tyrus.bundles', name: 'tyrus-standalone-client-jdk', version: '1.12'
+ implementation 'com.squareup.okhttp3:okhttp:4.9.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/examples/android/native_streaming/app/proguard-rules.pro b/examples/android/native_streaming/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/examples/android/native_streaming/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/native_example/ExampleInstrumentedTest.java b/examples/android/native_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/native_example/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..0ef15fe
--- /dev/null
+++ b/examples/android/native_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/native_example/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.canonical.anbox.streaming.sdk.native_example", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/AndroidManifest.xml b/examples/android/native_streaming/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4fb2a14
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/cpp/CMakeLists.txt b/examples/android/native_streaming/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..0b55edd
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.4.1)
+
+set(ANBOX_STREAM_SDK_DIR CACHE STRING "Path to the Anbox Stream SDK")
+if (NOT ANBOX_STREAM_SDK_DIR)
+ message(FATAL_ERROR "Missing path to Anbox Stream SDK")
+endif()
+
+add_library(anbox-stream STATIC IMPORTED)
+set_target_properties(anbox-stream PROPERTIES IMPORTED_LOCATION ${ANBOX_STREAM_SDK_DIR}/native/libs/android/${ANDROID_ABI}/libanbox-stream.so)
+
+find_library(log-lib log)
+
+set(SOURCES
+ bindings.cpp
+ jni_helpers.cpp
+ jni_helpers.h)
+
+add_library(anbox-stream-bindings SHARED ${SOURCES})
+target_include_directories(anbox-stream-bindings PRIVATE "${ANBOX_STREAM_SDK_DIR}/native/include")
+target_link_libraries(anbox-stream-bindings
+ ${log-lib}
+ GLESv3
+ EGL
+ OpenSLES
+ android
+ anbox-stream)
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/cpp/bindings.cpp b/examples/android/native_streaming/app/src/main/cpp/bindings.cpp
new file mode 100644
index 0000000..090f0f2
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/cpp/bindings.cpp
@@ -0,0 +1,653 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+#include "jni_helpers.h"
+
+#include "anbox-stream.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+
+#define RETURN_ON_ERROR(op, ...) \
+ do { \
+ SLresult err = (op); \
+ if (err != SL_RESULT_SUCCESS) { \
+ __android_log_print(ANDROID_LOG_ERROR, "AnboxStream", "%s failed: %d", #op, err); \
+ return __VA_ARGS__; \
+ } \
+ } while (0)
+
+namespace {
+class AudioBufferQueue {
+ public:
+ AudioBufferQueue() = default;
+ ~AudioBufferQueue() = default;
+
+ void pop(uint8_t *data, size_t len) {
+ constexpr const std::chrono::milliseconds max_wait_duration{100};
+ std::unique_lock l(mutex_);
+ const auto ret = cond_.wait_for(l, max_wait_duration, [this, len](){
+ return audio_buffer_.size() > len;
+ });
+
+ if (ret) {
+ ::memcpy(reinterpret_cast(data), &audio_buffer_[0], len);
+ audio_buffer_.erase(audio_buffer_.begin(), audio_buffer_.begin() + len);
+ } else {
+ // Silence audio output when timeout occurs
+ ::memset(reinterpret_cast(data), 0, len);
+ }
+ }
+
+ void append(const uint8_t *data, size_t len) {
+ std::unique_lock l(mutex_);
+ audio_buffer_.insert(audio_buffer_.end(), data, data + len);
+ cond_.notify_one();
+ }
+
+ private:
+ std::vector audio_buffer_;
+ std::mutex mutex_;
+ std::condition_variable cond_;
+};
+
+class AudioPlayer {
+ public:
+ static constexpr const SLuint32 NumOfOpenSLESBuffers = 1;
+
+ AudioPlayer() = default;
+ ~AudioPlayer() {
+ terminate();
+ }
+
+ bool initialize(AnboxStreamAudioSpec spec) {
+ if (!create_pcm_format(spec)) {
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Invalid audio specification");
+ return false;
+ }
+
+ if (!create_engine()) {
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to create SL engine");
+ return false;
+ }
+
+ if (!create_output_mix()) {
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to create SL output mix");
+ return false;
+ }
+
+ if (!create_player()) {
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to create SL player");
+ return false;
+ }
+
+ return true;
+ }
+
+ bool start_playout() {
+ RETURN_ON_ERROR((*player_)->SetPlayState(player_, SL_PLAYSTATE_PLAYING), false);
+
+ // After setting the player state to SL_PLAYSTATE_PLAYING, we need to active
+ // callback immediately to feed the very first audio chunk manually.
+ consume_audio_buffer();
+ return true;
+ }
+
+ void terminate() {
+ if (player_ != nullptr) {
+ (*player_)->SetPlayState(player_, SL_PLAYSTATE_STOPPED);
+ player_ = nullptr;
+ player_object_ = nullptr;
+ }
+
+ if (simple_buffer_queue_ != nullptr) {
+ (*simple_buffer_queue_)->Clear(simple_buffer_queue_);
+ simple_buffer_queue_ = nullptr;
+ }
+
+ volume_ = nullptr;
+ engine_object_ = nullptr;
+ engine_ = nullptr;
+ output_mix_object_ = nullptr;
+ }
+
+ void add_audio_buffer(const uint8_t* audio_data, size_t len) {
+ audio_buffer_queue_.append(audio_data, len);
+ }
+
+ void consume_audio_buffer() {
+ const auto audio_chunk_size = enqueue_audio_chunk_.size();
+ ::memset(&enqueue_audio_chunk_[0], 0, audio_chunk_size);
+ audio_buffer_queue_.pop(&enqueue_audio_chunk_[0], audio_chunk_size);
+
+ // Enqueue pcm audio data for playback.
+ auto ret = (*simple_buffer_queue_)->Enqueue(simple_buffer_queue_,
+ &enqueue_audio_chunk_[0], audio_chunk_size);
+ if (ret != SL_RESULT_SUCCESS)
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to enqueue audio buffer");
+ }
+
+ private:
+ bool create_engine() {
+ if (engine_object_ != nullptr)
+ return true;
+
+ // Create SL engine
+ const SLEngineOption option[] = {{SL_ENGINEOPTION_THREADSAFE, static_cast(SL_BOOLEAN_TRUE)}};
+ RETURN_ON_ERROR(slCreateEngine(&engine_object_, 1, option, 0, NULL, NULL), false);
+ RETURN_ON_ERROR((*engine_object_)->Realize(engine_object_, SL_BOOLEAN_FALSE), false);
+ RETURN_ON_ERROR((*engine_object_)->GetInterface(engine_object_, SL_IID_ENGINE, &engine_), false);
+
+ return true;
+ }
+
+ bool create_output_mix() {
+ if (output_mix_object_ != nullptr)
+ return true;
+
+ RETURN_ON_ERROR((*engine_)->CreateOutputMix(engine_, &output_mix_object_, 0, nullptr, nullptr), false);
+ RETURN_ON_ERROR((*output_mix_object_)->Realize(output_mix_object_, SL_BOOLEAN_FALSE), false);
+
+ return true;
+ }
+
+ bool create_player() {
+ if (player_object_ != nullptr)
+ return true;
+
+ SLDataLocator_AndroidSimpleBufferQueue simple_buffer_queue = {
+ SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NumOfOpenSLESBuffers};
+ SLDataSource audio_source = {&simple_buffer_queue, &pcm_format_};
+
+ SLDataLocator_OutputMix locator_output_mix = {SL_DATALOCATOR_OUTPUTMIX,
+ output_mix_object_};
+ SLDataSink audio_sink = {&locator_output_mix, nullptr};
+
+ const SLInterfaceID interface_ids[] = {SL_IID_ANDROIDCONFIGURATION,
+ SL_IID_BUFFERQUEUE, SL_IID_VOLUME};
+ const SLboolean interface_required[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE,
+ SL_BOOLEAN_TRUE};
+
+ // Create the SL audio player
+ RETURN_ON_ERROR((*engine_)->CreateAudioPlayer(
+ engine_, &player_object_, &audio_source, &audio_sink,
+ sizeof(interface_ids)/sizeof(SLInterfaceID), interface_ids, interface_required), false);
+
+ SLAndroidConfigurationItf player_config;
+ RETURN_ON_ERROR((*player_object_)->GetInterface(
+ player_object_, SL_IID_ANDROIDCONFIGURATION, &player_config), false);
+
+ // SL_ANDROID_STREAM_MEDIA is used here over SL_ANDROID_STREAM_VOICE
+ // for a better audio output with a bigger volume.
+ SLint32 stream_type = SL_ANDROID_STREAM_MEDIA;
+ RETURN_ON_ERROR((*player_config)->SetConfiguration(
+ player_config, SL_ANDROID_KEY_STREAM_TYPE, &stream_type, sizeof(SLint32)), false);
+
+ RETURN_ON_ERROR((*player_object_)->Realize(player_object_, SL_BOOLEAN_FALSE), false);
+ RETURN_ON_ERROR((*player_object_)->GetInterface(player_object_, SL_IID_PLAY, &player_), false);
+
+ RETURN_ON_ERROR((*player_object_)->GetInterface(
+ player_object_, SL_IID_BUFFERQUEUE, &simple_buffer_queue_), false);
+
+ // Register callback fun for pcm data playback.
+ RETURN_ON_ERROR((*simple_buffer_queue_)->RegisterCallback(
+ simple_buffer_queue_, audio_playback_callback, this), false);
+
+ RETURN_ON_ERROR((*player_object_)->GetInterface(
+ player_object_, SL_IID_VOLUME, &volume_), false);
+
+ // Allocate audio chunk buffer
+ // Use 10ms audio frame here to align 16-bit linear PCM audio data in
+ // frames of 10 ms processed by APM in WebRTC.
+ // NOTE: the unit of sample rate is in milliHertz and not Hertz.
+ auto sampleRateInHz = pcm_format_.samplesPerSec / 1000;
+ auto len = sampleRateInHz * 10 / 1000 * pcm_format_.numChannels * pcm_format_.bitsPerSample / 8;
+ enqueue_audio_chunk_ = std::vector(len);
+
+ return true;
+ }
+
+ bool create_pcm_format(AnboxStreamAudioSpec spec) {
+ switch (spec.freq) {
+ case 8000:
+ pcm_format_.samplesPerSec = SL_SAMPLINGRATE_8;
+ break;
+ case 16000:
+ pcm_format_.samplesPerSec = SL_SAMPLINGRATE_16;
+ break;
+ case 44100:
+ pcm_format_.samplesPerSec = SL_SAMPLINGRATE_44_1;
+ break;
+ case 48000:
+ pcm_format_.samplesPerSec = SL_SAMPLINGRATE_48;
+ break;
+ default:
+ return false;
+ }
+
+ switch (spec.format) {
+ case ANBOX_STREAM_AUDIO_FORMAT_PCM_8_BIT:
+ pcm_format_.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_8;
+ break;
+ case ANBOX_STREAM_AUDIO_FORMAT_PCM_16_BIT:
+ pcm_format_.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
+ break;
+ case ANBOX_STREAM_AUDIO_FORMAT_PCM_32_BIT:
+ pcm_format_.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_32;
+ break;
+ default:
+ return false;
+ }
+
+ pcm_format_.containerSize = pcm_format_.bitsPerSample;
+ pcm_format_.formatType = SL_DATAFORMAT_PCM;
+ pcm_format_.endianness = SL_BYTEORDER_LITTLEENDIAN;
+ pcm_format_.numChannels = static_cast(spec.channels);
+ switch (pcm_format_.numChannels) {
+ case 1:
+ pcm_format_.channelMask = SL_SPEAKER_FRONT_CENTER;
+ break;
+ case 2:
+ pcm_format_.channelMask = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
+ break;
+ default :
+ return false;
+ }
+ return true;
+ }
+
+ static void audio_playback_callback(SLAndroidSimpleBufferQueueItf bf, void *context) {
+ auto *audio_player = reinterpret_cast(context);
+ audio_player->consume_audio_buffer();
+ }
+
+ SLDataFormat_PCM pcm_format_;
+ SLAndroidSimpleBufferQueueItf simple_buffer_queue_;
+ SLEngineItf engine_{nullptr};
+ SLPlayItf player_{nullptr};
+ SLVolumeItf volume_{nullptr};
+ SLObjectItf engine_object_{nullptr};
+ SLObjectItf output_mix_object_{nullptr};
+ SLObjectItf player_object_{nullptr};
+
+ AudioBufferQueue audio_buffer_queue_;
+ std::vector enqueue_audio_chunk_;
+};
+
+struct Context {
+ Context() :
+ cfg(nullptr, reinterpret_cast(anbox_stream_config_release)),
+ stream(nullptr, reinterpret_cast(anbox_stream_release)) {}
+
+ JavaVM* vm = nullptr;
+ jobject bindings_obj = nullptr;
+ std::unique_ptr cfg;
+ std::unique_ptr stream;
+ ANativeWindow* window = nullptr;
+ std::thread render_thread;
+ std::atomic_bool running{false};
+ AudioPlayer audio_player;
+};
+
+struct StunServer {
+ std::vector urls;
+ std::string username;
+ std::string password;
+
+ static bool createListFromObject(JNIEnv* env, jobjectArray obj, std::vector& stun_servers) {
+ for (int n = 0; n < env->GetArrayLength(obj); n++) {
+ StunServer server;
+ jobject server_obj = env->GetObjectArrayElement(obj, n);
+ if (!jni_get_string_vector(env, server_obj, "urls", server.urls))
+ return false;
+ if (!jni_get_string(env, server_obj, "username", server.username))
+ return false;
+ if (!jni_get_string(env, server_obj, "password", server.password))
+ return false;
+ stun_servers.push_back(server);
+ }
+
+ return true;
+ }
+};
+
+Context *get_context(JNIEnv* env, jobject instance) {
+ return reinterpret_cast(jni_get_pointer(env, instance, "context"));
+}
+
+void run_render_thread(Context* ctx) {
+ auto display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ if (display == EGL_NO_DISPLAY)
+ return;
+
+ eglInitialize(display, nullptr, nullptr);
+
+ EGLConfig config;
+ EGLint num_configs;
+ const EGLint config_attribs[] = {
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_BLUE_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_RED_SIZE, 8,
+ EGL_NONE
+ };
+ eglChooseConfig(display, config_attribs, &config, 1, &num_configs);
+
+ EGLint format;
+ eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
+ ANativeWindow_setBuffersGeometry(ctx->window, 0, 0, format);
+
+ auto surface = eglCreateWindowSurface(display, config, ctx->window, 0);
+ if (surface == EGL_NO_SURFACE)
+ return;
+
+ EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
+ auto context = eglCreateContext(display, config, 0, context_attribs);
+ if (context == EGL_NO_CONTEXT)
+ return;
+
+ eglMakeCurrent(display, surface, surface, context);
+
+ while (ctx->running) {
+ anbox_stream_set_viewport_size(ctx->stream.get(),
+ ANativeWindow_getWidth(ctx->window),
+ ANativeWindow_getHeight(ctx->window));
+
+ anbox_stream_render_frame(ctx->stream.get(), 100);
+ eglSwapBuffers(display, surface);
+ }
+}
+
+void stop_stream(Context* ctx) {
+ if (ctx->running) {
+ const auto status = anbox_stream_disconnect(ctx->stream.get());
+ if (status != ANBOX_STATUS_OK)
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to disconnect stream");
+
+ ctx->running = false;
+ if (ctx->render_thread.joinable())
+ ctx->render_thread.join();
+
+ ctx->audio_player.terminate();
+ }
+}
+
+void on_stream_connected(void* user_data) {
+ auto ctx = reinterpret_cast(user_data);
+
+ __android_log_write(ANDROID_LOG_INFO, "AnboxStream", "Stream is connected, starting rendering ...");
+
+ ctx->running = true;
+ ctx->render_thread = std::thread(&run_render_thread, ctx);
+ if (!ctx->audio_player.start_playout())
+ __android_log_write(ANDROID_LOG_ERROR, "AnboxStream", "Failed to do audio playback");
+}
+
+void on_audio_data_ready(const uint8_t *audio_data, size_t data_size, void *user_data) {
+ auto ctx = reinterpret_cast(user_data);
+ ctx->audio_player.add_audio_buffer(audio_data, data_size);
+}
+
+void on_stream_disconnected(void* user_data) {
+ auto ctx = reinterpret_cast(user_data);
+
+ __android_log_write(ANDROID_LOG_INFO, "AnboxStream", "Stream is disconnect");
+
+ stop_stream(ctx);
+
+ JNIEnv* env = nullptr;
+ if (ctx->vm->AttachCurrentThread(&env, nullptr) != 0)
+ return;
+
+ jclass bindings_class = env->GetObjectClass(ctx->bindings_obj);
+ if (!bindings_class) {
+ ctx->vm->DetachCurrentThread();
+ return;
+ }
+
+ jmethodID on_stream_disconnected_method = env->GetMethodID(bindings_class, "onStreamDisconnected", "()V");
+ if (!on_stream_disconnected_method) {
+ ctx->vm->DetachCurrentThread();
+ return;
+ }
+
+ env->CallVoidMethod(ctx->bindings_obj, on_stream_disconnected_method);
+}
+} // namespace
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_startStreaming(
+ JNIEnv *env, jobject thiz, jstring signaling_url, jobjectArray stunServers, jobject surface, jint width, jint height, jboolean useInsecureTLS) {
+ if (!surface || width <= 0 || height <= 0)
+ return false;
+
+ auto ctx = get_context(env, thiz);
+ if (ctx)
+ return false;
+
+ ctx = new Context;
+ env->GetJavaVM(&ctx->vm);
+ ctx->bindings_obj = env->NewGlobalRef(thiz);
+
+ ctx->window = ANativeWindow_fromSurface(env, surface);
+ if (!ctx->window)
+ return false;
+
+ AnboxStreamAudioSpec audio_output_spec{ANBOX_STREAM_AUDIO_FORMAT_PCM_16_BIT, 48000, 2};
+ anbox_stream_config_set_audio_spec(
+ ctx->cfg.get(), ANBOX_STREAM_AUDIO_STREAM_TYPE_OUTPUT, audio_output_spec);
+ if (!ctx->audio_player.initialize(audio_output_spec))
+ return false;
+
+ anbox_set_log_callback([](AnboxLogLevel level, const char* msg, void* user_data) {
+ __android_log_write(ANDROID_LOG_INFO, "AnboxStream", msg);
+ }, nullptr);
+
+ ctx->cfg.reset(anbox_stream_config_new());
+ if (!ctx->cfg) {
+ delete ctx;
+ return false;
+ }
+
+ const char* url = env->GetStringUTFChars(signaling_url, 0);
+ anbox_stream_config_set_signaling_url(ctx->cfg.get(), url);
+ env->ReleaseStringUTFChars(signaling_url, url);
+
+ anbox_stream_config_set_use_insecure_tls(ctx->cfg.get(), useInsecureTLS);
+
+ std::vector stun_servers;
+ if (!StunServer::createListFromObject(env, stunServers, stun_servers))
+ return false;
+
+ for (const auto& server : stun_servers) {
+ // We have to convert our std::string based URLs to a list of
+ // const char* typed URLs
+ std::vector raw_urls;
+ std::transform(
+ server.urls.begin(),
+ server.urls.end(),
+ std::back_inserter(raw_urls),
+ [](const std::string& s) -> const char* {
+ return s.c_str();
+ });
+
+ anbox_stream_config_add_stun_server(
+ ctx->cfg.get(),
+ raw_urls.data(),
+ server.urls.size(),
+ server.username.c_str(),
+ server.password.c_str());
+ }
+
+ ctx->stream.reset(anbox_stream_new(ctx->cfg.get()));
+ if (!ctx->stream) {
+ delete ctx;
+ return false;
+ }
+
+ anbox_stream_set_connected_callback(ctx->stream.get(), on_stream_connected, ctx);
+ anbox_stream_set_disconnected_callback(ctx->stream.get(), on_stream_disconnected, ctx);
+ anbox_stream_set_audio_data_ready_callback(ctx->stream.get(), on_audio_data_ready, ctx);
+
+ __android_log_write(ANDROID_LOG_INFO, "AnboxStream", "Connecting Anbox Stream ...");
+
+ auto status = anbox_stream_connect(ctx->stream.get());
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ jni_set_pointer(env, thiz, "context", ctx);
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_stopStreaming(
+ JNIEnv *env, jobject thiz) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ stop_stream(ctx);
+
+ delete ctx;
+
+ jni_set_pointer(env, thiz, "context", nullptr);
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendTouchMove(JNIEnv *env, jobject thiz, jint id, jint x, jint y) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_TOUCH_MOVE;
+ msg.touch_move.id = id;
+ msg.touch_move.x = x;
+ msg.touch_move.y = y;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendTouchStart(
+ JNIEnv *env, jobject thiz, jint id, jint x, jint y) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_TOUCH_START;
+ msg.touch_start.id = id;
+ msg.touch_start.x = x;
+ msg.touch_start.y = y;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendTouchEnd(
+ JNIEnv *env, jobject thiz, jint id) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_TOUCH_END;
+ msg.touch_end.id = id;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendTouchCancel(
+ JNIEnv *env, jobject thiz, jint id) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_TOUCH_CANCEL;
+ msg.touch_end.id = id;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendMouseMove(
+ JNIEnv *env, jobject thiz, jint x, jint y, jint rx, jint ry) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_MOUSE_MOVE;
+ msg.mouse_move.x = x;
+ msg.mouse_move.y = y;
+ msg.mouse_move.rx = rx;
+ msg.mouse_move.ry = ry;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
+
+extern "C"
+JNIEXPORT jboolean JNICALL
+Java_com_canonical_anbox_streaming_sdk_native_1example_NativeBindings_sendMouseButton(
+ JNIEnv *env, jobject thiz, jint button, jboolean pressed) {
+ auto ctx = get_context(env, thiz);
+ if (!ctx)
+ return false;
+
+ AnboxStreamControlMessage msg;
+ msg.type = ANBOX_STREAM_CONTROL_MESSAGE_TYPE_MOUSE_BUTTON;
+ msg.mouse_button.button = button;
+ msg.mouse_button.pressed = pressed;
+
+ auto status = anbox_stream_send_message(ctx->stream.get(), &msg);
+ if (status != ANBOX_STATUS_OK)
+ return false;
+
+ return true;
+}
diff --git a/examples/android/native_streaming/app/src/main/cpp/jni_helpers.cpp b/examples/android/native_streaming/app/src/main/cpp/jni_helpers.cpp
new file mode 100644
index 0000000..f5d60c0
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/cpp/jni_helpers.cpp
@@ -0,0 +1,81 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+#include "jni_helpers.h"
+
+bool jni_get_string(JNIEnv* env, jobject obj, const char* name, std::string& str) {
+ jclass cls = env->GetObjectClass(obj);
+ if (!cls)
+ return false;
+
+ jfieldID field_id = env->GetFieldID(cls, name, "Ljava/lang/String;");
+ if (!field_id)
+ return false;
+
+ auto str_obj = (jstring) env->GetObjectField(obj, field_id);
+ if (!str_obj)
+ return false;
+
+ const char *raw_str = env->GetStringUTFChars(str_obj, 0);
+ if (!raw_str)
+ return false;
+
+ str = raw_str;
+ env->ReleaseStringUTFChars(str_obj, raw_str);
+
+ return true;
+}
+
+bool jni_get_string_vector(JNIEnv* env, jobject obj, const char* name, std::vector& strs) {
+ jclass cls = env->GetObjectClass(obj);
+ if (!cls)
+ return false;
+
+ jfieldID array_id = env->GetFieldID(cls, name, "[Ljava/lang/String;");
+ if (!array_id)
+ return false;
+
+ jobjectArray array_obj = reinterpret_cast(env->GetObjectField(obj, array_id));
+ if (!array_obj)
+ return false;
+
+ for (int n = 0; n < env->GetArrayLength(array_obj); n++) {
+ jstring str_obj = (jstring) env->GetObjectArrayElement(array_obj, n);
+ if (!str_obj)
+ return false;
+
+ const char *raw_str = env->GetStringUTFChars(str_obj, 0);
+ if (!raw_str)
+ return false;
+
+ strs.push_back(std::string(raw_str));
+ env->ReleaseStringUTFChars(str_obj, raw_str);
+ }
+
+ return true;
+}
+
+void *jni_get_pointer(JNIEnv *env, jobject instance, const char *name) {
+ jclass cls = env->GetObjectClass(instance);
+ if (!cls)
+ return nullptr;
+
+ jfieldID id = env->GetFieldID(cls, name, "J");
+ if (!id)
+ return nullptr;
+
+ return (void *) env->GetLongField(instance, id);
+}
+
+bool jni_set_pointer(JNIEnv *env, jobject instance, const char *name, void *ptr) {
+ jclass cls = env->GetObjectClass(instance);
+ if (!cls)
+ return false;
+
+ jfieldID id = env->GetFieldID(cls, name, "J");
+ if (!id)
+ return false;
+
+ env->SetLongField(instance, id, (long) ptr);
+ return true;
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/cpp/jni_helpers.h b/examples/android/native_streaming/app/src/main/cpp/jni_helpers.h
new file mode 100644
index 0000000..7fca79c
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/cpp/jni_helpers.h
@@ -0,0 +1,17 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+#ifndef ANBOX_STREAMING_NATIVE_EXAMPLE_JNI_HELPERS_H
+#define ANBOX_STREAMING_NATIVE_EXAMPLE_JNI_HELPERS_H
+
+#include
+#include
+
+#include
+
+bool jni_get_string(JNIEnv* env, jobject obj, const char* name, std::string& str);
+bool jni_get_string_vector(JNIEnv* env, jobject obj, const char* name, std::vector& strs);
+void *jni_get_pointer(JNIEnv *env, jobject instance, const char *name);
+bool jni_set_pointer(JNIEnv *env, jobject instance, const char *name, void *ptr);
+
+#endif //ANBOX_STREAMING_NATIVE_EXAMPLE_JNI_HELPERS_H
diff --git a/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/MainActivity.java b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/MainActivity.java
new file mode 100644
index 0000000..dd24a49
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/MainActivity.java
@@ -0,0 +1,192 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Switch;
+import android.widget.Toast;
+import android.content.res.Configuration;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+public class MainActivity extends AppCompatActivity {
+ private static final String LOG_TAG = MainActivity.class.getSimpleName();
+
+ public static final String EXTRA_SIGNALING_URL
+ = "com.canonical.anbox.streaming.sdk.native_example.EXTRA_SIGNALING_URL";
+ public static final String EXTRA_STUN_SERVERS
+ = "com.canonical.anbox.streaming.sdk.native_example.EXTRA_STUN_SERVERS";
+ public static final String EXTRA_USE_INSECURE_TLS
+ = "com.canonical.anbox.streaming.sdk.native_example.EXTRA_USE_INSECURE_TLS";
+
+ public static final MediaType MEDIA_TYPE_JSON
+ = MediaType.parse("application/json; charset=utf-8");
+
+ private OkHttpClient mClient = createHTTPClient().build();
+
+ public static OkHttpClient.Builder createHTTPClient() {
+ try {
+ final TrustManager[] trustAllCerts = new TrustManager[]{
+ new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ }
+
+ @Override
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ final X509Certificate[] acceptedIssuers = {};
+ return acceptedIssuers;
+ }
+ }
+ };
+
+ final SSLContext sslContext = SSLContext.getInstance("SSL");
+ sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
+ final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+ builder.sslSocketFactory(sslSocketFactory, (X509TrustManager)trustAllCerts[0]);
+ builder.hostnameVerifier((hostname, session) -> true);
+
+ return builder;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ public void startStreaming(View view) {
+ final EditText apiTokenBox = findViewById(R.id.api_token);
+ final EditText gatewayURLBox = findViewById(R.id.gateway_url);
+ final EditText appNameBox = findViewById(R.id.app_name);
+ final Switch useInsecureTLSSwitch = findViewById(R.id.use_insecure_tls);
+
+ if (apiTokenBox.getText().length() == 0 ||
+ gatewayURLBox.getText().length() == 0 ||
+ appNameBox.getText().length() == 0) {
+ Toast.makeText(this, "Missing gateway URL, API token or application name", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // In case of the given URL contains a trailing slash, we get rid of it
+ // since it potentially causes IOException when talking to stream gateway.
+ String getwayURL = gatewayURLBox.getText().toString();
+ if (getwayURL.charAt(getwayURL.length() - 1) == '/') {
+ getwayURL = getwayURL.substring(0, getwayURL.length() - 1);
+ }
+
+ // The app is locked to portrait mode so we can teach the remote Android instance
+ // to use a portrait screen size too
+ int width = 720, height = 1280;
+
+ JSONObject sessionInfo = new JSONObject();
+ JSONObject screenInfo = new JSONObject();
+ try {
+ screenInfo.put("width", width);
+ screenInfo.put("height", height);
+ screenInfo.put("fps", 30);
+ sessionInfo.put("app", appNameBox.getText());
+ sessionInfo.put("screen", screenInfo);
+ } catch (JSONException e) {
+ Toast.makeText(this, "Failed to create session specification", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Request createSessionReq = new Request.Builder()
+ .url(getwayURL + "/1.0/sessions")
+ .post(RequestBody.create(MEDIA_TYPE_JSON, sessionInfo.toString()))
+ .addHeader("Authorization", "Macaroon root=" + apiTokenBox.getText().toString())
+ .build();
+
+ mClient.newCall(createSessionReq).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ Log.e(LOG_TAG, "Failed to create session: " + e.getMessage());
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ try (ResponseBody responseBody = response.body()) {
+ if (!response.isSuccessful()) {
+ Log.w(LOG_TAG, responseBody.string());
+ throw new IOException("Unexpected code " + response);
+ }
+
+ JSONObject sessionResp = new JSONObject(responseBody.string());
+ JSONObject metaDataObj = sessionResp.getJSONObject("metadata");
+
+ String signalingURL = metaDataObj.getString("url");
+
+ ArrayList stunServers = new ArrayList<>();
+ JSONArray stunServersArray = metaDataObj.getJSONArray("stun_servers");
+ for (int n = 0; n < stunServersArray.length(); n++) {
+ JSONObject stunServerObj = stunServersArray.getJSONObject(n);
+ StunServer server = new StunServer();
+
+ JSONArray urlsArray = stunServerObj.getJSONArray("urls");
+ List urls = new ArrayList();
+ for (int m = 0; m < urlsArray.length(); m++) {
+ urls.add(urlsArray.getString(m));
+ }
+ server.urls = urls.toArray(new String[0]);
+
+ if (stunServerObj.has("username"))
+ server.username = stunServerObj.getString("username");
+ if (stunServerObj.has("password"))
+ server.password = stunServerObj.getString("password");
+
+ stunServers.add(server);
+ }
+
+ Intent intent = new Intent(MainActivity.this, StreamActivity.class);
+ intent.putExtra(EXTRA_SIGNALING_URL, signalingURL);
+ intent.putParcelableArrayListExtra(EXTRA_STUN_SERVERS, stunServers);
+ intent.putExtra(EXTRA_USE_INSECURE_TLS, useInsecureTLSSwitch.isChecked());
+ startActivity(intent);
+ } catch (JSONException | IOException e) {
+ throw new IOException("Received invalid response");
+ }
+ }
+ });
+ }
+}
diff --git a/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/NativeBindings.java b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/NativeBindings.java
new file mode 100644
index 0000000..1c052d9
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/NativeBindings.java
@@ -0,0 +1,42 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import android.util.Log;
+import android.view.Surface;
+
+import java.util.List;
+
+public class NativeBindings {
+ private final String LOG_TAG = NativeBindings.class.getSimpleName();
+
+ Listener mListener;
+
+ public interface Listener {
+ void onStreamDisconnected();
+ }
+
+ static {
+ System.loadLibrary("anbox-stream-bindings");
+ }
+
+ private long context;
+
+ public void setListener(Listener l) {
+ mListener = l;
+ }
+
+ public void onStreamDisconnected() {
+ mListener.onStreamDisconnected();
+ }
+
+ public native boolean startStreaming(String signalingURL, Object[] stunServers, Surface surface, int width, int height, boolean useInsecureTLS);
+ public native boolean stopStreaming();
+ public native boolean sendTouchStart(int id, int x, int y);
+ public native boolean sendTouchMove(int id, int x, int y);
+ public native boolean sendTouchEnd(int id);
+ public native boolean sendTouchCancel(int id);
+ public native boolean sendMouseMove(int x, int y, int rx, int ry);
+ public native boolean sendMouseButton(int button, boolean pressed);
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StreamActivity.java b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StreamActivity.java
new file mode 100644
index 0000000..a1700ce
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StreamActivity.java
@@ -0,0 +1,181 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.os.Parcel;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import java.lang.annotation.Native;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class StreamActivity extends AppCompatActivity implements NativeBindings.Listener {
+ private NativeBindings bindings;
+ private String sessionURL;
+ private List stunServers = new ArrayList<>();
+ private boolean useInsecureTLS;
+
+ public StreamActivity() {
+ bindings = new NativeBindings();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_stream);
+
+ Intent intent = getIntent();
+ this.sessionURL = intent.getStringExtra(MainActivity.EXTRA_SIGNALING_URL);
+ this.stunServers = intent.getParcelableArrayListExtra(MainActivity.EXTRA_STUN_SERVERS);
+ this.useInsecureTLS = intent.getBooleanExtra(MainActivity.EXTRA_USE_INSECURE_TLS, true);
+
+ this.bindings.setListener(this);
+ }
+
+ @Override
+ public void onStreamDisconnected() {
+ finish();
+ }
+
+ private class StreamView extends SurfaceView
+ implements SurfaceHolder.Callback, View.OnTouchListener {
+ private final String LOG_TAG = StreamView.class.getSimpleName();
+
+ private NativeBindings bindings;
+ private String sessionURL;
+ private List stunServers;
+ private boolean useInsecureTLS;
+
+ public StreamView(Context context, NativeBindings bindings, String sessionURL, List stunServers, boolean useInsecureTLS) {
+ super(context);
+ this.bindings = bindings;
+ this.sessionURL = sessionURL;
+ this.stunServers = stunServers;
+ this.getHolder().addCallback(this);
+ this.useInsecureTLS = useInsecureTLS;
+
+ setOnTouchListener(this);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) { }
+
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (!this.bindings.startStreaming(sessionURL, stunServers.toArray(), holder.getSurface(), width, height, useInsecureTLS)) {
+ Toast.makeText(StreamActivity.this, "Failed to start streaming", Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ if (!this.bindings.stopStreaming())
+ Toast.makeText(StreamActivity.this, "Failed to stop streaming", Toast.LENGTH_SHORT).show();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ @Override
+ public boolean onCapturedPointerEvent(MotionEvent event) {
+ super.onCapturedPointerEvent(event);
+
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+
+ if (event.getAction() == MotionEvent.ACTION_MOVE) {
+ this.bindings.sendMouseMove(x, y, 0, 0);
+ } else if (event.getAction() == MotionEvent.ACTION_BUTTON_PRESS) {
+ this.bindings.sendMouseButton(event.getActionButton(), true);
+ } else if (event.getAction() == MotionEvent.ACTION_BUTTON_RELEASE) {
+ this.bindings.sendMouseButton(event.getActionButton(), false);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int idx = -1;
+ int action = event.getActionMasked();
+ final int pointerCount = event.getPointerCount();
+
+ switch (action) {
+ case MotionEvent.ACTION_MOVE:
+ for (idx = 0; idx < pointerCount; idx++) {
+ final int id = event.getPointerId(idx);
+ final int x = (int) event.getX(idx);
+ final int y = (int) event.getY(idx);
+ this.bindings.sendTouchMove(id, x, y);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_DOWN:
+ idx = 0;
+ case MotionEvent.ACTION_POINTER_UP:
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ if (idx == -1)
+ idx = event.getActionIndex();
+
+ final int id = event.getPointerId(idx);
+ final int x = (int) event.getX(idx);
+ final int y = (int) event.getY(idx);
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP)
+ this.bindings.sendTouchEnd(id);
+ else if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN)
+ this.bindings.sendTouchStart(id, x, y);
+
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ for (idx = 0; idx < pointerCount; idx++) {
+ final int id = event.getPointerId(idx);
+ this.bindings.sendTouchCancel(id);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return true;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+
+ StreamView streamView = new StreamView(this.getApplicationContext(), bindings, sessionURL, stunServers, useInsecureTLS);
+ ViewGroup vg = findViewById(android.R.id.content);
+ ViewGroup.LayoutParams params = vg.getLayoutParams();
+ this.addContentView(streamView, params);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StunServer.java b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StunServer.java
new file mode 100644
index 0000000..0f4e509
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/native_example/StunServer.java
@@ -0,0 +1,46 @@
+// Anbox Streaming SDK
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class StunServer implements Parcelable {
+ public String[] urls = {};
+ public String username = "";
+ public String password = "";
+
+ StunServer() {
+ }
+
+ protected StunServer(Parcel in) {
+ urls = in.createStringArray();
+ username = in.readString();
+ password = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringArray(urls);
+ dest.writeString(username);
+ dest.writeString(password);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public StunServer createFromParcel(Parcel in) {
+ return new StunServer(in);
+ }
+
+ @Override
+ public StunServer[] newArray(int size) {
+ return new StunServer[size];
+ }
+ };
+}
diff --git a/examples/android/native_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/android/native_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/drawable/ic_launcher_background.xml b/examples/android/native_streaming/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/native_streaming/app/src/main/res/layout/activity_main.xml b/examples/android/native_streaming/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..47ac791
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/native_streaming/app/src/main/res/layout/activity_stream.xml b/examples/android/native_streaming/app/src/main/res/layout/activity_stream.xml
new file mode 100644
index 0000000..daf2250
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/layout/activity_stream.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/examples/android/native_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/examples/android/native_streaming/app/src/main/res/values/colors.xml b/examples/android/native_streaming/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..4faecfa
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/values/dimens.xml b/examples/android/native_streaming/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..125df87
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+ 16dp
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/values/strings.xml b/examples/android/native_streaming/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..c1f0aa6
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+ Anbox Streaming Native Example
+ StreamActivity
+ URL of the gateway
+ Gateway API token
+ Stream
+ Name of the application to stream
+ Use Insecure TLS
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/main/res/values/styles.xml b/examples/android/native_streaming/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..21d9ced
--- /dev/null
+++ b/examples/android/native_streaming/app/src/main/res/values/styles.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/native_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/native_example/ExampleUnitTest.java b/examples/android/native_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/native_example/ExampleUnitTest.java
new file mode 100644
index 0000000..4917b52
--- /dev/null
+++ b/examples/android/native_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/native_example/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.canonical.anbox.streaming.sdk.native_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/build.gradle b/examples/android/native_streaming/build.gradle
new file mode 100644
index 0000000..b58dc04
--- /dev/null
+++ b/examples/android/native_streaming/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/examples/android/native_streaming/gradle.properties b/examples/android/native_streaming/gradle.properties
new file mode 100644
index 0000000..c52ac9b
--- /dev/null
+++ b/examples/android/native_streaming/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.jar b/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.properties b/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e6bd414
--- /dev/null
+++ b/examples/android/native_streaming/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Jul 02 09:49:06 CEST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/examples/android/native_streaming/gradlew b/examples/android/native_streaming/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/examples/android/native_streaming/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/examples/android/native_streaming/gradlew.bat b/examples/android/native_streaming/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/examples/android/native_streaming/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/android/native_streaming/settings.gradle b/examples/android/native_streaming/settings.gradle
new file mode 100644
index 0000000..30ca36d
--- /dev/null
+++ b/examples/android/native_streaming/settings.gradle
@@ -0,0 +1,2 @@
+include ':app'
+rootProject.name = "Anbox Streaming Native Example"
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.gitignore b/examples/android/webview_streaming/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/examples/android/webview_streaming/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/examples/android/webview_streaming/.idea/.name b/examples/android/webview_streaming/.idea/.name
new file mode 100644
index 0000000..6e490a1
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/.name
@@ -0,0 +1 @@
+WebViewStreaming
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.idea/codeStyles/Project.xml b/examples/android/webview_streaming/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..681f41a
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.idea/gradle.xml b/examples/android/webview_streaming/.idea/gradle.xml
new file mode 100644
index 0000000..440480e
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.idea/misc.xml b/examples/android/webview_streaming/.idea/misc.xml
new file mode 100644
index 0000000..37a7509
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.idea/runConfigurations.xml b/examples/android/webview_streaming/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/.idea/vcs.xml b/examples/android/webview_streaming/.idea/vcs.xml
new file mode 100644
index 0000000..4fce1d8
--- /dev/null
+++ b/examples/android/webview_streaming/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/.gitignore b/examples/android/webview_streaming/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/examples/android/webview_streaming/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/examples/android/webview_streaming/app/build.gradle b/examples/android/webview_streaming/app/build.gradle
new file mode 100644
index 0000000..44754a3
--- /dev/null
+++ b/examples/android/webview_streaming/app/build.gradle
@@ -0,0 +1,35 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 29
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ applicationId "com.canonical.anbox.streaming.sdk.webview_example"
+ minSdkVersion 26
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.webkit:webkit:1.2.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+}
diff --git a/examples/android/webview_streaming/app/proguard-rules.pro b/examples/android/webview_streaming/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/examples/android/webview_streaming/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/examples/android/webview_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleInstrumentedTest.java b/examples/android/webview_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..b41ce17
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/androidTest/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package com.canonical.anbox.streaming.sdk.webview_example;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ assertEquals("com.canonical.anbox.streaming.sdk.webview_example", appContext.getPackageName());
+ }
+}
diff --git a/examples/android/webview_streaming/app/src/main/AndroidManifest.xml b/examples/android/webview_streaming/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5bbeb94
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/assets/css/style.css b/examples/android/webview_streaming/app/src/main/assets/css/style.css
new file mode 100644
index 0000000..5e93bc5
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/assets/css/style.css
@@ -0,0 +1,3 @@
+body {
+ background-color:#000000
+}
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/assets/index.html b/examples/android/webview_streaming/app/src/main/assets/index.html
new file mode 100644
index 0000000..977778e
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/assets/index.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Anbox Streaming SDK Example
+
+
+
+
+
+
+
diff --git a/examples/android/webview_streaming/app/src/main/assets/js/anbox-stream-sdk.js b/examples/android/webview_streaming/app/src/main/assets/js/anbox-stream-sdk.js
new file mode 100644
index 0000000..68f3e9e
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/assets/js/anbox-stream-sdk.js
@@ -0,0 +1,1386 @@
+// Anbox Stream SDK
+// Copyright 2019 Canonical Ltd. All rights reserved.
+
+class AnboxStream {
+ /**
+ * AnboxStream creates a connection between your client and an Android instance and
+ * displays its video & audio feed in an HTML5 player
+ * @param options: {object}
+ * @param options.connector {object} WebRTC Stream connector.
+ * @param options.targetElement {string} ID of the DOM element to attach the video to.
+ * @param options.fullScreen {boolean} Stream video in full screen mode. (default: false)
+ * @param [options.stunServers] {object[]} List of additional STUN/TURN servers.
+ * @param [options.stunServers[].urls] {string[]} URLs the same STUN/TURN server can be reached on.
+ * @param [options.stunServers[].username] {string} Username used when authenticating with the STUN/TURN server.
+ * @param [options.stunServers[].password] {string} Password used when authenticating with the STUN/TURN server.
+ * @param [options.devices] {object} Configuration settings for the streaming client device.
+ * @param [options.devices.microphone=false] {boolean} Enable audio capture from microphone and send it to the remote peer.
+ * @param [options.devices.speaker=true] {boolean} Enable audio playout through the default audio playback device.
+ * @param [options.controls] {object} Configuration how the client can interact with the stream.
+ * @param [options.controls.keyboard=true] {boolean} Send key presses to the Android instance.
+ * @param [options.controls.mouse=true] {boolean} Send mouse events to the Android instance.
+ * @param [options.controls.gamepad=true] {boolean} Send gamepad events to the Android instance.
+ * @param [options.callbacks] {object} A list of callbacks to react on stream lifecycle events.
+ * @param [options.callbacks.ready=none] {function} Called when the video and audio stream are ready to be inserted in the DOM.
+ * @param [options.callbacks.error=none] {function} Called on stream error with the message as parameter.
+ * @param [options.callbacks.done=none] {function} Called when the stream is closed.
+ * @param [options.callbacks.statsUpdated=none] {function} Called when the overall webrtc peer connection statistics are updated.
+ * @param [options.experimental] {object} Experimental features. Not recommended on production.
+ * @param [options.experimental.disableBrowserBlock=false] {boolean} Don't throw an error if an unsupported browser is detected.
+ */
+ constructor(options) {
+ if (this._nullOrUndef(options))
+ throw new Error('invalid options');
+
+ this._fillDefaults(options);
+ this._validateOptions(options);
+ this._options = options;
+
+ if (!this._options.disableBrowserBlock)
+ this._detectUnsupportedBrowser();
+
+ this._id = Math.random().toString(36).substr(2, 9);
+ this._containerID = options.targetElement;
+ this._videoID = 'anbox-stream-video-' + this._id;
+ this._audioID = 'anbox-stream-audio-' + this._id;
+
+ // WebRTC
+ this._ws = null; // WebSocket
+ this._pc = null; // PeerConnection
+ this._controlChan = null; // Channel to send inputs
+ this._timedout = false;
+ this._timer = -1;
+ this._disconnectedTimer = -1;
+ this._ready = false;
+ this._session_id = "";
+
+ // Media streams
+ this._videoStream = null;
+ this._audioStream = null;
+ this._audioInputStream = null;
+
+ // Control options
+ this._modifierState = 0;
+ this._dimensions = null;
+ this._gamepadManager = null;
+ this._lastTouchMoves = [];
+
+ // Stats
+ this._statsTimerId = -1;
+ this._timeElapse = 0;
+ this._stats = {
+ video: {
+ bandwidthMbit: 0,
+ totalBytesReceived: 0,
+ fps: 0
+ },
+ network: {
+ currentRtt: 0
+ },
+ audioInput: {
+ bandwidthMbit: 0,
+ bytesSent: 0,
+ },
+ audioOutput: {
+ bandwidthMbit: 0,
+ bytesReceived: 0
+ },
+ }
+ };
+
+ _includeStunServers(stun_servers) {
+ for (var n = 0; n < stun_servers.length; n++) {
+ this._options.stunServers.push({
+ "urls": stun_servers[n].urls,
+ "username": stun_servers[n].username,
+ "credential": stun_servers[n].password
+ });
+ }
+ };
+
+ /**
+ * Connect a new instance for the configured application or attach to an existing one
+ */
+ async connect() {
+ if (this._options.fullScreen)
+ this._requestFullscreen()
+
+ let session = {};
+ try {
+ session = await this._options.connector.connect()
+ } catch (e) {
+ this._stopStreamingOnError(e.message);
+ return
+ }
+
+ this._session_id = session.id
+
+ if (session.websocket === undefined || session.websocket.length === 0) {
+ this._stopStreamingOnError('connector did not return signaling information');
+ return
+ }
+
+ // add additional stun servers if provided
+ if (session.stunServers.length > 0)
+ this._includeStunServers(session.stunServers);
+
+ this._connectSignaler(session.websocket)
+ };
+
+ /**
+ * Disconnect an existing stream and remove the video & audio elements.
+ *
+ * This will stop the underlying Android instance.
+ */
+ disconnect() {
+ this._stopStreaming();
+ this._options.connector.disconnect();
+ };
+
+ /**
+ * Toggle fullscreen for the streamed video.
+ *
+ * IMPORTANT: fullscreen can only be toggled following a user input.
+ * If you call this method when your page loads, it will not work.
+ */
+ _requestFullscreen() {
+ if (!document.fullscreenEnabled) {
+ console.error("fullscreen not supported");
+ return
+ }
+ const fullscreenExited = () => {
+ if (document.fullscreenElement === null) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ video.style.width = null;
+ video.style.height = null;
+ }
+ }
+ };
+ // Clean up previous event listeners
+ document.removeEventListener('fullscreenchange', fullscreenExited, false);
+ document.addEventListener('fullscreenchange', fullscreenExited, false);
+
+ // We don't put the video element itself in fullscreen because of
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=462164
+ // To work around it we put the outer container in fullscreen and scale the video
+ // to fit it. When exiting fullscreen we undo style changes done to the video element
+ const videoContainer = document.getElementById(this._containerID);
+ if (videoContainer.requestFullscreen) {
+ videoContainer.requestFullscreen().catch(err => {
+ console.log(`Failed to enter full-screen mode: ${err.message} (${err.name})`);
+ });
+ } else if (videoContainer.mozRequestFullScreen) { /* Firefox */
+ videoContainer.mozRequestFullScreen();
+ } else if (videoContainer.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
+ videoContainer.webkitRequestFullscreen();
+ } else if (videoContainer.msRequestFullscreen) { /* IE/Edge */
+ videoContainer.msRequestFullscreen();
+ }
+ };
+
+ /**
+ * Exit fullscreen mode.
+ */
+ exitFullscreen() {
+ document.exitFullscreen();
+ };
+
+ /**
+ * Return the stream ID you can use to access video and audio elements with getElementById
+ */
+ getId() {
+ return this._id;
+ }
+
+ /**
+ * Send a location update to the connected Android instance
+ *
+ * For WGS84 format gps data, where a numeric latitude or longitude is given, geographic coordinates are
+ * expressed as decimal fractions. With this system the geo coordinate of Berlin is: latitude 52.520008°, longitude 13.404954°.
+ *
+ * For NMEA format gps data, where a numeric latitude or longitude is given, the two digits
+ * immediately to the left of the decimal point are whole minutes, to the right are decimals of minutes,
+ * and the remaining digits to the left of the whole minutes are whole degrees.
+ *
+ * eg. 4533.35 is 45 degrees and 33.35 minutes. ".35" of a minute is exactly 21 seconds.
+ *
+ * @param update: {object}
+ * @param update.format {string} GPS data format ("nmea" or "wgs84" default: "wgs84")
+ * @param update.time {number} Time in milliseconds since the start of the epoch
+ * @param update.latitude {number} Latitude of the location (positive values mean northern hemisphere and negative values mean southern hemisphere)
+ * @param update.longitude {number} Longitude of the location (positive values mean northern hemisphere and negative values mean southern hemisphere)
+ * @param update.altitude {number} Altitude in meters
+ * @param update.speed {number} Current speed in meter per second
+ * @param update.bearing {number} Current bearing in degree
+ */
+ sendLocationUpdate(update) {
+ if (this._nullOrUndef(update.time) ||
+ this._nullOrUndef(update.latitude) ||
+ this._nullOrUndef(update.longitude) ||
+ this._nullOrUndef(update.altitude) ||
+ this._nullOrUndef(update.speed) ||
+ this._nullOrUndef(update.bearing)) {
+ throw new Error("incomplete location update")
+ }
+
+ if (!this._nullOrUndef(update.format) &&
+ update.format !== "nmea" &&
+ update.format !== "wgs84") {
+ throw new Error("invalid gps data format")
+ }
+
+ this._sendControlMessage("location::update-position", update);
+ }
+
+ _connectSignaler(url) {
+ let ws = new WebSocket(url);
+ ws.onopen = this._onWsOpen.bind(this);
+ ws.onclose = this._onWsClose.bind(this);
+ ws.onerror = this._onWsError.bind(this);
+ ws.onmessage = this._onWsMessage.bind(this);
+
+ this._ws = ws;
+ this._timer = window.setTimeout(this._onSignalerTimeout.bind(this), 5 * 60 * 1000);
+ }
+
+ _detectUnsupportedBrowser() {
+ if (navigator.userAgent.indexOf("Chrome") === -1 &&
+ navigator.userAgent.indexOf("Firefox") === -1 &&
+ navigator.userAgent.indexOf("Safari") === -1)
+ throw new Error("unsupported browser");
+ };
+
+ _fillDefaults(options) {
+ if (this._nullOrUndef(options.fullScreen))
+ options.fullScreen = false;
+
+ if (this._nullOrUndef(options.controls))
+ options.controls = {};
+
+ if (this._nullOrUndef(options.devices))
+ options.devices = {};
+
+ if (this._nullOrUndef(options.devices.microphone))
+ options.devices.microphone = false;
+
+ if (this._nullOrUndef(options.devices.speaker))
+ options.devices.speaker = true;
+
+ if (this._nullOrUndef(options.controls.keyboard))
+ options.controls.keyboard = true;
+
+ if (this._nullOrUndef(options.controls.mouse))
+ options.controls.mouse = true;
+
+ if (this._nullOrUndef(options.controls.gamepad))
+ options.controls.gamepad = true;
+
+ if (this._nullOrUndef(options.stunServers))
+ options.stunServers = [];
+
+ if (this._nullOrUndef(options.callbacks))
+ options.callbacks = {};
+
+ if (this._nullOrUndef(options.callbacks.ready))
+ options.callbacks.ready = () => {};
+
+ if (this._nullOrUndef(options.callbacks.error))
+ options.callbacks.error = () => {};
+
+ if (this._nullOrUndef(options.callbacks.done))
+ options.callbacks.done = () => {};
+
+ if (this._nullOrUndef(options.disableBrowserBlock))
+ options.disableBrowserBlock = false;
+ };
+
+ _validateOptions(options) {
+ if (this._nullOrUndef(options.targetElement))
+ throw new Error('missing targetElement parameter');
+ if (document.getElementById(options.targetElement) === null)
+ throw new Error(`target element "${options.targetElement}" does not exist`);
+
+ if (this._nullOrUndef(options.connector))
+ throw new Error('missing connector');
+
+ if (typeof(options.connector.connect) !== "function")
+ throw new Error('missing "connect" method on connector');
+
+ if (typeof(options.connector.disconnect) !== "function")
+ throw new Error('missing "disconnect" method on connector');
+ }
+
+ _insertMedia(videoSource, audioSource) {
+ this._ready = true;
+ let mediaContainer = document.getElementById(this._containerID);
+ mediaContainer.style.display = "flex"
+ mediaContainer.style.justifyContent = "center"
+ mediaContainer.style.alignItems = "center"
+
+ const video = document.createElement('video');
+ video.style.margin = "0";
+ video.style.height = "auto";
+ video.style.width = "auto";
+
+ video.srcObject = videoSource;
+ video.muted = true;
+ video.autoplay = true;
+ video.controls = false;
+ video.id = this._videoID;
+ video.playsInline = true;
+ video.onplay = () => {
+ this._onResize()
+ this._registerControls();
+ };
+ mediaContainer.appendChild(video);
+
+ if (this._options.devices.speaker) {
+ const audio = document.createElement('audio');
+ audio.id = this._audioID;
+ audio.srcObject = audioSource;
+ audio.autoplay = true;
+ audio.controls = false;
+ mediaContainer.appendChild(audio);
+ }
+
+ };
+
+ _removeMedia() {
+ const video = document.getElementById(this._videoID);
+ const audio = document.getElementById(this._audioID);
+
+ if (video)
+ video.remove();
+ if (audio)
+ audio.remove();
+ };
+
+ _stopStreaming() {
+ // Notify the other side that we're disconnecting to speed up potential reconnects
+ this._sendControlMessage("stream::disconnect", {});
+
+ if (this._disconnectedTimer > 0) {
+ window.clearTimeout(this._disconnectedTimer);
+ this._disconnectedTimer = -1;
+ }
+
+ if (this._audioInputStream)
+ this._audioInputStream.getTracks().forEach(track => track.stop());
+
+ if (this._pc !== null) {
+ this._pc.close();
+ this._pc = null;
+ }
+ if (this._ws !== null) {
+ this._ws.close();
+ this._ws = null;
+ }
+ this._unregisterControls();
+ this._removeMedia();
+
+ if (this._statsTimerId !== -1)
+ window.clearInterval(this._statsTimerId)
+
+ if (this._gamepadManager) {
+ this._gamepadManager.stopPolling()
+ }
+ this._options.callbacks.done()
+ };
+
+ _onSignalerTimeout() {
+ if (this._pc == null || this._pc.iceConnectionState === 'connected')
+ return;
+
+ this._timedout = true;
+ this._stopStreaming();
+ };
+
+ _onRtcOfferCreated(description) {
+ this._pc.setLocalDescription(description);
+ let msg = {type: 'offer', sdp: btoa(description.sdp)};
+ if (this._ws.readyState === 1)
+ this._ws.send(JSON.stringify(msg));
+ };
+
+ _onRtcTrack(event) {
+ const kind = event.track.kind;
+ if (kind === 'video') {
+ this._videoStream = event.streams[0];
+ this._videoStream.onremovetrack = this._stopStreaming;
+ } else if (kind === 'audio') {
+ this._audioStream = event.streams[0];
+ this._audioStream.onremovetrack = this._stopStreaming;
+ }
+
+ // Start streaming until audio and video tracks both are available
+ if (this._videoStream && (!this._options.devices.speaker || this._audioStream)) {
+ this._insertMedia(this._videoStream, this._audioStream);
+ this._startStatsUpdater();
+ this._options.callbacks.ready(this._session_id);
+ }
+ };
+
+ _startStatsUpdater() {
+ if (this._nullOrUndef(this._options.callbacks.statsUpdated))
+ return
+
+ this._statsTimerId = window.setInterval(() => {
+ if (this._nullOrUndef(this._pc))
+ return
+
+ this._timeElapse++
+ this._pc.getStats(null).then(stats => {
+ stats.forEach(report => {
+ // Instead of dumping all the statistics, we only provide
+ // limited sets of stats to the caller.
+ Object.keys(report).forEach(statName => {
+ if (statName === "ssrc") {
+ if ("mediaType" in report) {
+ let mediaType = report["mediaType"];
+ if (mediaType === "video") {
+ if ("bytesReceived" in report) {
+ let bytesReceived = report["bytesReceived"]
+ let diff = 0;
+ if (this._stats.video.totalBytesReceived > bytesReceived)
+ diff = bytesReceived;
+ else
+ diff = bytesReceived - this._stats.video.totalBytesReceived;
+
+ this._stats.video.bandwidthMbit = diff;
+ this._stats.video.totalBytesReceived = bytesReceived;
+ }
+ } else if (mediaType === "audio") {
+ if ("packetsSent" in report) {
+ if ("bytesSent" in report) {
+ let bytesSent = report["bytesSent"];
+ let diff = 0;
+ if (this._stats.audioInput.bytesSent > bytesSent)
+ diff = bytesSent;
+ else
+ diff = bytesSent - this._stats.audioInput.bytesSent;
+
+ this._stats.audioInput.bandwidthMbit = diff;
+ this._stats.audioInput.bytesSent = bytesSent;
+ }
+ } else {
+ if ("bytesReceived" in report) {
+ let bytesReceived = report["bytesReceived"];
+ let diff = 0;
+ if (this._stats.audioOutput.bytesReceived > bytesReceived)
+ diff = bytesReceived;
+ else
+ diff = bytesReceived - this._stats.audioOutput.bytesReceived;
+
+ this._stats.audioOutput.bandwidthMbit = diff;
+ this._stats.audioOutput.bytesReceived = bytesReceived;
+ }
+ }
+ }
+ }
+ } else if (statName === "type" && report["type"] === "candidate-pair") {
+ if ("nominated" in report && report["nominated"] &&
+ "state" in report && report["state"] === "succeeded" &&
+ "currentRoundTripTime" in report) {
+ this._stats.network.currentRtt = report["currentRoundTripTime"] * 1000;
+ }
+ } else if (statName === "type" && report["type"] === "inbound-rtp") {
+ if ("framesDecoded" in report)
+ this._stats.video.fps = Math.round(report["framesDecoded"] / this._timeElapse);
+ }
+ });
+ });
+ });
+
+ this._options.callbacks.statsUpdated(this._stats);
+ },
+ // TODO: enable stats update interval configurable
+ 1000);
+ }
+
+ _onConnectionTimeout() {
+ this._disconnectedTimer = -1;
+ this._stopStreamingOnError('Connection lost');
+ }
+
+ _onRtcIceConnectionStateChange() {
+ if (this._pc === null)
+ return;
+
+ if (this._pc.iceConnectionState === 'failed') {
+ this._stopStreamingOnError('Failed to establish a connection via ICE');
+ } else if (this._pc.iceConnectionState === 'disconnected') {
+ // When we end up here the connection may not have closed but we
+ // just have a temorary network problem. We wait for a moment and
+ // if the connection isn't restablished we stop streaming
+ this._disconnectedTimer = window.setTimeout(this._onConnectionTimeout.bind(this), 10 * 1000);
+ } else if (this._pc.iceConnectionState === 'closed') {
+ if (this._timedout) {
+ this._stopStreamingOnError('Connection timed out');
+ return;
+ }
+ this._stopStreaming();
+ } else if (this._pc.iceConnectionState === 'connected') {
+ if (this._disconnectedTimer > 0) {
+ window.clearTimeout(this._disconnectedTimer);
+ this._disconnectedTimer = -1;
+ }
+ window.clearTimeout(this._timer);
+ this._ws.close();
+ }
+ };
+
+ _onRtcIceCandidate(event) {
+ if (event.candidate !== null && event.candidate.candidate !== "") {
+ const msg = {
+ type: 'candidate',
+ candidate: btoa(event.candidate.candidate),
+ sdpMid: event.candidate.sdpMid,
+ sdpMLineIndex: event.candidate.sdpMLineIndex,
+ };
+ if (this._ws.readyState === 1)
+ this._ws.send(JSON.stringify(msg));
+ }
+ };
+
+ controls = {
+ touch: {
+ 'mousemove': this._onMouseMove.bind(this),
+ 'mousedown': this._onMouseButton.bind(this),
+ 'mouseup': this._onMouseButton.bind(this),
+ 'mousewheel': this._onMouseWheel.bind(this),
+ 'touchstart': this._onTouchStart.bind(this),
+ 'touchend': this._onTouchEnd.bind(this),
+ 'touchcancel': this._onTouchCancel.bind(this),
+ 'touchmove': this._onTouchMove.bind(this),
+ },
+ keyboard: {
+ 'keydown': this._onKey.bind(this),
+ 'keyup': this._onKey.bind(this),
+ 'gamepadconnected': this._queryGamePadEvents.bind(this)
+ }
+ }
+
+ _registerControls() {
+ window.addEventListener('resize', this._onResize)
+
+ if (this._options.controls.mouse) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ for (const controlName in this.controls.touch)
+ video.addEventListener(controlName, this.controls.touch[controlName]);
+ }
+ }
+
+ if (this._options.controls.keyboard) {
+ for (const controlName in this.controls.keyboard)
+ window.addEventListener(controlName, this.controls.keyboard[controlName]);
+ }
+ };
+
+ _unregisterControls() {
+ window.removeEventListener('resize', this._onResize)
+
+ // Removing the video container should automatically remove all event listeners
+ // but this is dependant on the garbage collector, so we manually do it if we can
+ if (this._options.controls.mouse) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ for (const controlName in this.controls.touch)
+ video.removeEventListener(controlName, this.controls.touch[controlName])
+ }
+ }
+
+ if (this._options.controls.keyboard) {
+ for (const controlName in this.controls.keyboard)
+ window.removeEventListener(controlName, this.controls.keyboard[controlName]);
+ }
+ };
+
+ _onResize() {
+ const video = document.getElementById(this._videoID)
+ const container = document.getElementById(this._containerID)
+ if (video === null || container === null)
+ return;
+
+ // We calculate the distance to the closest window border while keeping aspect ratio intact.
+ const videoHeight = video.clientHeight
+ const videoWidth = video.clientWidth
+ const containerHeight = container.clientHeight
+ const containerWidth = container.clientWidth
+
+ // Depending on the aspect ratio, one size will grow faster than the other
+ const widthGrowthAcceleration = Math.max(video.videoWidth / video.videoHeight, 1)
+ const heightGrowthAcceleration = Math.max(video.videoHeight / video.videoWidth, 1)
+
+ // So we apply that acceleration to find the shortest stretch
+ const widthFinalStretch = Math.round((containerWidth - videoWidth) * heightGrowthAcceleration)
+ const heightFinalStretch = Math.round((containerHeight - videoHeight) * widthGrowthAcceleration)
+
+ if (widthFinalStretch < heightFinalStretch) {
+ video.style.width = containerWidth.toString() + "px"
+ video.style.height = "100%"
+ } else {
+ video.style.height = containerHeight.toString() + "px"
+ video.style.width = "100%"
+ }
+ // _refreshWindowMath relies on the video having the final dimensions.
+ // We MUST do it after the video dimensions have been calculated.
+ this._refreshWindowMath()
+ }
+
+ _clientToServerX(clientX, d) {
+ let serverX = Math.round((clientX - d.containerOffsetX) * d.scalingFactorX);
+ if (serverX === d.frameW - 1) serverX = d.frameW;
+ if (serverX > d.frameW) serverX = d.frameW;
+ // FIXME: instead of locking the touch here, we should trigger a touchEnd
+ if (serverX < 0) serverX = 0;
+ return serverX;
+ };
+
+ _clientToServerY(clientY, m) {
+ let serverY = Math.round((clientY - m.containerOffsetY) * m.scalingFactorY);
+ if (serverY === m.frameH - 1) serverY = m.frameH;
+ if (serverY > m.frameH) serverY = m.frameH;
+ // FIXME: instead of locking the touch here, we should trigger a touchEnd
+ if (serverY < 0) serverY = 0;
+ return serverY;
+ };
+
+ _triggerModifierEvent(event, key) {
+ if (event.getModifierState(key)) {
+ if (!(this._modifierState & _modifierEnum[key])) {
+ this._modifierState = this._modifierState | _modifierEnum[key];
+ this._sendInputEvent('key', {code: _keyScancodes[key], pressed: true});
+ }
+ } else {
+ if ((this._modifierState & _modifierEnum[key])) {
+ this._modifierState = this._modifierState & ~_modifierEnum[key];
+ this._sendInputEvent('key', {code: _keyScancodes[key], pressed: false});
+ }
+ }
+ };
+
+ _sendInputEvent(type, data) {
+ this._sendControlMessage('input::' + type, data);
+ }
+
+ _sendControlMessage(type, data) {
+ if (this._pc === null || this._controlChan.readyState !== 'open')
+ return;
+ this._controlChan.send(JSON.stringify({type: type, data: data}));
+ };
+
+ _refreshWindowMath() {
+ let video = document.getElementById(this._videoID);
+
+ // timing issues can occur when removing the component
+ if (!video) {
+ return
+ }
+
+ const windowW = video.offsetWidth;
+ const windowH = video.offsetHeight;
+ const frameW = video.videoWidth;
+ const frameH = video.videoHeight;
+
+ const multi = Math.min(windowW / frameW, windowH / frameH);
+ const vpWidth = frameW * multi;
+ const vpHeight = frameH * multi;
+
+ this._dimensions = {
+ scalingFactorX: frameW / vpWidth,
+ scalingFactorY: frameH / vpHeight,
+ containerOffsetX: Math.max((windowW - vpWidth) / 2.0, 0),
+ containerOffsetY: Math.max((windowH - vpHeight) / 2.0, 0),
+ frameW,
+ frameH,
+ };
+ };
+
+ _onMouseMove(event) {
+ const x = this._clientToServerX(event.offsetX, this._dimensions);
+ const y = this._clientToServerY(event.offsetY, this._dimensions);
+ this._sendInputEvent('mouse-move', {x: x, y: y, rx: event.movementX, ry: event.movementY});
+ };
+
+ _onMouseButton(event) {
+ const down = event.type === 'mousedown';
+ let button;
+
+ if (down && event.button === 0 && event.ctrlKey && event.shiftKey)
+ return;
+
+ switch (event.button) {
+ case 0: button = 1; break;
+ case 1: button = 2; break;
+ case 2: button = 3; break;
+ case 3: button = 4; break;
+ case 4: button = 5; break;
+ default: break;
+ }
+
+ this._sendInputEvent('mouse-button', {button: button, pressed: down});
+ };
+
+ _onMouseWheel(event) {
+ let move_step = (delta) => {
+ if (delta === 0)
+ return 0
+ return delta > 0 ? -1 : 1
+ }
+ const movex = move_step(event.deltaX)
+ const movey = move_step(event.deltaY)
+ if (movex !== 0 || movey !== 0)
+ this._sendInputEvent('mouse-wheel', {x: movex, y: movey});
+ };
+
+ _onKey(event) {
+ // Disable any problematic browser shortcuts
+ if (event.code === 'F5' || // Reload
+ (event.code === 'KeyR' && event.ctrlKey) || // Reload
+ (event.code === 'F5' && event.ctrlKey) || // Hard reload
+ (event.code === 'KeyI' && event.ctrlKey && event.shiftKey) ||
+ (event.code === 'F11') || // Fullscreen
+ (event.code === 'F12') // Developer tools
+ ) return;
+
+ event.preventDefault();
+
+ const numpad_key_prefix = 'Numpad'
+ const code = _keyScancodes[event.code];
+ const pressed = (event.type === 'keydown');
+ if (code) {
+ // NOTE: no need to check the following modifier keys
+ // 'ScrollLock', 'NumLock', 'CapsLock'
+ // as they're mapped to event.code correctly
+ const modifierKeys = ['Control', 'Shift', 'Alt', 'Meta', 'AltGraph'];
+ for (let i = 0; i < modifierKeys.length; i++) {
+ this._triggerModifierEvent(event, modifierKeys[i]);
+ }
+
+ this._sendInputEvent('key', {code: code, pressed: pressed});
+ } else if (event.code.startsWith(numpad_key_prefix)) {
+ // 1. Use the event.key over event.code for the key code if a key event(digit only) triggered
+ // from NumPad when NumLock is detected off The reason here is that event.code always remains
+ // the same no matter NumLock is detected on or off. Also Anbox doesn't respect these keycodes
+ // since Anbox just propagates those keycodes from client to the container and there is no
+ // corresponding input event codes mapping all key codes comming from NumPad.
+ //
+ // See: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
+ //
+ // The event.key reflects the correct human readable key code in the above case.
+ //
+ // 2. For mathematics symbols(+, *), we have to convert them to corresponding linux input code
+ // with shift modifiers attached because of the same reason(no keycode mapping in kernel).
+ let is_digit_key = (code) => {
+ const last_char = code.charAt(code.length - 1);
+ return (last_char >= '0' && last_char <= '9')
+ }
+
+ let event_code = event.code.substr(numpad_key_prefix.length);
+ if (is_digit_key(event.code)) {
+ if (event.getModifierState("NumLock"))
+ event_code = "Digit" + event_code
+ else
+ event_code = event.key
+ this._sendInputEvent('key', {code: _keyScancodes[event_code], pressed: pressed});
+ } else {
+ let attach_shift = false
+ if (event_code in _numPadMapper) {
+ if (event_code === "Add" || event_code === "Multiply")
+ attach_shift = true
+ event_code = _numPadMapper[event_code]
+ }
+ if (attach_shift)
+ this._sendInputEvent('key', {code: _keyScancodes["Shift"], pressed: pressed});
+ this._sendInputEvent('key', {code: _keyScancodes[event_code], pressed: pressed});
+ }
+ }
+ };
+
+ _touchEvent(event, eventType) {
+ const v = document.getElementById(this._videoID)
+ event.preventDefault();
+ for (let n = 0; n < event.changedTouches.length; n++) {
+ let touch = event.changedTouches[n];
+ let id = touch.identifier;
+ const videoOffset = v.getBoundingClientRect()
+ let x = this._clientToServerX(touch.clientX - videoOffset.left, this._dimensions);
+ let y = this._clientToServerY(touch.clientY- videoOffset.top, this._dimensions);
+ let e = {id: id, x: x, y: y}
+ if (eventType === "touch-move") {
+ // We should not fire the duplicated touch-move event as this will have a bad impact
+ // on Android input dispatching, which could cause ANR if the touched window's input
+ // channel is full.
+ if (this._updateTouchMoveEvent(e))
+ this._sendInputEvent(eventType, e);
+ } else {
+ if (eventType === "touch-cancel" || eventType === "touch-end")
+ this._lastTouchMoves = []
+ this._sendInputEvent(eventType, e);
+ }
+ }
+ };
+
+ _updateTouchMoveEvent(event) {
+ for (let lastMove of this._lastTouchMoves) {
+ if (lastMove.id === event.id) {
+ if (lastMove.x === event.x && lastMove.y === event.y)
+ return false
+ lastMove.x = event.x
+ lastMove.y = event.y
+ return true
+ }
+ }
+
+ this._lastTouchMoves.push(event)
+ return true
+ }
+
+ _onTouchStart(event) {this._touchEvent(event, 'touch-start')};
+ _onTouchEnd(event) {this._touchEvent(event, 'touch-end')};
+ _onTouchCancel(event) {this._touchEvent(event, 'touch-cancel')};
+ _onTouchMove(event) {this._touchEvent(event, 'touch-move')};
+
+ _queryGamePadEvents() {
+ if (!this._options.controls.gamepad)
+ return;
+ let gamepads = navigator.getGamepads();
+ if (gamepads.length > 0) {
+ this._gamepadManager = new _gamepadEventManager(this._sendInputEvent.bind(this));
+ this._gamepadManager.startPolling();
+ }
+ };
+
+ _openMicrophone() {
+ // NOTE:
+ // 1. We must wait for the audio input stream being added
+ // to the peer connection before creating offer, otherwise
+ // the remote end won't receive the media track for audio capture.
+ // 2. If a user doesn't grant the permission to use microphone
+ // we still create offer anyway but capturing the audio data from
+ // microphone won't work.
+ navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: false
+ })
+ .then(this._onAudioInputStreamAvailable.bind(this))
+ .catch(e => {
+ this._stopStreamingOnError(`failed to open microphone: ${e.name}`);
+ })
+ }
+
+ _onAudioInputStreamAvailable(stream) {
+ this._audioInputStream = stream;
+ const audioTracks = this._audioInputStream.getAudioTracks();
+ if (audioTracks.length > 0) {
+ console.log(`using Audio device: ${audioTracks[0].label}`);
+ }
+ this._audioInputStream.getTracks().forEach(
+ track => this._pc.addTrack(track, this._audioInputStream));
+
+ this._createOffer();
+ }
+
+ _createOffer() {
+ this._pc.createOffer().then(this._onRtcOfferCreated.bind(this)).catch(function(err) {
+ this._stopStreamingOnError(`failed to create offer: ${err}`);
+ });
+ }
+
+ _nullOrUndef(obj) { return obj === null || obj === undefined };
+
+ _onWsOpen() {
+ const config = { iceServers: this._options.stunServers };
+ this._pc = new RTCPeerConnection(config);
+ this._pc.ontrack = this._onRtcTrack.bind(this);
+ this._pc.oniceconnectionstatechange = this._onRtcIceConnectionStateChange.bind(this);
+ this._pc.onicecandidate = this._onRtcIceCandidate.bind(this);
+
+ let audio_direction = 'inactive'
+ if (this._options.devices.speaker) {
+ if (this._options.devices.microphone)
+ audio_direction = 'sendrecv'
+ else
+ audio_direction = 'recvonly'
+ }
+ this._pc.addTransceiver('audio', {direction: audio_direction})
+ this._pc.addTransceiver('video', {direction: 'recvonly'})
+ this._controlChan = this._pc.createDataChannel('control');
+
+ if (this._options.devices.microphone)
+ this._openMicrophone();
+ else
+ this._createOffer();
+ };
+
+ _onWsClose() {
+ if (!this._ready) {
+ // When the connection was closed from the gateway side we have to
+ // stop the timer here to avoid it triggering when we already
+ // terminated our connection
+ if (this._timer > 0)
+ window.clearTimeout(this._timer);
+
+ this._stopStreamingOnError('Connection was interrupted while connecting');
+ }
+ };
+
+ _onWsError(event) {
+ this._stopStreamingOnError('failed to communicate with backend service');
+ };
+
+ _onWsMessage(event) {
+ const msg = JSON.parse(event.data);
+ if (msg.type === 'answer') {
+ this._pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(msg.sdp)}));
+ } else if (msg.type === 'candidate') {
+ this._pc.addIceCandidate({'candidate': atob(msg.candidate), 'sdpMLineIndex': msg.sdpMLineIndex, 'sdpMid': msg.sdpMid})
+ } else {
+ console.log('Unknown message type ' + msg.type);
+ }
+ };
+
+ _stopStreamingOnError(errorMsg) {
+ this._options.callbacks.error(new Error(errorMsg));
+ this._stopStreaming();
+ }
+}
+
+class _gamepadEventManager {
+ constructor(sendEvent) {
+ this._polling = false;
+ this._state = {};
+ this._dpad_remap_start_index = 6;
+ this._dpad_standard_start_index = 12;
+ this._sendInputEvent = sendEvent
+ }
+
+ startPolling() {
+ if (this._polling === true)
+ return;
+
+ // Since chrome only supports event polling and we don't want
+ // to send any gamepad events to Android isntance if the state
+ // of any button or axis of gamepad is not changed. Hence we
+ // cache all keys state whenever it gets connected and provide
+ // event-driven gamepad events mechanism for gamepad events processing.
+ let gamepads = navigator.getGamepads();
+ for (let i = 0; i < gamepads.length; i++) {
+ if (gamepads[i])
+ this.cacheState(gamepads[i]);
+ }
+
+ this._polling = true;
+ this.tick();
+ };
+
+ stopPolling() {
+ if (this._polling === true)
+ this._polling = false;
+ };
+
+ tick() {
+ this.queryEvents();
+ if (this._polling)
+ window.requestAnimationFrame(this.tick.bind(this));
+ };
+
+ queryEvents() {
+ let gamepads = navigator.getGamepads();
+ for (let i = 0; i < gamepads.length; i++) {
+ let gamepad = gamepads[i];
+ if (gamepad) {
+ // A new gamepad is added
+ if (!this._state[gamepad])
+ this.cacheState(gamepad);
+ else {
+ const buttons = gamepad.buttons;
+ const cacheButtons = this._state[gamepad].buttons;
+ for (let j = 0; j < buttons.length; j++) {
+ if (cacheButtons[j].pressed !== buttons[j].pressed) {
+ // Check the table at the following link that describes the buttons/axes
+ // index and their physical locations.
+ this._sendInputEvent('gamepad-button', {id: gamepad.index, index: j, pressed: buttons[j].pressed});
+ cacheButtons[j].pressed = buttons[j].pressed;
+ }
+ }
+
+ // NOTE: For some game controllers, E.g. PS3 or Xbox 360 controller, DPAD buttons
+ // were translated to axes via html5 gamepad APIs and located in gamepad.axes array
+ // indexed starting from 6 to 7.
+ // When a DPAD button is pressed/unpressed, the corresponding value as follows
+ //
+ // Button | Index | Pressed | Unpressed |
+ // DPAD_LEFT_BUTTON | 6 | -1 | 0 |
+ // DPAD_RIGHT_BUTTON | 6 | 1 | 0 |
+ // DPAD_UP_BUTTON | 7 | -1 | 0 |
+ // DPAD_DOWN_BUTTON | 7 | 1 | 0 |
+ //
+ // When the above button was pressed/unpressed, we will send the gamepad-button
+ // event instead.
+ const axes = gamepad.axes;
+ let dpad_button_index = 0;
+ const cacheAxes = this._state[gamepad].axes;
+ for (let k = 0; k < axes.length; k++) {
+ if (cacheAxes[k] !== axes[k]) {
+ switch (true) {
+ case k < this._dpad_remap_start_index: // Standard axes
+ this._sendInputEvent('gamepad-axes', {id: gamepad.index, index: k, value: axes[k]});
+ break;
+ case k === this._dpad_remap_start_index: // DPAD left and right buttons
+ if (axes[k] === 0) {}
+ else if (axes[k] === -1) {
+ dpad_button_index = this._dpad_standard_start_index + 2;
+ } else {
+ dpad_button_index = this._dpad_standard_start_index + 3;
+ }
+
+ this._sendInputEvent('gamepad-button', {
+ id: gamepad.index,
+ index: dpad_button_index,
+ pressed: axes[k] !== 0
+ });
+ break;
+ case k === this._dpad_remap_start_index + 1: // DPAD up and down buttons
+ if (axes[k] === 0) {}
+ else if (axes[k] === -1) {
+ dpad_button_index = this._dpad_standard_start_index;
+ } else {
+ dpad_button_index = this._dpad_standard_start_index + 1;
+ }
+
+ this._sendInputEvent('gamepad-button', {
+ id: gamepad.index,
+ index: dpad_button_index,
+ pressed: axes[k] !== 0
+ });
+ break;
+ default:
+ console.log("Unsupported axes index", k);
+ break;
+ }
+ cacheAxes[k] = axes[k];
+ }
+ }
+ }
+ }
+ }
+ };
+
+ cacheState(gamepad) {
+ if (!gamepad)
+ return;
+
+ const gamepadState = {};
+ const buttons = gamepad.buttons;
+ for (let index = 0; index < buttons.length; index++) {
+ let buttonState = {
+ pressed: buttons[index].pressed
+ };
+ if (gamepadState.buttons)
+ gamepadState.buttons.push(buttonState);
+ else
+ gamepadState.buttons = [buttonState];
+ }
+
+ const axes = gamepad.axes;
+ for (let index = 0; index < axes.length; index++) {
+ if (gamepadState.axes)
+ gamepadState.axes.push(axes[index]);
+ else
+ gamepadState.axes = [axes[index]];
+ }
+
+ this._state[gamepad] = gamepadState;
+ }
+}
+
+const _keyScancodes = {
+ KeyA: 4,
+ KeyB: 5,
+ KeyC: 6,
+ KeyD: 7,
+ KeyE: 8,
+ KeyF: 9,
+ KeyG: 10,
+ KeyH: 11,
+ KeyI: 12,
+ KeyJ: 13,
+ KeyK: 14,
+ KeyL: 15,
+ KeyM: 16,
+ KeyN: 17,
+ KeyO: 18,
+ KeyP: 19,
+ KeyQ: 20,
+ KeyR: 21,
+ KeyS: 22,
+ KeyT: 23,
+ KeyU: 24,
+ KeyV: 25,
+ KeyW: 26,
+ KeyX: 27,
+ KeyY: 28,
+ KeyZ: 29,
+ Digit1: 30,
+ Digit2: 31,
+ Digit3: 32,
+ Digit4: 33,
+ Digit5: 34,
+ Digit6: 35,
+ Digit7: 36,
+ Digit8: 37,
+ Digit9: 38,
+ Digit0: 39,
+ Enter: 40,
+ Escape: 41,
+ Backspace: 42,
+ Tab: 43,
+ Space: 44,
+ Minus: 45,
+ Equal: 46,
+ BracketLeft: 47,
+ BracketRight: 48,
+ Backslash: 49,
+ Semicolon: 51,
+ Comma: 54,
+ Period: 55,
+ Slash: 56,
+ CapsLock: 57,
+ F1: 58,
+ F2: 59,
+ F3: 60,
+ F4: 61,
+ F5: 62,
+ F6: 63,
+ F7: 64,
+ F8: 65,
+ F9: 66,
+ F10: 67,
+ F11: 68,
+ F12: 69,
+ PrintScreen: 70,
+ ScrollLock: 71,
+ Pause: 72,
+ Insert: 73,
+ Home: 74,
+ PageUp: 75,
+ Delete: 76,
+ End: 77,
+ PageDown: 78,
+ ArrowRight: 79,
+ ArrowLeft: 80,
+ ArrowDown: 81,
+ ArrowUp: 82,
+ Control: 83,
+ Shift: 84,
+ Alt: 85,
+ Meta: 86,
+ AltGraph: 87,
+ NumLock: 88,
+};
+
+const _modifierEnum = {
+ Control: 0x1,
+ Shift: 0x2,
+ Alt: 0x4,
+ Meta: 0x8,
+ AltGraph: 0x10,
+};
+
+const _numPadMapper = {
+ Divide: "Slash",
+ Decimal: "Period",
+ Subtract: "Minus",
+ Add: "Equal",
+ Multiply: "Digit8",
+}
+
+class AnboxStreamGatewayConnector {
+ _nullOrUndef(obj) { return obj === null || obj === undefined };
+
+ /**
+ * Connector for the Anbox Stream Gateway. If no connector is specified for
+ * the SDK, this connector will be used by default.
+ * @param options {object}
+ * @param options.url {string} URL to the Stream Gateway. Must use http or https scheme
+ * @param options.authToken {string} Authentication token for the Stream Gateway
+ * @param options.session {object} Details about the session to create
+ * @param [options.session.region=""] {string} Where the session will be created. If
+ * empty, the gateway will try to determine the best region based on user IP
+ * @param [options.session.id] {string} If specified, try to join the instance rather than
+ * creating a new one
+ * @param [options.session.app] {string} Application name to run. If a sessionID is specifed
+ * this field is ignored
+ * @param [options.session.app_version=-1] {number} Specific version of the application to run.
+ * If it's not specified, the latest published application version will be in use for a
+ * session creation.
+ * @param [options.session.joinable] {boolean} If set to true, the session is joinable after the
+ * current user disconnected. The session stays alive for 30 minutes afterwards if not
+ * joined again. If false, the session will be automatically terminated after the user
+ * disconnected.
+ * @param [options.session.idle_time_min] {number} Idle time of the container in
+ * minutes. If set to zero, the session will be kept active until terminated.
+ * @param options.screen {object} Display settings for the Android instance to create
+ * @param [options.screen.width=1280] {number} Screen width in pixel
+ * @param [options.screen.height=720] {number} Screen height in pixel
+ * @param [options.screen.fps=60] {number} Desired number of frames per second
+ * @param [options.screen.density=240] {number} Pixel density
+ * @param options.extraData {string} Json format extra data for a session creation. (optional)
+ */
+ constructor(options) {
+ if (this._nullOrUndef(options))
+ throw Error("missing options");
+
+ if (this._nullOrUndef(options.url))
+ throw new Error('missing url parameter');
+
+ if (!options.url.includes('https') && !options.url.includes('http'))
+ throw new Error('unsupported scheme');
+
+ if (this._nullOrUndef(options.authToken))
+ throw new Error('missing authToken parameter');
+
+ if (this._nullOrUndef(options.session))
+ options.session = {};
+
+ if (this._nullOrUndef(options.session.region))
+ options.session.region = "";
+
+ if (this._nullOrUndef(options.session.id) && this._nullOrUndef(options.session.app))
+ throw new Error("session.app or session.id required");
+
+ if (this._nullOrUndef(options.session.joinable))
+ options.session.joinable = false;
+
+ // Display settings
+ if (this._nullOrUndef(options.screen))
+ options.screen = {};
+
+ if (this._nullOrUndef(options.screen.width))
+ options.screen.width = 1280;
+
+ if (this._nullOrUndef(options.screen.height))
+ options.screen.height = 720;
+
+ if (this._nullOrUndef(options.screen.fps))
+ options.screen.fps = 60;
+
+ if (this._nullOrUndef(options.screen.density))
+ options.screen.density = 240;
+
+ if (this._nullOrUndef(options.extraData) || options.extraData.length === 0)
+ options.extraData = "null";
+
+ this._options = options
+ }
+
+ async connect() {
+ if (this._nullOrUndef(this._options.session.id)) {
+ return await this._createSession();
+ } else {
+ return await this._joinSession();
+ }
+ };
+
+
+ async _createSession() {
+ try {
+ var extra_data_obj = JSON.parse(this._options.extraData)
+ } catch (e) {
+ throw new Error(`invalid json format extra data was given: ${e.name}`);
+ }
+
+ const appInfo = {
+ app: this._options.session.app,
+ region: this._options.session.region,
+ joinable: this._options.session.joinable,
+ screen: {
+ width: this._options.screen.width,
+ height: this._options.screen.height,
+ fps: this._options.screen.fps,
+ density: this._options.screen.density,
+ },
+ extra_data: extra_data_obj
+ };
+
+ if (!this._nullOrUndef(this._options.session.idle_time_min))
+ appInfo['idle_time_min'] = this._options.session.idle_time_min;
+
+ if (!this._nullOrUndef(this._options.session.app_version)
+ && this._options.session.app_version.length !== 0)
+ appInfo['app_version'] = this._options.session.app_version
+
+ const rawResp = await fetch(this._options.url + '/1.0/sessions/', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(appInfo),
+ });
+ if (rawResp === undefined || rawResp.status !== 201)
+ throw new Error("Failed to create session");
+
+ const response = await rawResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ return {
+ id: response.metadata.id,
+ websocket: response.metadata.url,
+ stunServers: response.metadata.stun_servers
+ };
+ };
+
+
+ async _joinSession() {
+ // Fetch all necessary information about the session including its websocket
+ // URL with a fresh authentication token
+ const rawSessionResp = await fetch(
+ this._options.url + '/1.0/sessions/' + this._options.session.id + '/', {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ }
+ });
+ if (rawSessionResp === undefined || rawSessionResp.status !== 200)
+ throw new Error("Session does not exist anymore");
+
+ var response = await rawSessionResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ const rawJoinResp = await fetch(
+ this._options.url + '/1.0/sessions/' + this._options.session.id + '/join', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ }
+ })
+ if (rawJoinResp === undefined || rawJoinResp.status !== 200)
+ throw new Error("Session does not exist anymore");
+
+ response = await rawJoinResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ return {
+ id: this._options.session.id,
+ websocket: response.metadata.url,
+ stunServers: response.metadata.stun_servers
+ };
+ }
+
+ // no-op
+ disconnect() {}
+}
+
+export { AnboxStreamGatewayConnector, AnboxStream };
diff --git a/examples/android/webview_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/webview_example/MainActivity.java b/examples/android/webview_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/webview_example/MainActivity.java
new file mode 100644
index 0000000..b3326c2
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/java/com/canonical/anbox/streaming/sdk/webview_example/MainActivity.java
@@ -0,0 +1,98 @@
+// Anbox - The Android in a Box runtime environment
+// Copyright 2020 Canonical Ltd. All rights reserved.
+
+package com.canonical.anbox.streaming.sdk.webview_example;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.webkit.WebResourceError;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.webkit.WebViewAssetLoader;
+import androidx.webkit.WebViewAssetLoader.AssetsPathHandler;
+import androidx.webkit.internal.AssetHelper;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+public class MainActivity extends AppCompatActivity {
+ private final String TAG = "AnboxWebViewStreaming";
+ private WebView webview;
+
+ @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ webview = (WebView) findViewById(R.id.webview);
+ webview.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ view.loadUrl(url);
+ return true;
+ }
+ });
+
+ WebSettings webSettings = webview.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
+ webSettings.setLoadWithOverviewMode(true);
+
+ webSettings.setAllowUniversalAccessFromFileURLs(true);
+ webSettings.setAllowFileAccess(true);
+ webSettings.setAllowContentAccess(true);
+ webSettings.setAllowFileAccessFromFileURLs(true);
+ webSettings.setAllowUniversalAccessFromFileURLs(true);
+
+ webview.setWebContentsDebuggingEnabled(true);
+
+ final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
+ .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this))
+ .build();
+
+ webview.setWebViewClient(new WebViewClient() {
+ @Override
+ public WebResourceResponse shouldInterceptRequest(WebView view,
+ WebResourceRequest request) {
+ if (!request.isForMainFrame() && Objects.requireNonNull(request.getUrl().getPath()).endsWith(".js")) {
+ Log.d(TAG, " js file request need to set mime/type " + request.getUrl().getPath());
+ try {
+ return new WebResourceResponse("application/javascript", null,
+ new BufferedInputStream(view.getContext().getAssets().open(request.getUrl().getPath().replace("/assets/",""))));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return assetLoader.shouldInterceptRequest(request.getUrl());
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.M)
+ @Override
+ public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
+ super.onReceivedError(view, request, error);
+ Log.d(TAG, "error: " + request.getUrl());
+ }
+ });
+
+ webview.loadUrl("https://appassets.androidplatform.net/assets/index.html");
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (webview.canGoBack()) {
+ webview.goBack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/android/webview_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/res/drawable/ic_launcher_background.xml b/examples/android/webview_streaming/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/android/webview_streaming/app/src/main/res/layout/activity_main.xml b/examples/android/webview_streaming/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5012a8e
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/examples/android/webview_streaming/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/examples/android/webview_streaming/app/src/main/res/values/colors.xml b/examples/android/webview_streaming/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..030098f
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
diff --git a/examples/android/webview_streaming/app/src/main/res/values/strings.xml b/examples/android/webview_streaming/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..67bf10a
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ WebViewStreaming
+
diff --git a/examples/android/webview_streaming/app/src/main/res/values/styles.xml b/examples/android/webview_streaming/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..66829a6
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/main/res/values/styles.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/examples/android/webview_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleUnitTest.java b/examples/android/webview_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleUnitTest.java
new file mode 100644
index 0000000..bffcb2d
--- /dev/null
+++ b/examples/android/webview_streaming/app/src/test/java/com/canonical/anbox/streaming/sdk/webview_example/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.canonical.anbox.streaming.sdk.webview_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/examples/android/webview_streaming/build.gradle b/examples/android/webview_streaming/build.gradle
new file mode 100644
index 0000000..2daa5cd
--- /dev/null
+++ b/examples/android/webview_streaming/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.6.2'
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/examples/android/webview_streaming/gradle.properties b/examples/android/webview_streaming/gradle.properties
new file mode 100644
index 0000000..199d16e
--- /dev/null
+++ b/examples/android/webview_streaming/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.jar b/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.properties b/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..998d6fb
--- /dev/null
+++ b/examples/android/webview_streaming/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jun 22 18:40:28 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
diff --git a/examples/android/webview_streaming/gradlew b/examples/android/webview_streaming/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/examples/android/webview_streaming/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/examples/android/webview_streaming/gradlew.bat b/examples/android/webview_streaming/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/examples/android/webview_streaming/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/examples/android/webview_streaming/settings.gradle b/examples/android/webview_streaming/settings.gradle
new file mode 100644
index 0000000..6ee4389
--- /dev/null
+++ b/examples/android/webview_streaming/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name='WebViewStreaming'
+include ':app'
diff --git a/examples/js/native/README.md b/examples/js/native/README.md
new file mode 100644
index 0000000..aca56f5
--- /dev/null
+++ b/examples/js/native/README.md
@@ -0,0 +1,20 @@
+# Anbox Stream SDK Example
+
+This directory contains the bare minimum to start a stream on the Anbox Streaming Stack.
+
+## Prerequisites
+For this example you'll need the following:
+
+- An Anbox Streaming Stack deployment
+- A Stream Gateway API token
+- At least one registered application on AMS
+
+## Running the example
+You'll need a webserver serving the content for the example. A simple server can be created
+with the following command in the example directory:
+
+ python3 -m http.server 8080
+
+And open your web browser to `127.0.0.1:8080`.
+
+
diff --git a/examples/js/native/anbox-stream-sdk.js b/examples/js/native/anbox-stream-sdk.js
new file mode 100644
index 0000000..68f3e9e
--- /dev/null
+++ b/examples/js/native/anbox-stream-sdk.js
@@ -0,0 +1,1386 @@
+// Anbox Stream SDK
+// Copyright 2019 Canonical Ltd. All rights reserved.
+
+class AnboxStream {
+ /**
+ * AnboxStream creates a connection between your client and an Android instance and
+ * displays its video & audio feed in an HTML5 player
+ * @param options: {object}
+ * @param options.connector {object} WebRTC Stream connector.
+ * @param options.targetElement {string} ID of the DOM element to attach the video to.
+ * @param options.fullScreen {boolean} Stream video in full screen mode. (default: false)
+ * @param [options.stunServers] {object[]} List of additional STUN/TURN servers.
+ * @param [options.stunServers[].urls] {string[]} URLs the same STUN/TURN server can be reached on.
+ * @param [options.stunServers[].username] {string} Username used when authenticating with the STUN/TURN server.
+ * @param [options.stunServers[].password] {string} Password used when authenticating with the STUN/TURN server.
+ * @param [options.devices] {object} Configuration settings for the streaming client device.
+ * @param [options.devices.microphone=false] {boolean} Enable audio capture from microphone and send it to the remote peer.
+ * @param [options.devices.speaker=true] {boolean} Enable audio playout through the default audio playback device.
+ * @param [options.controls] {object} Configuration how the client can interact with the stream.
+ * @param [options.controls.keyboard=true] {boolean} Send key presses to the Android instance.
+ * @param [options.controls.mouse=true] {boolean} Send mouse events to the Android instance.
+ * @param [options.controls.gamepad=true] {boolean} Send gamepad events to the Android instance.
+ * @param [options.callbacks] {object} A list of callbacks to react on stream lifecycle events.
+ * @param [options.callbacks.ready=none] {function} Called when the video and audio stream are ready to be inserted in the DOM.
+ * @param [options.callbacks.error=none] {function} Called on stream error with the message as parameter.
+ * @param [options.callbacks.done=none] {function} Called when the stream is closed.
+ * @param [options.callbacks.statsUpdated=none] {function} Called when the overall webrtc peer connection statistics are updated.
+ * @param [options.experimental] {object} Experimental features. Not recommended on production.
+ * @param [options.experimental.disableBrowserBlock=false] {boolean} Don't throw an error if an unsupported browser is detected.
+ */
+ constructor(options) {
+ if (this._nullOrUndef(options))
+ throw new Error('invalid options');
+
+ this._fillDefaults(options);
+ this._validateOptions(options);
+ this._options = options;
+
+ if (!this._options.disableBrowserBlock)
+ this._detectUnsupportedBrowser();
+
+ this._id = Math.random().toString(36).substr(2, 9);
+ this._containerID = options.targetElement;
+ this._videoID = 'anbox-stream-video-' + this._id;
+ this._audioID = 'anbox-stream-audio-' + this._id;
+
+ // WebRTC
+ this._ws = null; // WebSocket
+ this._pc = null; // PeerConnection
+ this._controlChan = null; // Channel to send inputs
+ this._timedout = false;
+ this._timer = -1;
+ this._disconnectedTimer = -1;
+ this._ready = false;
+ this._session_id = "";
+
+ // Media streams
+ this._videoStream = null;
+ this._audioStream = null;
+ this._audioInputStream = null;
+
+ // Control options
+ this._modifierState = 0;
+ this._dimensions = null;
+ this._gamepadManager = null;
+ this._lastTouchMoves = [];
+
+ // Stats
+ this._statsTimerId = -1;
+ this._timeElapse = 0;
+ this._stats = {
+ video: {
+ bandwidthMbit: 0,
+ totalBytesReceived: 0,
+ fps: 0
+ },
+ network: {
+ currentRtt: 0
+ },
+ audioInput: {
+ bandwidthMbit: 0,
+ bytesSent: 0,
+ },
+ audioOutput: {
+ bandwidthMbit: 0,
+ bytesReceived: 0
+ },
+ }
+ };
+
+ _includeStunServers(stun_servers) {
+ for (var n = 0; n < stun_servers.length; n++) {
+ this._options.stunServers.push({
+ "urls": stun_servers[n].urls,
+ "username": stun_servers[n].username,
+ "credential": stun_servers[n].password
+ });
+ }
+ };
+
+ /**
+ * Connect a new instance for the configured application or attach to an existing one
+ */
+ async connect() {
+ if (this._options.fullScreen)
+ this._requestFullscreen()
+
+ let session = {};
+ try {
+ session = await this._options.connector.connect()
+ } catch (e) {
+ this._stopStreamingOnError(e.message);
+ return
+ }
+
+ this._session_id = session.id
+
+ if (session.websocket === undefined || session.websocket.length === 0) {
+ this._stopStreamingOnError('connector did not return signaling information');
+ return
+ }
+
+ // add additional stun servers if provided
+ if (session.stunServers.length > 0)
+ this._includeStunServers(session.stunServers);
+
+ this._connectSignaler(session.websocket)
+ };
+
+ /**
+ * Disconnect an existing stream and remove the video & audio elements.
+ *
+ * This will stop the underlying Android instance.
+ */
+ disconnect() {
+ this._stopStreaming();
+ this._options.connector.disconnect();
+ };
+
+ /**
+ * Toggle fullscreen for the streamed video.
+ *
+ * IMPORTANT: fullscreen can only be toggled following a user input.
+ * If you call this method when your page loads, it will not work.
+ */
+ _requestFullscreen() {
+ if (!document.fullscreenEnabled) {
+ console.error("fullscreen not supported");
+ return
+ }
+ const fullscreenExited = () => {
+ if (document.fullscreenElement === null) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ video.style.width = null;
+ video.style.height = null;
+ }
+ }
+ };
+ // Clean up previous event listeners
+ document.removeEventListener('fullscreenchange', fullscreenExited, false);
+ document.addEventListener('fullscreenchange', fullscreenExited, false);
+
+ // We don't put the video element itself in fullscreen because of
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=462164
+ // To work around it we put the outer container in fullscreen and scale the video
+ // to fit it. When exiting fullscreen we undo style changes done to the video element
+ const videoContainer = document.getElementById(this._containerID);
+ if (videoContainer.requestFullscreen) {
+ videoContainer.requestFullscreen().catch(err => {
+ console.log(`Failed to enter full-screen mode: ${err.message} (${err.name})`);
+ });
+ } else if (videoContainer.mozRequestFullScreen) { /* Firefox */
+ videoContainer.mozRequestFullScreen();
+ } else if (videoContainer.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
+ videoContainer.webkitRequestFullscreen();
+ } else if (videoContainer.msRequestFullscreen) { /* IE/Edge */
+ videoContainer.msRequestFullscreen();
+ }
+ };
+
+ /**
+ * Exit fullscreen mode.
+ */
+ exitFullscreen() {
+ document.exitFullscreen();
+ };
+
+ /**
+ * Return the stream ID you can use to access video and audio elements with getElementById
+ */
+ getId() {
+ return this._id;
+ }
+
+ /**
+ * Send a location update to the connected Android instance
+ *
+ * For WGS84 format gps data, where a numeric latitude or longitude is given, geographic coordinates are
+ * expressed as decimal fractions. With this system the geo coordinate of Berlin is: latitude 52.520008°, longitude 13.404954°.
+ *
+ * For NMEA format gps data, where a numeric latitude or longitude is given, the two digits
+ * immediately to the left of the decimal point are whole minutes, to the right are decimals of minutes,
+ * and the remaining digits to the left of the whole minutes are whole degrees.
+ *
+ * eg. 4533.35 is 45 degrees and 33.35 minutes. ".35" of a minute is exactly 21 seconds.
+ *
+ * @param update: {object}
+ * @param update.format {string} GPS data format ("nmea" or "wgs84" default: "wgs84")
+ * @param update.time {number} Time in milliseconds since the start of the epoch
+ * @param update.latitude {number} Latitude of the location (positive values mean northern hemisphere and negative values mean southern hemisphere)
+ * @param update.longitude {number} Longitude of the location (positive values mean northern hemisphere and negative values mean southern hemisphere)
+ * @param update.altitude {number} Altitude in meters
+ * @param update.speed {number} Current speed in meter per second
+ * @param update.bearing {number} Current bearing in degree
+ */
+ sendLocationUpdate(update) {
+ if (this._nullOrUndef(update.time) ||
+ this._nullOrUndef(update.latitude) ||
+ this._nullOrUndef(update.longitude) ||
+ this._nullOrUndef(update.altitude) ||
+ this._nullOrUndef(update.speed) ||
+ this._nullOrUndef(update.bearing)) {
+ throw new Error("incomplete location update")
+ }
+
+ if (!this._nullOrUndef(update.format) &&
+ update.format !== "nmea" &&
+ update.format !== "wgs84") {
+ throw new Error("invalid gps data format")
+ }
+
+ this._sendControlMessage("location::update-position", update);
+ }
+
+ _connectSignaler(url) {
+ let ws = new WebSocket(url);
+ ws.onopen = this._onWsOpen.bind(this);
+ ws.onclose = this._onWsClose.bind(this);
+ ws.onerror = this._onWsError.bind(this);
+ ws.onmessage = this._onWsMessage.bind(this);
+
+ this._ws = ws;
+ this._timer = window.setTimeout(this._onSignalerTimeout.bind(this), 5 * 60 * 1000);
+ }
+
+ _detectUnsupportedBrowser() {
+ if (navigator.userAgent.indexOf("Chrome") === -1 &&
+ navigator.userAgent.indexOf("Firefox") === -1 &&
+ navigator.userAgent.indexOf("Safari") === -1)
+ throw new Error("unsupported browser");
+ };
+
+ _fillDefaults(options) {
+ if (this._nullOrUndef(options.fullScreen))
+ options.fullScreen = false;
+
+ if (this._nullOrUndef(options.controls))
+ options.controls = {};
+
+ if (this._nullOrUndef(options.devices))
+ options.devices = {};
+
+ if (this._nullOrUndef(options.devices.microphone))
+ options.devices.microphone = false;
+
+ if (this._nullOrUndef(options.devices.speaker))
+ options.devices.speaker = true;
+
+ if (this._nullOrUndef(options.controls.keyboard))
+ options.controls.keyboard = true;
+
+ if (this._nullOrUndef(options.controls.mouse))
+ options.controls.mouse = true;
+
+ if (this._nullOrUndef(options.controls.gamepad))
+ options.controls.gamepad = true;
+
+ if (this._nullOrUndef(options.stunServers))
+ options.stunServers = [];
+
+ if (this._nullOrUndef(options.callbacks))
+ options.callbacks = {};
+
+ if (this._nullOrUndef(options.callbacks.ready))
+ options.callbacks.ready = () => {};
+
+ if (this._nullOrUndef(options.callbacks.error))
+ options.callbacks.error = () => {};
+
+ if (this._nullOrUndef(options.callbacks.done))
+ options.callbacks.done = () => {};
+
+ if (this._nullOrUndef(options.disableBrowserBlock))
+ options.disableBrowserBlock = false;
+ };
+
+ _validateOptions(options) {
+ if (this._nullOrUndef(options.targetElement))
+ throw new Error('missing targetElement parameter');
+ if (document.getElementById(options.targetElement) === null)
+ throw new Error(`target element "${options.targetElement}" does not exist`);
+
+ if (this._nullOrUndef(options.connector))
+ throw new Error('missing connector');
+
+ if (typeof(options.connector.connect) !== "function")
+ throw new Error('missing "connect" method on connector');
+
+ if (typeof(options.connector.disconnect) !== "function")
+ throw new Error('missing "disconnect" method on connector');
+ }
+
+ _insertMedia(videoSource, audioSource) {
+ this._ready = true;
+ let mediaContainer = document.getElementById(this._containerID);
+ mediaContainer.style.display = "flex"
+ mediaContainer.style.justifyContent = "center"
+ mediaContainer.style.alignItems = "center"
+
+ const video = document.createElement('video');
+ video.style.margin = "0";
+ video.style.height = "auto";
+ video.style.width = "auto";
+
+ video.srcObject = videoSource;
+ video.muted = true;
+ video.autoplay = true;
+ video.controls = false;
+ video.id = this._videoID;
+ video.playsInline = true;
+ video.onplay = () => {
+ this._onResize()
+ this._registerControls();
+ };
+ mediaContainer.appendChild(video);
+
+ if (this._options.devices.speaker) {
+ const audio = document.createElement('audio');
+ audio.id = this._audioID;
+ audio.srcObject = audioSource;
+ audio.autoplay = true;
+ audio.controls = false;
+ mediaContainer.appendChild(audio);
+ }
+
+ };
+
+ _removeMedia() {
+ const video = document.getElementById(this._videoID);
+ const audio = document.getElementById(this._audioID);
+
+ if (video)
+ video.remove();
+ if (audio)
+ audio.remove();
+ };
+
+ _stopStreaming() {
+ // Notify the other side that we're disconnecting to speed up potential reconnects
+ this._sendControlMessage("stream::disconnect", {});
+
+ if (this._disconnectedTimer > 0) {
+ window.clearTimeout(this._disconnectedTimer);
+ this._disconnectedTimer = -1;
+ }
+
+ if (this._audioInputStream)
+ this._audioInputStream.getTracks().forEach(track => track.stop());
+
+ if (this._pc !== null) {
+ this._pc.close();
+ this._pc = null;
+ }
+ if (this._ws !== null) {
+ this._ws.close();
+ this._ws = null;
+ }
+ this._unregisterControls();
+ this._removeMedia();
+
+ if (this._statsTimerId !== -1)
+ window.clearInterval(this._statsTimerId)
+
+ if (this._gamepadManager) {
+ this._gamepadManager.stopPolling()
+ }
+ this._options.callbacks.done()
+ };
+
+ _onSignalerTimeout() {
+ if (this._pc == null || this._pc.iceConnectionState === 'connected')
+ return;
+
+ this._timedout = true;
+ this._stopStreaming();
+ };
+
+ _onRtcOfferCreated(description) {
+ this._pc.setLocalDescription(description);
+ let msg = {type: 'offer', sdp: btoa(description.sdp)};
+ if (this._ws.readyState === 1)
+ this._ws.send(JSON.stringify(msg));
+ };
+
+ _onRtcTrack(event) {
+ const kind = event.track.kind;
+ if (kind === 'video') {
+ this._videoStream = event.streams[0];
+ this._videoStream.onremovetrack = this._stopStreaming;
+ } else if (kind === 'audio') {
+ this._audioStream = event.streams[0];
+ this._audioStream.onremovetrack = this._stopStreaming;
+ }
+
+ // Start streaming until audio and video tracks both are available
+ if (this._videoStream && (!this._options.devices.speaker || this._audioStream)) {
+ this._insertMedia(this._videoStream, this._audioStream);
+ this._startStatsUpdater();
+ this._options.callbacks.ready(this._session_id);
+ }
+ };
+
+ _startStatsUpdater() {
+ if (this._nullOrUndef(this._options.callbacks.statsUpdated))
+ return
+
+ this._statsTimerId = window.setInterval(() => {
+ if (this._nullOrUndef(this._pc))
+ return
+
+ this._timeElapse++
+ this._pc.getStats(null).then(stats => {
+ stats.forEach(report => {
+ // Instead of dumping all the statistics, we only provide
+ // limited sets of stats to the caller.
+ Object.keys(report).forEach(statName => {
+ if (statName === "ssrc") {
+ if ("mediaType" in report) {
+ let mediaType = report["mediaType"];
+ if (mediaType === "video") {
+ if ("bytesReceived" in report) {
+ let bytesReceived = report["bytesReceived"]
+ let diff = 0;
+ if (this._stats.video.totalBytesReceived > bytesReceived)
+ diff = bytesReceived;
+ else
+ diff = bytesReceived - this._stats.video.totalBytesReceived;
+
+ this._stats.video.bandwidthMbit = diff;
+ this._stats.video.totalBytesReceived = bytesReceived;
+ }
+ } else if (mediaType === "audio") {
+ if ("packetsSent" in report) {
+ if ("bytesSent" in report) {
+ let bytesSent = report["bytesSent"];
+ let diff = 0;
+ if (this._stats.audioInput.bytesSent > bytesSent)
+ diff = bytesSent;
+ else
+ diff = bytesSent - this._stats.audioInput.bytesSent;
+
+ this._stats.audioInput.bandwidthMbit = diff;
+ this._stats.audioInput.bytesSent = bytesSent;
+ }
+ } else {
+ if ("bytesReceived" in report) {
+ let bytesReceived = report["bytesReceived"];
+ let diff = 0;
+ if (this._stats.audioOutput.bytesReceived > bytesReceived)
+ diff = bytesReceived;
+ else
+ diff = bytesReceived - this._stats.audioOutput.bytesReceived;
+
+ this._stats.audioOutput.bandwidthMbit = diff;
+ this._stats.audioOutput.bytesReceived = bytesReceived;
+ }
+ }
+ }
+ }
+ } else if (statName === "type" && report["type"] === "candidate-pair") {
+ if ("nominated" in report && report["nominated"] &&
+ "state" in report && report["state"] === "succeeded" &&
+ "currentRoundTripTime" in report) {
+ this._stats.network.currentRtt = report["currentRoundTripTime"] * 1000;
+ }
+ } else if (statName === "type" && report["type"] === "inbound-rtp") {
+ if ("framesDecoded" in report)
+ this._stats.video.fps = Math.round(report["framesDecoded"] / this._timeElapse);
+ }
+ });
+ });
+ });
+
+ this._options.callbacks.statsUpdated(this._stats);
+ },
+ // TODO: enable stats update interval configurable
+ 1000);
+ }
+
+ _onConnectionTimeout() {
+ this._disconnectedTimer = -1;
+ this._stopStreamingOnError('Connection lost');
+ }
+
+ _onRtcIceConnectionStateChange() {
+ if (this._pc === null)
+ return;
+
+ if (this._pc.iceConnectionState === 'failed') {
+ this._stopStreamingOnError('Failed to establish a connection via ICE');
+ } else if (this._pc.iceConnectionState === 'disconnected') {
+ // When we end up here the connection may not have closed but we
+ // just have a temorary network problem. We wait for a moment and
+ // if the connection isn't restablished we stop streaming
+ this._disconnectedTimer = window.setTimeout(this._onConnectionTimeout.bind(this), 10 * 1000);
+ } else if (this._pc.iceConnectionState === 'closed') {
+ if (this._timedout) {
+ this._stopStreamingOnError('Connection timed out');
+ return;
+ }
+ this._stopStreaming();
+ } else if (this._pc.iceConnectionState === 'connected') {
+ if (this._disconnectedTimer > 0) {
+ window.clearTimeout(this._disconnectedTimer);
+ this._disconnectedTimer = -1;
+ }
+ window.clearTimeout(this._timer);
+ this._ws.close();
+ }
+ };
+
+ _onRtcIceCandidate(event) {
+ if (event.candidate !== null && event.candidate.candidate !== "") {
+ const msg = {
+ type: 'candidate',
+ candidate: btoa(event.candidate.candidate),
+ sdpMid: event.candidate.sdpMid,
+ sdpMLineIndex: event.candidate.sdpMLineIndex,
+ };
+ if (this._ws.readyState === 1)
+ this._ws.send(JSON.stringify(msg));
+ }
+ };
+
+ controls = {
+ touch: {
+ 'mousemove': this._onMouseMove.bind(this),
+ 'mousedown': this._onMouseButton.bind(this),
+ 'mouseup': this._onMouseButton.bind(this),
+ 'mousewheel': this._onMouseWheel.bind(this),
+ 'touchstart': this._onTouchStart.bind(this),
+ 'touchend': this._onTouchEnd.bind(this),
+ 'touchcancel': this._onTouchCancel.bind(this),
+ 'touchmove': this._onTouchMove.bind(this),
+ },
+ keyboard: {
+ 'keydown': this._onKey.bind(this),
+ 'keyup': this._onKey.bind(this),
+ 'gamepadconnected': this._queryGamePadEvents.bind(this)
+ }
+ }
+
+ _registerControls() {
+ window.addEventListener('resize', this._onResize)
+
+ if (this._options.controls.mouse) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ for (const controlName in this.controls.touch)
+ video.addEventListener(controlName, this.controls.touch[controlName]);
+ }
+ }
+
+ if (this._options.controls.keyboard) {
+ for (const controlName in this.controls.keyboard)
+ window.addEventListener(controlName, this.controls.keyboard[controlName]);
+ }
+ };
+
+ _unregisterControls() {
+ window.removeEventListener('resize', this._onResize)
+
+ // Removing the video container should automatically remove all event listeners
+ // but this is dependant on the garbage collector, so we manually do it if we can
+ if (this._options.controls.mouse) {
+ const video = document.getElementById(this._videoID);
+ if (video) {
+ for (const controlName in this.controls.touch)
+ video.removeEventListener(controlName, this.controls.touch[controlName])
+ }
+ }
+
+ if (this._options.controls.keyboard) {
+ for (const controlName in this.controls.keyboard)
+ window.removeEventListener(controlName, this.controls.keyboard[controlName]);
+ }
+ };
+
+ _onResize() {
+ const video = document.getElementById(this._videoID)
+ const container = document.getElementById(this._containerID)
+ if (video === null || container === null)
+ return;
+
+ // We calculate the distance to the closest window border while keeping aspect ratio intact.
+ const videoHeight = video.clientHeight
+ const videoWidth = video.clientWidth
+ const containerHeight = container.clientHeight
+ const containerWidth = container.clientWidth
+
+ // Depending on the aspect ratio, one size will grow faster than the other
+ const widthGrowthAcceleration = Math.max(video.videoWidth / video.videoHeight, 1)
+ const heightGrowthAcceleration = Math.max(video.videoHeight / video.videoWidth, 1)
+
+ // So we apply that acceleration to find the shortest stretch
+ const widthFinalStretch = Math.round((containerWidth - videoWidth) * heightGrowthAcceleration)
+ const heightFinalStretch = Math.round((containerHeight - videoHeight) * widthGrowthAcceleration)
+
+ if (widthFinalStretch < heightFinalStretch) {
+ video.style.width = containerWidth.toString() + "px"
+ video.style.height = "100%"
+ } else {
+ video.style.height = containerHeight.toString() + "px"
+ video.style.width = "100%"
+ }
+ // _refreshWindowMath relies on the video having the final dimensions.
+ // We MUST do it after the video dimensions have been calculated.
+ this._refreshWindowMath()
+ }
+
+ _clientToServerX(clientX, d) {
+ let serverX = Math.round((clientX - d.containerOffsetX) * d.scalingFactorX);
+ if (serverX === d.frameW - 1) serverX = d.frameW;
+ if (serverX > d.frameW) serverX = d.frameW;
+ // FIXME: instead of locking the touch here, we should trigger a touchEnd
+ if (serverX < 0) serverX = 0;
+ return serverX;
+ };
+
+ _clientToServerY(clientY, m) {
+ let serverY = Math.round((clientY - m.containerOffsetY) * m.scalingFactorY);
+ if (serverY === m.frameH - 1) serverY = m.frameH;
+ if (serverY > m.frameH) serverY = m.frameH;
+ // FIXME: instead of locking the touch here, we should trigger a touchEnd
+ if (serverY < 0) serverY = 0;
+ return serverY;
+ };
+
+ _triggerModifierEvent(event, key) {
+ if (event.getModifierState(key)) {
+ if (!(this._modifierState & _modifierEnum[key])) {
+ this._modifierState = this._modifierState | _modifierEnum[key];
+ this._sendInputEvent('key', {code: _keyScancodes[key], pressed: true});
+ }
+ } else {
+ if ((this._modifierState & _modifierEnum[key])) {
+ this._modifierState = this._modifierState & ~_modifierEnum[key];
+ this._sendInputEvent('key', {code: _keyScancodes[key], pressed: false});
+ }
+ }
+ };
+
+ _sendInputEvent(type, data) {
+ this._sendControlMessage('input::' + type, data);
+ }
+
+ _sendControlMessage(type, data) {
+ if (this._pc === null || this._controlChan.readyState !== 'open')
+ return;
+ this._controlChan.send(JSON.stringify({type: type, data: data}));
+ };
+
+ _refreshWindowMath() {
+ let video = document.getElementById(this._videoID);
+
+ // timing issues can occur when removing the component
+ if (!video) {
+ return
+ }
+
+ const windowW = video.offsetWidth;
+ const windowH = video.offsetHeight;
+ const frameW = video.videoWidth;
+ const frameH = video.videoHeight;
+
+ const multi = Math.min(windowW / frameW, windowH / frameH);
+ const vpWidth = frameW * multi;
+ const vpHeight = frameH * multi;
+
+ this._dimensions = {
+ scalingFactorX: frameW / vpWidth,
+ scalingFactorY: frameH / vpHeight,
+ containerOffsetX: Math.max((windowW - vpWidth) / 2.0, 0),
+ containerOffsetY: Math.max((windowH - vpHeight) / 2.0, 0),
+ frameW,
+ frameH,
+ };
+ };
+
+ _onMouseMove(event) {
+ const x = this._clientToServerX(event.offsetX, this._dimensions);
+ const y = this._clientToServerY(event.offsetY, this._dimensions);
+ this._sendInputEvent('mouse-move', {x: x, y: y, rx: event.movementX, ry: event.movementY});
+ };
+
+ _onMouseButton(event) {
+ const down = event.type === 'mousedown';
+ let button;
+
+ if (down && event.button === 0 && event.ctrlKey && event.shiftKey)
+ return;
+
+ switch (event.button) {
+ case 0: button = 1; break;
+ case 1: button = 2; break;
+ case 2: button = 3; break;
+ case 3: button = 4; break;
+ case 4: button = 5; break;
+ default: break;
+ }
+
+ this._sendInputEvent('mouse-button', {button: button, pressed: down});
+ };
+
+ _onMouseWheel(event) {
+ let move_step = (delta) => {
+ if (delta === 0)
+ return 0
+ return delta > 0 ? -1 : 1
+ }
+ const movex = move_step(event.deltaX)
+ const movey = move_step(event.deltaY)
+ if (movex !== 0 || movey !== 0)
+ this._sendInputEvent('mouse-wheel', {x: movex, y: movey});
+ };
+
+ _onKey(event) {
+ // Disable any problematic browser shortcuts
+ if (event.code === 'F5' || // Reload
+ (event.code === 'KeyR' && event.ctrlKey) || // Reload
+ (event.code === 'F5' && event.ctrlKey) || // Hard reload
+ (event.code === 'KeyI' && event.ctrlKey && event.shiftKey) ||
+ (event.code === 'F11') || // Fullscreen
+ (event.code === 'F12') // Developer tools
+ ) return;
+
+ event.preventDefault();
+
+ const numpad_key_prefix = 'Numpad'
+ const code = _keyScancodes[event.code];
+ const pressed = (event.type === 'keydown');
+ if (code) {
+ // NOTE: no need to check the following modifier keys
+ // 'ScrollLock', 'NumLock', 'CapsLock'
+ // as they're mapped to event.code correctly
+ const modifierKeys = ['Control', 'Shift', 'Alt', 'Meta', 'AltGraph'];
+ for (let i = 0; i < modifierKeys.length; i++) {
+ this._triggerModifierEvent(event, modifierKeys[i]);
+ }
+
+ this._sendInputEvent('key', {code: code, pressed: pressed});
+ } else if (event.code.startsWith(numpad_key_prefix)) {
+ // 1. Use the event.key over event.code for the key code if a key event(digit only) triggered
+ // from NumPad when NumLock is detected off The reason here is that event.code always remains
+ // the same no matter NumLock is detected on or off. Also Anbox doesn't respect these keycodes
+ // since Anbox just propagates those keycodes from client to the container and there is no
+ // corresponding input event codes mapping all key codes comming from NumPad.
+ //
+ // See: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
+ //
+ // The event.key reflects the correct human readable key code in the above case.
+ //
+ // 2. For mathematics symbols(+, *), we have to convert them to corresponding linux input code
+ // with shift modifiers attached because of the same reason(no keycode mapping in kernel).
+ let is_digit_key = (code) => {
+ const last_char = code.charAt(code.length - 1);
+ return (last_char >= '0' && last_char <= '9')
+ }
+
+ let event_code = event.code.substr(numpad_key_prefix.length);
+ if (is_digit_key(event.code)) {
+ if (event.getModifierState("NumLock"))
+ event_code = "Digit" + event_code
+ else
+ event_code = event.key
+ this._sendInputEvent('key', {code: _keyScancodes[event_code], pressed: pressed});
+ } else {
+ let attach_shift = false
+ if (event_code in _numPadMapper) {
+ if (event_code === "Add" || event_code === "Multiply")
+ attach_shift = true
+ event_code = _numPadMapper[event_code]
+ }
+ if (attach_shift)
+ this._sendInputEvent('key', {code: _keyScancodes["Shift"], pressed: pressed});
+ this._sendInputEvent('key', {code: _keyScancodes[event_code], pressed: pressed});
+ }
+ }
+ };
+
+ _touchEvent(event, eventType) {
+ const v = document.getElementById(this._videoID)
+ event.preventDefault();
+ for (let n = 0; n < event.changedTouches.length; n++) {
+ let touch = event.changedTouches[n];
+ let id = touch.identifier;
+ const videoOffset = v.getBoundingClientRect()
+ let x = this._clientToServerX(touch.clientX - videoOffset.left, this._dimensions);
+ let y = this._clientToServerY(touch.clientY- videoOffset.top, this._dimensions);
+ let e = {id: id, x: x, y: y}
+ if (eventType === "touch-move") {
+ // We should not fire the duplicated touch-move event as this will have a bad impact
+ // on Android input dispatching, which could cause ANR if the touched window's input
+ // channel is full.
+ if (this._updateTouchMoveEvent(e))
+ this._sendInputEvent(eventType, e);
+ } else {
+ if (eventType === "touch-cancel" || eventType === "touch-end")
+ this._lastTouchMoves = []
+ this._sendInputEvent(eventType, e);
+ }
+ }
+ };
+
+ _updateTouchMoveEvent(event) {
+ for (let lastMove of this._lastTouchMoves) {
+ if (lastMove.id === event.id) {
+ if (lastMove.x === event.x && lastMove.y === event.y)
+ return false
+ lastMove.x = event.x
+ lastMove.y = event.y
+ return true
+ }
+ }
+
+ this._lastTouchMoves.push(event)
+ return true
+ }
+
+ _onTouchStart(event) {this._touchEvent(event, 'touch-start')};
+ _onTouchEnd(event) {this._touchEvent(event, 'touch-end')};
+ _onTouchCancel(event) {this._touchEvent(event, 'touch-cancel')};
+ _onTouchMove(event) {this._touchEvent(event, 'touch-move')};
+
+ _queryGamePadEvents() {
+ if (!this._options.controls.gamepad)
+ return;
+ let gamepads = navigator.getGamepads();
+ if (gamepads.length > 0) {
+ this._gamepadManager = new _gamepadEventManager(this._sendInputEvent.bind(this));
+ this._gamepadManager.startPolling();
+ }
+ };
+
+ _openMicrophone() {
+ // NOTE:
+ // 1. We must wait for the audio input stream being added
+ // to the peer connection before creating offer, otherwise
+ // the remote end won't receive the media track for audio capture.
+ // 2. If a user doesn't grant the permission to use microphone
+ // we still create offer anyway but capturing the audio data from
+ // microphone won't work.
+ navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: false
+ })
+ .then(this._onAudioInputStreamAvailable.bind(this))
+ .catch(e => {
+ this._stopStreamingOnError(`failed to open microphone: ${e.name}`);
+ })
+ }
+
+ _onAudioInputStreamAvailable(stream) {
+ this._audioInputStream = stream;
+ const audioTracks = this._audioInputStream.getAudioTracks();
+ if (audioTracks.length > 0) {
+ console.log(`using Audio device: ${audioTracks[0].label}`);
+ }
+ this._audioInputStream.getTracks().forEach(
+ track => this._pc.addTrack(track, this._audioInputStream));
+
+ this._createOffer();
+ }
+
+ _createOffer() {
+ this._pc.createOffer().then(this._onRtcOfferCreated.bind(this)).catch(function(err) {
+ this._stopStreamingOnError(`failed to create offer: ${err}`);
+ });
+ }
+
+ _nullOrUndef(obj) { return obj === null || obj === undefined };
+
+ _onWsOpen() {
+ const config = { iceServers: this._options.stunServers };
+ this._pc = new RTCPeerConnection(config);
+ this._pc.ontrack = this._onRtcTrack.bind(this);
+ this._pc.oniceconnectionstatechange = this._onRtcIceConnectionStateChange.bind(this);
+ this._pc.onicecandidate = this._onRtcIceCandidate.bind(this);
+
+ let audio_direction = 'inactive'
+ if (this._options.devices.speaker) {
+ if (this._options.devices.microphone)
+ audio_direction = 'sendrecv'
+ else
+ audio_direction = 'recvonly'
+ }
+ this._pc.addTransceiver('audio', {direction: audio_direction})
+ this._pc.addTransceiver('video', {direction: 'recvonly'})
+ this._controlChan = this._pc.createDataChannel('control');
+
+ if (this._options.devices.microphone)
+ this._openMicrophone();
+ else
+ this._createOffer();
+ };
+
+ _onWsClose() {
+ if (!this._ready) {
+ // When the connection was closed from the gateway side we have to
+ // stop the timer here to avoid it triggering when we already
+ // terminated our connection
+ if (this._timer > 0)
+ window.clearTimeout(this._timer);
+
+ this._stopStreamingOnError('Connection was interrupted while connecting');
+ }
+ };
+
+ _onWsError(event) {
+ this._stopStreamingOnError('failed to communicate with backend service');
+ };
+
+ _onWsMessage(event) {
+ const msg = JSON.parse(event.data);
+ if (msg.type === 'answer') {
+ this._pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(msg.sdp)}));
+ } else if (msg.type === 'candidate') {
+ this._pc.addIceCandidate({'candidate': atob(msg.candidate), 'sdpMLineIndex': msg.sdpMLineIndex, 'sdpMid': msg.sdpMid})
+ } else {
+ console.log('Unknown message type ' + msg.type);
+ }
+ };
+
+ _stopStreamingOnError(errorMsg) {
+ this._options.callbacks.error(new Error(errorMsg));
+ this._stopStreaming();
+ }
+}
+
+class _gamepadEventManager {
+ constructor(sendEvent) {
+ this._polling = false;
+ this._state = {};
+ this._dpad_remap_start_index = 6;
+ this._dpad_standard_start_index = 12;
+ this._sendInputEvent = sendEvent
+ }
+
+ startPolling() {
+ if (this._polling === true)
+ return;
+
+ // Since chrome only supports event polling and we don't want
+ // to send any gamepad events to Android isntance if the state
+ // of any button or axis of gamepad is not changed. Hence we
+ // cache all keys state whenever it gets connected and provide
+ // event-driven gamepad events mechanism for gamepad events processing.
+ let gamepads = navigator.getGamepads();
+ for (let i = 0; i < gamepads.length; i++) {
+ if (gamepads[i])
+ this.cacheState(gamepads[i]);
+ }
+
+ this._polling = true;
+ this.tick();
+ };
+
+ stopPolling() {
+ if (this._polling === true)
+ this._polling = false;
+ };
+
+ tick() {
+ this.queryEvents();
+ if (this._polling)
+ window.requestAnimationFrame(this.tick.bind(this));
+ };
+
+ queryEvents() {
+ let gamepads = navigator.getGamepads();
+ for (let i = 0; i < gamepads.length; i++) {
+ let gamepad = gamepads[i];
+ if (gamepad) {
+ // A new gamepad is added
+ if (!this._state[gamepad])
+ this.cacheState(gamepad);
+ else {
+ const buttons = gamepad.buttons;
+ const cacheButtons = this._state[gamepad].buttons;
+ for (let j = 0; j < buttons.length; j++) {
+ if (cacheButtons[j].pressed !== buttons[j].pressed) {
+ // Check the table at the following link that describes the buttons/axes
+ // index and their physical locations.
+ this._sendInputEvent('gamepad-button', {id: gamepad.index, index: j, pressed: buttons[j].pressed});
+ cacheButtons[j].pressed = buttons[j].pressed;
+ }
+ }
+
+ // NOTE: For some game controllers, E.g. PS3 or Xbox 360 controller, DPAD buttons
+ // were translated to axes via html5 gamepad APIs and located in gamepad.axes array
+ // indexed starting from 6 to 7.
+ // When a DPAD button is pressed/unpressed, the corresponding value as follows
+ //
+ // Button | Index | Pressed | Unpressed |
+ // DPAD_LEFT_BUTTON | 6 | -1 | 0 |
+ // DPAD_RIGHT_BUTTON | 6 | 1 | 0 |
+ // DPAD_UP_BUTTON | 7 | -1 | 0 |
+ // DPAD_DOWN_BUTTON | 7 | 1 | 0 |
+ //
+ // When the above button was pressed/unpressed, we will send the gamepad-button
+ // event instead.
+ const axes = gamepad.axes;
+ let dpad_button_index = 0;
+ const cacheAxes = this._state[gamepad].axes;
+ for (let k = 0; k < axes.length; k++) {
+ if (cacheAxes[k] !== axes[k]) {
+ switch (true) {
+ case k < this._dpad_remap_start_index: // Standard axes
+ this._sendInputEvent('gamepad-axes', {id: gamepad.index, index: k, value: axes[k]});
+ break;
+ case k === this._dpad_remap_start_index: // DPAD left and right buttons
+ if (axes[k] === 0) {}
+ else if (axes[k] === -1) {
+ dpad_button_index = this._dpad_standard_start_index + 2;
+ } else {
+ dpad_button_index = this._dpad_standard_start_index + 3;
+ }
+
+ this._sendInputEvent('gamepad-button', {
+ id: gamepad.index,
+ index: dpad_button_index,
+ pressed: axes[k] !== 0
+ });
+ break;
+ case k === this._dpad_remap_start_index + 1: // DPAD up and down buttons
+ if (axes[k] === 0) {}
+ else if (axes[k] === -1) {
+ dpad_button_index = this._dpad_standard_start_index;
+ } else {
+ dpad_button_index = this._dpad_standard_start_index + 1;
+ }
+
+ this._sendInputEvent('gamepad-button', {
+ id: gamepad.index,
+ index: dpad_button_index,
+ pressed: axes[k] !== 0
+ });
+ break;
+ default:
+ console.log("Unsupported axes index", k);
+ break;
+ }
+ cacheAxes[k] = axes[k];
+ }
+ }
+ }
+ }
+ }
+ };
+
+ cacheState(gamepad) {
+ if (!gamepad)
+ return;
+
+ const gamepadState = {};
+ const buttons = gamepad.buttons;
+ for (let index = 0; index < buttons.length; index++) {
+ let buttonState = {
+ pressed: buttons[index].pressed
+ };
+ if (gamepadState.buttons)
+ gamepadState.buttons.push(buttonState);
+ else
+ gamepadState.buttons = [buttonState];
+ }
+
+ const axes = gamepad.axes;
+ for (let index = 0; index < axes.length; index++) {
+ if (gamepadState.axes)
+ gamepadState.axes.push(axes[index]);
+ else
+ gamepadState.axes = [axes[index]];
+ }
+
+ this._state[gamepad] = gamepadState;
+ }
+}
+
+const _keyScancodes = {
+ KeyA: 4,
+ KeyB: 5,
+ KeyC: 6,
+ KeyD: 7,
+ KeyE: 8,
+ KeyF: 9,
+ KeyG: 10,
+ KeyH: 11,
+ KeyI: 12,
+ KeyJ: 13,
+ KeyK: 14,
+ KeyL: 15,
+ KeyM: 16,
+ KeyN: 17,
+ KeyO: 18,
+ KeyP: 19,
+ KeyQ: 20,
+ KeyR: 21,
+ KeyS: 22,
+ KeyT: 23,
+ KeyU: 24,
+ KeyV: 25,
+ KeyW: 26,
+ KeyX: 27,
+ KeyY: 28,
+ KeyZ: 29,
+ Digit1: 30,
+ Digit2: 31,
+ Digit3: 32,
+ Digit4: 33,
+ Digit5: 34,
+ Digit6: 35,
+ Digit7: 36,
+ Digit8: 37,
+ Digit9: 38,
+ Digit0: 39,
+ Enter: 40,
+ Escape: 41,
+ Backspace: 42,
+ Tab: 43,
+ Space: 44,
+ Minus: 45,
+ Equal: 46,
+ BracketLeft: 47,
+ BracketRight: 48,
+ Backslash: 49,
+ Semicolon: 51,
+ Comma: 54,
+ Period: 55,
+ Slash: 56,
+ CapsLock: 57,
+ F1: 58,
+ F2: 59,
+ F3: 60,
+ F4: 61,
+ F5: 62,
+ F6: 63,
+ F7: 64,
+ F8: 65,
+ F9: 66,
+ F10: 67,
+ F11: 68,
+ F12: 69,
+ PrintScreen: 70,
+ ScrollLock: 71,
+ Pause: 72,
+ Insert: 73,
+ Home: 74,
+ PageUp: 75,
+ Delete: 76,
+ End: 77,
+ PageDown: 78,
+ ArrowRight: 79,
+ ArrowLeft: 80,
+ ArrowDown: 81,
+ ArrowUp: 82,
+ Control: 83,
+ Shift: 84,
+ Alt: 85,
+ Meta: 86,
+ AltGraph: 87,
+ NumLock: 88,
+};
+
+const _modifierEnum = {
+ Control: 0x1,
+ Shift: 0x2,
+ Alt: 0x4,
+ Meta: 0x8,
+ AltGraph: 0x10,
+};
+
+const _numPadMapper = {
+ Divide: "Slash",
+ Decimal: "Period",
+ Subtract: "Minus",
+ Add: "Equal",
+ Multiply: "Digit8",
+}
+
+class AnboxStreamGatewayConnector {
+ _nullOrUndef(obj) { return obj === null || obj === undefined };
+
+ /**
+ * Connector for the Anbox Stream Gateway. If no connector is specified for
+ * the SDK, this connector will be used by default.
+ * @param options {object}
+ * @param options.url {string} URL to the Stream Gateway. Must use http or https scheme
+ * @param options.authToken {string} Authentication token for the Stream Gateway
+ * @param options.session {object} Details about the session to create
+ * @param [options.session.region=""] {string} Where the session will be created. If
+ * empty, the gateway will try to determine the best region based on user IP
+ * @param [options.session.id] {string} If specified, try to join the instance rather than
+ * creating a new one
+ * @param [options.session.app] {string} Application name to run. If a sessionID is specifed
+ * this field is ignored
+ * @param [options.session.app_version=-1] {number} Specific version of the application to run.
+ * If it's not specified, the latest published application version will be in use for a
+ * session creation.
+ * @param [options.session.joinable] {boolean} If set to true, the session is joinable after the
+ * current user disconnected. The session stays alive for 30 minutes afterwards if not
+ * joined again. If false, the session will be automatically terminated after the user
+ * disconnected.
+ * @param [options.session.idle_time_min] {number} Idle time of the container in
+ * minutes. If set to zero, the session will be kept active until terminated.
+ * @param options.screen {object} Display settings for the Android instance to create
+ * @param [options.screen.width=1280] {number} Screen width in pixel
+ * @param [options.screen.height=720] {number} Screen height in pixel
+ * @param [options.screen.fps=60] {number} Desired number of frames per second
+ * @param [options.screen.density=240] {number} Pixel density
+ * @param options.extraData {string} Json format extra data for a session creation. (optional)
+ */
+ constructor(options) {
+ if (this._nullOrUndef(options))
+ throw Error("missing options");
+
+ if (this._nullOrUndef(options.url))
+ throw new Error('missing url parameter');
+
+ if (!options.url.includes('https') && !options.url.includes('http'))
+ throw new Error('unsupported scheme');
+
+ if (this._nullOrUndef(options.authToken))
+ throw new Error('missing authToken parameter');
+
+ if (this._nullOrUndef(options.session))
+ options.session = {};
+
+ if (this._nullOrUndef(options.session.region))
+ options.session.region = "";
+
+ if (this._nullOrUndef(options.session.id) && this._nullOrUndef(options.session.app))
+ throw new Error("session.app or session.id required");
+
+ if (this._nullOrUndef(options.session.joinable))
+ options.session.joinable = false;
+
+ // Display settings
+ if (this._nullOrUndef(options.screen))
+ options.screen = {};
+
+ if (this._nullOrUndef(options.screen.width))
+ options.screen.width = 1280;
+
+ if (this._nullOrUndef(options.screen.height))
+ options.screen.height = 720;
+
+ if (this._nullOrUndef(options.screen.fps))
+ options.screen.fps = 60;
+
+ if (this._nullOrUndef(options.screen.density))
+ options.screen.density = 240;
+
+ if (this._nullOrUndef(options.extraData) || options.extraData.length === 0)
+ options.extraData = "null";
+
+ this._options = options
+ }
+
+ async connect() {
+ if (this._nullOrUndef(this._options.session.id)) {
+ return await this._createSession();
+ } else {
+ return await this._joinSession();
+ }
+ };
+
+
+ async _createSession() {
+ try {
+ var extra_data_obj = JSON.parse(this._options.extraData)
+ } catch (e) {
+ throw new Error(`invalid json format extra data was given: ${e.name}`);
+ }
+
+ const appInfo = {
+ app: this._options.session.app,
+ region: this._options.session.region,
+ joinable: this._options.session.joinable,
+ screen: {
+ width: this._options.screen.width,
+ height: this._options.screen.height,
+ fps: this._options.screen.fps,
+ density: this._options.screen.density,
+ },
+ extra_data: extra_data_obj
+ };
+
+ if (!this._nullOrUndef(this._options.session.idle_time_min))
+ appInfo['idle_time_min'] = this._options.session.idle_time_min;
+
+ if (!this._nullOrUndef(this._options.session.app_version)
+ && this._options.session.app_version.length !== 0)
+ appInfo['app_version'] = this._options.session.app_version
+
+ const rawResp = await fetch(this._options.url + '/1.0/sessions/', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(appInfo),
+ });
+ if (rawResp === undefined || rawResp.status !== 201)
+ throw new Error("Failed to create session");
+
+ const response = await rawResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ return {
+ id: response.metadata.id,
+ websocket: response.metadata.url,
+ stunServers: response.metadata.stun_servers
+ };
+ };
+
+
+ async _joinSession() {
+ // Fetch all necessary information about the session including its websocket
+ // URL with a fresh authentication token
+ const rawSessionResp = await fetch(
+ this._options.url + '/1.0/sessions/' + this._options.session.id + '/', {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ }
+ });
+ if (rawSessionResp === undefined || rawSessionResp.status !== 200)
+ throw new Error("Session does not exist anymore");
+
+ var response = await rawSessionResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ const rawJoinResp = await fetch(
+ this._options.url + '/1.0/sessions/' + this._options.session.id + '/join', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json, text/plain, */*',
+ 'Authorization': 'Macaroon root=' + this._options.authToken,
+ 'Content-Type': 'application/json',
+ }
+ })
+ if (rawJoinResp === undefined || rawJoinResp.status !== 200)
+ throw new Error("Session does not exist anymore");
+
+ response = await rawJoinResp.json();
+ if (response === undefined || response.status !== "success")
+ throw new Error(response.error);
+
+ return {
+ id: this._options.session.id,
+ websocket: response.metadata.url,
+ stunServers: response.metadata.stun_servers
+ };
+ }
+
+ // no-op
+ disconnect() {}
+}
+
+export { AnboxStreamGatewayConnector, AnboxStream };
diff --git a/examples/js/native/example.html b/examples/js/native/example.html
new file mode 100644
index 0000000..13f6e0b
--- /dev/null
+++ b/examples/js/native/example.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ Anbox Streaming SDK Example
+
+
+
+
+ Anbox Streaming Stack example
+
+
+
+
\ No newline at end of file
diff --git a/examples/linux/streaming/CMakeLists.txt b/examples/linux/streaming/CMakeLists.txt
new file mode 100644
index 0000000..cad1bfd
--- /dev/null
+++ b/examples/linux/streaming/CMakeLists.txt
@@ -0,0 +1,25 @@
+# Anbox Stream SDK
+# Copyright 2020 Canonical Ltd. All rights reserved.
+
+project(anbox-stream-sdk-native-example)
+cmake_minimum_required(VERSION 3.1.3)
+
+set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+if (NOT CMAKE_BUILD_TYPE)
+ message(STATUS "No build type selected, default to release")
+ set(CMAKE_BUILD_TYPE "release")
+endif()
+
+find_package(SDL2 REQUIRED)
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GLESv2 REQUIRED glesv2)
+pkg_check_modules(LIBSOUP REQUIRED libsoup-2.4)
+
+set(ANBOX_STREAM_SDK_DIR CACHE STRING "Path to the Anbox Stream SDK")
+if (NOT ANBOX_STREAM_SDK_DIR)
+ message(FATAL_ERROR "Missing path to Anbox Stream SDK")
+endif()
+
+add_subdirectory(src)
\ No newline at end of file
diff --git a/examples/linux/streaming/COPYRIGHT b/examples/linux/streaming/COPYRIGHT
new file mode 100644
index 0000000..7b6a56e
--- /dev/null
+++ b/examples/linux/streaming/COPYRIGHT
@@ -0,0 +1,33 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: anbox-stream-sdk-native-example
+
+Files: *
+Copyright: 2020 Canonical Ltd. All rights reserved.
+License: Proprietary
+
+Files: external/json/*
+Copyright: 2013-2020 Niels Lohmann
+License: MIT
+
+Files: external/flags/*
+Copyright: 2012 The WebRTC project authors. All Rights Reserved.
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
+ this software and associated documentation files (the "Software"), to deal in
+ the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ of the Software, and to permit persons to whom the Software is furnished to do
+ so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE 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 COPYRIGHT HOLDERS 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/examples/linux/streaming/README.md b/examples/linux/streaming/README.md
new file mode 100644
index 0000000..a76c12d
--- /dev/null
+++ b/examples/linux/streaming/README.md
@@ -0,0 +1,46 @@
+# Anbox Stream SDK - Native Example (linux)
+
+This example demonstrates the implementation of a native Linux application
+using the Anbox Stream SDK with OpenGL ES and SDL2.
+
+## Install necessary dependencies
+
+The example requires a few dependencies to be installed:
+
+ $ sudo apt install -y \
+ build-essential \
+ cmake \
+ cmake-extras \
+ libsoup2.4-dev \
+ libsdl2-dev \
+ libgles2-mesa-dev \
+
+## Build the example
+
+In order to build the example we have to configure the build via cmake
+first. The example requires you to specfy the path to the unpacked Anbox
+Stream SDK in order to consume the header and library files:
+
+ $ mkdir build
+ $ (cd build ; cmake -DANBOX_STREAM_DIR=/path/to/anbox/stream/sdk ..)
+
+When the configuration succeeded you can build the example:
+
+ $ (cd build ; make)
+
+## Running the example
+
+After the example is build you can run it with the following command
+
+ # If your Anbox Stream Gateway does not use a self signed certificate,
+ # you can drop the --insecure-tls flag
+ $ build/src/sdl2_client \
+ --url=https://:4000 \
+ --api-token= \
+ --application= \
+ --insecure-tls
+
+Also see the help output of the `sdl2_client` executable for more available
+program options.
+
+ $ build/src/sdl2_client --help
diff --git a/examples/linux/streaming/external/flags/args.hpp b/examples/linux/streaming/external/flags/args.hpp
new file mode 100644
index 0000000..a1f420e
--- /dev/null
+++ b/examples/linux/streaming/external/flags/args.hpp
@@ -0,0 +1,4310 @@
+/* A simple header-only C++ argument parser library.
+ *
+ * https://github.com/Taywee/args
+ *
+ * Copyright (c) 2016-2020 Taylor C. Richberger and Pavel
+ * Belikov
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE 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 COPYRIGHT HOLDERS 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 SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/** \file args.hxx
+ * \brief this single-header lets you use all of the args functionality
+ *
+ * The important stuff is done inside the args namespace
+ */
+
+#ifndef ARGS_HXX
+#define ARGS_HXX
+
+#define ARGS_VERSION "6.2.3"
+#define ARGS_VERSION_MAJOR 6
+#define ARGS_VERSION_MINOR 2
+#define ARGS_VERSION_PATCH 3
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if defined(_MSC_VER) && _MSC_VER <= 1800
+#define noexcept
+#endif
+
+#ifdef ARGS_TESTNAMESPACE
+namespace argstest
+{
+#else
+
+/** \namespace args
+ * \brief contains all the functionality of the args library
+ */
+namespace args
+{
+#endif
+ /** Getter to grab the value from the argument type.
+ *
+ * If the Get() function of the type returns a reference, so does this, and
+ * the value will be modifiable.
+ */
+ template
+ auto get(Option &option_) -> decltype(option_.Get())
+ {
+ return option_.Get();
+ }
+
+ /** (INTERNAL) Count UTF-8 glyphs
+ *
+ * This is not reliable, and will fail for combinatory glyphs, but it's
+ * good enough here for now.
+ *
+ * \param string The string to count glyphs from
+ * \return The UTF-8 glyphs in the string
+ */
+ inline std::string::size_type Glyphs(const std::string &string_)
+ {
+ std::string::size_type length = 0;
+ for (const char c: string_)
+ {
+ if ((c & 0xc0) != 0x80)
+ {
+ ++length;
+ }
+ }
+ return length;
+ }
+
+ /** (INTERNAL) Wrap a vector of words into a vector of lines
+ *
+ * Empty words are skipped. Word "\n" forces wrapping.
+ *
+ * \param begin The begin iterator
+ * \param end The end iterator
+ * \param width The width of the body
+ * \param firstlinewidth the width of the first line, defaults to the width of the body
+ * \param firstlineindent the indent of the first line, defaults to 0
+ * \return the vector of lines
+ */
+ template
+ inline std::vector Wrap(It begin,
+ It end,
+ const std::string::size_type width,
+ std::string::size_type firstlinewidth = 0,
+ std::string::size_type firstlineindent = 0)
+ {
+ std::vector output;
+ std::string line(firstlineindent, ' ');
+ bool empty = true;
+
+ if (firstlinewidth == 0)
+ {
+ firstlinewidth = width;
+ }
+
+ auto currentwidth = firstlinewidth;
+
+ for (auto it = begin; it != end; ++it)
+ {
+ if (it->empty())
+ {
+ continue;
+ }
+
+ if (*it == "\n")
+ {
+ if (!empty)
+ {
+ output.push_back(line);
+ line.clear();
+ empty = true;
+ currentwidth = width;
+ }
+
+ continue;
+ }
+
+ auto itemsize = Glyphs(*it);
+ if ((line.length() + 1 + itemsize) > currentwidth)
+ {
+ if (!empty)
+ {
+ output.push_back(line);
+ line.clear();
+ empty = true;
+ currentwidth = width;
+ }
+ }
+
+ if (itemsize > 0)
+ {
+ if (!empty)
+ {
+ line += ' ';
+ }
+
+ line += *it;
+ empty = false;
+ }
+ }
+
+ if (!empty)
+ {
+ output.push_back(line);
+ }
+
+ return output;
+ }
+
+ namespace detail
+ {
+ template
+ std::string Join(const T& array, const std::string &delimiter)
+ {
+ std::string res;
+ for (auto &element : array)
+ {
+ if (!res.empty())
+ {
+ res += delimiter;
+ }
+
+ res += element;
+ }
+
+ return res;
+ }
+ }
+
+ /** (INTERNAL) Wrap a string into a vector of lines
+ *
+ * This is quick and hacky, but works well enough. You can specify a
+ * different width for the first line
+ *
+ * \param width The width of the body
+ * \param firstlinewid the width of the first line, defaults to the width of the body
+ * \return the vector of lines
+ */
+ inline std::vector Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0)
+ {
+ // Preserve existing line breaks
+ const auto newlineloc = in.find('\n');
+ if (newlineloc != in.npos)
+ {
+ auto first = Wrap(std::string(in, 0, newlineloc), width);
+ auto second = Wrap(std::string(in, newlineloc + 1), width);
+ first.insert(
+ std::end(first),
+ std::make_move_iterator(std::begin(second)),
+ std::make_move_iterator(std::end(second)));
+ return first;
+ }
+
+ std::istringstream stream(in);
+ std::string::size_type indent = 0;
+
+ for (char c : in)
+ {
+ if (!isspace(c))
+ {
+ break;
+ }
+ ++indent;
+ }
+
+ return Wrap(std::istream_iterator(stream), std::istream_iterator(),
+ width, firstlinewidth, indent);
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Error class, for when ARGS_NOEXCEPT is defined
+ enum class Error
+ {
+ None,
+ Usage,
+ Parse,
+ Validation,
+ Required,
+ Map,
+ Extra,
+ Help,
+ Subparser,
+ Completion,
+ };
+#else
+ /** Base error class
+ */
+ class Error : public std::runtime_error
+ {
+ public:
+ Error(const std::string &problem) : std::runtime_error(problem) {}
+ virtual ~Error() {}
+ };
+
+ /** Errors that occur during usage
+ */
+ class UsageError : public Error
+ {
+ public:
+ UsageError(const std::string &problem) : Error(problem) {}
+ virtual ~UsageError() {}
+ };
+
+ /** Errors that occur during regular parsing
+ */
+ class ParseError : public Error
+ {
+ public:
+ ParseError(const std::string &problem) : Error(problem) {}
+ virtual ~ParseError() {}
+ };
+
+ /** Errors that are detected from group validation after parsing finishes
+ */
+ class ValidationError : public Error
+ {
+ public:
+ ValidationError(const std::string &problem) : Error(problem) {}
+ virtual ~ValidationError() {}
+ };
+
+ /** Errors that when a required flag is omitted
+ */
+ class RequiredError : public ValidationError
+ {
+ public:
+ RequiredError(const std::string &problem) : ValidationError(problem) {}
+ virtual ~RequiredError() {}
+ };
+
+ /** Errors in map lookups
+ */
+ class MapError : public ParseError
+ {
+ public:
+ MapError(const std::string &problem) : ParseError(problem) {}
+ virtual ~MapError() {}
+ };
+
+ /** Error that occurs when a singular flag is specified multiple times
+ */
+ class ExtraError : public ParseError
+ {
+ public:
+ ExtraError(const std::string &problem) : ParseError(problem) {}
+ virtual ~ExtraError() {}
+ };
+
+ /** An exception that indicates that the user has requested help
+ */
+ class Help : public Error
+ {
+ public:
+ Help(const std::string &flag) : Error(flag) {}
+ virtual ~Help() {}
+ };
+
+ /** (INTERNAL) An exception that emulates coroutine-like control flow for subparsers.
+ */
+ class SubparserError : public Error
+ {
+ public:
+ SubparserError() : Error("") {}
+ virtual ~SubparserError() {}
+ };
+
+ /** An exception that contains autocompletion reply
+ */
+ class Completion : public Error
+ {
+ public:
+ Completion(const std::string &flag) : Error(flag) {}
+ virtual ~Completion() {}
+ };
+#endif
+
+ /** A simple unified option type for unified initializer lists for the Matcher class.
+ */
+ struct EitherFlag
+ {
+ const bool isShort;
+ const char shortFlag;
+ const std::string longFlag;
+ EitherFlag(const std::string &flag) : isShort(false), shortFlag(), longFlag(flag) {}
+ EitherFlag(const char *flag) : isShort(false), shortFlag(), longFlag(flag) {}
+ EitherFlag(const char flag) : isShort(true), shortFlag(flag), longFlag() {}
+
+ /** Get just the long flags from an initializer list of EitherFlags
+ */
+ static std::unordered_set GetLong(std::initializer_list flags)
+ {
+ std::unordered_set longFlags;
+ for (const EitherFlag &flag: flags)
+ {
+ if (!flag.isShort)
+ {
+ longFlags.insert(flag.longFlag);
+ }
+ }
+ return longFlags;
+ }
+
+ /** Get just the short flags from an initializer list of EitherFlags
+ */
+ static std::unordered_set GetShort(std::initializer_list flags)
+ {
+ std::unordered_set shortFlags;
+ for (const EitherFlag &flag: flags)
+ {
+ if (flag.isShort)
+ {
+ shortFlags.insert(flag.shortFlag);
+ }
+ }
+ return shortFlags;
+ }
+
+ std::string str() const
+ {
+ return isShort ? std::string(1, shortFlag) : longFlag;
+ }
+
+ std::string str(const std::string &shortPrefix, const std::string &longPrefix) const
+ {
+ return isShort ? shortPrefix + std::string(1, shortFlag) : longPrefix + longFlag;
+ }
+ };
+
+
+
+ /** A class of "matchers", specifying short and flags that can possibly be
+ * matched.
+ *
+ * This is supposed to be constructed and then passed in, not used directly
+ * from user code.
+ */
+ class Matcher
+ {
+ private:
+ const std::unordered_set shortFlags;
+ const std::unordered_set longFlags;
+
+ public:
+ /** Specify short and long flags separately as iterators
+ *
+ * ex: `args::Matcher(shortFlags.begin(), shortFlags.end(), longFlags.begin(), longFlags.end())`
+ */
+ template
+ Matcher(ShortIt shortFlagsStart, ShortIt shortFlagsEnd, LongIt longFlagsStart, LongIt longFlagsEnd) :
+ shortFlags(shortFlagsStart, shortFlagsEnd),
+ longFlags(longFlagsStart, longFlagsEnd)
+ {
+ if (shortFlags.empty() && longFlags.empty())
+ {
+#ifndef ARGS_NOEXCEPT
+ throw UsageError("empty Matcher");
+#endif
+ }
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ Error GetError() const noexcept
+ {
+ return shortFlags.empty() && longFlags.empty() ? Error::Usage : Error::None;
+ }
+#endif
+
+ /** Specify short and long flags separately as iterables
+ *
+ * ex: `args::Matcher(shortFlags, longFlags)`
+ */
+ template
+ Matcher(Short &&shortIn, Long &&longIn) :
+ Matcher(std::begin(shortIn), std::end(shortIn), std::begin(longIn), std::end(longIn))
+ {}
+
+ /** Specify a mixed single initializer-list of both short and long flags
+ *
+ * This is the fancy one. It takes a single initializer list of
+ * any number of any mixed kinds of flags. Chars are
+ * automatically interpreted as short flags, and strings are
+ * automatically interpreted as long flags:
+ *
+ * args::Matcher{'a'}
+ * args::Matcher{"foo"}
+ * args::Matcher{'h', "help"}
+ * args::Matcher{"foo", 'f', 'F', "FoO"}
+ */
+ Matcher(std::initializer_list in) :
+ Matcher(EitherFlag::GetShort(in), EitherFlag::GetLong(in)) {}
+
+ Matcher(Matcher &&other) : shortFlags(std::move(other.shortFlags)), longFlags(std::move(other.longFlags))
+ {}
+
+ ~Matcher() {}
+
+ /** (INTERNAL) Check if there is a match of a short flag
+ */
+ bool Match(const char flag) const
+ {
+ return shortFlags.find(flag) != shortFlags.end();
+ }
+
+ /** (INTERNAL) Check if there is a match of a long flag
+ */
+ bool Match(const std::string &flag) const
+ {
+ return longFlags.find(flag) != longFlags.end();
+ }
+
+ /** (INTERNAL) Check if there is a match of a flag
+ */
+ bool Match(const EitherFlag &flag) const
+ {
+ return flag.isShort ? Match(flag.shortFlag) : Match(flag.longFlag);
+ }
+
+ /** (INTERNAL) Get all flag strings as a vector, with the prefixes embedded
+ */
+ std::vector GetFlagStrings() const
+ {
+ std::vector flagStrings;
+ flagStrings.reserve(shortFlags.size() + longFlags.size());
+ for (const char flag: shortFlags)
+ {
+ flagStrings.emplace_back(flag);
+ }
+ for (const std::string &flag: longFlags)
+ {
+ flagStrings.emplace_back(flag);
+ }
+ return flagStrings;
+ }
+
+ /** (INTERNAL) Get long flag if it exists or any short flag
+ */
+ EitherFlag GetLongOrAny() const
+ {
+ if (!longFlags.empty())
+ {
+ return *longFlags.begin();
+ }
+
+ if (!shortFlags.empty())
+ {
+ return *shortFlags.begin();
+ }
+
+ // should be unreachable
+ return ' ';
+ }
+
+ /** (INTERNAL) Get short flag if it exists or any long flag
+ */
+ EitherFlag GetShortOrAny() const
+ {
+ if (!shortFlags.empty())
+ {
+ return *shortFlags.begin();
+ }
+
+ if (!longFlags.empty())
+ {
+ return *longFlags.begin();
+ }
+
+ // should be unreachable
+ return ' ';
+ }
+ };
+
+ /** Attributes for flags.
+ */
+ enum class Options
+ {
+ /** Default options.
+ */
+ None = 0x0,
+
+ /** Flag can't be passed multiple times.
+ */
+ Single = 0x01,
+
+ /** Flag can't be omitted.
+ */
+ Required = 0x02,
+
+ /** Flag is excluded from usage line.
+ */
+ HiddenFromUsage = 0x04,
+
+ /** Flag is excluded from options help.
+ */
+ HiddenFromDescription = 0x08,
+
+ /** Flag is global and can be used in any subcommand.
+ */
+ Global = 0x10,
+
+ /** Flag stops a parser.
+ */
+ KickOut = 0x20,
+
+ /** Flag is excluded from auto completion.
+ */
+ HiddenFromCompletion = 0x40,
+
+ /** Flag is excluded from options help and usage line
+ */
+ Hidden = HiddenFromUsage | HiddenFromDescription | HiddenFromCompletion,
+ };
+
+ inline Options operator | (Options lhs, Options rhs)
+ {
+ return static_cast(static_cast(lhs) | static_cast(rhs));
+ }
+
+ inline Options operator & (Options lhs, Options rhs)
+ {
+ return static_cast(static_cast(lhs) & static_cast(rhs));
+ }
+
+ class FlagBase;
+ class PositionalBase;
+ class Command;
+ class ArgumentParser;
+
+ /** A simple structure of parameters for easy user-modifyable help menus
+ */
+ struct HelpParams
+ {
+ /** The width of the help menu
+ */
+ unsigned int width = 80;
+ /** The indent of the program line
+ */
+ unsigned int progindent = 2;
+ /** The indent of the program trailing lines for long parameters
+ */
+ unsigned int progtailindent = 4;
+ /** The indent of the description and epilogs
+ */
+ unsigned int descriptionindent = 4;
+ /** The indent of the flags
+ */
+ unsigned int flagindent = 6;
+ /** The indent of the flag descriptions
+ */
+ unsigned int helpindent = 40;
+ /** The additional indent each group adds
+ */
+ unsigned int eachgroupindent = 2;
+
+ /** The minimum gutter between each flag and its help
+ */
+ unsigned int gutter = 1;
+
+ /** Show the terminator when both options and positional parameters are present
+ */
+ bool showTerminator = true;
+
+ /** Show the {OPTIONS} on the prog line when this is true
+ */
+ bool showProglineOptions = true;
+
+ /** Show the positionals on the prog line when this is true
+ */
+ bool showProglinePositionals = true;
+
+ /** The prefix for short flags
+ */
+ std::string shortPrefix;
+
+ /** The prefix for long flags
+ */
+ std::string longPrefix;
+
+ /** The separator for short flags
+ */
+ std::string shortSeparator;
+
+ /** The separator for long flags
+ */
+ std::string longSeparator;
+
+ /** The program name for help generation
+ */
+ std::string programName;
+
+ /** Show command's flags
+ */
+ bool showCommandChildren = false;
+
+ /** Show command's descriptions and epilog
+ */
+ bool showCommandFullHelp = false;
+
+ /** The postfix for progline when showProglineOptions is true and command has any flags
+ */
+ std::string proglineOptions = "{OPTIONS}";
+
+ /** The prefix for progline when command has any subcommands
+ */
+ std::string proglineCommand = "COMMAND";
+
+ /** The prefix for progline value
+ */
+ std::string proglineValueOpen = " <";
+
+ /** The postfix for progline value
+ */
+ std::string proglineValueClose = ">";
+
+ /** The prefix for progline required argument
+ */
+ std::string proglineRequiredOpen = "";
+
+ /** The postfix for progline required argument
+ */
+ std::string proglineRequiredClose = "";
+
+ /** The prefix for progline non-required argument
+ */
+ std::string proglineNonrequiredOpen = "[";
+
+ /** The postfix for progline non-required argument
+ */
+ std::string proglineNonrequiredClose = "]";
+
+ /** Show flags in program line
+ */
+ bool proglineShowFlags = false;
+
+ /** Use short flags in program lines when possible
+ */
+ bool proglinePreferShortFlags = false;
+
+ /** Program line prefix
+ */
+ std::string usageString;
+
+ /** String shown in help before flags descriptions
+ */
+ std::string optionsString = "OPTIONS:";
+
+ /** Display value name after all the long and short flags
+ */
+ bool useValueNameOnce = false;
+
+ /** Show value name
+ */
+ bool showValueName = true;
+
+ /** Add newline before flag description
+ */
+ bool addNewlineBeforeDescription = false;
+
+ /** The prefix for option value
+ */
+ std::string valueOpen = "[";
+
+ /** The postfix for option value
+ */
+ std::string valueClose = "]";
+
+ /** Add choices to argument description
+ */
+ bool addChoices = false;
+
+ /** The prefix for choices
+ */
+ std::string choiceString = "\nOne of: ";
+
+ /** Add default values to argument description
+ */
+ bool addDefault = false;
+
+ /** The prefix for default values
+ */
+ std::string defaultString = "\nDefault: ";
+ };
+
+ /** A number of arguments which can be consumed by an option.
+ *
+ * Represents a closed interval [min, max].
+ */
+ struct Nargs
+ {
+ const size_t min;
+ const size_t max;
+
+ Nargs(size_t min_, size_t max_) : min{min_}, max{max_}
+ {
+#ifndef ARGS_NOEXCEPT
+ if (max < min)
+ {
+ throw UsageError("Nargs: max > min");
+ }
+#endif
+ }
+
+ Nargs(size_t num_) : min{num_}, max{num_}
+ {
+ }
+
+ friend bool operator == (const Nargs &lhs, const Nargs &rhs)
+ {
+ return lhs.min == rhs.min && lhs.max == rhs.max;
+ }
+
+ friend bool operator != (const Nargs &lhs, const Nargs &rhs)
+ {
+ return !(lhs == rhs);
+ }
+ };
+
+ /** Base class for all match types
+ */
+ class Base
+ {
+ private:
+ Options options = {};
+
+ protected:
+ bool matched = false;
+ const std::string help;
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ mutable Error error = Error::None;
+ mutable std::string errorMsg;
+#endif
+
+ public:
+ Base(const std::string &help_, Options options_ = {}) : options(options_), help(help_) {}
+ virtual ~Base() {}
+
+ Options GetOptions() const noexcept
+ {
+ return options;
+ }
+
+ bool IsRequired() const noexcept
+ {
+ return (GetOptions() & Options::Required) != Options::None;
+ }
+
+ virtual bool Matched() const noexcept
+ {
+ return matched;
+ }
+
+ virtual void Validate(const std::string &, const std::string &) const
+ {
+ }
+
+ operator bool() const noexcept
+ {
+ return Matched();
+ }
+
+ virtual std::vector> GetDescription(const HelpParams &, const unsigned indentLevel) const
+ {
+ std::tuple description;
+ std::get<1>(description) = help;
+ std::get<2>(description) = indentLevel;
+ return { std::move(description) };
+ }
+
+ virtual std::vector GetCommands()
+ {
+ return {};
+ }
+
+ virtual bool IsGroup() const
+ {
+ return false;
+ }
+
+ virtual FlagBase *Match(const EitherFlag &)
+ {
+ return nullptr;
+ }
+
+ virtual PositionalBase *GetNextPositional()
+ {
+ return nullptr;
+ }
+
+ virtual std::vector GetAllFlags()
+ {
+ return {};
+ }
+
+ virtual bool HasFlag() const
+ {
+ return false;
+ }
+
+ virtual bool HasPositional() const
+ {
+ return false;
+ }
+
+ virtual bool HasCommand() const
+ {
+ return false;
+ }
+
+ virtual std::vector GetProgramLine(const HelpParams &) const
+ {
+ return {};
+ }
+
+ /// Sets a kick-out value for building subparsers
+ void KickOut(bool kickout_) noexcept
+ {
+ if (kickout_)
+ {
+ options = options | Options::KickOut;
+ }
+ else
+ {
+ options = static_cast(static_cast(options) & ~static_cast(Options::KickOut));
+ }
+ }
+
+ /// Gets the kick-out value for building subparsers
+ bool KickOut() const noexcept
+ {
+ return (options & Options::KickOut) != Options::None;
+ }
+
+ virtual void Reset() noexcept
+ {
+ matched = false;
+#ifdef ARGS_NOEXCEPT
+ error = Error::None;
+ errorMsg.clear();
+#endif
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ virtual Error GetError() const
+ {
+ return error;
+ }
+
+ /// Only for ARGS_NOEXCEPT
+ std::string GetErrorMsg() const
+ {
+ return errorMsg;
+ }
+#endif
+ };
+
+ /** Base class for all match types that have a name
+ */
+ class NamedBase : public Base
+ {
+ protected:
+ const std::string name;
+ bool kickout = false;
+ std::string defaultString;
+ bool defaultStringManual = false;
+ std::vector choicesStrings;
+ bool choicesStringManual = false;
+
+ virtual std::string GetDefaultString(const HelpParams&) const { return {}; }
+
+ virtual std::vector GetChoicesStrings(const HelpParams&) const { return {}; }
+
+ virtual std::string GetNameString(const HelpParams&) const { return Name(); }
+
+ void AddDescriptionPostfix(std::string &dest, const bool isManual, const std::string &manual, bool isGenerated, const std::string &generated, const std::string &str) const
+ {
+ if (isManual && !manual.empty())
+ {
+ dest += str;
+ dest += manual;
+ }
+ else if (!isManual && isGenerated && !generated.empty())
+ {
+ dest += str;
+ dest += generated;
+ }
+ }
+
+ public:
+ NamedBase(const std::string &name_, const std::string &help_, Options options_ = {}) : Base(help_, options_), name(name_) {}
+ virtual ~NamedBase() {}
+
+ /** Sets default value string that will be added to argument description.
+ * Use empty string to disable it for this argument.
+ */
+ void HelpDefault(const std::string &str)
+ {
+ defaultStringManual = true;
+ defaultString = str;
+ }
+
+ /** Gets default value string that will be added to argument description.
+ */
+ std::string HelpDefault(const HelpParams ¶ms) const
+ {
+ return defaultStringManual ? defaultString : GetDefaultString(params);
+ }
+
+ /** Sets choices strings that will be added to argument description.
+ * Use empty vector to disable it for this argument.
+ */
+ void HelpChoices(const std::vector &array)
+ {
+ choicesStringManual = true;
+ choicesStrings = array;
+ }
+
+ /** Gets choices strings that will be added to argument description.
+ */
+ std::vector HelpChoices(const HelpParams ¶ms) const
+ {
+ return choicesStringManual ? choicesStrings : GetChoicesStrings(params);
+ }
+
+ virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned indentLevel) const override
+ {
+ std::tuple description;
+ std::get<0>(description) = GetNameString(params);
+ std::get<1>(description) = help;
+ std::get<2>(description) = indentLevel;
+
+ AddDescriptionPostfix(std::get<1>(description), choicesStringManual, detail::Join(choicesStrings, ", "), params.addChoices, detail::Join(GetChoicesStrings(params), ", "), params.choiceString);
+ AddDescriptionPostfix(std::get<1>(description), defaultStringManual, defaultString, params.addDefault, GetDefaultString(params), params.defaultString);
+
+ return { std::move(description) };
+ }
+
+ virtual std::string Name() const
+ {
+ return name;
+ }
+ };
+
+ namespace detail
+ {
+ template
+ using vector = std::vector>;
+
+ template
+ using unordered_map = std::unordered_map,
+ std::equal_to, std::allocator > >;
+
+ template
+ class is_streamable
+ {
+ template
+ static auto test(int)
+ -> decltype( std::declval() << std::declval(), std::true_type() );
+
+ template
+ static auto test(...) -> std::false_type;
+
+ public:
+ using type = decltype(test(0));
+ };
+
+ template
+ using IsConvertableToString = typename is_streamable::type;
+
+ template
+ typename std::enable_if::value, std::string>::type
+ ToString(const T &value)
+ {
+ std::ostringstream s;
+ s << value;
+ return s.str();
+ }
+
+ template
+ typename std::enable_if::value, std::string>::type
+ ToString(const T &)
+ {
+ return {};
+ }
+
+ template
+ std::vector MapKeysToStrings(const T &map)
+ {
+ std::vector res;
+ using K = typename std::decayfirst)>::type;
+ if (IsConvertableToString::value)
+ {
+ for (const auto &p : map)
+ {
+ res.push_back(detail::ToString(p.first));
+ }
+
+ std::sort(res.begin(), res.end());
+ }
+ return res;
+ }
+ }
+
+ /** Base class for all flag options
+ */
+ class FlagBase : public NamedBase
+ {
+ protected:
+ const Matcher matcher;
+
+ virtual std::string GetNameString(const HelpParams ¶ms) const override
+ {
+ const std::string postfix = !params.showValueName || NumberOfArguments() == 0 ? std::string() : Name();
+ std::string flags;
+ const auto flagStrings = matcher.GetFlagStrings();
+ const bool useValueNameOnce = flagStrings.size() == 1 ? false : params.useValueNameOnce;
+ for (auto it = flagStrings.begin(); it != flagStrings.end(); ++it)
+ {
+ auto &flag = *it;
+ if (it != flagStrings.begin())
+ {
+ flags += ", ";
+ }
+
+ flags += flag.isShort ? params.shortPrefix : params.longPrefix;
+ flags += flag.str();
+
+ if (!postfix.empty() && (!useValueNameOnce || it + 1 == flagStrings.end()))
+ {
+ flags += flag.isShort ? params.shortSeparator : params.longSeparator;
+ flags += params.valueOpen + postfix + params.valueClose;
+ }
+ }
+
+ return flags;
+ }
+
+ public:
+ FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : NamedBase(name_, help_, extraError_ ? Options::Single : Options()), matcher(std::move(matcher_)) {}
+
+ FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : NamedBase(name_, help_, options_), matcher(std::move(matcher_)) {}
+
+ virtual ~FlagBase() {}
+
+ virtual FlagBase *Match(const EitherFlag &flag) override
+ {
+ if (matcher.Match(flag))
+ {
+ if ((GetOptions() & Options::Single) != Options::None && matched)
+ {
+ std::ostringstream problem;
+ problem << "Flag '" << flag.str() << "' was passed multiple times, but is only allowed to be passed once";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Extra;
+ errorMsg = problem.str();
+#else
+ throw ExtraError(problem.str());
+#endif
+ }
+ matched = true;
+ return this;
+ }
+ return nullptr;
+ }
+
+ virtual std::vector GetAllFlags() override
+ {
+ return { this };
+ }
+
+ const Matcher &GetMatcher() const
+ {
+ return matcher;
+ }
+
+ virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override
+ {
+ if (!Matched() && IsRequired())
+ {
+ std::ostringstream problem;
+ problem << "Flag '" << matcher.GetLongOrAny().str(shortPrefix, longPrefix) << "' is required";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Required;
+ errorMsg = problem.str();
+#else
+ throw RequiredError(problem.str());
+#endif
+ }
+ }
+
+ virtual std::vector GetProgramLine(const HelpParams ¶ms) const override
+ {
+ if (!params.proglineShowFlags)
+ {
+ return {};
+ }
+
+ const std::string postfix = NumberOfArguments() == 0 ? std::string() : Name();
+ const EitherFlag flag = params.proglinePreferShortFlags ? matcher.GetShortOrAny() : matcher.GetLongOrAny();
+ std::string res = flag.str(params.shortPrefix, params.longPrefix);
+ if (!postfix.empty())
+ {
+ res += params.proglineValueOpen + postfix + params.proglineValueClose;
+ }
+
+ return { IsRequired() ? params.proglineRequiredOpen + res + params.proglineRequiredClose
+ : params.proglineNonrequiredOpen + res + params.proglineNonrequiredClose };
+ }
+
+ virtual bool HasFlag() const override
+ {
+ return true;
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ virtual Error GetError() const override
+ {
+ const auto nargs = NumberOfArguments();
+ if (nargs.min > nargs.max)
+ {
+ return Error::Usage;
+ }
+
+ const auto matcherError = matcher.GetError();
+ if (matcherError != Error::None)
+ {
+ return matcherError;
+ }
+
+ return error;
+ }
+#endif
+
+ /** Defines how many values can be consumed by this option.
+ *
+ * \return closed interval [min, max]
+ */
+ virtual Nargs NumberOfArguments() const noexcept = 0;
+
+ /** Parse values of this option.
+ *
+ * \param value Vector of values. It's size must be in NumberOfArguments() interval.
+ */
+ virtual void ParseValue(const std::vector &value) = 0;
+ };
+
+ /** Base class for value-accepting flag options
+ */
+ class ValueFlagBase : public FlagBase
+ {
+ public:
+ ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : FlagBase(name_, help_, std::move(matcher_), extraError_) {}
+ ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {}
+ virtual ~ValueFlagBase() {}
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ {
+ return 1;
+ }
+ };
+
+ class CompletionFlag : public ValueFlagBase
+ {
+ public:
+ std::vector reply;
+ size_t cword = 0;
+ std::string syntax;
+
+ template
+ CompletionFlag(GroupClass &group_, Matcher &&matcher_): ValueFlagBase("completion", "completion flag", std::move(matcher_), Options::Hidden)
+ {
+ group_.AddCompletion(*this);
+ }
+
+ virtual ~CompletionFlag() {}
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ {
+ return 2;
+ }
+
+ virtual void ParseValue(const std::vector &value_) override
+ {
+ syntax = value_.at(0);
+ std::istringstream(value_.at(1)) >> cword;
+ }
+
+ /** Get the completion reply
+ */
+ std::string Get() noexcept
+ {
+ return detail::Join(reply, "\n");
+ }
+
+ virtual void Reset() noexcept override
+ {
+ ValueFlagBase::Reset();
+ cword = 0;
+ syntax.clear();
+ reply.clear();
+ }
+ };
+
+
+ /** Base class for positional options
+ */
+ class PositionalBase : public NamedBase
+ {
+ protected:
+ bool ready;
+
+ public:
+ PositionalBase(const std::string &name_, const std::string &help_, Options options_ = {}) : NamedBase(name_, help_, options_), ready(true) {}
+ virtual ~PositionalBase() {}
+
+ bool Ready()
+ {
+ return ready;
+ }
+
+ virtual void ParseValue(const std::string &value_) = 0;
+
+ virtual void Reset() noexcept override
+ {
+ matched = false;
+ ready = true;
+#ifdef ARGS_NOEXCEPT
+ error = Error::None;
+ errorMsg.clear();
+#endif
+ }
+
+ virtual PositionalBase *GetNextPositional() override
+ {
+ return Ready() ? this : nullptr;
+ }
+
+ virtual bool HasPositional() const override
+ {
+ return true;
+ }
+
+ virtual std::vector GetProgramLine(const HelpParams ¶ms) const override
+ {
+ return { IsRequired() ? params.proglineRequiredOpen + Name() + params.proglineRequiredClose
+ : params.proglineNonrequiredOpen + Name() + params.proglineNonrequiredClose };
+ }
+
+ virtual void Validate(const std::string &, const std::string &) const override
+ {
+ if (IsRequired() && !Matched())
+ {
+ std::ostringstream problem;
+ problem << "Option '" << Name() << "' is required";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Required;
+ errorMsg = problem.str();
+#else
+ throw RequiredError(problem.str());
+#endif
+ }
+ }
+ };
+
+ /** Class for all kinds of validating groups, including ArgumentParser
+ */
+ class Group : public Base
+ {
+ private:
+ std::vector children;
+ std::function validator;
+
+ public:
+ /** Default validators
+ */
+ struct Validators
+ {
+ static bool Xor(const Group &group)
+ {
+ return group.MatchedChildren() == 1;
+ }
+
+ static bool AtLeastOne(const Group &group)
+ {
+ return group.MatchedChildren() >= 1;
+ }
+
+ static bool AtMostOne(const Group &group)
+ {
+ return group.MatchedChildren() <= 1;
+ }
+
+ static bool All(const Group &group)
+ {
+ return group.Children().size() == group.MatchedChildren();
+ }
+
+ static bool AllOrNone(const Group &group)
+ {
+ return (All(group) || None(group));
+ }
+
+ static bool AllChildGroups(const Group &group)
+ {
+ return std::none_of(std::begin(group.Children()), std::end(group.Children()), [](const Base* child) -> bool {
+ return child->IsGroup() && !child->Matched();
+ });
+ }
+
+ static bool DontCare(const Group &)
+ {
+ return true;
+ }
+
+ static bool CareTooMuch(const Group &)
+ {
+ return false;
+ }
+
+ static bool None(const Group &group)
+ {
+ return group.MatchedChildren() == 0;
+ }
+ };
+ /// If help is empty, this group will not be printed in help output
+ Group(const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) {}
+ /// If help is empty, this group will not be printed in help output
+ Group(Group &group_, const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_)
+ {
+ group_.Add(*this);
+ }
+ virtual ~Group() {}
+
+ /** Append a child to this Group.
+ */
+ void Add(Base &child)
+ {
+ children.emplace_back(&child);
+ }
+
+ /** Get all this group's children
+ */
+ const std::vector &Children() const
+ {
+ return children;
+ }
+
+ /** Return the first FlagBase that matches flag, or nullptr
+ *
+ * \param flag The flag with prefixes stripped
+ * \return the first matching FlagBase pointer, or nullptr if there is no match
+ */
+ virtual FlagBase *Match(const EitherFlag &flag) override
+ {
+ for (Base *child: Children())
+ {
+ if (FlagBase *match = child->Match(flag))
+ {
+ return match;
+ }
+ }
+ return nullptr;
+ }
+
+ virtual std::vector GetAllFlags() override
+ {
+ std::vector res;
+ for (Base *child: Children())
+ {
+ auto childRes = child->GetAllFlags();
+ res.insert(res.end(), childRes.begin(), childRes.end());
+ }
+ return res;
+ }
+
+ virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override
+ {
+ for (Base *child: Children())
+ {
+ child->Validate(shortPrefix, longPrefix);
+ }
+ }
+
+ /** Get the next ready positional, or nullptr if there is none
+ *
+ * \return the first ready PositionalBase pointer, or nullptr if there is no match
+ */
+ virtual PositionalBase *GetNextPositional() override
+ {
+ for (Base *child: Children())
+ {
+ if (auto next = child->GetNextPositional())
+ {
+ return next;
+ }
+ }
+ return nullptr;
+ }
+
+ /** Get whether this has any FlagBase children
+ *
+ * \return Whether or not there are any FlagBase children
+ */
+ virtual bool HasFlag() const override
+ {
+ return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasFlag(); });
+ }
+
+ /** Get whether this has any PositionalBase children
+ *
+ * \return Whether or not there are any PositionalBase children
+ */
+ virtual bool HasPositional() const override
+ {
+ return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); });
+ }
+
+ /** Get whether this has any Command children
+ *
+ * \return Whether or not there are any Command children
+ */
+ virtual bool HasCommand() const override
+ {
+ return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasCommand(); });
+ }
+
+ /** Count the number of matched children this group has
+ */
+ std::vector::size_type MatchedChildren() const
+ {
+ // Cast to avoid warnings from -Wsign-conversion
+ return static_cast::size_type>(
+ std::count_if(std::begin(Children()), std::end(Children()), [](const Base *child){return child->Matched();}));
+ }
+
+ /** Whether or not this group matches validation
+ */
+ virtual bool Matched() const noexcept override
+ {
+ return validator(*this);
+ }
+
+ /** Get validation
+ */
+ bool Get() const
+ {
+ return Matched();
+ }
+
+ /** Get all the child descriptions for help generation
+ */
+ virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override
+ {
+ std::vector> descriptions;
+
+ // Push that group description on the back if not empty
+ unsigned addindent = 0;
+ if (!help.empty())
+ {
+ descriptions.emplace_back(help, "", indent);
+ addindent = 1;
+ }
+
+ for (Base *child: Children())
+ {
+ if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None)
+ {
+ continue;
+ }
+
+ auto groupDescriptions = child->GetDescription(params, indent + addindent);
+ descriptions.insert(
+ std::end(descriptions),
+ std::make_move_iterator(std::begin(groupDescriptions)),
+ std::make_move_iterator(std::end(groupDescriptions)));
+ }
+ return descriptions;
+ }
+
+ /** Get the names of positional parameters
+ */
+ virtual std::vector GetProgramLine(const HelpParams ¶ms) const override
+ {
+ std::vector names;
+ for (Base *child: Children())
+ {
+ if ((child->GetOptions() & Options::HiddenFromUsage) != Options::None)
+ {
+ continue;
+ }
+
+ auto groupNames = child->GetProgramLine(params);
+ names.insert(
+ std::end(names),
+ std::make_move_iterator(std::begin(groupNames)),
+ std::make_move_iterator(std::end(groupNames)));
+ }
+ return names;
+ }
+
+ virtual std::vector GetCommands() override
+ {
+ std::vector res;
+ for (const auto &child : Children())
+ {
+ auto subparsers = child->GetCommands();
+ res.insert(std::end(res), std::begin(subparsers), std::end(subparsers));
+ }
+ return res;
+ }
+
+ virtual bool IsGroup() const override
+ {
+ return true;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ Base::Reset();
+
+ for (auto &child: Children())
+ {
+ child->Reset();
+ }
+#ifdef ARGS_NOEXCEPT
+ error = Error::None;
+ errorMsg.clear();
+#endif
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ virtual Error GetError() const override
+ {
+ if (error != Error::None)
+ {
+ return error;
+ }
+
+ auto it = std::find_if(Children().begin(), Children().end(), [](const Base *child){return child->GetError() != Error::None;});
+ if (it == Children().end())
+ {
+ return Error::None;
+ } else
+ {
+ return (*it)->GetError();
+ }
+ }
+#endif
+
+ };
+
+ /** Class for using global options in ArgumentParser.
+ */
+ class GlobalOptions : public Group
+ {
+ public:
+ GlobalOptions(Group &base, Base &options_) : Group(base, {}, Group::Validators::DontCare, Options::Global)
+ {
+ Add(options_);
+ }
+ };
+
+ /** Utility class for building subparsers with coroutines/callbacks.
+ *
+ * Brief example:
+ * \code
+ * Command command(argumentParser, "command", "my command", [](args::Subparser &s)
+ * {
+ * // your command flags/positionals
+ * s.Parse(); //required
+ * //your command code
+ * });
+ * \endcode
+ *
+ * For ARGS_NOEXCEPT mode don't forget to check `s.GetError()` after `s.Parse()`
+ * and return if it isn't equals to args::Error::None.
+ *
+ * \sa Command
+ */
+ class Subparser : public Group
+ {
+ private:
+ std::vector args;
+ std::vector kicked;
+ ArgumentParser *parser = nullptr;
+ const HelpParams &helpParams;
+ const Command &command;
+ bool isParsed = false;
+
+ public:
+ Subparser(std::vector args_, ArgumentParser &parser_, const Command &command_, const HelpParams &helpParams_)
+ : Group({}, Validators::AllChildGroups), args(std::move(args_)), parser(&parser_), helpParams(helpParams_), command(command_)
+ {
+ }
+
+ Subparser(const Command &command_, const HelpParams &helpParams_) : Group({}, Validators::AllChildGroups), helpParams(helpParams_), command(command_)
+ {
+ }
+
+ Subparser(const Subparser&) = delete;
+ Subparser(Subparser&&) = delete;
+ Subparser &operator = (const Subparser&) = delete;
+ Subparser &operator = (Subparser&&) = delete;
+
+ const Command &GetCommand()
+ {
+ return command;
+ }
+
+ /** (INTERNAL) Determines whether Parse was called or not.
+ */
+ bool IsParsed() const
+ {
+ return isParsed;
+ }
+
+ /** Continue parsing arguments for new command.
+ */
+ void Parse();
+
+ /** Returns a vector of kicked out arguments.
+ *
+ * \sa Base::KickOut
+ */
+ const std::vector &KickedOut() const noexcept
+ {
+ return kicked;
+ }
+ };
+
+ /** Main class for building subparsers.
+ *
+ * /sa Subparser
+ */
+ class Command : public Group
+ {
+ private:
+ friend class Subparser;
+
+ std::string name;
+ std::string help;
+ std::string description;
+ std::string epilog;
+ std::string proglinePostfix;
+
+ std::function parserCoroutine;
+ bool commandIsRequired = true;
+ Command *selectedCommand = nullptr;
+
+ mutable std::vector> subparserDescription;
+ mutable std::vector subparserProgramLine;
+ mutable bool subparserHasFlag = false;
+ mutable bool subparserHasPositional = false;
+ mutable bool subparserHasCommand = false;
+#ifdef ARGS_NOEXCEPT
+ mutable Error subparserError = Error::None;
+#endif
+ mutable Subparser *subparser = nullptr;
+
+ protected:
+
+ class RaiiSubparser
+ {
+ public:
+ RaiiSubparser(ArgumentParser &parser_, std::vector args_);
+ RaiiSubparser(const Command &command_, const HelpParams ¶ms_);
+
+ ~RaiiSubparser()
+ {
+ command.subparser = oldSubparser;
+ }
+
+ Subparser &Parser()
+ {
+ return parser;
+ }
+
+ private:
+ const Command &command;
+ Subparser parser;
+ Subparser *oldSubparser;
+ };
+
+ Command() = default;
+
+ std::function &GetCoroutine()
+ {
+ return selectedCommand != nullptr ? selectedCommand->GetCoroutine() : parserCoroutine;
+ }
+
+ Command &SelectedCommand()
+ {
+ Command *res = this;
+ while (res->selectedCommand != nullptr)
+ {
+ res = res->selectedCommand;
+ }
+
+ return *res;
+ }
+
+ const Command &SelectedCommand() const
+ {
+ const Command *res = this;
+ while (res->selectedCommand != nullptr)
+ {
+ res = res->selectedCommand;
+ }
+
+ return *res;
+ }
+
+ void UpdateSubparserHelp(const HelpParams ¶ms) const
+ {
+ if (parserCoroutine)
+ {
+ RaiiSubparser coro(*this, params);
+#ifndef ARGS_NOEXCEPT
+ try
+ {
+ parserCoroutine(coro.Parser());
+ }
+ catch (args::SubparserError&)
+ {
+ }
+#else
+ parserCoroutine(coro.Parser());
+#endif
+ }
+ }
+
+ public:
+ Command(Group &base_, std::string name_, std::string help_, std::function coroutine_ = {})
+ : name(std::move(name_)), help(std::move(help_)), parserCoroutine(std::move(coroutine_))
+ {
+ base_.Add(*this);
+ }
+
+ /** The description that appears on the prog line after options
+ */
+ const std::string &ProglinePostfix() const
+ { return proglinePostfix; }
+
+ /** The description that appears on the prog line after options
+ */
+ void ProglinePostfix(const std::string &proglinePostfix_)
+ { this->proglinePostfix = proglinePostfix_; }
+
+ /** The description that appears above options
+ */
+ const std::string &Description() const
+ { return description; }
+ /** The description that appears above options
+ */
+
+ void Description(const std::string &description_)
+ { this->description = description_; }
+
+ /** The description that appears below options
+ */
+ const std::string &Epilog() const
+ { return epilog; }
+
+ /** The description that appears below options
+ */
+ void Epilog(const std::string &epilog_)
+ { this->epilog = epilog_; }
+
+ /** The name of command
+ */
+ const std::string &Name() const
+ { return name; }
+
+ /** The description of command
+ */
+ const std::string &Help() const
+ { return help; }
+
+ /** If value is true, parser will fail if no command was parsed.
+ *
+ * Default: true.
+ */
+ void RequireCommand(bool value)
+ { commandIsRequired = value; }
+
+ virtual bool IsGroup() const override
+ { return false; }
+
+ virtual bool Matched() const noexcept override
+ { return Base::Matched(); }
+
+ operator bool() const noexcept
+ { return Matched(); }
+
+ void Match() noexcept
+ { matched = true; }
+
+ void SelectCommand(Command *c) noexcept
+ {
+ selectedCommand = c;
+
+ if (c != nullptr)
+ {
+ c->Match();
+ }
+ }
+
+ virtual FlagBase *Match(const EitherFlag &flag) override
+ {
+ if (selectedCommand != nullptr)
+ {
+ if (auto *res = selectedCommand->Match(flag))
+ {
+ return res;
+ }
+
+ for (auto *child: Children())
+ {
+ if ((child->GetOptions() & Options::Global) != Options::None)
+ {
+ if (auto *res = child->Match(flag))
+ {
+ return res;
+ }
+ }
+ }
+
+ return nullptr;
+ }
+
+ if (subparser != nullptr)
+ {
+ return subparser->Match(flag);
+ }
+
+ return Matched() ? Group::Match(flag) : nullptr;
+ }
+
+ virtual std::vector GetAllFlags() override
+ {
+ std::vector res;
+
+ if (!Matched())
+ {
+ return res;
+ }
+
+ for (auto *child: Children())
+ {
+ if (selectedCommand == nullptr || (child->GetOptions() & Options::Global) != Options::None)
+ {
+ auto childFlags = child->GetAllFlags();
+ res.insert(res.end(), childFlags.begin(), childFlags.end());
+ }
+ }
+
+ if (selectedCommand != nullptr)
+ {
+ auto childFlags = selectedCommand->GetAllFlags();
+ res.insert(res.end(), childFlags.begin(), childFlags.end());
+ }
+
+ if (subparser != nullptr)
+ {
+ auto childFlags = subparser->GetAllFlags();
+ res.insert(res.end(), childFlags.begin(), childFlags.end());
+ }
+
+ return res;
+ }
+
+ virtual PositionalBase *GetNextPositional() override
+ {
+ if (selectedCommand != nullptr)
+ {
+ if (auto *res = selectedCommand->GetNextPositional())
+ {
+ return res;
+ }
+
+ for (auto *child: Children())
+ {
+ if ((child->GetOptions() & Options::Global) != Options::None)
+ {
+ if (auto *res = child->GetNextPositional())
+ {
+ return res;
+ }
+ }
+ }
+
+ return nullptr;
+ }
+
+ if (subparser != nullptr)
+ {
+ return subparser->GetNextPositional();
+ }
+
+ return Matched() ? Group::GetNextPositional() : nullptr;
+ }
+
+ virtual bool HasFlag() const override
+ {
+ return subparserHasFlag || Group::HasFlag();
+ }
+
+ virtual bool HasPositional() const override
+ {
+ return subparserHasPositional || Group::HasPositional();
+ }
+
+ virtual bool HasCommand() const override
+ {
+ return true;
+ }
+
+ std::vector GetCommandProgramLine(const HelpParams ¶ms) const
+ {
+ UpdateSubparserHelp(params);
+
+ auto res = Group::GetProgramLine(params);
+ res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end());
+
+ if (!params.proglineCommand.empty() && (Group::HasCommand() || subparserHasCommand))
+ {
+ res.insert(res.begin(), commandIsRequired ? params.proglineCommand : "[" + params.proglineCommand + "]");
+ }
+
+ if (!Name().empty())
+ {
+ res.insert(res.begin(), Name());
+ }
+
+ if ((subparserHasFlag || Group::HasFlag()) && params.showProglineOptions && !params.proglineShowFlags)
+ {
+ res.push_back(params.proglineOptions);
+ }
+
+ if (!ProglinePostfix().empty())
+ {
+ std::string line;
+ for (char c : ProglinePostfix())
+ {
+ if (isspace(c))
+ {
+ if (!line.empty())
+ {
+ res.push_back(line);
+ line.clear();
+ }
+
+ if (c == '\n')
+ {
+ res.push_back("\n");
+ }
+ }
+ else
+ {
+ line += c;
+ }
+ }
+
+ if (!line.empty())
+ {
+ res.push_back(line);
+ }
+ }
+
+ return res;
+ }
+
+ virtual std::vector GetProgramLine(const HelpParams ¶ms) const override
+ {
+ if (!Matched())
+ {
+ return {};
+ }
+
+ return GetCommandProgramLine(params);
+ }
+
+ virtual std::vector GetCommands() override
+ {
+ if (selectedCommand != nullptr)
+ {
+ return selectedCommand->GetCommands();
+ }
+
+ if (Matched())
+ {
+ return Group::GetCommands();
+ }
+
+ return { this };
+ }
+
+ virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override
+ {
+ std::vector> descriptions;
+ unsigned addindent = 0;
+
+ UpdateSubparserHelp(params);
+
+ if (!Matched())
+ {
+ if (params.showCommandFullHelp)
+ {
+ std::ostringstream s;
+ bool empty = true;
+ for (const auto &progline: GetCommandProgramLine(params))
+ {
+ if (!empty)
+ {
+ s << ' ';
+ }
+ else
+ {
+ empty = false;
+ }
+
+ s << progline;
+ }
+
+ descriptions.emplace_back(s.str(), "", indent);
+ }
+ else
+ {
+ descriptions.emplace_back(Name(), help, indent);
+ }
+
+ if (!params.showCommandChildren && !params.showCommandFullHelp)
+ {
+ return descriptions;
+ }
+
+ addindent = 1;
+ }
+
+ if (params.showCommandFullHelp && !Matched())
+ {
+ descriptions.emplace_back("", "", indent + addindent);
+ descriptions.emplace_back(Description().empty() ? Help() : Description(), "", indent + addindent);
+ descriptions.emplace_back("", "", indent + addindent);
+ }
+
+ for (Base *child: Children())
+ {
+ if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None)
+ {
+ continue;
+ }
+
+ auto groupDescriptions = child->GetDescription(params, indent + addindent);
+ descriptions.insert(
+ std::end(descriptions),
+ std::make_move_iterator(std::begin(groupDescriptions)),
+ std::make_move_iterator(std::end(groupDescriptions)));
+ }
+
+ for (auto childDescription: subparserDescription)
+ {
+ std::get<2>(childDescription) += indent + addindent;
+ descriptions.push_back(std::move(childDescription));
+ }
+
+ if (params.showCommandFullHelp && !Matched())
+ {
+ descriptions.emplace_back("", "", indent + addindent);
+ if (!Epilog().empty())
+ {
+ descriptions.emplace_back(Epilog(), "", indent + addindent);
+ descriptions.emplace_back("", "", indent + addindent);
+ }
+ }
+
+ return descriptions;
+ }
+
+ virtual void Validate(const std::string &shortprefix, const std::string &longprefix) const override
+ {
+ if (!Matched())
+ {
+ return;
+ }
+
+ auto onValidationError = [&]
+ {
+ std::ostringstream problem;
+ problem << "Group validation failed somewhere!";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Validation;
+ errorMsg = problem.str();
+#else
+ throw ValidationError(problem.str());
+#endif
+ };
+
+ for (Base *child: Children())
+ {
+ if (child->IsGroup() && !child->Matched())
+ {
+ onValidationError();
+ }
+
+ child->Validate(shortprefix, longprefix);
+ }
+
+ if (subparser != nullptr)
+ {
+ subparser->Validate(shortprefix, longprefix);
+ if (!subparser->Matched())
+ {
+ onValidationError();
+ }
+ }
+
+ if (selectedCommand == nullptr && commandIsRequired && (Group::HasCommand() || subparserHasCommand))
+ {
+ std::ostringstream problem;
+ problem << "Command is required";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Validation;
+ errorMsg = problem.str();
+#else
+ throw ValidationError(problem.str());
+#endif
+ }
+ }
+
+ virtual void Reset() noexcept override
+ {
+ Group::Reset();
+ selectedCommand = nullptr;
+ subparserProgramLine.clear();
+ subparserDescription.clear();
+ subparserHasFlag = false;
+ subparserHasPositional = false;
+ subparserHasCommand = false;
+#ifdef ARGS_NOEXCEPT
+ subparserError = Error::None;
+#endif
+ }
+
+#ifdef ARGS_NOEXCEPT
+ /// Only for ARGS_NOEXCEPT
+ virtual Error GetError() const override
+ {
+ if (!Matched())
+ {
+ return Error::None;
+ }
+
+ if (error != Error::None)
+ {
+ return error;
+ }
+
+ if (subparserError != Error::None)
+ {
+ return subparserError;
+ }
+
+ return Group::GetError();
+ }
+#endif
+ };
+
+ /** The main user facing command line argument parser class
+ */
+ class ArgumentParser : public Command
+ {
+ friend class Subparser;
+
+ private:
+ std::string longprefix;
+ std::string shortprefix;
+
+ std::string longseparator;
+
+ std::string terminator;
+
+ bool allowJoinedShortValue = true;
+ bool allowJoinedLongValue = true;
+ bool allowSeparateShortValue = true;
+ bool allowSeparateLongValue = true;
+
+ CompletionFlag *completion = nullptr;
+ bool readCompletion = false;
+
+ protected:
+ enum class OptionType
+ {
+ LongFlag,
+ ShortFlag,
+ Positional
+ };
+
+ OptionType ParseOption(const std::string &s, bool allowEmpty = false)
+ {
+ if (s.find(longprefix) == 0 && (allowEmpty || s.length() > longprefix.length()))
+ {
+ return OptionType::LongFlag;
+ }
+
+ if (s.find(shortprefix) == 0 && (allowEmpty || s.length() > shortprefix.length()))
+ {
+ return OptionType::ShortFlag;
+ }
+
+ return OptionType::Positional;
+ }
+
+ template
+ bool Complete(FlagBase &flag, It it, It end)
+ {
+ auto nextIt = it;
+ if (!readCompletion || (++nextIt != end))
+ {
+ return false;
+ }
+
+ const auto &chunk = *it;
+ for (auto &choice : flag.HelpChoices(helpParams))
+ {
+ AddCompletionReply(chunk, choice);
+ }
+
+#ifndef ARGS_NOEXCEPT
+ throw Completion(completion->Get());
+#else
+ return true;
+#endif
+ }
+
+ /** (INTERNAL) Parse flag's values
+ *
+ * \param arg The string to display in error message as a flag name
+ * \param[in, out] it The iterator to first value. It will point to the last value
+ * \param end The end iterator
+ * \param joinedArg Joined value (e.g. bar in --foo=bar)
+ * \param canDiscardJoined If true joined value can be parsed as flag not as a value (as in -abcd)
+ * \param[out] values The vector to store parsed arg's values
+ */
+ template
+ std::string ParseArgsValues(FlagBase &flag, const std::string &arg, It &it, It end,
+ const bool allowSeparate, const bool allowJoined,
+ const bool hasJoined, const std::string &joinedArg,
+ const bool canDiscardJoined, std::vector &values)
+ {
+ values.clear();
+
+ Nargs nargs = flag.NumberOfArguments();
+
+ if (hasJoined && !allowJoined && nargs.min != 0)
+ {
+ return "Flag '" + arg + "' was passed a joined argument, but these are disallowed";
+ }
+
+ if (hasJoined)
+ {
+ if (!canDiscardJoined || nargs.max != 0)
+ {
+ values.push_back(joinedArg);
+ }
+ } else if (!allowSeparate)
+ {
+ if (nargs.min != 0)
+ {
+ return "Flag '" + arg + "' was passed a separate argument, but these are disallowed";
+ }
+ } else
+ {
+ auto valueIt = it;
+ ++valueIt;
+
+ while (valueIt != end &&
+ values.size() < nargs.max &&
+ (nargs.min == nargs.max || ParseOption(*valueIt) == OptionType::Positional))
+ {
+ if (Complete(flag, valueIt, end))
+ {
+ it = end;
+ return "";
+ }
+
+ values.push_back(*valueIt);
+ ++it;
+ ++valueIt;
+ }
+ }
+
+ if (values.size() > nargs.max)
+ {
+ return "Passed an argument into a non-argument flag: " + arg;
+ } else if (values.size() < nargs.min)
+ {
+ if (nargs.min == 1 && nargs.max == 1)
+ {
+ return "Flag '" + arg + "' requires an argument but received none";
+ } else if (nargs.min == 1)
+ {
+ return "Flag '" + arg + "' requires at least one argument but received none";
+ } else if (nargs.min != nargs.max)
+ {
+ return "Flag '" + arg + "' requires at least " + std::to_string(nargs.min) +
+ " arguments but received " + std::to_string(values.size());
+ } else
+ {
+ return "Flag '" + arg + "' requires " + std::to_string(nargs.min) +
+ " arguments but received " + std::to_string(values.size());
+ }
+ }
+
+ return {};
+ }
+
+ template
+ bool ParseLong(It &it, It end)
+ {
+ const auto &chunk = *it;
+ const auto argchunk = chunk.substr(longprefix.size());
+ // Try to separate it, in case of a separator:
+ const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator);
+ // If the separator is in the argument, separate it.
+ const auto arg = (separator != argchunk.npos ?
+ std::string(argchunk, 0, separator)
+ : argchunk);
+ const auto joined = (separator != argchunk.npos ?
+ argchunk.substr(separator + longseparator.size())
+ : std::string());
+
+ if (auto flag = Match(arg))
+ {
+ std::vector values;
+ const std::string errorMessage = ParseArgsValues(*flag, arg, it, end, allowSeparateLongValue, allowJoinedLongValue,
+ separator != argchunk.npos, joined, false, values);
+ if (!errorMessage.empty())
+ {
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return false;
+#endif
+ }
+
+ if (!readCompletion)
+ {
+ flag->ParseValue(values);
+ }
+
+ if (flag->KickOut())
+ {
+ ++it;
+ return false;
+ }
+ } else
+ {
+ const std::string errorMessage("Flag could not be matched: " + arg);
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return false;
+#endif
+ }
+
+ return true;
+ }
+
+ template
+ bool ParseShort(It &it, It end)
+ {
+ const auto &chunk = *it;
+ const auto argchunk = chunk.substr(shortprefix.size());
+ for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit)
+ {
+ const auto arg = *argit;
+
+ if (auto flag = Match(arg))
+ {
+ const std::string value(argit + 1, std::end(argchunk));
+ std::vector values;
+ const std::string errorMessage = ParseArgsValues(*flag, std::string(1, arg), it, end,
+ allowSeparateShortValue, allowJoinedShortValue,
+ !value.empty(), value, !value.empty(), values);
+
+ if (!errorMessage.empty())
+ {
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return false;
+#endif
+ }
+
+ if (!readCompletion)
+ {
+ flag->ParseValue(values);
+ }
+
+ if (flag->KickOut())
+ {
+ ++it;
+ return false;
+ }
+
+ if (!values.empty())
+ {
+ break;
+ }
+ } else
+ {
+ const std::string errorMessage("Flag could not be matched: '" + std::string(1, arg) + "'");
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return false;
+#endif
+ }
+ }
+
+ return true;
+ }
+
+ bool AddCompletionReply(const std::string &cur, const std::string &choice)
+ {
+ if (cur.empty() || choice.find(cur) == 0)
+ {
+ if (completion->syntax == "bash" && ParseOption(choice) == OptionType::LongFlag && choice.find(longseparator) != std::string::npos)
+ {
+ completion->reply.push_back(choice.substr(choice.find(longseparator) + 1));
+ } else
+ {
+ completion->reply.push_back(choice);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ template
+ bool Complete(It it, It end)
+ {
+ auto nextIt = it;
+ if (!readCompletion || (++nextIt != end))
+ {
+ return false;
+ }
+
+ const auto &chunk = *it;
+ auto pos = GetNextPositional();
+ std::vector commands = GetCommands();
+ const auto optionType = ParseOption(chunk, true);
+
+ if (!commands.empty() && (chunk.empty() || optionType == OptionType::Positional))
+ {
+ for (auto &cmd : commands)
+ {
+ if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+ {
+ AddCompletionReply(chunk, cmd->Name());
+ }
+ }
+ } else
+ {
+ bool hasPositionalCompletion = true;
+
+ if (!commands.empty())
+ {
+ for (auto &cmd : commands)
+ {
+ if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+ {
+ AddCompletionReply(chunk, cmd->Name());
+ }
+ }
+ } else if (pos)
+ {
+ if ((pos->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+ {
+ auto choices = pos->HelpChoices(helpParams);
+ hasPositionalCompletion = !choices.empty() || optionType != OptionType::Positional;
+ for (auto &choice : choices)
+ {
+ AddCompletionReply(chunk, choice);
+ }
+ }
+ }
+
+ if (hasPositionalCompletion)
+ {
+ auto flags = GetAllFlags();
+ for (auto flag : flags)
+ {
+ if ((flag->GetOptions() & Options::HiddenFromCompletion) != Options::None)
+ {
+ continue;
+ }
+
+ auto &matcher = flag->GetMatcher();
+ if (!AddCompletionReply(chunk, matcher.GetShortOrAny().str(shortprefix, longprefix)))
+ {
+ for (auto &flagName : matcher.GetFlagStrings())
+ {
+ if (AddCompletionReply(chunk, flagName.str(shortprefix, longprefix)))
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ if (optionType == OptionType::LongFlag && allowJoinedLongValue)
+ {
+ const auto separator = longseparator.empty() ? chunk.npos : chunk.find(longseparator);
+ if (separator != chunk.npos)
+ {
+ std::string arg(chunk, 0, separator);
+ if (auto flag = this->Match(arg.substr(longprefix.size())))
+ {
+ for (auto &choice : flag->HelpChoices(helpParams))
+ {
+ AddCompletionReply(chunk, arg + longseparator + choice);
+ }
+ }
+ }
+ } else if (optionType == OptionType::ShortFlag && allowJoinedShortValue)
+ {
+ if (chunk.size() > shortprefix.size() + 1)
+ {
+ auto arg = chunk.at(shortprefix.size());
+ //TODO: support -abcVALUE where a and b take no value
+ if (auto flag = this->Match(arg))
+ {
+ for (auto &choice : flag->HelpChoices(helpParams))
+ {
+ AddCompletionReply(chunk, shortprefix + arg + choice);
+ }
+ }
+ }
+ }
+ }
+ }
+
+#ifndef ARGS_NOEXCEPT
+ throw Completion(completion->Get());
+#else
+ return true;
+#endif
+ }
+
+ template
+ It Parse(It begin, It end)
+ {
+ bool terminated = false;
+ std::vector commands = GetCommands();
+
+ // Check all arg chunks
+ for (auto it = begin; it != end; ++it)
+ {
+ if (Complete(it, end))
+ {
+ return end;
+ }
+
+ const auto &chunk = *it;
+
+ if (!terminated && chunk == terminator)
+ {
+ terminated = true;
+ } else if (!terminated && ParseOption(chunk) == OptionType::LongFlag)
+ {
+ if (!ParseLong(it, end))
+ {
+ return it;
+ }
+ } else if (!terminated && ParseOption(chunk) == OptionType::ShortFlag)
+ {
+ if (!ParseShort(it, end))
+ {
+ return it;
+ }
+ } else if (!terminated && !commands.empty())
+ {
+ auto itCommand = std::find_if(commands.begin(), commands.end(), [&chunk](Command *c) { return c->Name() == chunk; });
+ if (itCommand == commands.end())
+ {
+ const std::string errorMessage("Unknown command: " + chunk);
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return it;
+#endif
+ }
+
+ SelectCommand(*itCommand);
+
+ if (const auto &coroutine = GetCoroutine())
+ {
+ ++it;
+ RaiiSubparser coro(*this, std::vector(it, end));
+ coroutine(coro.Parser());
+#ifdef ARGS_NOEXCEPT
+ error = GetError();
+ if (error != Error::None)
+ {
+ return end;
+ }
+
+ if (!coro.Parser().IsParsed())
+ {
+ error = Error::Usage;
+ return end;
+ }
+#else
+ if (!coro.Parser().IsParsed())
+ {
+ throw UsageError("Subparser::Parse was not called");
+ }
+#endif
+
+ break;
+ }
+
+ commands = GetCommands();
+ } else
+ {
+ auto pos = GetNextPositional();
+ if (pos)
+ {
+ pos->ParseValue(chunk);
+
+ if (pos->KickOut())
+ {
+ return ++it;
+ }
+ } else
+ {
+ const std::string errorMessage("Passed in argument, but no positional arguments were ready to receive it: " + chunk);
+#ifndef ARGS_NOEXCEPT
+ throw ParseError(errorMessage);
+#else
+ error = Error::Parse;
+ errorMsg = errorMessage;
+ return it;
+#endif
+ }
+ }
+
+ if (!readCompletion && completion != nullptr && completion->Matched())
+ {
+#ifdef ARGS_NOEXCEPT
+ error = Error::Completion;
+#endif
+ readCompletion = true;
+ ++it;
+ const auto argsLeft = static_cast(std::distance(it, end));
+ if (completion->cword == 0 || argsLeft <= 1 || completion->cword >= argsLeft)
+ {
+#ifndef ARGS_NOEXCEPT
+ throw Completion("");
+#endif
+ }
+
+ std::vector curArgs(++it, end);
+ curArgs.resize(completion->cword);
+
+ if (completion->syntax == "bash")
+ {
+ // bash tokenizes --flag=value as --flag=value
+ for (size_t idx = 0; idx < curArgs.size(); )
+ {
+ if (idx > 0 && curArgs[idx] == "=")
+ {
+ curArgs[idx - 1] += "=";
+ // Avoid warnings from -Wsign-conversion
+ const auto signedIdx = static_cast(idx);
+ if (idx + 1 < curArgs.size())
+ {
+ curArgs[idx - 1] += curArgs[idx + 1];
+ curArgs.erase(curArgs.begin() + signedIdx, curArgs.begin() + signedIdx + 2);
+ } else
+ {
+ curArgs.erase(curArgs.begin() + signedIdx);
+ }
+ } else
+ {
+ ++idx;
+ }
+ }
+
+ }
+#ifndef ARGS_NOEXCEPT
+ try
+ {
+ Parse(curArgs.begin(), curArgs.end());
+ throw Completion("");
+ }
+ catch (Completion &)
+ {
+ throw;
+ }
+ catch (args::Error&)
+ {
+ throw Completion("");
+ }
+#else
+ return Parse(curArgs.begin(), curArgs.end());
+#endif
+ }
+ }
+
+ Validate(shortprefix, longprefix);
+ return end;
+ }
+
+ public:
+ HelpParams helpParams;
+
+ ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string())
+ {
+ Description(description_);
+ Epilog(epilog_);
+ LongPrefix("--");
+ ShortPrefix("-");
+ LongSeparator("=");
+ Terminator("--");
+ SetArgumentSeparations(true, true, true, true);
+ matched = true;
+ }
+
+ void AddCompletion(CompletionFlag &completionFlag)
+ {
+ completion = &completionFlag;
+ Add(completionFlag);
+ }
+
+ /** The program name for help generation
+ */
+ const std::string &Prog() const
+ { return helpParams.programName; }
+ /** The program name for help generation
+ */
+ void Prog(const std::string &prog_)
+ { this->helpParams.programName = prog_; }
+
+ /** The prefix for long flags
+ */
+ const std::string &LongPrefix() const
+ { return longprefix; }
+ /** The prefix for long flags
+ */
+ void LongPrefix(const std::string &longprefix_)
+ {
+ this->longprefix = longprefix_;
+ this->helpParams.longPrefix = longprefix_;
+ }
+
+ /** The prefix for short flags
+ */
+ const std::string &ShortPrefix() const
+ { return shortprefix; }
+ /** The prefix for short flags
+ */
+ void ShortPrefix(const std::string &shortprefix_)
+ {
+ this->shortprefix = shortprefix_;
+ this->helpParams.shortPrefix = shortprefix_;
+ }
+
+ /** The separator for long flags
+ */
+ const std::string &LongSeparator() const
+ { return longseparator; }
+ /** The separator for long flags
+ */
+ void LongSeparator(const std::string &longseparator_)
+ {
+ if (longseparator_.empty())
+ {
+ const std::string errorMessage("longseparator can not be set to empty");
+#ifdef ARGS_NOEXCEPT
+ error = Error::Usage;
+ errorMsg = errorMessage;
+#else
+ throw UsageError(errorMessage);
+#endif
+ } else
+ {
+ this->longseparator = longseparator_;
+ this->helpParams.longSeparator = allowJoinedLongValue ? longseparator_ : " ";
+ }
+ }
+
+ /** The terminator that forcibly separates flags from positionals
+ */
+ const std::string &Terminator() const
+ { return terminator; }
+ /** The terminator that forcibly separates flags from positionals
+ */
+ void Terminator(const std::string &terminator_)
+ { this->terminator = terminator_; }
+
+ /** Get the current argument separation parameters.
+ *
+ * See SetArgumentSeparations for details on what each one means.
+ */
+ void GetArgumentSeparations(
+ bool &allowJoinedShortValue_,
+ bool &allowJoinedLongValue_,
+ bool &allowSeparateShortValue_,
+ bool &allowSeparateLongValue_) const
+ {
+ allowJoinedShortValue_ = this->allowJoinedShortValue;
+ allowJoinedLongValue_ = this->allowJoinedLongValue;
+ allowSeparateShortValue_ = this->allowSeparateShortValue;
+ allowSeparateLongValue_ = this->allowSeparateLongValue;
+ }
+
+ /** Change allowed option separation.
+ *
+ * \param allowJoinedShortValue_ Allow a short flag that accepts an argument to be passed its argument immediately next to it (ie. in the same argv field)
+ * \param allowJoinedLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by the longseparator (ie. in the same argv field)
+ * \param allowSeparateShortValue_ Allow a short flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field)
+ * \param allowSeparateLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field)
+ */
+ void SetArgumentSeparations(
+ const bool allowJoinedShortValue_,
+ const bool allowJoinedLongValue_,
+ const bool allowSeparateShortValue_,
+ const bool allowSeparateLongValue_)
+ {
+ this->allowJoinedShortValue = allowJoinedShortValue_;
+ this->allowJoinedLongValue = allowJoinedLongValue_;
+ this->allowSeparateShortValue = allowSeparateShortValue_;
+ this->allowSeparateLongValue = allowSeparateLongValue_;
+
+ this->helpParams.longSeparator = allowJoinedLongValue ? longseparator : " ";
+ this->helpParams.shortSeparator = allowJoinedShortValue ? "" : " ";
+ }
+
+ /** Pass the help menu into an ostream
+ */
+ void Help(std::ostream &help_) const
+ {
+ auto &command = SelectedCommand();
+ const auto &commandDescription = command.Description().empty() ? command.Help() : command.Description();
+ const auto description_text = Wrap(commandDescription, helpParams.width - helpParams.descriptionindent);
+ const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent);
+
+ const bool hasoptions = command.HasFlag();
+ const bool hasarguments = command.HasPositional();
+
+ std::vector prognameline;
+ prognameline.push_back(helpParams.usageString);
+ prognameline.push_back(Prog());
+ auto commandProgLine = command.GetProgramLine(helpParams);
+ prognameline.insert(prognameline.end(), commandProgLine.begin(), commandProgLine.end());
+
+ const auto proglines = Wrap(prognameline.begin(), prognameline.end(),
+ helpParams.width - (helpParams.progindent + helpParams.progtailindent),
+ helpParams.width - helpParams.progindent);
+ auto progit = std::begin(proglines);
+ if (progit != std::end(proglines))
+ {
+ help_ << std::string(helpParams.progindent, ' ') << *progit << '\n';
+ ++progit;
+ }
+ for (; progit != std::end(proglines); ++progit)
+ {
+ help_ << std::string(helpParams.progtailindent, ' ') << *progit << '\n';
+ }
+
+ help_ << '\n';
+
+ if (!description_text.empty())
+ {
+ for (const auto &line: description_text)
+ {
+ help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n";
+ }
+ help_ << "\n";
+ }
+
+ bool lastDescriptionIsNewline = false;
+
+ if (!helpParams.optionsString.empty())
+ {
+ help_ << std::string(helpParams.progindent, ' ') << helpParams.optionsString << "\n\n";
+ }
+
+ for (const auto &desc: command.GetDescription(helpParams, 0))
+ {
+ lastDescriptionIsNewline = std::get<0>(desc).empty() && std::get<1>(desc).empty();
+ const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent;
+ const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter));
+ const auto info = Wrap(std::get<1>(desc), helpParams.width - (helpParams.helpindent + groupindent));
+
+ std::string::size_type flagssize = 0;
+ for (auto flagsit = std::begin(flags); flagsit != std::end(flags); ++flagsit)
+ {
+ if (flagsit != std::begin(flags))
+ {
+ help_ << '\n';
+ }
+ help_ << std::string(groupindent + helpParams.flagindent, ' ') << *flagsit;
+ flagssize = Glyphs(*flagsit);
+ }
+
+ auto infoit = std::begin(info);
+ // groupindent is on both sides of this inequality, and therefore can be removed
+ if ((helpParams.flagindent + flagssize + helpParams.gutter) > helpParams.helpindent || infoit == std::end(info) || helpParams.addNewlineBeforeDescription)
+ {
+ help_ << '\n';
+ } else
+ {
+ // groupindent is on both sides of the minus sign, and therefore doesn't actually need to be in here
+ help_ << std::string(helpParams.helpindent - (helpParams.flagindent + flagssize), ' ') << *infoit << '\n';
+ ++infoit;
+ }
+ for (; infoit != std::end(info); ++infoit)
+ {
+ help_ << std::string(groupindent + helpParams.helpindent, ' ') << *infoit << '\n';
+ }
+ }
+ if (hasoptions && hasarguments && helpParams.showTerminator)
+ {
+ lastDescriptionIsNewline = false;
+ for (const auto &item: Wrap(std::string("\"") + terminator + "\" can be used to terminate flag options and force all following arguments to be treated as positional options", helpParams.width - helpParams.flagindent))
+ {
+ help_ << std::string(helpParams.flagindent, ' ') << item << '\n';
+ }
+ }
+
+ if (!lastDescriptionIsNewline)
+ {
+ help_ << "\n";
+ }
+
+ for (const auto &line: epilog_text)
+ {
+ help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n";
+ }
+ }
+
+ /** Generate a help menu as a string.
+ *
+ * \return the help text as a single string
+ */
+ std::string Help() const
+ {
+ std::ostringstream help_;
+ Help(help_);
+ return help_.str();
+ }
+
+ virtual void Reset() noexcept override
+ {
+ Command::Reset();
+ matched = true;
+ readCompletion = false;
+ }
+
+ /** Parse all arguments.
+ *
+ * \param begin an iterator to the beginning of the argument list
+ * \param end an iterator to the past-the-end element of the argument list
+ * \return the iterator after the last parsed value. Only useful for kick-out
+ */
+ template
+ It ParseArgs(It begin, It end)
+ {
+ // Reset all Matched statuses and errors
+ Reset();
+#ifdef ARGS_NOEXCEPT
+ error = GetError();
+ if (error != Error::None)
+ {
+ return end;
+ }
+#endif
+ return Parse(begin, end);
+ }
+
+ /** Parse all arguments.
+ *
+ * \param args an iterable of the arguments
+ * \return the iterator after the last parsed value. Only useful for kick-out
+ */
+ template
+ auto ParseArgs(const T &args) -> decltype(std::begin(args))
+ {
+ return ParseArgs(std::begin(args), std::end(args));
+ }
+
+ /** Convenience function to parse the CLI from argc and argv
+ *
+ * Just assigns the program name and vectorizes arguments for passing into ParseArgs()
+ *
+ * \return whether or not all arguments were parsed. This works for detecting kick-out, but is generally useless as it can't do anything with it.
+ */
+ bool ParseCLI(const int argc, const char * const * argv)
+ {
+ if (Prog().empty())
+ {
+ Prog(argv[0]);
+ }
+ const std::vector args(argv + 1, argv + argc);
+ return ParseArgs(args) == std::end(args);
+ }
+
+ template
+ bool ParseCLI(const T &args)
+ {
+ return ParseArgs(args) == std::end(args);
+ }
+ };
+
+ inline Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_)
+ : command(parser_.SelectedCommand()), parser(std::move(args_), parser_, command, parser_.helpParams), oldSubparser(command.subparser)
+ {
+ command.subparser = &parser;
+ }
+
+ inline Command::RaiiSubparser::RaiiSubparser(const Command &command_, const HelpParams ¶ms_): command(command_), parser(command, params_), oldSubparser(command.subparser)
+ {
+ command.subparser = &parser;
+ }
+
+ inline void Subparser::Parse()
+ {
+ isParsed = true;
+ Reset();
+ command.subparserDescription = GetDescription(helpParams, 0);
+ command.subparserHasFlag = HasFlag();
+ command.subparserHasPositional = HasPositional();
+ command.subparserHasCommand = HasCommand();
+ command.subparserProgramLine = GetProgramLine(helpParams);
+ if (parser == nullptr)
+ {
+#ifndef ARGS_NOEXCEPT
+ throw args::SubparserError();
+#else
+ error = Error::Subparser;
+ return;
+#endif
+ }
+
+ auto it = parser->Parse(args.begin(), args.end());
+ command.Validate(parser->ShortPrefix(), parser->LongPrefix());
+ kicked.assign(it, args.end());
+
+#ifdef ARGS_NOEXCEPT
+ command.subparserError = GetError();
+#endif
+ }
+
+ inline std::ostream &operator<<(std::ostream &os, const ArgumentParser &parser)
+ {
+ parser.Help(os);
+ return os;
+ }
+
+ /** Boolean argument matcher
+ */
+ class Flag : public FlagBase
+ {
+ public:
+ Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): FlagBase(name_, help_, std::move(matcher_), options_)
+ {
+ group_.Add(*this);
+ }
+
+ Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false): Flag(group_, name_, help_, std::move(matcher_), extraError_ ? Options::Single : Options::None)
+ {
+ }
+
+ virtual ~Flag() {}
+
+ /** Get whether this was matched
+ */
+ bool Get() const
+ {
+ return Matched();
+ }
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ {
+ return 0;
+ }
+
+ virtual void ParseValue(const std::vector&) override
+ {
+ }
+ };
+
+ /** Help flag class
+ *
+ * Works like a regular flag, but throws an instance of Help when it is matched
+ */
+ class HelpFlag : public Flag
+ {
+ public:
+ HelpFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_ = {}): Flag(group_, name_, help_, std::move(matcher_), options_) {}
+
+ virtual ~HelpFlag() {}
+
+ virtual void ParseValue(const std::vector &)
+ {
+#ifdef ARGS_NOEXCEPT
+ error = Error::Help;
+ errorMsg = Name();
+#else
+ throw Help(Name());
+#endif
+ }
+
+ /** Get whether this was matched
+ */
+ bool Get() const noexcept
+ {
+ return Matched();
+ }
+ };
+
+ /** A flag class that simply counts the number of times it's matched
+ */
+ class CounterFlag : public Flag
+ {
+ private:
+ const int startcount;
+ int count;
+
+ public:
+ CounterFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const int startcount_ = 0, Options options_ = {}):
+ Flag(group_, name_, help_, std::move(matcher_), options_), startcount(startcount_), count(startcount_) {}
+
+ virtual ~CounterFlag() {}
+
+ virtual FlagBase *Match(const EitherFlag &arg) override
+ {
+ auto me = FlagBase::Match(arg);
+ if (me)
+ {
+ ++count;
+ }
+ return me;
+ }
+
+ /** Get the count
+ */
+ int &Get() noexcept
+ {
+ return count;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ FlagBase::Reset();
+ count = startcount;
+ }
+ };
+
+ /** A flag class that calls a function when it's matched
+ */
+ class ActionFlag : public FlagBase
+ {
+ private:
+ std::function &)> action;
+ Nargs nargs;
+
+ public:
+ ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, std::function &)> action_, Options options_ = {}):
+ FlagBase(name_, help_, std::move(matcher_), options_), action(std::move(action_)), nargs(nargs_)
+ {
+ group_.Add(*this);
+ }
+
+ ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}):
+ FlagBase(name_, help_, std::move(matcher_), options_), nargs(1)
+ {
+ group_.Add(*this);
+ action = [action_](const std::vector &a) { return action_(a.at(0)); };
+ }
+
+ ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}):
+ FlagBase(name_, help_, std::move(matcher_), options_), nargs(0)
+ {
+ group_.Add(*this);
+ action = [action_](const std::vector &) { return action_(); };
+ }
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ { return nargs; }
+
+ virtual void ParseValue(const std::vector &value) override
+ { action(value); }
+ };
+
+ /** A default Reader class for argument classes
+ *
+ * If destination type is assignable to std::string it uses an assignment to std::string.
+ * Otherwise ValueReader simply uses a std::istringstream to read into the destination type, and
+ * raises a ParseError if there are any characters left.
+ */
+ struct ValueReader
+ {
+ template
+ typename std::enable_if::value, bool>::type
+ operator ()(const std::string &name, const std::string &value, T &destination)
+ {
+ std::istringstream ss(value);
+ bool failed = !(ss >> destination);
+
+ if (!failed)
+ {
+ ss >> std::ws;
+ }
+
+ if (ss.rdbuf()->in_avail() > 0 || failed)
+ {
+#ifdef ARGS_NOEXCEPT
+ (void)name;
+ return false;
+#else
+ std::ostringstream problem;
+ problem << "Argument '" << name << "' received invalid value type '" << value << "'";
+ throw ParseError(problem.str());
+#endif
+ }
+ return true;
+ }
+
+ template
+ typename std::enable_if::value, bool>::type
+ operator()(const std::string &, const std::string &value, T &destination)
+ {
+ destination = value;
+ return true;
+ }
+ };
+
+ /** An argument-accepting flag class
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ typename Reader = ValueReader>
+ class ValueFlag : public ValueFlagBase
+ {
+ protected:
+ T value;
+ T defaultValue;
+
+ virtual std::string GetDefaultString(const HelpParams&) const override
+ {
+ return detail::ToString(defaultValue);
+ }
+
+ private:
+ Reader reader;
+
+ public:
+
+ ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), value(defaultValue_), defaultValue(defaultValue_)
+ {
+ group_.Add(*this);
+ }
+
+ ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), const bool extraError_ = false): ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, extraError_ ? Options::Single : Options::None)
+ {
+ }
+
+ ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): ValueFlag(group_, name_, help_, std::move(matcher_), T(), options_)
+ {
+ }
+
+ virtual ~ValueFlag() {}
+
+ virtual void ParseValue(const std::vector &values_) override
+ {
+ const std::string &value_ = values_.at(0);
+
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, this->value))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, this->value);
+#endif
+ }
+
+ virtual void Reset() noexcept override
+ {
+ ValueFlagBase::Reset();
+ value = defaultValue;
+ }
+
+ /** Get the value
+ */
+ T &Get() noexcept
+ {
+ return value;
+ }
+
+ /** Get the default value
+ */
+ const T &GetDefault() noexcept
+ {
+ return defaultValue;
+ }
+ };
+
+ /** An optional argument-accepting flag class
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ typename Reader = ValueReader>
+ class ImplicitValueFlag : public ValueFlag
+ {
+ protected:
+ T implicitValue;
+
+ public:
+
+ ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &implicitValue_, const T &defaultValue_ = T(), Options options_ = {})
+ : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(implicitValue_)
+ {
+ }
+
+ ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), Options options_ = {})
+ : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(defaultValue_)
+ {
+ }
+
+ ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_)
+ : ValueFlag(group_, name_, help_, std::move(matcher_), {}, options_), implicitValue()
+ {
+ }
+
+ virtual ~ImplicitValueFlag() {}
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ {
+ return {0, 1};
+ }
+
+ virtual void ParseValue(const std::vector &value_) override
+ {
+ if (value_.empty())
+ {
+ this->value = implicitValue;
+ } else
+ {
+ ValueFlag::ParseValue(value_);
+ }
+ }
+ };
+
+ /** A variadic arguments accepting flag class
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam List the list type that houses the values
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ template class List = detail::vector,
+ typename Reader = ValueReader>
+ class NargsValueFlag : public FlagBase
+ {
+ protected:
+
+ List values;
+ const List defaultValues;
+ Nargs nargs;
+ Reader reader;
+
+ public:
+
+ typedef List Container;
+ typedef T value_type;
+ typedef typename Container::allocator_type allocator_type;
+ typedef typename Container::pointer pointer;
+ typedef typename Container::const_pointer const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef typename Container::size_type size_type;
+ typedef typename Container::difference_type difference_type;
+ typedef typename Container::iterator iterator;
+ typedef typename Container::const_iterator const_iterator;
+ typedef std::reverse_iterator reverse_iterator;
+ typedef std::reverse_iterator const_reverse_iterator;
+
+ NargsValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, const List &defaultValues_ = {}, Options options_ = {})
+ : FlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_),nargs(nargs_)
+ {
+ group_.Add(*this);
+ }
+
+ virtual ~NargsValueFlag() {}
+
+ virtual Nargs NumberOfArguments() const noexcept override
+ {
+ return nargs;
+ }
+
+ virtual void ParseValue(const std::vector &values_) override
+ {
+ values.clear();
+
+ for (const std::string &value : values_)
+ {
+ T v;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value, v))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value, v);
+#endif
+ values.insert(std::end(values), v);
+ }
+ }
+
+ List &Get() noexcept
+ {
+ return values;
+ }
+
+ iterator begin() noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator begin() const noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator cbegin() const noexcept
+ {
+ return values.cbegin();
+ }
+
+ iterator end() noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator end() const noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator cend() const noexcept
+ {
+ return values.cend();
+ }
+
+ virtual void Reset() noexcept override
+ {
+ FlagBase::Reset();
+ values = defaultValues;
+ }
+
+ virtual FlagBase *Match(const EitherFlag &arg) override
+ {
+ const bool wasMatched = Matched();
+ auto me = FlagBase::Match(arg);
+ if (me && !wasMatched)
+ {
+ values.clear();
+ }
+ return me;
+ }
+ };
+
+ /** An argument-accepting flag class that pushes the found values into a list
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam List the list type that houses the values
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ template class List = detail::vector,
+ typename Reader = ValueReader>
+ class ValueFlagList : public ValueFlagBase
+ {
+ private:
+ using Container = List;
+ Container values;
+ const Container defaultValues;
+ Reader reader;
+
+ public:
+
+ typedef T value_type;
+ typedef typename Container::allocator_type allocator_type;
+ typedef typename Container::pointer pointer;
+ typedef typename Container::const_pointer const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef typename Container::size_type size_type;
+ typedef typename Container::difference_type difference_type;
+ typedef typename Container::iterator iterator;
+ typedef typename Container::const_iterator const_iterator;
+ typedef std::reverse_iterator reverse_iterator;
+ typedef std::reverse_iterator const_reverse_iterator;
+
+ ValueFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Container &defaultValues_ = Container(), Options options_ = {}):
+ ValueFlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_)
+ {
+ group_.Add(*this);
+ }
+
+ virtual ~ValueFlagList() {}
+
+ virtual void ParseValue(const std::vector &values_) override
+ {
+ const std::string &value_ = values_.at(0);
+
+ T v;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, v))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, v);
+#endif
+ values.insert(std::end(values), v);
+ }
+
+ /** Get the values
+ */
+ Container &Get() noexcept
+ {
+ return values;
+ }
+
+ virtual std::string Name() const override
+ {
+ return name + std::string("...");
+ }
+
+ virtual void Reset() noexcept override
+ {
+ ValueFlagBase::Reset();
+ values = defaultValues;
+ }
+
+ virtual FlagBase *Match(const EitherFlag &arg) override
+ {
+ const bool wasMatched = Matched();
+ auto me = FlagBase::Match(arg);
+ if (me && !wasMatched)
+ {
+ values.clear();
+ }
+ return me;
+ }
+
+ iterator begin() noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator begin() const noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator cbegin() const noexcept
+ {
+ return values.cbegin();
+ }
+
+ iterator end() noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator end() const noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator cend() const noexcept
+ {
+ return values.cend();
+ }
+ };
+
+ /** A mapping value flag class
+ *
+ * \tparam K the type to extract the argument as
+ * \tparam T the type to store the result as
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ * \tparam Map The Map type. Should operate like std::map or std::unordered_map
+ */
+ template <
+ typename K,
+ typename T,
+ typename Reader = ValueReader,
+ template class Map = detail::unordered_map>
+ class MapFlag : public ValueFlagBase
+ {
+ private:
+ const Map map;
+ T value;
+ const T defaultValue;
+ Reader reader;
+
+ protected:
+ virtual std::vector GetChoicesStrings(const HelpParams &) const override
+ {
+ return detail::MapKeysToStrings(map);
+ }
+
+ public:
+
+ MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), map(map_), value(defaultValue_), defaultValue(defaultValue_)
+ {
+ group_.Add(*this);
+ }
+
+ MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_ = T(), const bool extraError_ = false): MapFlag(group_, name_, help_, std::move(matcher_), map_, defaultValue_, extraError_ ? Options::Single : Options::None)
+ {
+ }
+
+ MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, Options options_): MapFlag(group_, name_, help_, std::move(matcher_), map_, T(), options_)
+ {
+ }
+
+ virtual ~MapFlag() {}
+
+ virtual void ParseValue(const std::vector &values_) override
+ {
+ const std::string &value_ = values_.at(0);
+
+ K key;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, key))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, key);
+#endif
+ auto it = map.find(key);
+ if (it == std::end(map))
+ {
+ std::ostringstream problem;
+ problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Map;
+ errorMsg = problem.str();
+#else
+ throw MapError(problem.str());
+#endif
+ } else
+ {
+ this->value = it->second;
+ }
+ }
+
+ /** Get the value
+ */
+ T &Get() noexcept
+ {
+ return value;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ ValueFlagBase::Reset();
+ value = defaultValue;
+ }
+ };
+
+ /** A mapping value flag list class
+ *
+ * \tparam K the type to extract the argument as
+ * \tparam T the type to store the result as
+ * \tparam List the list type that houses the values
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ * \tparam Map The Map type. Should operate like std::map or std::unordered_map
+ */
+ template <
+ typename K,
+ typename T,
+ template class List = detail::vector,
+ typename Reader = ValueReader,
+ template class Map = detail::unordered_map>
+ class MapFlagList : public ValueFlagBase
+ {
+ private:
+ using Container = List;
+ const Map map;
+ Container values;
+ const Container defaultValues;
+ Reader reader;
+
+ protected:
+ virtual std::vector GetChoicesStrings(const HelpParams &) const override
+ {
+ return detail::MapKeysToStrings(map);
+ }
+
+ public:
+ typedef T value_type;
+ typedef typename Container::allocator_type allocator_type;
+ typedef typename Container::pointer pointer;
+ typedef typename Container::const_pointer const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef typename Container::size_type size_type;
+ typedef typename Container::difference_type difference_type;
+ typedef typename Container::iterator iterator;
+ typedef typename Container::const_iterator const_iterator;
+ typedef std::reverse_iterator reverse_iterator;
+ typedef std::reverse_iterator const_reverse_iterator;
+
+ MapFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const Container &defaultValues_ = Container()): ValueFlagBase(name_, help_, std::move(matcher_)), map(map_), values(defaultValues_), defaultValues(defaultValues_)
+ {
+ group_.Add(*this);
+ }
+
+ virtual ~MapFlagList() {}
+
+ virtual void ParseValue(const std::vector &values_) override
+ {
+ const std::string &value = values_.at(0);
+
+ K key;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value, key))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value, key);
+#endif
+ auto it = map.find(key);
+ if (it == std::end(map))
+ {
+ std::ostringstream problem;
+ problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Map;
+ errorMsg = problem.str();
+#else
+ throw MapError(problem.str());
+#endif
+ } else
+ {
+ this->values.emplace_back(it->second);
+ }
+ }
+
+ /** Get the value
+ */
+ Container &Get() noexcept
+ {
+ return values;
+ }
+
+ virtual std::string Name() const override
+ {
+ return name + std::string("...");
+ }
+
+ virtual void Reset() noexcept override
+ {
+ ValueFlagBase::Reset();
+ values = defaultValues;
+ }
+
+ virtual FlagBase *Match(const EitherFlag &arg) override
+ {
+ const bool wasMatched = Matched();
+ auto me = FlagBase::Match(arg);
+ if (me && !wasMatched)
+ {
+ values.clear();
+ }
+ return me;
+ }
+
+ iterator begin() noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator begin() const noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator cbegin() const noexcept
+ {
+ return values.cbegin();
+ }
+
+ iterator end() noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator end() const noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator cend() const noexcept
+ {
+ return values.cend();
+ }
+ };
+
+ /** A positional argument class
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ typename Reader = ValueReader>
+ class Positional : public PositionalBase
+ {
+ private:
+ T value;
+ const T defaultValue;
+ Reader reader;
+ public:
+ Positional(Group &group_, const std::string &name_, const std::string &help_, const T &defaultValue_ = T(), Options options_ = {}): PositionalBase(name_, help_, options_), value(defaultValue_), defaultValue(defaultValue_)
+ {
+ group_.Add(*this);
+ }
+
+ Positional(Group &group_, const std::string &name_, const std::string &help_, Options options_): Positional(group_, name_, help_, T(), options_)
+ {
+ }
+
+ virtual ~Positional() {}
+
+ virtual void ParseValue(const std::string &value_) override
+ {
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, this->value))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, this->value);
+#endif
+ ready = false;
+ matched = true;
+ }
+
+ /** Get the value
+ */
+ T &Get() noexcept
+ {
+ return value;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ PositionalBase::Reset();
+ value = defaultValue;
+ }
+ };
+
+ /** A positional argument class that pushes the found values into a list
+ *
+ * \tparam T the type to extract the argument as
+ * \tparam List the list type that houses the values
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ */
+ template <
+ typename T,
+ template class List = detail::vector,
+ typename Reader = ValueReader>
+ class PositionalList : public PositionalBase
+ {
+ private:
+ using Container = List;
+ Container values;
+ const Container defaultValues;
+ Reader reader;
+
+ public:
+ typedef T value_type;
+ typedef typename Container::allocator_type allocator_type;
+ typedef typename Container::pointer pointer;
+ typedef typename Container::const_pointer const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef typename Container::size_type size_type;
+ typedef typename Container::difference_type difference_type;
+ typedef typename Container::iterator iterator;
+ typedef typename Container::const_iterator const_iterator;
+ typedef std::reverse_iterator reverse_iterator;
+ typedef std::reverse_iterator const_reverse_iterator;
+
+ PositionalList(Group &group_, const std::string &name_, const std::string &help_, const Container &defaultValues_ = Container(), Options options_ = {}): PositionalBase(name_, help_, options_), values(defaultValues_), defaultValues(defaultValues_)
+ {
+ group_.Add(*this);
+ }
+
+ PositionalList(Group &group_, const std::string &name_, const std::string &help_, Options options_): PositionalList(group_, name_, help_, {}, options_)
+ {
+ }
+
+ virtual ~PositionalList() {}
+
+ virtual void ParseValue(const std::string &value_) override
+ {
+ T v;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, v))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, v);
+#endif
+ values.insert(std::end(values), v);
+ matched = true;
+ }
+
+ virtual std::string Name() const override
+ {
+ return name + std::string("...");
+ }
+
+ /** Get the values
+ */
+ Container &Get() noexcept
+ {
+ return values;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ PositionalBase::Reset();
+ values = defaultValues;
+ }
+
+ virtual PositionalBase *GetNextPositional() override
+ {
+ const bool wasMatched = Matched();
+ auto me = PositionalBase::GetNextPositional();
+ if (me && !wasMatched)
+ {
+ values.clear();
+ }
+ return me;
+ }
+
+ iterator begin() noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator begin() const noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator cbegin() const noexcept
+ {
+ return values.cbegin();
+ }
+
+ iterator end() noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator end() const noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator cend() const noexcept
+ {
+ return values.cend();
+ }
+ };
+
+ /** A positional argument mapping class
+ *
+ * \tparam K the type to extract the argument as
+ * \tparam T the type to store the result as
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ * \tparam Map The Map type. Should operate like std::map or std::unordered_map
+ */
+ template <
+ typename K,
+ typename T,
+ typename Reader = ValueReader,
+ template class Map = detail::unordered_map>
+ class MapPositional : public PositionalBase
+ {
+ private:
+ const Map map;
+ T value;
+ const T defaultValue;
+ Reader reader;
+
+ protected:
+ virtual std::vector GetChoicesStrings(const HelpParams &) const override
+ {
+ return detail::MapKeysToStrings(map);
+ }
+
+ public:
+
+ MapPositional(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const T &defaultValue_ = T(), Options options_ = {}):
+ PositionalBase(name_, help_, options_), map(map_), value(defaultValue_), defaultValue(defaultValue_)
+ {
+ group_.Add(*this);
+ }
+
+ virtual ~MapPositional() {}
+
+ virtual void ParseValue(const std::string &value_) override
+ {
+ K key;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, key))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, key);
+#endif
+ auto it = map.find(key);
+ if (it == std::end(map))
+ {
+ std::ostringstream problem;
+ problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Map;
+ errorMsg = problem.str();
+#else
+ throw MapError(problem.str());
+#endif
+ } else
+ {
+ this->value = it->second;
+ ready = false;
+ matched = true;
+ }
+ }
+
+ /** Get the value
+ */
+ T &Get() noexcept
+ {
+ return value;
+ }
+
+ virtual void Reset() noexcept override
+ {
+ PositionalBase::Reset();
+ value = defaultValue;
+ }
+ };
+
+ /** A positional argument mapping list class
+ *
+ * \tparam K the type to extract the argument as
+ * \tparam T the type to store the result as
+ * \tparam List the list type that houses the values
+ * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+ * \tparam Map The Map type. Should operate like std::map or std::unordered_map
+ */
+ template <
+ typename K,
+ typename T,
+ template class List = detail::vector,
+ typename Reader = ValueReader,
+ template class Map = detail::unordered_map>
+ class MapPositionalList : public PositionalBase
+ {
+ private:
+ using Container = List;
+
+ const Map map;
+ Container values;
+ const Container defaultValues;
+ Reader reader;
+
+ protected:
+ virtual std::vector GetChoicesStrings(const HelpParams &) const override
+ {
+ return detail::MapKeysToStrings(map);
+ }
+
+ public:
+ typedef T value_type;
+ typedef typename Container::allocator_type allocator_type;
+ typedef typename Container::pointer pointer;
+ typedef typename Container::const_pointer const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef typename Container::size_type size_type;
+ typedef typename Container::difference_type difference_type;
+ typedef typename Container::iterator iterator;
+ typedef typename Container::const_iterator const_iterator;
+ typedef std::reverse_iterator reverse_iterator;
+ typedef std::reverse_iterator const_reverse_iterator;
+
+ MapPositionalList(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const Container &defaultValues_ = Container(), Options options_ = {}):
+ PositionalBase(name_, help_, options_), map(map_), values(defaultValues_), defaultValues(defaultValues_)
+ {
+ group_.Add(*this);
+ }
+
+ virtual ~MapPositionalList() {}
+
+ virtual void ParseValue(const std::string &value_) override
+ {
+ K key;
+#ifdef ARGS_NOEXCEPT
+ if (!reader(name, value_, key))
+ {
+ error = Error::Parse;
+ }
+#else
+ reader(name, value_, key);
+#endif
+ auto it = map.find(key);
+ if (it == std::end(map))
+ {
+ std::ostringstream problem;
+ problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+ error = Error::Map;
+ errorMsg = problem.str();
+#else
+ throw MapError(problem.str());
+#endif
+ } else
+ {
+ this->values.emplace_back(it->second);
+ matched = true;
+ }
+ }
+
+ /** Get the value
+ */
+ Container &Get() noexcept
+ {
+ return values;
+ }
+
+ virtual std::string Name() const override
+ {
+ return name + std::string("...");
+ }
+
+ virtual void Reset() noexcept override
+ {
+ PositionalBase::Reset();
+ values = defaultValues;
+ }
+
+ virtual PositionalBase *GetNextPositional() override
+ {
+ const bool wasMatched = Matched();
+ auto me = PositionalBase::GetNextPositional();
+ if (me && !wasMatched)
+ {
+ values.clear();
+ }
+ return me;
+ }
+
+ iterator begin() noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator begin() const noexcept
+ {
+ return values.begin();
+ }
+
+ const_iterator cbegin() const noexcept
+ {
+ return values.cbegin();
+ }
+
+ iterator end() noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator end() const noexcept
+ {
+ return values.end();
+ }
+
+ const_iterator cend() const noexcept
+ {
+ return values.cend();
+ }
+ };
+}
+
+#endif
diff --git a/examples/linux/streaming/external/json/json.hpp b/examples/linux/streaming/external/json/json.hpp
new file mode 100644
index 0000000..cc822a5
--- /dev/null
+++ b/examples/linux/streaming/external/json/json.hpp
@@ -0,0 +1,24665 @@
+/*
+ __ _____ _____ _____
+ __| | __| | | | JSON for Modern C++
+| | |__ | | | | | | version 3.8.0
+|_____|_____|_____|_|___| https://github.com/nlohmann/json
+
+Licensed under the MIT License .
+SPDX-License-Identifier: MIT
+Copyright (c) 2013-2019 Niels Lohmann .
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE 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 COPYRIGHT HOLDERS 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 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef INCLUDE_NLOHMANN_JSON_HPP_
+#define INCLUDE_NLOHMANN_JSON_HPP_
+
+#define NLOHMANN_JSON_VERSION_MAJOR 3
+#define NLOHMANN_JSON_VERSION_MINOR 8
+#define NLOHMANN_JSON_VERSION_PATCH 0
+
+#include // all_of, find, for_each
+#include // assert
+#include // nullptr_t, ptrdiff_t, size_t
+#include