webrtc: add android webrtcsrc example

This commit adds an Android `webrtcsrc` based example with the following
features:

* A first view allows retrieving the producer list from the signaller (peer ids
  are uuids which are too long to tap, especially using an onscreen keyboard).
* Selecting a producer opens a second view. The first available video stream is
  rendered on a native Surface. All the audio streams are rendered using
  `autoaudiosink`.

Available Settings:

* Signaller URI.
* A toggle to prefer hardware decoding for OPUS, otherwise the app defaults to
  raising `opusdec`'s rank. Hardware decoding was moved aside since it was found
  to crash the app on all tested devices (2 smartphones, 1 tv).

**Warning**: in order to ease testing, this demonstration application enables
unencrypted network communication. See `AndroidManifest.xml`.

The application uses the technologies currenlty proposed by Android Studio when
creating a new project:

* Kotlin as the default language, which is fully interoperable with Java and
  uses the same SDK.
* gradle 8.6.
* kotlin dialect for gradle. The structure is mostly the same as the previously
  preferred dialect, for which examples can be found online readily.
* However, JNI code generation still uses Makefiles (instead of CMake) due to
  the need to call [`gstreamer-1.0.mk`] for `gstreamer_android` generation.
  Note: on-going work on that front:
  - https://gitlab.freedesktop.org/gstreamer/cerbero/-/merge_requests/1466
  - https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/6794

Current limitations:

* x86 support is currently discarded as `gstreamer_android` libs generation
  fails (observed with `gstreamer-1.0-android-universal-1.24.3`).
* A selector could be added to let the user chose the video streams and
  possibly decide whether to render all audio streams or just select one.

Nice to have:

* Support for the synchronization features of the `webrtc-precise-sync-recv`
  example (NTP clock, RFC 7273).
* It could be nice to use Rust for the specific native code.

[`gstreamer-1.0.mk`]: https://gitlab.freedesktop.org/gstreamer/cerbero/-/blob/main/data/ndk-build/gstreamer-1.0.mk

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1578>
This commit is contained in:
François Laignel 2024-05-24 17:33:46 +02:00 committed by GStreamer Marge Bot
parent 58e91c154c
commit 4259d284bd
61 changed files with 2496 additions and 0 deletions

View file

@ -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.

View file

@ -0,0 +1,13 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# white list
!.gradle/config.properties

View file

@ -0,0 +1,2 @@
#Fri May 17 23:09:24 CEST 2024
java.home=/app/extra/android-studio/jbr

View file

@ -0,0 +1,4 @@
build/
.externalNativeBuild/
assets/
gst-android-build/

View file

@ -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)
}

View file

@ -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

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:glEsVersion="0x00020000" />
<!-- /!\ IMPORTANT /!\
In order to ease testing, this demonstration app uses the following item:
android:usesCleartextTraffic="true"
This is to allow running the signaller without the need for a certificate.
The above item MUST be removed in a production app.
-->
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GStreamerWebrtcsrc"
android:usesCleartextTraffic="true" >
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.GStreamer.WebRTCSrc">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SettingsActivity"
android:label="Settings"
android:theme="@style/Theme.GStreamer.WebRTCSrc"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity"/>
</activity>
</application>
</manifest>

View file

@ -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

View file

@ -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

View file

@ -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 <string.h>
#include <stdint.h>
#include <jni.h>
#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <gst/video/videooverlay.h>
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, &micro, &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;
}

View file

@ -0,0 +1 @@
/* This is needed purely to force linking libc++_shared */

View file

@ -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.

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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()
}
}

View file

@ -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<Peer>()) { 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<Peer>())
Snackbar.make(binding.root, getString(R.string.no_producers_message),
Snackbar.LENGTH_LONG).show()
}
}
.onFailure { e ->
producerTableRowAdapter.setProducers(listOf<Peer>())
Snackbar.make(binding.root, String.format(getString(R.string.producer_retrieval_error),e.localizedMessage),
Snackbar.LENGTH_LONG).show()
}
}
}

View file

@ -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<EditTextPreference>("signaller_uri")!!
signallerUriPref.setOnBindEditTextListener { editText ->
editText.inputType = InputType.TYPE_TEXT_VARIATION_URI
}
}
}
}

View file

@ -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<Peer>, private val onClick: (Peer) -> Unit) :
RecyclerView.Adapter<ProducerTableRowAdapter.ViewHolder>() {
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<Peer>) {
producers = list
notifyDataSetChanged()
}
}

View file

@ -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<Frame>, expected: String): JsonObject {
while (true) {
for (frame in incoming) {
if (frame is Frame.Text) {
val msg = Json.decodeFromString<JsonObject>(frame.readText())
val type = Json.decodeFromJsonElement<String>(
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<List<Peer>> {
var res: Result<List<Peer>>? = 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<String>(
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<String>(
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<Array<JsonElement>>(
peerStatus["producers"]
?: throw Exception("list message missing 'producers'")
)
res = Result.success(list.map {
Log.i(CAT, "Got Producer $it")
Json.decodeFromJsonElement<Peer>(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"
}
}

View file

@ -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<String, String>?)

View file

@ -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<ByteVar>`, 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()
}
}
}

View file

@ -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)
}
}

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ConsumerFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<TextView
android:id="@+id/consumer_producer_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/consumer_producer_label"
android:padding="5dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/consumer_producer_id"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/consumer_producer_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="@string/consumer_producer_id"
android:padding="5dp"
app:layout_constraintTop_toBottomOf="@id/consumer_producer_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="15dp"
android:paddingBottom="5dp"
android:orientation="vertical">
<org.freedesktop.gstreamer.ui.GStreamerSurfaceView
android:id="@+id/surface_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/main_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_main_content"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ProducerListFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp">
<Button
android:id="@+id/producer_list_refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/producer_list_refresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:layout_width="150dp"
android:layout_height="wrap_content"
android:text="@string/producer_id_label"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/producer_meta_label"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_table_producer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableLayout
android:id="@+id/producer_table_heading_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TableRow
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="5dp"
android:paddingBottom="5dp"
>
<TextView
android:id="@+id/tv_producer_id"
style="@style/producer_table_row_item"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:gravity="start" />
<TextView
android:id="@+id/tv_producer_meta"
style="@style/producer_table_row_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start" />
</TableRow>
</TableLayout>
</LinearLayout>

View file

@ -0,0 +1,23 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/settings"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View file

@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="org.freedesktop.gstreamer.examples.webrtcsrc.MainActivity">
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/ProducerListFragment">
<fragment
android:id="@+id/ProducerListFragment"
android:name="org.freedesktop.gstreamer.examples.webrtcsrc.ProducerListFragment"
android:label="@string/producer_list_label"
tools:layout="@layout/producer_list_fragment">
<action
android:id="@+id/startConsumer"
app:destination="@id/ConsumerFragment"/>
</fragment>
<fragment
android:id="@+id/ConsumerFragment"
android:name="org.freedesktop.gstreamer.examples.webrtcsrc.ConsumerFragment"
android:label="@string/consumer_label"
tools:layout="@layout/consumer_fragment">
<argument
android:name="producerId"
app:argType="string" />
<action
android:id="@+id/getBackToProducerList"
app:destination="@id/ProducerListFragment" />
</fragment>
</navigation>

View file

@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.GStreamerWebrtcsrc" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View file

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.GStreamerWebrtcsrc" parent="Base.Theme.GStreamerWebrtcsrc">
<!-- Transparent system bars for edge-to-edge. -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
</style>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View file

@ -0,0 +1,31 @@
<resources>
<string name="app_name">GStreamer WebRTCSrc</string>
<string name="action_settings">Settings</string>
<!-- Producer List -->
<string name="producer_list_label">WebRTC Producers</string>
<string name="producer_id_label">Producer Id</string>
<string name="producer_meta_label">Meta</string>
<string name="producer_list_refresh">Refresh</string>
<string name="no_producers_message">No producers found</string>
<string name="producer_retrieval_error">Error retrieving producers:\n%1$s</string>
<!-- Consumer -->
<string name="consumer_label">WebRTC Consumer</string>
<string name="consumer_back">Back</string>
<string name="consumer_producer_label">Receiving from:</string>
<string name="consumer_producer_id">-</string>
<!-- Settings Signaller -->
<string name="signaller_header">Signaller</string>
<string name="signaller_uri_label">URI</string>
<string name="signaller_uri">ws://192.168.1.22:8443</string>
<!-- Settings Audio -->
<string name="audio_header">Audio</string>
<string name="prefer_opus_hardware_decoder_label">Prefer OPUS hardware decoding</string>
<string name="prefer_opus_hardware_decoder_summary">
OPUS hardware decoding crashed the app on some devices.
Enabling this will lower OPUS software decoder rank.
</string>
</resources>

View file

@ -0,0 +1,6 @@
<resources>
<style name="producer_table_row_item" parent="@android:style/TextAppearance.Large">
<item name="android:gravity">start</item>
<item name="android:paddingEnd">15dp</item>
</style>
</resources>

View file

@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.GStreamer.WebRTCSrc" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.GStreamer.WebRTCSrc" parent="Base.Theme.GStreamer.WebRTCSrc" />
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View file

@ -0,0 +1,18 @@
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/signaller_header">
<EditTextPreference
app:key="signaller_uri"
app:title="@string/signaller_uri_label"
app:defaultValue="@string/signaller_uri"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/audio_header">
<SwitchPreferenceCompat
app:key="prefer_opus_hardware_decoder"
app:title="@string/prefer_opus_hardware_decoder_label"
app:summary="@string/prefer_opus_hardware_decoder_summary" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.androidx.navigation.safeargs) apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}

View file

@ -0,0 +1,26 @@
# 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 -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-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
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View file

@ -0,0 +1,35 @@
[versions]
agp = "8.4.1"
appcompat = "1.6.1"
constraintlayout = "2.1.4"
coreKtx = "1.13.1"
kotlin = "1.9.0"
kotlinxSerializationJson = "1.6.3"
ktor = "2.3.11"
lifecycleKtx = "2.8.0"
material = "1.12.0"
navigationFragmentKtx = "2.7.7"
navigationUiKtx = "2.7.7"
navSafeArgsPlugin = "2.7.7"
preference = "1.2.1"
recyclerview = "1.3.2"
workRuntimeKtx = "2.9.0"
[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleKtx" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
androidx-navigation-safeargs = { id = "androidx.navigation.safeargs", version.ref = "navSafeArgsPlugin" }

View file

@ -0,0 +1,6 @@
#Mon May 13 19:01:42 CEST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
net/webrtc/examples/android/webrtcsrc/gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## 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='"-Xmx64m" "-Xms64m"'
# 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 or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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"
exec "$JAVACMD" "$@"

View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@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 Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@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="-Xmx64m" "-Xms64m"
@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 execute
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 execute
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
: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 %*
: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

View file

@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "GStreamer WebRTCSrc"
include(":app")