diff --git a/net/webrtc/examples/README.md b/net/webrtc/examples/README.md index 4088e8f6..9142dadf 100644 --- a/net/webrtc/examples/README.md +++ b/net/webrtc/examples/README.md @@ -129,3 +129,65 @@ cargo r --example webrtc-precise-sync-recv -- --expect-clock-signalling cargo r --example webrtc-precise-sync-send -- --clock ntp --do-clock-signalling \ --video-streams 0 --audio-streams 2 ``` + +## Android + +### `webrtcsrc` based Android application + +An Android demonstration application which retrieves available producers from +the signaller and renders audio and video streams. + +**Important**: in order to ease testing, this demonstration application enables +unencrypted network communication. See `app/src/main/AndroidManifest.xml` for +details. + +#### Build the application + +* Download the latest Android prebuilt binaries from: + https://gstreamer.freedesktop.org/download/ +* Uncompress / untar the package, e.g. under `/opt/android/`. +* Define the `GSTREAMER_ROOT_ANDROID` environment variable with the + directory chosen at previous step. +* Install a recent version of Android Studio (tested with 2023.3.1.18). +* Open the project from the folder `android/webrtcsrc`. +* Have Android Studio download and install the required SDK & NDK. +* Click the build button or build and run on the target device. +* The resulting `apk` is generated under: + `android/webrtcsrc/app/build/outputs/apk/debug`. + +For more details, refer to: +* https://gstreamer.freedesktop.org/documentation/installing/for-android-development.html + +Once the SDK & NDK are installed, you can use `gradlew` to build and install +the apk (make sure the device is visible from adb): + +```shell +# From the android/webrtcsrc directory +./gradlew installDebug +``` + +#### Install the application + +Prerequisites: activate developer mode on the target device. + +There are several ways to install the application: + +* The easiest is to click the run button in Android Studio. +* You can also install the `apk` using `adb`. + +Depending on your host OS, you might need to define `udev` rules. See: +https://github.com/M0Rf30/android-udev-rules + +#### Setup + +1. Run the Signaller from the `gst-plugins-rs` root directory: + ```shell + cargo run --bin gst-webrtc-signalling-server + ``` +2. In the Android app, tap the 3 dots button -> Settings and edit the Signaller + URI. +3. Add a producer, e.g. using `gst-launch` & `webrtcsink` or run: + ```shell + cargo r --example webrtc-precise-sync-send + ``` +4. Click the `Refresh` button on the Producer List view of the app. diff --git a/net/webrtc/examples/android/webrtcsrc/.gitignore b/net/webrtc/examples/android/webrtcsrc/.gitignore new file mode 100644 index 00000000..123b7b0d --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# white list +!.gradle/config.properties diff --git a/net/webrtc/examples/android/webrtcsrc/.gradle/config.properties b/net/webrtc/examples/android/webrtcsrc/.gradle/config.properties new file mode 100644 index 00000000..c497a67f --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/.gradle/config.properties @@ -0,0 +1,2 @@ +#Fri May 17 23:09:24 CEST 2024 +java.home=/app/extra/android-studio/jbr diff --git a/net/webrtc/examples/android/webrtcsrc/app/.gitignore b/net/webrtc/examples/android/webrtcsrc/app/.gitignore new file mode 100644 index 00000000..cac73620 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/.gitignore @@ -0,0 +1,4 @@ +build/ +.externalNativeBuild/ +assets/ +gst-android-build/ diff --git a/net/webrtc/examples/android/webrtcsrc/app/build.gradle.kts b/net/webrtc/examples/android/webrtcsrc/app/build.gradle.kts new file mode 100644 index 00000000..52e4ee95 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/build.gradle.kts @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat +import java.util.Date + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.androidx.navigation.safeargs) + kotlin("plugin.serialization") version "1.9.22" +} + +android { + namespace = "org.freedesktop.gstreamer.examples.webrtcsrc" + compileSdk = 34 + ndkVersion = "25.2.9519653" + + defaultConfig { + applicationId = "org.freedesktop.gstreamer.examples.WebRTCSrc" + minSdk = 28 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + + externalNativeBuild { + ndkBuild { + var gstRoot: String? + if (project.hasProperty("gstAndroidRoot")) + gstRoot = project.property("gstAndroidRoot").toString() + else + gstRoot = System.getenv("GSTREAMER_ROOT_ANDROID") + if (gstRoot == null) + throw GradleException("GSTREAMER_ROOT_ANDROID must be set, or 'gstAndroidRoot' must be defined in your gradle.properties in the top level directory of the unpacked universal GStreamer Android binaries") + + arguments("NDK_APPLICATION_MK=src/main/cpp/Application.mk", "GSTREAMER_JAVA_SRC_DIR=src/main/java", "GSTREAMER_ROOT_ANDROID=$gstRoot", "V=1") + + targets("gstreamer_webrtcsrc") + + // All archs except MIPS and MIPS64 are supported + //abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64") + // FIXME for some reasons x86 generation fails (observed with gstreamer-1.0-android-universal-1.24.3) + abiFilters("armeabi-v7a", "arm64-v8a", "x86_64") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + applicationVariants.all { + val variant = this + variant.outputs + .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } + .forEach { output -> + val date = SimpleDateFormat("YYYYMMdd").format(Date()) + val outputFileName = "GstExamples.WebRTCSrc-${variant.baseName}-${variant.versionName}-${date}.apk" + output.outputFileName = outputFileName + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } + externalNativeBuild { + ndkBuild { + path = file("src/main/cpp/Android.mk") + } + } +} + +dependencies { + implementation(libs.androidx.appcompat) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.preference) + implementation(libs.androidx.recyclerview) + implementation(libs.androidx.work.runtime.ktx) + implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.okhttp) + implementation(libs.material) +} diff --git a/net/webrtc/examples/android/webrtcsrc/app/proguard-rules.pro b/net/webrtc/examples/android/webrtcsrc/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/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/net/webrtc/examples/android/webrtcsrc/app/src/main/AndroidManifest.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d3385ba8 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Android.mk b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Android.mk new file mode 100644 index 00000000..0fc1b5b9 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Android.mk @@ -0,0 +1,47 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := gstreamer_webrtcsrc +LOCAL_SRC_FILES := WebRTCSrc.c dummy.cpp + +LOCAL_SHARED_LIBRARIES := gstreamer_android +LOCAL_LDLIBS := -llog -landroid +include $(BUILD_SHARED_LIBRARY) + +ifndef GSTREAMER_ROOT_ANDROID +$(error GSTREAMER_ROOT_ANDROID is not defined!) +endif + +ifeq ($(TARGET_ARCH_ABI),armeabi) +GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/arm +else ifeq ($(TARGET_ARCH_ABI),armeabi-v7a) +GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/armv7 +else ifeq ($(TARGET_ARCH_ABI),arm64-v8a) +GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/arm64 +else ifeq ($(TARGET_ARCH_ABI),x86) +GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/x86 +else ifeq ($(TARGET_ARCH_ABI),x86_64) +GSTREAMER_ROOT := $(GSTREAMER_ROOT_ANDROID)/x86_64 +else +$(error Target arch ABI not supported: $(TARGET_ARCH_ABI)) +endif + +GSTREAMER_NDK_BUILD_PATH := $(GSTREAMER_ROOT)/share/gst-android/ndk-build/ + +include $(GSTREAMER_NDK_BUILD_PATH)/plugins.mk + +GSTREAMER_PLUGINS_CORE_CUSTOM := coreelements app audioconvert audiorate audioresample videorate videoconvertscale autodetect +GSTREAMER_PLUGINS_CODECS_CUSTOM := videoparsersbad vpx opus audioparsers opusparse androidmedia +GSTREAMER_PLUGINS_NET_CUSTOM := tcp rtsp rtp rtpmanager udp srtp webrtc dtls nice rswebrtc rsrtp +GSTREAMER_PLUGINS_PLAYBACK := playback +GSTREAMER_PLUGINS := $(GSTREAMER_PLUGINS_CORE_CUSTOM) $(GSTREAMER_PLUGINS_CODECS_CUSTOM) $(GSTREAMER_PLUGINS_NET_CUSTOM) \ + $(GSTREAMER_PLUGINS_ENCODING) \ + $(GSTREAMER_PLUGINS_SYS) \ + $(GSTREAMER_PLUGINS_PLAYBACK) + +GSTREAMER_EXTRA_DEPS := gstreamer-video-1.0 glib-2.0 + +G_IO_MODULES = openssl + +include $(GSTREAMER_NDK_BUILD_PATH)/gstreamer-1.0.mk diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Application.mk b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Application.mk new file mode 100644 index 00000000..fd2823ee --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/Application.mk @@ -0,0 +1,4 @@ +APP_PLATFORM = 15 +APP_ABI = armeabi-v7a arm64-v8a x86 x86_64 +APP_STL = c++_shared +#APP_ABI = armeabi-v7a arm64-v8a x86_64 diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/WebRTCSrc.c b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/WebRTCSrc.c new file mode 100644 index 00000000..f3d07f8b --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/WebRTCSrc.c @@ -0,0 +1,483 @@ +/* Android GStreamer WebRTCSrc native code. + * + * The following file implements the native logic for an Android application to be able to + * to start an audio / video stream from a producer using GStreamer WebRTCSrc. This is designed + * to work in tandem with the managed Kotlin class `WebRTCSrc`. + * + * - The managed class is in charge of spawning an event loop, initializing the native libraries and + * scheduling calls to native functions (such as `startPipeline`, `stopPipeline`, `setSurface`, ...). + * The native implementation of these functions expects the calls to be handled one at a time, + * which is the case when scheduling is handled by an `android.os.Handler`. + * - See the `native_methods[]` for the mapping between the managed class and the native functions. + * - See also the comment for `attach_current_thread()` regarding calling managed call from a thread + * spawned by C code. + */ + +#include +#include +#include +#include +#include +#include + +#include + +GST_DEBUG_CATEGORY_STATIC (debug_category); +#define GST_CAT_DEFAULT debug_category + +/* The WebRTCSrc Context which gathers objects needed between native calls for a streaming session */ +typedef struct _WebRTCSrcCtx { + jobject managed_webrtcsrc; + GstElement *pipe; + GstBus *bus; + GMutex video_pad_mutex; + ANativeWindow *native_window; + GstElement *webrtcsrc, *video_sink; +} WebRTCSrcCtx; + +static JavaVM *java_vm; +/* The identifier of the field of the managed class where the WebRTCSrc Context from which + * current streaming session can be retrieved */ +static jfieldID webrtcsrc_ctx_field_id; +static jmethodID on_error_method_id; +static jmethodID on_latency_message_method_id; + +/* Retrieves the WebRTCSrc Context for current streaming session + * from the managed WebRTCSrc object `self` */ +static WebRTCSrcCtx * +get_webrtcsrc_ctx(JNIEnv *env, jobject self) { + jobject field = (*env)->GetObjectField(env, self, webrtcsrc_ctx_field_id); + return (WebRTCSrcCtx *) (field != NULL ? (*env)->GetDirectBufferAddress(env, field) : NULL); +} + +/* Registers this thread with the VM + * + * - Calls from the managed code originate from a JVM thread which don't need to be attached. + * - Calls from threads spawned by C code must be attached before calling managed code. + * Ex.: `on_error()` is called from the `bus` sync handler, which is triggered on the thread + * on which the error originated. + * + * The attached thread must be detached when no more managed code is to be called. + * See `detach_current_thread()`. + */ +static JNIEnv * +attach_current_thread() { + JNIEnv *env; + JavaVMAttachArgs args; + + GST_LOG("Attaching thread %p", g_thread_self()); + args.version = JNI_VERSION_1_4; + args.name = NULL; + args.group = NULL; + + if ((*java_vm)->AttachCurrentThread(java_vm, &env, &args) < 0) { + GST_ERROR ("Failed to attach current thread"); + return NULL; + } + + return env; +} + +/* Unregisters this thread from the VM + * + * See `attach_current_thread()` for details. + */ +static void +detach_current_thread() { + GST_LOG("Detaching thread %p", g_thread_self()); + (*java_vm)->DetachCurrentThread(java_vm); +} + +static GstElement * +handle_media_stream(GstPad *pad, GstElement *pipe, const char *convert_name, + const char *sink_name) { + GstPad *queue_pad; + GstElement *queue, *conv, *sink; + GstPadLinkReturn ret; + + queue = gst_element_factory_make("queue", NULL); + g_assert(queue); + conv = gst_element_factory_make(convert_name, NULL); + g_assert(conv); + sink = gst_element_factory_make(sink_name, NULL); + g_assert(sink); + if (g_strcmp0(convert_name, "audioconvert") == 0) { + GstElement *resample = gst_element_factory_make("audioresample", NULL); + g_assert_nonnull(resample); + gst_bin_add_many(GST_BIN(pipe), queue, conv, resample, sink, NULL); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(conv); + gst_element_sync_state_with_parent(resample); + gst_element_sync_state_with_parent(sink); + gst_element_link_many(queue, conv, resample, sink, NULL); + } else { + gst_bin_add_many(GST_BIN(pipe), queue, conv, sink, NULL); + gst_element_sync_state_with_parent(queue); + gst_element_sync_state_with_parent(conv); + gst_element_sync_state_with_parent(sink); + gst_element_link_many(queue, conv, sink, NULL); + } + + queue_pad = gst_element_get_static_pad(queue, "sink"); + + ret = gst_pad_link(pad, queue_pad); + g_assert(ret == GST_PAD_LINK_OK); + gst_object_unref(queue_pad); + + return sink; +} + +static void +on_incoming_stream(__attribute__((unused)) GstElement *webrtcsrc, GstPad *pad, WebRTCSrcCtx *ctx) { + const gchar *name = gst_pad_get_name(pad); + + if (g_str_has_prefix(name, "video")) { + g_mutex_lock(&ctx->video_pad_mutex); + + if (ctx->video_sink == NULL) { + GST_DEBUG("Handling video pad %s", name); + ctx->video_sink = + handle_media_stream(pad, ctx->pipe, "videoconvert", + "glimagesink"); + if (ctx->native_window) + gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(ctx->video_sink), + (guintptr) ctx->native_window); + } else { + GstElement *sink; + GstPad *sinkpad; + GstPadLinkReturn ret; + + GST_INFO("Ignoring additional video pad %s", name); + sink = gst_element_factory_make("fakesink", NULL); + g_assert(sink); + + gst_bin_add(GST_BIN(ctx->pipe), sink); + gst_element_sync_state_with_parent(sink); + + sinkpad = gst_element_get_static_pad(sink, "sink"); + ret = gst_pad_link(pad, sinkpad); + g_assert(ret == GST_PAD_LINK_OK); + gst_object_unref(sinkpad); + } + + g_mutex_unlock(&ctx->video_pad_mutex); + } else if (g_str_has_prefix(name, "audio")) { + GST_DEBUG("Handling audio stream %s", name); + handle_media_stream(pad, ctx->pipe, "audioconvert", "autoaudiosink"); + } else { + GST_ERROR("Ignoring unknown pad %s", name); + } +} + +static void +on_error(JNIEnv *env, WebRTCSrcCtx *ctx, GError *err) { + jstring error_msg = (*env)->NewStringUTF(env, err->message); + (*env)->CallVoidMethod(env, ctx->managed_webrtcsrc, on_error_method_id, error_msg); + + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + + (*env)->DeleteLocalRef(env, error_msg); +} + +static GstBusSyncReply +bus_sync_handler(__attribute__((unused)) GstBus *bus, GstMessage *message, gpointer user_data) { + /* Ideally, we would pass the `GstMessage *` to the managed code, which would act + * as an asynchronous handler and decide what to do about it. However, passing a C pointer + * to managed code is a bit clumsy. For now, we handle the few cases on a case by case basis. + * + * Possible solution would be to use: https://kotlinlang.org/docs/native-c-interop.html + */ + + WebRTCSrcCtx *ctx = user_data; + + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_ERROR: { + JNIEnv *env; + GError *error = NULL; + gchar *debug = NULL; + + gst_message_parse_error(message, &error, &debug); + GST_ERROR("Error on bus: %s (debug: %s)", error->message, debug); + + /* Pass error message to app + * This function is called from a thread spawned by C code so we need to attach it + * before calling managed code. + */ + env = attach_current_thread(); + g_assert(env != NULL); + on_error(env, ctx, error); + detach_current_thread(); + + g_error_free(error); + g_free(debug); + break; + } + case GST_MESSAGE_WARNING: { + GError *error = NULL; + gchar *debug = NULL; + + gst_message_parse_warning(message, &error, &debug); + GST_WARNING("Warning on bus: %s (debug: %s)", error->message, debug); + g_error_free(error); + g_free(debug); + break; + } + case GST_MESSAGE_LATENCY: { + GST_LOG("Got Latency message"); + JNIEnv *env; + /* Notify the app a Latency message was posted + * This will cause the managed WebRTCSrc to trigger `recalculateLatency()`, + * which we couldn't trigger call immediately from here nor via `gst_element_call_async` + * without causing deadlocks. + */ + env = attach_current_thread(); + g_assert(env != NULL); + (*env)->CallVoidMethod(env, ctx->managed_webrtcsrc, on_latency_message_method_id); + detach_current_thread(); + + break; + } + default: + break; + } + + return GST_BUS_DROP; +} + +/* + * Java Bindings + */ + +static void +stop_pipeline(JNIEnv *env, jobject self) { + WebRTCSrcCtx *ctx = get_webrtcsrc_ctx(env, self); + + if (!ctx) + return; + + if (!ctx->pipe) + return; + + GST_DEBUG("Stopping pipeline"); + + gst_element_set_state(GST_ELEMENT(ctx->pipe), GST_STATE_NULL); + + gst_bus_set_sync_handler(ctx->bus, NULL, NULL, NULL); + gst_object_unref(ctx->bus); + gst_object_unref(ctx->pipe); + g_mutex_clear(&ctx->video_pad_mutex); + + ctx->webrtcsrc = NULL; + ctx->video_sink = NULL; + ctx->pipe = NULL; + + GST_DEBUG("Pipeline stopped"); +} + +static void +free_webrtcsrc_ctx(JNIEnv *env, jobject self) { + WebRTCSrcCtx *ctx = get_webrtcsrc_ctx(env, self); + + if (!ctx) + return; + + stop_pipeline(env, self); + + GST_DEBUG("Freeing Context"); + + (*env)->DeleteGlobalRef(env, ctx->managed_webrtcsrc); + + g_free(ctx); + (*env)->SetObjectField(env, self, webrtcsrc_ctx_field_id, NULL); +} + +static void +start_pipeline(JNIEnv *env, jobject self, jstring j_signaller_uri, jstring j_peer_id, + jboolean prefer_opus_hard_dec) { + WebRTCSrcCtx *ctx = get_webrtcsrc_ctx(env, self); + const gchar *signaller_uri, *peer_id; + GObject *signaller; + GstStateChangeReturn ret; + + g_assert(!ctx); + ctx = g_new0(WebRTCSrcCtx, 1); + (*env)->SetObjectField(env, + self, + webrtcsrc_ctx_field_id, + (*env)->NewDirectByteBuffer(env, (void *) ctx, sizeof(WebRTCSrcCtx))); + ctx->managed_webrtcsrc = (*env)->NewGlobalRef(env, self); + + /* Preferred OPUS decoder */ + GstRegistry *reg = gst_registry_get(); + GstPluginFeature *opusdec = gst_registry_lookup_feature(reg, "opusdec"); + if (opusdec != NULL) { + if (prefer_opus_hard_dec) { + GST_INFO("Lowering rank for 'opusdec'"); + gst_plugin_feature_set_rank(opusdec, GST_RANK_PRIMARY - 1); + } else { + GST_INFO("Raising rank for 'opusdec'"); + gst_plugin_feature_set_rank(opusdec, GST_RANK_PRIMARY + 1); + } + + gst_object_unref(opusdec); + } else { + GST_WARNING("Couldn't find element 'opusdec'"); + } + + GST_DEBUG("Preparing pipeline"); + + g_mutex_init(&ctx->video_pad_mutex); + ctx->video_sink = NULL; + ctx->pipe = gst_pipeline_new(NULL); + ctx->webrtcsrc = gst_element_factory_make("webrtcsrc", NULL); + g_assert(ctx->webrtcsrc != NULL); + gst_bin_add(GST_BIN(ctx->pipe), ctx->webrtcsrc); + + g_object_get(ctx->webrtcsrc, "signaller", &signaller, NULL); + g_assert(signaller != NULL); + + /* Signaller Uri & Peer Id */ + signaller_uri = (*env)->GetStringUTFChars(env, j_signaller_uri, NULL); + peer_id = (*env)->GetStringUTFChars(env, j_peer_id, NULL); + + GST_DEBUG("Setting producer id %s from signaller %s", peer_id, signaller_uri); + g_object_set(signaller, + "uri", signaller_uri, + "producer-peer-id", peer_id, NULL); + + g_object_unref(signaller); + + (*env)->ReleaseStringUTFChars(env, j_signaller_uri, signaller_uri); + (*env)->ReleaseStringUTFChars(env, j_peer_id, peer_id); + + /* Aiming for low latency by default. Could be an option along with pipeline latency */ + g_object_set(ctx->webrtcsrc, "do-retransmission", FALSE, NULL); + + g_signal_connect(ctx->webrtcsrc, "pad-added", G_CALLBACK(on_incoming_stream), ctx); + + ctx->bus = gst_pipeline_get_bus(GST_PIPELINE(ctx->pipe)); + gst_bus_set_sync_handler(ctx->bus, bus_sync_handler, ctx, NULL); + + GST_DEBUG("Starting pipeline"); + ret = gst_element_set_state(GST_ELEMENT(ctx->pipe), GST_STATE_PLAYING); + if (ret == GST_STATE_CHANGE_FAILURE) { + GST_ERROR("Failed to start the pipeline"); + stop_pipeline(env, self); + return; + } + + GST_DEBUG("Pipeline started"); +} + +static void +set_surface(JNIEnv *env, jobject self, jobject surface) { + WebRTCSrcCtx *ctx = get_webrtcsrc_ctx(env, self); + ANativeWindow *new_native_window; + + if (!ctx) + return; + + new_native_window = surface ? ANativeWindow_fromSurface(env, surface) + : NULL; + GST_DEBUG("Received surface %p (native window %p)", surface, + new_native_window); + + if (ctx->native_window) { + ANativeWindow_release(ctx->native_window); + } + + ctx->native_window = new_native_window; + + if (ctx->video_sink) + gst_video_overlay_set_window_handle( + GST_VIDEO_OVERLAY(ctx->video_sink), + (guintptr) new_native_window); +} + +static void +recalculate_latency(JNIEnv *env, jobject self) { + WebRTCSrcCtx *ctx = get_webrtcsrc_ctx(env, self); + if (!ctx) + return; + + GST_DEBUG("Recalculating latency"); + gst_bin_recalculate_latency(GST_BIN(ctx->pipe)); +} + +static void +class_init(JNIEnv *env, jclass klass) { + guint major, minor, micro, nano; + + /* There are restrictions on using gst_bus_set_sync_handler prior to version 1.18 */ + gst_version(&major, &minor, µ, &nano); + if (major < 1 || (major == 1 && minor < 18)) { + __android_log_print(ANDROID_LOG_ERROR, "WebRTCSrc (native)", + "Unsupported GStreamer version < 1.18"); + return; + } + + webrtcsrc_ctx_field_id = + (*env)->GetFieldID(env, klass, "nativeWebRTCSrcCtx", "Ljava/nio/ByteBuffer;"); + + on_error_method_id = + (*env)->GetMethodID(env, klass, "onError", "(Ljava/lang/String;)V"); + + on_latency_message_method_id = + (*env)->GetMethodID(env, klass, "onLatencyMessage", "()V"); + + if (!webrtcsrc_ctx_field_id || !on_error_method_id || !on_latency_message_method_id) { + /* We emit this message through the Android log instead of the GStreamer log because the later + * has not been initialized yet. + */ + __android_log_print(ANDROID_LOG_ERROR, "WebRTCSrc (native)", + "The calling class does not include necessary fields or methods"); + return; + } + + GST_DEBUG_CATEGORY_INIT(debug_category, "WebRTCSrc", 0, "GStreamer WebRTCSrc Android example"); + gst_debug_set_threshold_for_name("WebRTCSrc", GST_LEVEL_DEBUG); +} + +/* List of implemented native methods */ +static JNINativeMethod native_methods[] = { + {"nativeClassInit", "()V", (void *) class_init}, + {"nativeStartPipeline", "(Ljava/lang/String;Ljava/lang/String;Z)V", + (void *) start_pipeline}, + {"nativeStopPipeline", "()V", + (void *) stop_pipeline}, + {"nativeSetSurface", "(Landroid/view/Surface;)V", + (void *) set_surface}, + {"nativeRecalculateLatency", "()V", + (void *) recalculate_latency}, + {"nativeFreeWebRTCSrcContext", "()V", (void *) free_webrtcsrc_ctx}, +}; + +/* Library initializer */ +jint +JNI_OnLoad(JavaVM *vm, __attribute__((unused)) void *reserved) { + JNIEnv *env = NULL; + + java_vm = vm; + + if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) { + __android_log_print(ANDROID_LOG_ERROR, "WebRTCSrc (native)", "Could not retrieve JNIEnv"); + return 0; + } + jclass klass = (*env)->FindClass(env, "org/freedesktop/gstreamer/net/WebRTCSrc"); + if (!klass) { + __android_log_print(ANDROID_LOG_ERROR, "WebRTCSrc (native)", + "Could not retrieve class org.freedesktop.gstreamer.net.WebRTCSrc"); + return 0; + } + if ((*env)->RegisterNatives(env, klass, native_methods, + G_N_ELEMENTS(native_methods))) { + __android_log_print(ANDROID_LOG_ERROR, "WebRTCSrc (native)", + "Could not register native methods for org.freedesktop.gstreamer.net.WebRTCSrc"); + return 0; + } + + return JNI_VERSION_1_4; +} diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/dummy.cpp b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/dummy.cpp new file mode 100644 index 00000000..eddb12fe --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/cpp/dummy.cpp @@ -0,0 +1 @@ +/* This is needed purely to force linking libc++_shared */ \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/.gitignore b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/.gitignore new file mode 100644 index 00000000..6ead9ef6 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/.gitignore @@ -0,0 +1,6 @@ +# Generated: +androidmedia/ +# Note that gstreamer_android will generate (and overwrite) GStreamer.java +# during native code and libraries compilation. +# We keep it in git for dependent code to be able to refere to it if they are +# compiled before native code. diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/GStreamer.java b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/GStreamer.java new file mode 100644 index 00000000..d1bc9627 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/GStreamer.java @@ -0,0 +1,105 @@ +/** + * Copy this file into your Android project and call init(). If your project + * contains fonts and/or certificates in assets, uncomment copyFonts() and/or + * copyCaCertificates() lines in init(). + */ +package org.freedesktop.gstreamer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.content.Context; +import android.content.res.AssetManager; +import android.system.Os; + +public class GStreamer { + private static native void nativeInit(Context context) throws Exception; + + public static void init(Context context) throws Exception { + copyFonts(context); + copyCaCertificates(context); + nativeInit(context); + } + + private static void copyFonts(Context context) { + AssetManager assetManager = context.getAssets(); + File filesDir = context.getFilesDir(); + File fontsFCDir = new File (filesDir, "fontconfig"); + File fontsDir = new File (fontsFCDir, "fonts"); + File fontsCfg = new File (fontsFCDir, "fonts.conf"); + + fontsDir.mkdirs(); + + try { + /* Copy the config file */ + copyFile (assetManager, "fontconfig/fonts.conf", fontsCfg); + /* Copy the fonts */ + for(String filename : assetManager.list("fontconfig/fonts/truetype")) { + File font = new File(fontsDir, filename); + copyFile (assetManager, "fontconfig/fonts/truetype/" + filename, font); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void copyCaCertificates(Context context) { + AssetManager assetManager = context.getAssets(); + File filesDir = context.getFilesDir(); + File sslDir = new File (filesDir, "ssl"); + File certsDir = new File (sslDir, "certs"); + File certs = new File (certsDir, "ca-certificates.crt"); + + certsDir.mkdirs(); + + try { + /* Copy the certificates file */ + copyFile (assetManager, "ssl/certs/ca-certificates.crt", certs); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void copyFile(AssetManager assetManager, String assetPath, File outFile) throws IOException { + InputStream in = null; + OutputStream out = null; + IOException exception = null; + + if (outFile.exists()) + outFile.delete(); + + try { + in = assetManager.open(assetPath); + out = new FileOutputStream(outFile); + + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + out.flush(); + } catch (IOException e) { + exception = e; + } finally { + if (in != null) + try { + in.close(); + } catch (IOException e) { + if (exception == null) + exception = e; + } + if (out != null) + try { + out.close(); + } catch (IOException e) { + if (exception == null) + exception = e; + } + if (exception != null) + throw exception; + } + } +} diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ConsumerFragment.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ConsumerFragment.kt new file mode 100644 index 00000000..cd189cff --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ConsumerFragment.kt @@ -0,0 +1,94 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc + +import android.os.Bundle +import android.os.PowerManager +import android.util.Log +import android.view.LayoutInflater +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat.getSystemService +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import androidx.preference.PreferenceManager +import com.google.android.material.snackbar.Snackbar +import org.freedesktop.gstreamer.examples.webrtcsrc.databinding.ConsumerFragmentBinding +import org.freedesktop.gstreamer.net.WebRTCSrc +import org.freedesktop.gstreamer.ui.GStreamerSurfaceView + + +class ConsumerFragment : Fragment(), WebRTCSrc.ErrorListener, SurfaceHolder.Callback { + private var _binding: ConsumerFragmentBinding? = null + private val binding get() = _binding!! + // ConsumerFragmentArgs is generated by the androidx.navigation.safeargs plugin + // from declarations in res/navigation/nav_graph.xml + private val args: ConsumerFragmentArgs by navArgs() + + private lateinit var webRTCSrc: WebRTCSrc + private lateinit var gsv: GStreamerSurfaceView + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = ConsumerFragmentBinding.inflate(inflater, container, false) + + WebRTCSrc.init(requireActivity()) + webRTCSrc = WebRTCSrc(this) + + getSystemService(requireActivity(), PowerManager::class.java) + + gsv = binding.surfaceVideo + val sh = gsv.holder + sh.addCallback(this) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.consumerProducerId.text = args.producerId + } + + override fun onResume() { + super.onResume() + + val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val signallerUri = prefs.getString("signaller_uri", getString(R.string.signaller_uri))!! + val preferOpusHardwareDec = prefs.getBoolean("prefer_opus_hardware_decoder", false) + + webRTCSrc.startPipeline(signallerUri, args.producerId, preferOpusHardwareDec) + } + override fun onStop() { + super.onStop() + webRTCSrc.stopPipeline() + } + + override fun onDestroyView() { + webRTCSrc.close(); + super.onDestroyView() + _binding = null + } + + override fun onWebRTCSrcError(errorMessage: String) { + requireActivity().runOnUiThread() { + Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG).show() + } + } + + override fun surfaceCreated(holder: SurfaceHolder) { + Log.d("Consumer Fragment", "Surface created: $holder.surface"); + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.d("Consumer Fragment", "Surface changed to format $format dimens $width x $height"); + webRTCSrc.setSurface(holder.surface); + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.d("Consumer Fragment", "Surface destroyed"); + webRTCSrc.setSurface(null); + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/MainActivity.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/MainActivity.kt new file mode 100644 index 00000000..141121a4 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/MainActivity.kt @@ -0,0 +1,64 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import androidx.navigation.ui.setupActionBarWithNavController +import org.freedesktop.gstreamer.examples.webrtcsrc.databinding.MainActivityBinding +import org.freedesktop.gstreamer.examples.webrtcsrc.model.Peer + +class MainActivity : AppCompatActivity() { + + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var binding: MainActivityBinding + var producerId: Peer? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = MainActivityBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbar) + + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.nav_host_fragment_main_content) as NavHostFragment + val navController = navHostFragment.navController + + appBarConfiguration = AppBarConfiguration(navController.graph) + setupActionBarWithNavController(navController, appBarConfiguration) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + return when (item.itemId) { + R.id.action_settings -> { + val intent = Intent(this, SettingsActivity::class.java) + startActivity(intent) + + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_main_content) + return navController.navigateUp(appBarConfiguration) + || super.onSupportNavigateUp() + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ProducerListFragment.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ProducerListFragment.kt new file mode 100644 index 00000000..f6c1f06b --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/ProducerListFragment.kt @@ -0,0 +1,91 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.launch +import org.freedesktop.gstreamer.examples.webrtcsrc.adapter.ProducerTableRowAdapter +import org.freedesktop.gstreamer.examples.webrtcsrc.databinding.ProducerListFragmentBinding +import org.freedesktop.gstreamer.examples.webrtcsrc.io.SignallerClient +import org.freedesktop.gstreamer.examples.webrtcsrc.model.Peer + +class ProducerListFragment : Fragment() { + private var _binding: ProducerListFragmentBinding? = null + private val binding get() = _binding!! + private lateinit var recyclerView : RecyclerView + private lateinit var producerTableRowAdapter : ProducerTableRowAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + + _binding = ProducerListFragmentBinding.inflate(inflater, container, false) + + recyclerView = binding.recyclerViewTableProducer + producerTableRowAdapter = ProducerTableRowAdapter(listOf()) { producer -> + // ProducerListFragmentDirections is generated by the androidx.navigation.safeargs plugin + // from declarations in res/navigation/nav_graph.xml + val action = ProducerListFragmentDirections.startConsumer(producer.id) + findNavController().navigate(action) + } + + recyclerView.layoutManager = LinearLayoutManager(activity) + recyclerView.adapter = producerTableRowAdapter + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.producerListRefreshButton.setOnClickListener { + viewLifecycleOwner.lifecycleScope.launch { + retrieve() + } + } + } + + override fun onResume() { + viewLifecycleOwner.lifecycleScope.launch { + retrieve() + } + + super.onResume() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private suspend fun retrieve() { + val prefs = PreferenceManager.getDefaultSharedPreferences(requireActivity()) + val signallerUri = prefs.getString("signaller_uri", getString(R.string.signaller_uri))!! + + SignallerClient(signallerUri) + .retrieveProducers() + .onSuccess { list -> + if (list.isNotEmpty()) { + producerTableRowAdapter.setProducers(list) + } else { + producerTableRowAdapter.setProducers(listOf()) + Snackbar.make(binding.root, getString(R.string.no_producers_message), + Snackbar.LENGTH_LONG).show() + } + } + .onFailure { e -> + producerTableRowAdapter.setProducers(listOf()) + Snackbar.make(binding.root, String.format(getString(R.string.producer_retrieval_error),e.localizedMessage), + Snackbar.LENGTH_LONG).show() + } + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/SettingsActivity.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/SettingsActivity.kt new file mode 100644 index 00000000..3b5571b6 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/SettingsActivity.kt @@ -0,0 +1,38 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc + +import android.os.Bundle +import android.text.InputType +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager + +class SettingsActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.settings_activity) + + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, SettingsFragment()) + .commit() + } + + setSupportActionBar(findViewById(R.id.settings_toolbar)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + class SettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.preferences, rootKey) + + val signallerUriPref = findPreference("signaller_uri")!! + signallerUriPref.setOnBindEditTextListener { editText -> + editText.inputType = InputType.TYPE_TEXT_VARIATION_URI + } + } + } +} diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/adapter/ProducerTableRowAdapter.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/adapter/ProducerTableRowAdapter.kt new file mode 100644 index 00000000..c2611f5d --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/adapter/ProducerTableRowAdapter.kt @@ -0,0 +1,60 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc.adapter + +import android.annotation.SuppressLint +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import org.freedesktop.gstreamer.examples.webrtcsrc.R +import org.freedesktop.gstreamer.examples.webrtcsrc.model.Peer + +class ProducerTableRowAdapter(private var producers: List, private val onClick: (Peer) -> Unit) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): ViewHolder { + val v: View = LayoutInflater.from(viewGroup.context) + .inflate(R.layout.producer_table_row_layout, viewGroup, false) + return ViewHolder(v, onClick) + } + + override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) { + viewHolder.bind(producers[i]) + } + + override fun getItemCount() = producers.size + + class ViewHolder(itemView: View, val onClick: (Peer) -> Unit) : RecyclerView.ViewHolder(itemView) { + private val tvProducerId: TextView = itemView.findViewById(R.id.tv_producer_id) + private val tvProducerMeta: TextView = itemView.findViewById(R.id.tv_producer_meta) + private var curProducer: Peer? = null + + init { + itemView.setOnClickListener { + curProducer?.let { + onClick(it) + } + } + itemView.setOnFocusChangeListener { v, hasFocus -> + if (hasFocus) + v.setBackgroundColor(Color.LTGRAY) + else + v.setBackgroundColor(Color.TRANSPARENT) + } + } + + @SuppressLint("SetTextI18n") + fun bind(producer: Peer) { + tvProducerId.text = producer.id.substring(0..7) + "..." + tvProducerMeta.text = producer.meta.toString() + curProducer = producer + } + } + + @SuppressLint("NotifyDataSetChanged") + fun setProducers(list: List) { + producers = list + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/io/SignallerClient.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/io/SignallerClient.kt new file mode 100644 index 00000000..a5b8a17d --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/io/SignallerClient.kt @@ -0,0 +1,111 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc.io + +import android.util.Log +import io.ktor.client.HttpClient +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.wss +import io.ktor.websocket.Frame +import io.ktor.websocket.readText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.put +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.putJsonArray + +import org.freedesktop.gstreamer.examples.webrtcsrc.model.Peer + +class SignallerClient(private val uri: String) { + private suspend fun getNextMessage(incoming: ReceiveChannel, expected: String): JsonObject { + while (true) { + for (frame in incoming) { + if (frame is Frame.Text) { + val msg = Json.decodeFromString(frame.readText()) + val type = Json.decodeFromJsonElement( + msg["type"] ?: throw Exception("message missing 'type'") + ) + if (type == expected) { + Log.d(CAT, "Got $expected message") + return msg + } else { + Log.d(CAT, "Expected $expected, got $type => ignoring") + } + } + } + } + } + + suspend fun retrieveProducers(): Result> { + var res: Result>? = null + + val client = HttpClient { + install(WebSockets) + } + + withContext(Dispatchers.IO) { + Log.d(CAT, "Connecting to $uri") + + try { + client.wss(uri) { + // Register as listener + val welcome = getNextMessage(incoming, "welcome") + val ourId = Json.decodeFromJsonElement( + welcome["peerId"] ?: throw Exception("welcome missing 'peerId'") + ) + Log.i(CAT, "Our id: $ourId") + + val setStatusMsg = buildJsonObject { + put("type", "setPeerStatus") + putJsonArray("roles") { add("listener") } + } + outgoing.send(Frame.Text(Json.encodeToString(setStatusMsg))) + + var registered = false + while (!registered) { + val peerStatus = getNextMessage(incoming, "peerStatusChanged") + val peerId = Json.decodeFromJsonElement( + peerStatus["peerId"] + ?: throw Exception("peerStatusChanged missing 'peerId'") + ) + if (peerId == ourId) { + registered = true + } + } + + // Query producer list + val listMsg = buildJsonObject { + put("type", "list") + } + outgoing.send(Frame.Text(Json.encodeToString(listMsg))) + + val peerStatus = getNextMessage(incoming, "list") + val list = Json.decodeFromJsonElement>( + peerStatus["producers"] + ?: throw Exception("list message missing 'producers'") + ) + res = Result.success(list.map { + Log.i(CAT, "Got Producer $it") + Json.decodeFromJsonElement(it) + }) + } + + Log.d(CAT, "Disconnecting") + client.close() + } catch (e: Exception) { + Log.e(CAT, "Error retrieving producers: $e") + res = Result.failure(e) + } + } + return res!! + } + + companion object { + private const val CAT = "SignallerClient" + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/model/Peer.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/model/Peer.kt new file mode 100644 index 00000000..bb1e6fc7 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/examples/webrtcsrc/model/Peer.kt @@ -0,0 +1,6 @@ +package org.freedesktop.gstreamer.examples.webrtcsrc.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Peer(val id: String, val meta: Map?) diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/net/WebRTCSrc.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/net/WebRTCSrc.kt new file mode 100644 index 00000000..7f53a0d1 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/net/WebRTCSrc.kt @@ -0,0 +1,110 @@ +package org.freedesktop.gstreamer.net + +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import android.view.Surface +import org.freedesktop.gstreamer.GStreamer +import java.io.Closeable +import java.nio.ByteBuffer + +// This Managed class allows running a native GStreamer pipeline containing a `webrtcsrc` element. +// +// This class is responsible for creating and managing the pipeline event loop. +// +// Operations on the pipeline are guaranteed to be synchronized as long as they are posted +// to the `Handler` via `handler.post()`. +class WebRTCSrc(private val errorListener: ErrorListener) : + HandlerThread("GStreamer WebRTCSrc pipeline"), Closeable { + + private lateinit var handler: Handler + + init { + start() + } + + override fun onLooperPrepared() { + handler = Handler(this.looper) + } + + // This is a pointer to the native struct needed by C code + // to keep track of its context between calls. + // + // FIXME we would love to use `CPointer`, but despite our attempts, + // we were not able to import that type and resorting to code generation + // for this seemed overkill. + // + // See https://kotlinlang.org/docs/native-c-interop.html + private var nativeWebRTCSrcCtx: ByteBuffer? = null + + private external fun nativeSetSurface(surface: Surface?) + fun setSurface(surface: Surface?) { + handler.post { + nativeSetSurface(surface) + } + } + + private external fun nativeStartPipeline( + signallerUri: String, + peerId: String, + preferOpusHardwareDec: Boolean + ) + + fun startPipeline(signallerUri: String, peerId: String, preferOpusHardwareDec: Boolean) { + handler.post { + nativeStartPipeline(signallerUri, peerId, preferOpusHardwareDec) + } + } + + private external fun nativeStopPipeline() + + fun stopPipeline() { + handler.post { + nativeStopPipeline() + } + } + + private external fun nativeFreeWebRTCSrcContext() + override fun close() { + handler.post { + nativeFreeWebRTCSrcContext() + } + } + + interface ErrorListener { + fun onWebRTCSrcError(errorMessage: String) + } + + private fun onError(errorMessage: String) { + handler.post { + Log.d("WebRTCSrc", "Dispatching GStreamer error $errorMessage") + errorListener.onWebRTCSrcError(errorMessage) + nativeStopPipeline() + } + } + + private external fun nativeRecalculateLatency() + + fun onLatencyMessage() { + handler.post { + nativeRecalculateLatency() + } + } + + companion object { + @JvmStatic + private external fun nativeClassInit() + + @JvmStatic + fun init(context: Context) { + Log.d("WebRTCSrc", "Init") + + System.loadLibrary("gstreamer_android") + GStreamer.init(context) + + System.loadLibrary("gstreamer_webrtcsrc") + nativeClassInit() + } + } +} diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/ui/GStreamerSurfaceView.kt b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/ui/GStreamerSurfaceView.kt new file mode 100644 index 00000000..0150a6bf --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/java/org/freedesktop/gstreamer/ui/GStreamerSurfaceView.kt @@ -0,0 +1,72 @@ +/* kotlin port of: + https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/subprojects/gst-examples/webrtc/android/app/src/main/java/org/freedesktop/gstreamer/webrtc/GStreamerSurfaceView.java + */ + +package org.freedesktop.gstreamer.ui + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceView +import kotlin.math.max + + +class GStreamerSurfaceView(context: Context, attrs: AttributeSet) : SurfaceView(context, attrs) { + var mediaWidth: Int = 320 + var mediaHeight: Int = 240 + + // Called by the layout manager to find out our size and give us some rules. + // We will try to maximize our size, and preserve the media's aspect ratio if + // we are given the freedom to do so. + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (mediaWidth == 0 || mediaHeight == 0) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + + var width = 0 + var height = 0 + val wMode = MeasureSpec.getMode(widthMeasureSpec) + val hMode = MeasureSpec.getMode(heightMeasureSpec) + val wSize = MeasureSpec.getSize(widthMeasureSpec) + val hSize = MeasureSpec.getSize(heightMeasureSpec) + + Log.i("GStreamer", "onMeasure called with $mediaWidth x $mediaHeight") + when (wMode) { + MeasureSpec.AT_MOST -> { + width = if (hMode == MeasureSpec.EXACTLY) { + (hSize * mediaWidth / mediaHeight).coerceAtMost(wSize) + } else { + wSize + } + } + MeasureSpec.EXACTLY -> width = wSize + MeasureSpec.UNSPECIFIED -> width = mediaWidth + } + when (hMode) { + MeasureSpec.AT_MOST -> { + height = if (wMode == MeasureSpec.EXACTLY) { + (wSize * mediaHeight / mediaWidth).coerceAtMost(hSize) + } else { + hSize + } + } + MeasureSpec.EXACTLY -> height = hSize + MeasureSpec.UNSPECIFIED -> height = mediaHeight + } + // Finally, calculate best size when both axis are free + if (hMode == MeasureSpec.AT_MOST && wMode == MeasureSpec.AT_MOST) { + val correctHeight: Int = width * mediaHeight / mediaWidth + val correctWidth: Int = height * mediaWidth / mediaHeight + + if (correctHeight < height) height = correctHeight + else width = correctWidth + } + + // Obey minimum size + width = max(suggestedMinimumWidth.toDouble(), width.toDouble()).toInt() + height = max(suggestedMinimumHeight.toDouble(), height.toDouble()).toInt() + setMeasuredDimension(width, height) + } +} \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_background.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_foreground.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/consumer_fragment.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/consumer_fragment.xml new file mode 100644 index 00000000..c556d342 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/consumer_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_activity.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000..ab7b6264 --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_content.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_content.xml new file mode 100644 index 00000000..a55cddaa --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/main_content.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/producer_list_fragment.xml b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/producer_list_fragment.xml new file mode 100644 index 00000000..cf17c25c --- /dev/null +++ b/net/webrtc/examples/android/webrtcsrc/app/src/main/res/layout/producer_list_fragment.xml @@ -0,0 +1,63 @@ + + + + + +