From ff1b8da189229b0b291135c5d6951f0a8cbba61b Mon Sep 17 00:00:00 2001 From: Xavi Artigas Date: Tue, 23 Oct 2012 16:59:18 +0200 Subject: [PATCH] Initial Android Tutorial 4 --- .../android-tutorial-4/AndroidManifest.xml | 22 ++ .../android-tutorial-4/jni/Android.mk | 20 ++ .../android-tutorial-4/jni/tutorial-4.c | 337 ++++++++++++++++++ .../res/drawable-hdpi/gst_sdk_icon.png | Bin 0 -> 5937 bytes .../res/drawable-ldpi/gst_sdk_icon.png | Bin 0 -> 3136 bytes .../res/drawable-mdpi/gst_sdk_icon.png | Bin 0 -> 4062 bytes .../res/drawable-xhdpi/gst_sdk_icon.png | Bin 0 -> 7477 bytes .../android-tutorial-4/res/layout/main.xml | 63 ++++ .../android-tutorial-4/res/values/strings.xml | 18 + .../tutorial_4/GStreamerSurfaceView.java | 85 +++++ .../tutorial_4/Tutorial4.java | 143 ++++++++ 11 files changed, 688 insertions(+) create mode 100644 gst-sdk/tutorials/android-tutorial-4/AndroidManifest.xml create mode 100644 gst-sdk/tutorials/android-tutorial-4/jni/Android.mk create mode 100644 gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/drawable-hdpi/gst_sdk_icon.png create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/drawable-ldpi/gst_sdk_icon.png create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/drawable-mdpi/gst_sdk_icon.png create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/drawable-xhdpi/gst_sdk_icon.png create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/layout/main.xml create mode 100644 gst-sdk/tutorials/android-tutorial-4/res/values/strings.xml create mode 100644 gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/GStreamerSurfaceView.java create mode 100644 gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/Tutorial4.java diff --git a/gst-sdk/tutorials/android-tutorial-4/AndroidManifest.xml b/gst-sdk/tutorials/android-tutorial-4/AndroidManifest.xml new file mode 100644 index 0000000000..f09dd03933 --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/gst-sdk/tutorials/android-tutorial-4/jni/Android.mk b/gst-sdk/tutorials/android-tutorial-4/jni/Android.mk new file mode 100644 index 0000000000..ea48c78cd2 --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/jni/Android.mk @@ -0,0 +1,20 @@ +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := tutorial-4 +LOCAL_SRC_FILES := tutorial-4.c +LOCAL_SHARED_LIBRARIES := gstreamer_android +LOCAL_LDLIBS := -llog -landroid +include $(BUILD_SHARED_LIBRARY) + +ifndef GSTREAMER_SDK_ROOT +ifndef GSTREAMER_SDK_ROOT_ANDROID +$(error GSTREAMER_SDK_ROOT_ANDROID is not defined!) +endif +GSTREAMER_SDK_ROOT := $(GSTREAMER_SDK_ROOT_ANDROID) +endif +GSTREAMER_NDK_BUILD_PATH := $(GSTREAMER_SDK_ROOT)/share/gst-android/ndk-build/ +include $(GSTREAMER_NDK_BUILD_PATH)/plugins.mk +GSTREAMER_PLUGINS := $(GSTREAMER_PLUGINS_CORE) $(GSTREAMER_PLUGINS_CODECS) $(GSTREAMER_PLUGINS_SYS) $(GSTREAMER_PLUGINS_EFFECTS) +include $(GSTREAMER_NDK_BUILD_PATH)/gstreamer.mk diff --git a/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c b/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c new file mode 100644 index 0000000000..e739623c2a --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c @@ -0,0 +1,337 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +GST_DEBUG_CATEGORY_STATIC (debug_category); +#define GST_CAT_DEFAULT debug_category + +/* + * These macros provide a way to store the native pointer to CustomData, which might be 32 or 64 bits, into + * a jlong, which is always 64 bits, without warnings. + */ +#if GLIB_SIZEOF_VOID_P == 8 +# define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(*env)->GetLongField (env, thiz, fieldID) +# define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)data) +#else +# define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(jint)(*env)->GetLongField (env, thiz, fieldID) +# define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)(jint)data) +#endif + +/* Structure to contain all our information, so we can pass it to callbacks */ +typedef struct _CustomData { + jobject app; /* Application instance, used to call its methods. A global reference is kept. */ + GstElement *pipeline; /* The running pipeline */ + GMainContext *context; /* GLib context used to run the main loop */ + GMainLoop *main_loop; /* GLib main loop */ + gboolean initialized; /* To avoid informing the UI multiple times about the initialization */ + GstElement *video_sink; /* The video sink element which receives XOverlay commands */ + ANativeWindow *native_window; /* The Android native window where video will be rendered */ +} CustomData; + +/* These global variables cache values which are not changing during execution */ +static pthread_t gst_app_thread; +static pthread_key_t current_jni_env; +static JavaVM *java_vm; +static jfieldID custom_data_field_id; +static jmethodID set_message_method_id; +static jmethodID on_gstreamer_initialized_method_id; + +/* + * Private methods + */ + +/* Register this thread with the VM */ +static JNIEnv *attach_current_thread (void) { + JNIEnv *env; + JavaVMAttachArgs args; + + GST_DEBUG ("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; +} + +/* Unregister this thread from the VM */ +static void detach_current_thread (void *env) { + GST_DEBUG ("Detaching thread %p", g_thread_self ()); + (*java_vm)->DetachCurrentThread (java_vm); +} + +/* Retrieve the JNI environment for this thread */ +static JNIEnv *get_jni_env (void) { + JNIEnv *env; + + if ((env = pthread_getspecific (current_jni_env)) == NULL) { + env = attach_current_thread (); + pthread_setspecific (current_jni_env, env); + } + + return env; +} + +/* Change the content of the UI's TextView */ +static void set_ui_message (const gchar *message, CustomData *data) { + JNIEnv *env = get_jni_env (); + GST_DEBUG ("Setting message to: %s", message); + jstring jmessage = (*env)->NewStringUTF(env, message); + (*env)->CallVoidMethod (env, data->app, set_message_method_id, jmessage); + if ((*env)->ExceptionCheck (env)) { + GST_ERROR ("Failed to call Java method"); + (*env)->ExceptionClear (env); + } + (*env)->DeleteLocalRef (env, jmessage); +} + +/* Retrieve errors from the bus and show them on the UI */ +static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + GError *err; + gchar *debug_info; + gchar *message_string; + + gst_message_parse_error (msg, &err, &debug_info); + message_string = g_strdup_printf ("Error received from element %s: %s", GST_OBJECT_NAME (msg->src), err->message); + g_clear_error (&err); + g_free (debug_info); + set_ui_message (message_string, data); + g_free (message_string); + gst_element_set_state (data->pipeline, GST_STATE_NULL); +} + +/* Notify UI about pipeline state changes */ +static void state_changed_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + GstState old_state, new_state, pending_state; + gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state); + /* Only pay attention to messages coming from the pipeline, not its children */ + if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->pipeline)) { + gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state)); + set_ui_message(message, data); + g_free (message); + } +} + +/* Check if all conditions are met to report GStreamer as initialized. + * These conditions will change depending on the application */ +static void check_initialization_complete (CustomData *data) { + JNIEnv *env = get_jni_env (); + if (!data->initialized && data->native_window && data->main_loop) { + GST_DEBUG ("Initialization complete, notifying application. native_window:%p main_loop:%p", data->native_window, data->main_loop); + + /* The main loop is running and we received a native window, inform the sink about it */ + gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->video_sink), (guintptr)data->native_window); + + (*env)->CallVoidMethod (env, data->app, on_gstreamer_initialized_method_id); + if ((*env)->ExceptionCheck (env)) { + GST_ERROR ("Failed to call Java method"); + (*env)->ExceptionClear (env); + } + data->initialized = TRUE; + } +} + +/* Main method for the native code. This is executed on its own thread. */ +static void *app_function (void *userdata) { + JavaVMAttachArgs args; + GstBus *bus; + CustomData *data = (CustomData *)userdata; + GSource *bus_source; + GError *error = NULL; + + GST_DEBUG ("Creating pipeline in CustomData at %p", data); + + /* Create our own GLib Main Context and make it the default one */ + data->context = g_main_context_new (); + g_main_context_push_thread_default(data->context); + + /* Build pipeline */ + data->pipeline = gst_parse_launch("videotestsrc ! warptv ! ffmpegcolorspace ! autovideosink", &error); + if (error) { + gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message); + g_clear_error (&error); + set_ui_message(message, data); + g_free (message); + return NULL; + } + + /* Set the pipeline to READY, so it can already accept a window handle, if we have one */ + gst_element_set_state(data->pipeline, GST_STATE_READY); + + data->video_sink = gst_bin_get_by_interface(GST_BIN(data->pipeline), GST_TYPE_X_OVERLAY); + if (!data->video_sink) { + GST_ERROR ("Could not retrieve video sink"); + return NULL; + } + + /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */ + bus = gst_element_get_bus (data->pipeline); + bus_source = gst_bus_create_watch (bus); + g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func, NULL, NULL); + g_source_attach (bus_source, data->context); + g_source_unref (bus_source); + g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, data); + g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, data); + gst_object_unref (bus); + + /* Create a GLib Main Loop and set it to run */ + GST_DEBUG ("Entering main loop... (CustomData:%p)", data); + data->main_loop = g_main_loop_new (data->context, FALSE); + check_initialization_complete (data); + g_main_loop_run (data->main_loop); + GST_DEBUG ("Exited main loop"); + g_main_loop_unref (data->main_loop); + data->main_loop = NULL; + + /* Free resources */ + g_main_context_pop_thread_default(data->context); + g_main_context_unref (data->context); + gst_element_set_state (data->pipeline, GST_STATE_NULL); + gst_object_unref (data->video_sink); + gst_object_unref (data->pipeline); + + return NULL; +} + +/* + * Java Bindings + */ + +/* Instruct the native code to create its internal data structure, pipeline and thread */ +static void gst_native_init (JNIEnv* env, jobject thiz) { + CustomData *data = g_new0 (CustomData, 1); + SET_CUSTOM_DATA (env, thiz, custom_data_field_id, data); + GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-4", 0, "Android tutorial 4"); + gst_debug_set_threshold_for_name("tutorial-4", GST_LEVEL_DEBUG); + GST_DEBUG ("Created CustomData at %p", data); + data->app = (*env)->NewGlobalRef (env, thiz); + GST_DEBUG ("Created GlobalRef for app object at %p", data->app); + pthread_create (&gst_app_thread, NULL, &app_function, data); +} + +/* Quit the main loop, remove the native thread and free resources */ +static void gst_native_finalize (JNIEnv* env, jobject thiz) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + GST_DEBUG ("Quitting main loop..."); + g_main_loop_quit (data->main_loop); + GST_DEBUG ("Waiting for thread to finish..."); + pthread_join (gst_app_thread, NULL); + GST_DEBUG ("Deleting GlobalRef for app object at %p", data->app); + (*env)->DeleteGlobalRef (env, data->app); + GST_DEBUG ("Freeing CustomData at %p", data); + g_free (data); + SET_CUSTOM_DATA (env, thiz, custom_data_field_id, NULL); + GST_DEBUG ("Done finalizing"); +} + +/* Set pipeline to PLAYING state */ +static void gst_native_play (JNIEnv* env, jobject thiz) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + GST_DEBUG ("Setting state to PLAYING"); + gst_element_set_state (data->pipeline, GST_STATE_PLAYING); +} + +/* Set pipeline to PAUSED state */ +static void gst_native_pause (JNIEnv* env, jobject thiz) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + GST_DEBUG ("Setting state to PAUSED"); + gst_element_set_state (data->pipeline, GST_STATE_PAUSED); +} + +/* Static class initializer: retrieve method and field IDs */ +static jboolean gst_native_class_init (JNIEnv* env, jclass klass) { + custom_data_field_id = (*env)->GetFieldID (env, klass, "native_custom_data", "J"); + set_message_method_id = (*env)->GetMethodID (env, klass, "setMessage", "(Ljava/lang/String;)V"); + on_gstreamer_initialized_method_id = (*env)->GetMethodID (env, klass, "onGStreamerInitialized", "()V"); + + if (!custom_data_field_id || !set_message_method_id || !on_gstreamer_initialized_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, "tutorial-4", "The calling class does not implement all necessary interface methods"); + return JNI_FALSE; + } + return JNI_TRUE; +} + +static void gst_native_surface_init (JNIEnv *env, jobject thiz, jobject surface) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + ANativeWindow *new_native_window = ANativeWindow_fromSurface(env, surface); + GST_DEBUG ("Received surface %p (native window %p)", surface, new_native_window); + + if (data->native_window) { + ANativeWindow_release (data->native_window); + if (data->native_window == new_native_window) { + GST_DEBUG ("New native window is the same as the previous one", data->native_window); + if (data->video_sink) { + gst_x_overlay_expose(GST_X_OVERLAY (data->video_sink)); + gst_x_overlay_expose(GST_X_OVERLAY (data->video_sink)); + } + return; + } else { + GST_DEBUG ("Released previous native window %p", data->native_window); + data->initialized = FALSE; + } + } + data->native_window = new_native_window; + + check_initialization_complete (data); +} + +static void gst_native_surface_finalize (JNIEnv *env, jobject thiz) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + GST_DEBUG ("Releasing Native Window %p", data->native_window); + + if (data->video_sink) { + gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->video_sink), (guintptr)NULL); + gst_element_set_state (data->pipeline, GST_STATE_READY); + } + + ANativeWindow_release (data->native_window); + data->native_window = NULL; + data->initialized = FALSE; +} + +/* List of implemented native methods */ +static JNINativeMethod native_methods[] = { + { "nativeInit", "()V", (void *) gst_native_init}, + { "nativeFinalize", "()V", (void *) gst_native_finalize}, + { "nativePlay", "()V", (void *) gst_native_play}, + { "nativePause", "()V", (void *) gst_native_pause}, + { "nativeSurfaceInit", "(Ljava/lang/Object;)V", (void *) gst_native_surface_init}, + { "nativeSurfaceFinalize", "()V", (void *) gst_native_surface_finalize}, + { "nativeClassInit", "()Z", (void *) gst_native_class_init} +}; + +/* Library initializer */ +jint JNI_OnLoad(JavaVM *vm, 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, "tutorial-4", "Could not retrieve JNIEnv"); + return 0; + } + jclass klass = (*env)->FindClass (env, "com/gst_sdk_tutorials/tutorial_4/Tutorial4"); + (*env)->RegisterNatives (env, klass, native_methods, G_N_ELEMENTS(native_methods)); + + pthread_key_create (¤t_jni_env, detach_current_thread); + + return JNI_VERSION_1_4; +} diff --git a/gst-sdk/tutorials/android-tutorial-4/res/drawable-hdpi/gst_sdk_icon.png b/gst-sdk/tutorials/android-tutorial-4/res/drawable-hdpi/gst_sdk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..783fbcd7184c130f3e63451eed852066d71651b8 GIT binary patch literal 5937 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 literal 0 HcmV?d00001 diff --git a/gst-sdk/tutorials/android-tutorial-4/res/drawable-ldpi/gst_sdk_icon.png b/gst-sdk/tutorials/android-tutorial-4/res/drawable-ldpi/gst_sdk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae8cc0aa9e4bf30f3b502c7122eb4cba5a4270b GIT binary patch literal 3136 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 NYO3m}lq*@j`ag0n;(q`D literal 0 HcmV?d00001 diff --git a/gst-sdk/tutorials/android-tutorial-4/res/layout/main.xml b/gst-sdk/tutorials/android-tutorial-4/res/layout/main.xml new file mode 100644 index 0000000000..c3baac85a6 --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/res/layout/main.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/gst-sdk/tutorials/android-tutorial-4/res/values/strings.xml b/gst-sdk/tutorials/android-tutorial-4/res/values/strings.xml new file mode 100644 index 0000000000..99d0a0cb5d --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/res/values/strings.xml @@ -0,0 +1,18 @@ + + + Android tutorial 4 + Play + Stop + Select + Android tutorial 1 + Location + folder can\'t be read! + New + Select + File name: + Cancel + Save + No Data + #ffff0000 + Error + diff --git a/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/GStreamerSurfaceView.java b/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/GStreamerSurfaceView.java new file mode 100644 index 0000000000..c2cd2b2477 --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/GStreamerSurfaceView.java @@ -0,0 +1,85 @@ +package com.gst_sdk_tutorials.tutorial_4; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceView; +import android.view.View; + +// A simple SurfaceView whose width and height can be set from the outside +public class GStreamerSurfaceView extends SurfaceView { + public int media_width = 320; + public int media_height = 240; + + // Mandatory constructors, they do not do much + public GStreamerSurfaceView(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + public GStreamerSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public GStreamerSurfaceView (Context context) { + super(context); + } + + // 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 + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int width = 0, height = 0; + int wmode = View.MeasureSpec.getMode(widthMeasureSpec); + int hmode = View.MeasureSpec.getMode(heightMeasureSpec); + int wsize = View.MeasureSpec.getSize(widthMeasureSpec); + int hsize = View.MeasureSpec.getSize(heightMeasureSpec); + + Log.i ("GStreamer", "onMeasure called with " + media_width + "x" + media_height); + // Obey width rules + switch (wmode) { + case View.MeasureSpec.AT_MOST: + if (hmode == View.MeasureSpec.EXACTLY) { + width = Math.min(hsize * media_width / media_height, wsize); + break; + } + case View.MeasureSpec.EXACTLY: + width = wsize; + break; + case View.MeasureSpec.UNSPECIFIED: + width = media_width; + } + + // Obey height rules + switch (hmode) { + case View.MeasureSpec.AT_MOST: + if (wmode == View.MeasureSpec.EXACTLY) { + height = Math.min(wsize * media_height / media_width, hsize); + break; + } + case View.MeasureSpec.EXACTLY: + height = hsize; + break; + case View.MeasureSpec.UNSPECIFIED: + height = media_height; + } + + // Finally, calculate best size when both axis are free + if (hmode == View.MeasureSpec.AT_MOST && wmode == View.MeasureSpec.AT_MOST) { + int correct_height = width * media_height / media_width; + int correct_width = height * media_width / media_height; + + if (correct_height < height) + height = correct_height; + else + width = correct_width; + } + + // Obey minimum size + width = Math.max (getSuggestedMinimumWidth(), width); + height = Math.max (getSuggestedMinimumHeight(), height); + setMeasuredDimension(width, height); + } + +} diff --git a/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/Tutorial4.java b/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/Tutorial4.java new file mode 100644 index 0000000000..edefde331f --- /dev/null +++ b/gst-sdk/tutorials/android-tutorial-4/src/com/gst_sdk_tutorials/tutorial_4/Tutorial4.java @@ -0,0 +1,143 @@ +package com.gst_sdk_tutorials.tutorial_4; + +import android.app.Activity; +import android.os.Bundle; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import com.gstreamer.GStreamer; + +public class Tutorial4 extends Activity implements SurfaceHolder.Callback { + private native void nativeInit(); // Initialize native code, build pipeline, etc + private native void nativeFinalize(); // Destroy pipeline and shutdown native code + private native void nativePlay(); // Set pipeline to PLAYING + private native void nativePause(); // Set pipeline to PAUSED + private static native boolean nativeClassInit(); // Initialize native class: cache Method IDs for callbacks + private native void nativeSurfaceInit(Object surface); + private native void nativeSurfaceFinalize(); + private long native_custom_data; // Native code will use this to keep private data + + private boolean is_playing_desired; // Whether the user asked to go to PLAYING + + // Called when the activity is first created. + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + // Initialize GStreamer and warn if it fails + try { + GStreamer.init(this); + } catch (Exception e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); + finish(); + return; + } + + setContentView(R.layout.main); + + ImageButton play = (ImageButton) this.findViewById(R.id.button_play); + play.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + is_playing_desired = true; + nativePlay(); + } + }); + + ImageButton pause = (ImageButton) this.findViewById(R.id.button_stop); + pause.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + is_playing_desired = false; + nativePause(); + } + }); + + SurfaceView sv = (SurfaceView) this.findViewById(R.id.surface_video); + SurfaceHolder sh = sv.getHolder(); + sh.addCallback(this); + + if (savedInstanceState != null) { + is_playing_desired = savedInstanceState.getBoolean("playing"); + Log.i ("GStreamer", "Activity created. Saved state is playing:" + is_playing_desired); + } else { + is_playing_desired = false; + Log.i ("GStreamer", "Activity created. There is no saved state, playing: false"); + } + + // Start with disabled buttons, until native code is initialized + this.findViewById(R.id.button_play).setEnabled(false); + this.findViewById(R.id.button_stop).setEnabled(false); + + nativeInit(); + } + + protected void onSaveInstanceState (Bundle outState) { + Log.d ("GStreamer", "Saving state, playing:" + is_playing_desired); + outState.putBoolean("playing", is_playing_desired); + } + + protected void onDestroy() { + nativeFinalize(); + super.onDestroy(); + } + + // Called from native code. This sets the content of the TextView from the UI thread. + private void setMessage(final String message) { + final TextView tv = (TextView) this.findViewById(R.id.textview_message); + runOnUiThread (new Runnable() { + public void run() { + tv.setText(message); + } + }); + } + + // Called from native code. Native code calls this once it has created its pipeline and + // the main loop is running, so it is ready to accept commands. + private void onGStreamerInitialized () { + Log.i ("GStreamer", "Gst initialized. Restoring state, playing:" + is_playing_desired); + // Restore previous playing state + if (is_playing_desired) { + nativePlay(); + } else { + nativePause(); + } + + // Re-enable buttons, now that GStreamer is initialized + final Activity activity = this; + runOnUiThread(new Runnable() { + public void run() { + activity.findViewById(R.id.button_play).setEnabled(true); + activity.findViewById(R.id.button_stop).setEnabled(true); + } + }); + } + + static { + System.loadLibrary("gstreamer_android"); + System.loadLibrary("tutorial-4"); + nativeClassInit(); + } + + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + Log.d("GStreamer", "Surface changed to format " + format + " width " + + width + " height " + height); + nativeSurfaceInit (holder.getSurface()); + } + + public void surfaceCreated(SurfaceHolder holder) { + Log.d("GStreamer", "Surface created: " + holder.getSurface()); + } + + public void surfaceDestroyed(SurfaceHolder holder) { + Log.d("GStreamer", "Surface destroyed"); + nativeSurfaceFinalize (); + } + +}