Android: Implement JNI_OnLoad()

When building for Android, chances are that gstreamer is going to be
loaded from Java using System.loadLibrary(). In that case we can
initialize GStreamer (including static plugins), redirect log functions,
etc.

This code is copied from cerbero because it can be used with
gstreamer-full-1.0 too. Cerbero needs to be adapted to drop that code
and generate gst_init_static_plugins() function.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/617>
This commit is contained in:
Xavier Claessens 2020-09-04 20:52:47 -04:00 committed by GStreamer Marge Bot
parent 16ed0a6961
commit fa38827c44
4 changed files with 727 additions and 2 deletions

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

@ -7,3 +7,7 @@ if (bashcomp_found)
install_dir : bash_completions_dir,
install_tag : 'runtime')
endif
if host_system == 'android'
install_data('android/GStreamer.java', install_dir: get_option('datadir') / 'gst-android/ndk-build')
endif

View file

@ -0,0 +1,610 @@
/* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "gst_private.h"
#include "gst.h"
#include <jni.h>
#include <android/log.h>
#include <string.h>
/* AMC plugin uses g_module_symbol() to find those symbols */
GST_API jobject gst_android_get_application_context (void);
GST_API jobject gst_android_get_application_class_loader (void);
GST_API JavaVM *gst_android_get_java_vm (void);
/* XXX: Workaround for Android <21 making signal() an inline function
* around bsd_signal(), and Android >= 21 not having any bsd_signal()
* symbol but only signal().
* See https://bugzilla.gnome.org/show_bug.cgi?id=766235
*/
static gpointer
load_real_signal (gpointer data)
{
GModule *module;
gpointer ret = NULL;
module = g_module_open ("libc.so", G_MODULE_BIND_LOCAL);
g_module_symbol (module, "signal", &ret);
/* As fallback, let's try bsd_signal */
if (ret == NULL) {
g_warning ("Can't find signal(3) in libc.so!");
g_module_symbol (module, "bsd_signal", &ret);
}
g_module_close (module);
return ret;
}
__sighandler_t bsd_signal (int signum, __sighandler_t handler)
__attribute__ ((weak));
__sighandler_t
bsd_signal (int signum, __sighandler_t handler)
{
static GOnce gonce = G_ONCE_INIT;
__sighandler_t (*real_signal) (int signum, __sighandler_t handler);
g_once (&gonce, load_real_signal, NULL);
real_signal = gonce.retval;
g_assert (real_signal != NULL);
return real_signal (signum, handler);
}
static jobject _context = NULL;
static jobject _class_loader = NULL;
static JavaVM *_java_vm = NULL;
static GstClockTime _priv_gst_info_start_time;
static void
glib_print_handler (const gchar * string)
{
__android_log_print (ANDROID_LOG_INFO, "GLib+stdout", "%s", string);
}
static void
glib_printerr_handler (const gchar * string)
{
__android_log_print (ANDROID_LOG_ERROR, "GLib+stderr", "%s", string);
}
/* Based on GLib's default handler */
#define CHAR_IS_SAFE(wc) (!((wc < 0x20 && wc != '\t' && wc != '\n' && wc != '\r') || \
(wc == 0x7f) || \
(wc >= 0x80 && wc < 0xa0)))
#define FORMAT_UNSIGNED_BUFSIZE ((GLIB_SIZEOF_LONG * 3) + 3)
#define STRING_BUFFER_SIZE (FORMAT_UNSIGNED_BUFSIZE + 32)
#define ALERT_LEVELS (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)
#define DEFAULT_LEVELS (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE)
#define INFO_LEVELS (G_LOG_LEVEL_INFO | G_LOG_LEVEL_DEBUG)
static void
escape_string (GString * string)
{
const char *p = string->str;
gunichar wc;
while (p < string->str + string->len) {
gboolean safe;
wc = g_utf8_get_char_validated (p, -1);
if (wc == (gunichar) - 1 || wc == (gunichar) - 2) {
gchar *tmp;
guint pos;
pos = p - string->str;
/* Emit invalid UTF-8 as hex escapes
*/
tmp = g_strdup_printf ("\\x%02x", (guint) (guchar) * p);
g_string_erase (string, pos, 1);
g_string_insert (string, pos, tmp);
p = string->str + (pos + 4); /* Skip over escape sequence */
g_free (tmp);
continue;
}
if (wc == '\r') {
safe = *(p + 1) == '\n';
} else {
safe = CHAR_IS_SAFE (wc);
}
if (!safe) {
gchar *tmp;
guint pos;
pos = p - string->str;
/* Largest char we escape is 0x0a, so we don't have to worry
* about 8-digit \Uxxxxyyyy
*/
tmp = g_strdup_printf ("\\u%04x", wc);
g_string_erase (string, pos, g_utf8_next_char (p) - p);
g_string_insert (string, pos, tmp);
g_free (tmp);
p = string->str + (pos + 6); /* Skip over escape sequence */
} else
p = g_utf8_next_char (p);
}
}
static void
glib_log_handler (const gchar * log_domain, GLogLevelFlags log_level,
const gchar * message, gpointer user_data)
{
gchar *string;
GString *gstring;
const gchar *domains;
gint android_log_level;
gchar *tag;
if ((log_level & DEFAULT_LEVELS) || (log_level >> G_LOG_LEVEL_USER_SHIFT))
goto emit;
domains = g_getenv ("G_MESSAGES_DEBUG");
if (((log_level & INFO_LEVELS) == 0) ||
domains == NULL ||
(strcmp (domains, "all") != 0 && (!log_domain
|| !strstr (domains, log_domain))))
return;
emit:
if (log_domain)
tag = g_strdup_printf ("GLib+%s", log_domain);
else
tag = g_strdup ("GLib");
switch (log_level & G_LOG_LEVEL_MASK) {
case G_LOG_LEVEL_ERROR:
case G_LOG_LEVEL_CRITICAL:
android_log_level = ANDROID_LOG_ERROR;
break;
case G_LOG_LEVEL_WARNING:
android_log_level = ANDROID_LOG_WARN;
break;
case G_LOG_LEVEL_MESSAGE:
case G_LOG_LEVEL_INFO:
android_log_level = ANDROID_LOG_INFO;
break;
case G_LOG_LEVEL_DEBUG:
android_log_level = ANDROID_LOG_DEBUG;
break;
default:
android_log_level = ANDROID_LOG_VERBOSE;
break;
}
gstring = g_string_new (NULL);
if (!message) {
g_string_append (gstring, "(NULL) message");
} else {
GString *msg = g_string_new (message);
escape_string (msg);
g_string_append (gstring, msg->str);
g_string_free (msg, TRUE);
}
string = g_string_free (gstring, FALSE);
__android_log_print (android_log_level, tag, "%s", string);
g_free (string);
g_free (tag);
}
static void
gst_debug_logcat (GstDebugCategory * category, GstDebugLevel level,
const gchar * file, const gchar * function, gint line,
GObject * object, GstDebugMessage * message, gpointer unused)
{
GstClockTime elapsed;
gint android_log_level;
gchar *tag;
if (level > gst_debug_category_get_threshold (category))
return;
elapsed = GST_CLOCK_DIFF (_priv_gst_info_start_time,
gst_util_get_timestamp ());
switch (level) {
case GST_LEVEL_ERROR:
android_log_level = ANDROID_LOG_ERROR;
break;
case GST_LEVEL_WARNING:
android_log_level = ANDROID_LOG_WARN;
break;
case GST_LEVEL_FIXME:
case GST_LEVEL_INFO:
android_log_level = ANDROID_LOG_INFO;
break;
case GST_LEVEL_DEBUG:
android_log_level = ANDROID_LOG_DEBUG;
break;
default:
android_log_level = ANDROID_LOG_VERBOSE;
break;
}
tag = g_strdup_printf ("GStreamer+%s",
gst_debug_category_get_name (category));
if (object) {
gchar *obj;
if (GST_IS_PAD (object) && GST_OBJECT_NAME (object)) {
obj = g_strdup_printf ("<%s:%s>", GST_DEBUG_PAD_NAME (object));
} else if (GST_IS_OBJECT (object) && GST_OBJECT_NAME (object)) {
obj = g_strdup_printf ("<%s>", GST_OBJECT_NAME (object));
} else if (G_IS_OBJECT (object)) {
obj = g_strdup_printf ("<%s@%p>", G_OBJECT_TYPE_NAME (object), object);
} else {
obj = g_strdup_printf ("<%p>", object);
}
__android_log_print (android_log_level, tag,
"%" GST_TIME_FORMAT " %p %s:%d:%s:%s %s\n",
GST_TIME_ARGS (elapsed), g_thread_self (),
file, line, function, obj, gst_debug_message_get (message));
g_free (obj);
} else {
__android_log_print (android_log_level, tag,
"%" GST_TIME_FORMAT " %p %s:%d:%s %s\n",
GST_TIME_ARGS (elapsed), g_thread_self (),
file, line, function, gst_debug_message_get (message));
}
g_free (tag);
}
static gboolean
get_application_dirs (JNIEnv * env, jobject context, gchar ** cache_dir,
gchar ** files_dir)
{
jclass context_class;
jmethodID get_cache_dir_id, get_files_dir_id;
jclass file_class;
jmethodID get_absolute_path_id;
jobject dir;
jstring abs_path;
const gchar *abs_path_str;
*cache_dir = *files_dir = NULL;
context_class = (*env)->GetObjectClass (env, context);
if (!context_class) {
return FALSE;
}
get_cache_dir_id =
(*env)->GetMethodID (env, context_class, "getCacheDir",
"()Ljava/io/File;");
get_files_dir_id =
(*env)->GetMethodID (env, context_class, "getFilesDir",
"()Ljava/io/File;");
if (!get_cache_dir_id || !get_files_dir_id) {
(*env)->DeleteLocalRef (env, context_class);
return FALSE;
}
file_class = (*env)->FindClass (env, "java/io/File");
if (!file_class) {
(*env)->DeleteLocalRef (env, context_class);
return FALSE;
}
get_absolute_path_id =
(*env)->GetMethodID (env, file_class, "getAbsolutePath",
"()Ljava/lang/String;");
if (!get_absolute_path_id) {
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
dir = (*env)->CallObjectMethod (env, context, get_cache_dir_id);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
if (dir) {
abs_path = (*env)->CallObjectMethod (env, dir, get_absolute_path_id);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, dir);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
abs_path_str = (*env)->GetStringUTFChars (env, abs_path, NULL);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, abs_path);
(*env)->DeleteLocalRef (env, dir);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
*cache_dir = abs_path ? g_strdup (abs_path_str) : NULL;
(*env)->ReleaseStringUTFChars (env, abs_path, abs_path_str);
(*env)->DeleteLocalRef (env, abs_path);
(*env)->DeleteLocalRef (env, dir);
}
dir = (*env)->CallObjectMethod (env, context, get_files_dir_id);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
if (dir) {
abs_path = (*env)->CallObjectMethod (env, dir, get_absolute_path_id);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, dir);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
abs_path_str = (*env)->GetStringUTFChars (env, abs_path, NULL);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
(*env)->DeleteLocalRef (env, abs_path);
(*env)->DeleteLocalRef (env, dir);
(*env)->DeleteLocalRef (env, context_class);
(*env)->DeleteLocalRef (env, file_class);
return FALSE;
}
*files_dir = files_dir ? g_strdup (abs_path_str) : NULL;
(*env)->ReleaseStringUTFChars (env, abs_path, abs_path_str);
(*env)->DeleteLocalRef (env, abs_path);
(*env)->DeleteLocalRef (env, dir);
}
(*env)->DeleteLocalRef (env, file_class);
(*env)->DeleteLocalRef (env, context_class);
return TRUE;
}
jobject
gst_android_get_application_context (void)
{
return _context;
}
jobject
gst_android_get_application_class_loader (void)
{
return _class_loader;
}
JavaVM *
gst_android_get_java_vm (void)
{
return _java_vm;
}
static gboolean
init (JNIEnv * env, jobject context)
{
jclass context_cls = NULL;
jmethodID get_class_loader_id = 0;
jobject class_loader = NULL;
context_cls = (*env)->GetObjectClass (env, context);
if (!context_cls) {
return FALSE;
}
get_class_loader_id = (*env)->GetMethodID (env, context_cls,
"getClassLoader", "()Ljava/lang/ClassLoader;");
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
return FALSE;
}
class_loader = (*env)->CallObjectMethod (env, context, get_class_loader_id);
if ((*env)->ExceptionCheck (env)) {
(*env)->ExceptionDescribe (env);
(*env)->ExceptionClear (env);
return FALSE;
}
if (_context) {
(*env)->DeleteGlobalRef (env, _context);
}
_context = (*env)->NewGlobalRef (env, context);
if (_class_loader) {
(*env)->DeleteGlobalRef (env, _class_loader);
}
_class_loader = (*env)->NewGlobalRef (env, class_loader);
return TRUE;
}
static void
gst_android_init (JNIEnv * env, jobject context)
{
gchar *cache_dir;
gchar *files_dir;
gchar *registry;
GError *error = NULL;
if (!init (env, context)) {
__android_log_print (ANDROID_LOG_INFO, "GStreamer",
"GStreamer failed to initialize");
}
if (gst_is_initialized ()) {
__android_log_print (ANDROID_LOG_INFO, "GStreamer",
"GStreamer already initialized");
return;
}
if (!get_application_dirs (env, context, &cache_dir, &files_dir)) {
__android_log_print (ANDROID_LOG_ERROR, "GStreamer",
"Failed to get application dirs");
}
if (cache_dir) {
g_setenv ("TMP", cache_dir, TRUE);
g_setenv ("TEMP", cache_dir, TRUE);
g_setenv ("TMPDIR", cache_dir, TRUE);
g_setenv ("XDG_RUNTIME_DIR", cache_dir, TRUE);
g_setenv ("XDG_CACHE_HOME", cache_dir, TRUE);
registry = g_build_filename (cache_dir, "registry.bin", NULL);
g_setenv ("GST_REGISTRY", registry, TRUE);
g_free (registry);
g_setenv ("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no", TRUE);
/* TODO: Should probably also set GST_PLUGIN_SCANNER and GST_PLUGIN_SYSTEM_PATH */
}
if (files_dir) {
gchar *fontconfig, *certs;
g_setenv ("HOME", files_dir, TRUE);
g_setenv ("XDG_DATA_DIRS", files_dir, TRUE);
g_setenv ("XDG_CONFIG_DIRS", files_dir, TRUE);
g_setenv ("XDG_CONFIG_HOME", files_dir, TRUE);
g_setenv ("XDG_DATA_HOME", files_dir, TRUE);
fontconfig = g_build_filename (files_dir, "fontconfig", NULL);
g_setenv ("FONTCONFIG_PATH", fontconfig, TRUE);
g_free (fontconfig);
certs =
g_build_filename (files_dir, "ssl", "certs", "ca-certificates.crt",
NULL);
g_setenv ("CA_CERTIFICATES", certs, TRUE);
g_free (certs);
}
g_free (cache_dir);
g_free (files_dir);
/* Set GLib print handlers */
g_set_print_handler (glib_print_handler);
g_set_printerr_handler (glib_printerr_handler);
g_log_set_default_handler (glib_log_handler, NULL);
/* Set GStreamer log handlers */
gst_debug_remove_log_function (NULL);
gst_debug_set_default_threshold (GST_LEVEL_WARNING);
gst_debug_add_log_function ((GstLogFunction) gst_debug_logcat, NULL, NULL);
/* get time we started for debugging messages */
_priv_gst_info_start_time = gst_util_get_timestamp ();
if (!gst_init_check (NULL, NULL, &error)) {
gchar *message = g_strdup_printf ("GStreamer initialization failed: %s",
error && error->message ? error->message : "(no message)");
jclass exception_class = (*env)->FindClass (env, "java/lang/Exception");
__android_log_print (ANDROID_LOG_ERROR, "GStreamer", "%s", message);
(*env)->ThrowNew (env, exception_class, message);
g_free (message);
return;
}
__android_log_print (ANDROID_LOG_INFO, "GStreamer",
"GStreamer initialization complete");
}
static void
gst_android_init_jni (JNIEnv * env, jobject gstreamer, jobject context)
{
gst_android_init (env, context);
}
static JNINativeMethod native_methods[] = {
{"nativeInit", "(Landroid/content/Context;)V", (void *) gst_android_init_jni}
};
jint
JNI_OnLoad (JavaVM * vm, void *reserved)
{
JNIEnv *env = NULL;
if ((*vm)->GetEnv (vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) {
__android_log_print (ANDROID_LOG_ERROR, "GStreamer",
"Could not retrieve JNIEnv");
return 0;
}
jclass klass = (*env)->FindClass (env, "org/freedesktop/gstreamer/GStreamer");
if (!klass) {
__android_log_print (ANDROID_LOG_ERROR, "GStreamer",
"Could not retrieve class org.freedesktop.gstreamer.GStreamer. "
"Please copy GStreamer.java file into your project: "
"https://gitlab.freedesktop.org/gstreamer/gstreamer/-/tree/master/data/android/GStreamer.java");
return 0;
}
if ((*env)->RegisterNatives (env, klass, native_methods,
G_N_ELEMENTS (native_methods))) {
__android_log_print (ANDROID_LOG_ERROR, "GStreamer",
"Could not register native methods for org.freedesktop.gstreamer.GStreamer");
return 0;
}
/* Remember Java VM */
_java_vm = vm;
return JNI_VERSION_1_4;
}
void
JNI_OnUnload (JavaVM * vm, void *reversed)
{
JNIEnv *env = NULL;
if ((*vm)->GetEnv (vm, (void **) &env, JNI_VERSION_1_4) != JNI_OK) {
__android_log_print (ANDROID_LOG_ERROR, "GStreamer",
"Could not retrieve JNIEnv");
return;
}
if (_context) {
(*env)->DeleteGlobalRef (env, _context);
_context = NULL;
}
if (_class_loader) {
(*env)->DeleteGlobalRef (env, _class_loader);
_class_loader = NULL;
}
_java_vm = NULL;
}

View file

@ -150,6 +150,12 @@ gst_headers = files(
)
install_headers(gst_headers, subdir : 'gstreamer-1.0/gst')
extra_deps = []
if host_system == 'android'
gst_sources += 'gstandroid.c'
extra_deps += cc.find_library('log')
endif
gst_registry = get_option('registry')
if gst_registry
gst_registry_sources = files('gstregistrybinary.c')
@ -246,8 +252,8 @@ libgst = library('gstreamer-1.0', gst_sources,
include_directories('parse')],
link_with : printf_lib,
install : true,
dependencies : [gobject_dep, gmodule_dep, glib_dep, mathlib, dl_dep] + backtrace_deps
+ platform_deps,
dependencies : [gobject_dep, gmodule_dep, glib_dep, mathlib, dl_dep,
backtrace_deps, platform_deps, extra_deps],
)
pkg_name = 'gstreamer-1.0'