From 7a319d7feedfda24bd8cdb2479e991d01ba80786 Mon Sep 17 00:00:00 2001 From: Xavi Artigas Date: Thu, 25 Oct 2012 18:21:13 +0200 Subject: [PATCH] Add remaining bits from tutorial 5 to tutorial 4: Seeking, buffering, etc --- .../android-tutorial-4/jni/tutorial-4.c | 169 +++++++++++++++++- .../tutorial_4/Tutorial4.java | 104 ++++++++++- 2 files changed, 259 insertions(+), 14 deletions(-) diff --git a/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c b/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c index b62f4aa434..fb7182bb44 100644 --- a/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c +++ b/gst-sdk/tutorials/android-tutorial-4/jni/tutorial-4.c @@ -31,6 +31,11 @@ typedef struct _CustomData { GMainLoop *main_loop; /* GLib main loop */ gboolean initialized; /* To avoid informing the UI multiple times about the initialization */ ANativeWindow *native_window; /* The Android native window where video will be rendered */ + GstState state, target_state; + gint64 duration; + gint64 desired_position; + GstClockTime last_seek_time; + gboolean is_live; } CustomData; /* playbin2 flags */ @@ -44,6 +49,7 @@ 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 set_current_position_method_id; static jmethodID on_gstreamer_initialized_method_id; static jmethodID on_media_size_changed_method_id; @@ -100,6 +106,76 @@ static void set_ui_message (const gchar *message, CustomData *data) { (*env)->DeleteLocalRef (env, jmessage); } +static void set_current_ui_position (gint position, gint duration, CustomData *data) { + JNIEnv *env = get_jni_env (); + (*env)->CallVoidMethod (env, data->app, set_current_position_method_id, position, duration); + if ((*env)->ExceptionCheck (env)) { + GST_ERROR ("Failed to call Java method"); + (*env)->ExceptionClear (env); + } +} + +static gboolean refresh_ui (CustomData *data) { + GstFormat fmt = GST_FORMAT_TIME; + gint64 current = -1; + gint64 position; + + /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */ + if (!data || !data->pipeline || data->state < GST_STATE_PAUSED) + return TRUE; + + /* If we didn't know it yet, query the stream duration */ + if (!GST_CLOCK_TIME_IS_VALID (data->duration)) { + if (!gst_element_query_duration (data->pipeline, &fmt, &data->duration)) { + GST_WARNING ("Could not query current duration"); + } + } + + if (gst_element_query_position (data->pipeline, &fmt, &position)) { + /* Java expects these values in milliseconds, and GStreamer provides nanoseconds */ + set_current_ui_position (position / GST_MSECOND, data->duration / GST_MSECOND, data); + } + return TRUE; +} + +static void execute_seek (gint64 desired_position, CustomData *data); + +static gboolean delayed_seek_cb (CustomData *data) { + GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (data->desired_position)); + data->last_seek_time = GST_CLOCK_TIME_NONE; + execute_seek (data->desired_position, data); + return FALSE; +} + +static void execute_seek (gint64 desired_position, CustomData *data) { + gboolean res; + gint64 diff; + + if (desired_position == GST_CLOCK_TIME_NONE) + return; + + diff = gst_util_get_timestamp () - data->last_seek_time; + + if (GST_CLOCK_TIME_IS_VALID (data->last_seek_time) && diff < 500 * GST_MSECOND) { + GSource *timeout_source; + + if (!GST_CLOCK_TIME_IS_VALID (data->desired_position)) { + timeout_source = g_timeout_source_new (diff / GST_MSECOND); + g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, data, NULL); + g_source_attach (timeout_source, data->context); + g_source_unref (timeout_source); + } + data->desired_position = desired_position; + GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT, + GST_TIME_ARGS (desired_position), GST_TIME_ARGS (500 * GST_MSECOND - diff)); + } else { + GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (desired_position)); + res = gst_element_seek_simple (data->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, desired_position); + data->last_seek_time = gst_util_get_timestamp (); + data->desired_position = GST_CLOCK_TIME_NONE; + } +} + /* Retrieve errors from the bus and show them on the UI */ static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) { GError *err; @@ -112,9 +188,49 @@ static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) { g_free (debug_info); set_ui_message (message_string, data); g_free (message_string); + data->target_state = GST_STATE_NULL; gst_element_set_state (data->pipeline, GST_STATE_NULL); } +static void eos_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + set_ui_message (GST_MESSAGE_TYPE_NAME (msg), data); + refresh_ui (data); + data->target_state = GST_STATE_PAUSED; + data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL); + execute_seek (0, data); +} + +static void duration_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + data->duration = GST_CLOCK_TIME_NONE; +} + +static void buffering_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + gint percent; + + if (data->is_live) + return; + + gst_message_parse_buffering (msg, &percent); + if (percent < 100 && data->target_state >= GST_STATE_PAUSED) { + gchar * message_string = g_strdup_printf ("Buffering %d%%", percent); + gst_element_set_state (data->pipeline, GST_STATE_PAUSED); + set_ui_message (message_string, data); + g_free (message_string); + } else if (data->target_state >= GST_STATE_PLAYING) { + gst_element_set_state (data->pipeline, GST_STATE_PLAYING); + set_ui_message ("PLAYING", data); + } else if (data->target_state >= GST_STATE_PAUSED) { + set_ui_message ("PAUSED", data); + } +} + +static void clock_lost_cb (GstBus *bus, GstMessage *msg, CustomData *data) { + if (data->target_state >= GST_STATE_PLAYING) { + gst_element_set_state (data->pipeline, GST_STATE_PAUSED); + gst_element_set_state (data->pipeline, GST_STATE_PLAYING); + } +} + /* Retrieve the video sink's Caps and tell the application about the media size */ static void check_media_size (CustomData *data) { JNIEnv *env = get_jni_env (); @@ -155,6 +271,9 @@ static void state_changed_cb (GstBus *bus, GstMessage *msg, CustomData *data) { 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)) { + data->state = new_state; + if (data->state >= GST_STATE_PAUSED && GST_CLOCK_TIME_IS_VALID (data->desired_position)) + execute_seek (data->desired_position, data); gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state)); set_ui_message(message, data); g_free (message); @@ -190,7 +309,9 @@ static void *app_function (void *userdata) { JavaVMAttachArgs args; GstBus *bus; CustomData *data = (CustomData *)userdata; + GSource *timeout_source; GSource *bus_source; + GError *error = NULL; guint flags; GST_DEBUG ("Creating pipeline in CustomData at %p", data); @@ -200,9 +321,12 @@ static void *app_function (void *userdata) { g_main_context_push_thread_default(data->context); /* Build pipeline */ - data->pipeline = gst_element_factory_make ("playbin2", NULL); - if (!data->pipeline) { - set_ui_message("Unable to build pipeline", data); + data->pipeline = gst_parse_launch("playbin2", &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; } @@ -212,6 +336,7 @@ static void *app_function (void *userdata) { g_object_set (data->pipeline, "flags", flags, NULL); /* Set the pipeline to READY, so it can already accept a window handle, if we have one */ + data->target_state = GST_STATE_READY; gst_element_set_state(data->pipeline, GST_STATE_READY); /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */ @@ -221,9 +346,19 @@ static void *app_function (void *userdata) { 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::eos", (GCallback)eos_cb, data); g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, data); + g_signal_connect (G_OBJECT (bus), "message::duration", (GCallback)duration_cb, data); + g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, data); + g_signal_connect (G_OBJECT (bus), "message::clock-lost", (GCallback)clock_lost_cb, data); gst_object_unref (bus); + /* Register a function that GLib will call 4 times per second */ + timeout_source = g_timeout_source_new (250); + g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, data, NULL); + g_source_attach (timeout_source, data->context); + g_source_unref (timeout_source); + /* 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); @@ -236,6 +371,7 @@ static void *app_function (void *userdata) { /* Free resources */ g_main_context_pop_thread_default(data->context); g_main_context_unref (data->context); + data->target_state = GST_STATE_NULL; gst_element_set_state (data->pipeline, GST_STATE_NULL); gst_object_unref (data->pipeline); @@ -279,9 +415,12 @@ void gst_native_set_uri (JNIEnv* env, jobject thiz, jstring uri) { if (!data || !data->pipeline) return; const jbyte *char_uri = (*env)->GetStringUTFChars (env, uri, NULL); GST_DEBUG ("Setting URI to %s", char_uri); - gst_element_set_state (data->pipeline, GST_STATE_READY); + if (data->target_state >= GST_STATE_READY) + gst_element_set_state (data->pipeline, GST_STATE_READY); g_object_set(data->pipeline, "uri", char_uri, NULL); (*env)->ReleaseStringUTFChars (env, uri, char_uri); + data->duration = GST_CLOCK_TIME_NONE; + data->is_live = (gst_element_set_state (data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL); } /* Set pipeline to PLAYING state */ @@ -289,7 +428,8 @@ 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); + data->target_state = GST_STATE_PLAYING; + data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL); } /* Set pipeline to PAUSED state */ @@ -297,18 +437,32 @@ 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); + data->target_state = GST_STATE_PAUSED; + data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL); +} + +void gst_native_set_position (JNIEnv* env, jobject thiz, int milliseconds) { + CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id); + if (!data) return; + gint64 desired_position = (gint64)(milliseconds * GST_MSECOND); + if (data->state >= GST_STATE_PAUSED) { + execute_seek(desired_position, data); + } else { + GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (desired_position)); + data->desired_position = desired_position; + } } /* 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"); + set_current_position_method_id = (*env)->GetMethodID (env, klass, "setCurrentPosition", "(II)V"); on_gstreamer_initialized_method_id = (*env)->GetMethodID (env, klass, "onGStreamerInitialized", "()V"); on_media_size_changed_method_id = (*env)->GetMethodID (env, klass, "onMediaSizeChanged", "(II)V"); if (!custom_data_field_id || !set_message_method_id || !on_gstreamer_initialized_method_id || - !on_media_size_changed_method_id) { + !on_media_size_changed_method_id || !set_current_position_method_id) { /* We emit this message through the Android log instead of the GStreamer log because the later * has not been initialized yet. */ @@ -365,6 +519,7 @@ static JNINativeMethod native_methods[] = { { "nativeSetUri", "(Ljava/lang/String;)V", (void *) gst_native_set_uri}, { "nativePlay", "()V", (void *) gst_native_play}, { "nativePause", "()V", (void *) gst_native_pause}, + { "nativeSetPosition", "(I)V", (void*) gst_native_set_position}, { "nativeSurfaceInit", "(Ljava/lang/Object;)V", (void *) gst_native_surface_init}, { "nativeSurfaceFinalize", "()V", (void *) gst_native_surface_finalize}, { "nativeClassInit", "()Z", (void *) gst_native_class_init} 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 index ab1e2aed5a..b026ef0a86 100644 --- 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 @@ -1,5 +1,9 @@ package com.gst_sdk_tutorials.tutorial_4; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + import android.app.Activity; import android.os.Bundle; import android.util.Log; @@ -8,16 +12,19 @@ import android.view.SurfaceView; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; import android.widget.Toast; import com.gstreamer.GStreamer; -public class Tutorial4 extends Activity implements SurfaceHolder.Callback { +public class Tutorial4 extends Activity implements SurfaceHolder.Callback, OnSeekBarChangeListener { private native void nativeInit(); // Initialize native code, build pipeline, etc private native void nativeFinalize(); // Destroy pipeline and shutdown native code private native void nativeSetUri(String uri); // Set the URI of the media to play private native void nativePlay(); // Set pipeline to PLAYING + private native void nativeSetPosition(int milliseconds); // Seek to the indicated position, in milliseconds 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); // A new surface is available @@ -25,8 +32,13 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { 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 + private int position; // Current position, reported by native code + private int duration; // Current clip duration, reported by native code + private boolean is_local_media; // Whether this clip is stored locally or is being streamed + private int desired_position; // Position where the users wants to seek to + private String mediaUri; // URI of the clip being played - private String mediaUri = "http://docs.gstreamer.com/media/sintel_trailer-368p.ogv"; + private final String defaultMediaUri = "http://docs.gstreamer.com/media/sintel_trailer-368p.ogv"; // Called when the activity is first created. @Override @@ -65,13 +77,25 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { SurfaceHolder sh = sv.getHolder(); sh.addCallback(this); + SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar); + sb.setOnSeekBarChangeListener(this); + + // Retrieve our previous state, or initialize it to default values if (savedInstanceState != null) { is_playing_desired = savedInstanceState.getBoolean("playing"); - Log.i ("GStreamer", "Activity created. Saved state is playing:" + is_playing_desired); + position = savedInstanceState.getInt("position"); + duration = savedInstanceState.getInt("duration"); + mediaUri = savedInstanceState.getString("mediaUri"); + Log.i ("GStreamer", "Activity created with saved state:"); } else { is_playing_desired = false; - Log.i ("GStreamer", "Activity created. There is no saved state, playing: false"); + position = duration = 0; + mediaUri = defaultMediaUri; + Log.i ("GStreamer", "Activity created with no saved state:"); } + is_local_media = false; + Log.i ("GStreamer", " playing:" + is_playing_desired + " position:" + position + + " duration: " + duration + " uri: " + mediaUri); // Start with disabled buttons, until native code is initialized this.findViewById(R.id.button_play).setEnabled(false); @@ -81,8 +105,12 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { } protected void onSaveInstanceState (Bundle outState) { - Log.d ("GStreamer", "Saving state, playing:" + is_playing_desired); + Log.d ("GStreamer", "Saving state, playing:" + is_playing_desired + " position:" + position + + " duration: " + duration + " uri: " + mediaUri); outState.putBoolean("playing", is_playing_desired); + outState.putInt("position", position); + outState.putInt("duration", duration); + outState.putString("mediaUri", mediaUri); } protected void onDestroy() { @@ -100,12 +128,25 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { }); } + // Set the URI to play, and record whether it is a local or remote file + private void setMediaUri() { + nativeSetUri (mediaUri); + if (mediaUri.startsWith("file://")) is_local_media = true; + } + // 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); + Log.i ("GStreamer", "GStreamer initialized:"); + Log.i ("GStreamer", " playing:" + is_playing_desired + " position:" + position + " uri: " + mediaUri); + // Restore previous playing state - nativeSetUri (mediaUri); + setMediaUri (); + // Actually, move to one millisecond in the future. Otherwise, due to rounding errors between the + // milliseconds used here and the nanoseconds used by GStreamer, we would be jumping a bit behind + // where we were before. This, combined with seeking to keyframe positions, would skip one keyframe + // backwards on each iteration. + nativeSetPosition (position + 1); if (is_playing_desired) { nativePlay(); } else { @@ -122,6 +163,37 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { }); } + // The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether + // it is an actual pipeline position or the position the user is currently dragging to. + private void updateTimeWidget () { + final TextView tv = (TextView) this.findViewById(R.id.textview_time); + final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar); + final int pos = sb.getProgress(); + + SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + final String message = df.format(new Date (pos)) + " / " + df.format(new Date (duration)); + tv.setText(message); + } + + // Called from native code + private void setCurrentPosition(final int position, final int duration) { + final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar); + + // Ignore position messages from the pipeline if the seek bar is being dragged + if (sb.isPressed()) return; + + runOnUiThread (new Runnable() { + public void run() { + sb.setMax(duration); + sb.setProgress(position); + updateTimeWidget(); + } + }); + this.position = position; + this.duration = duration; + } + static { System.loadLibrary("gstreamer_android"); System.loadLibrary("tutorial-4"); @@ -156,4 +228,22 @@ public class Tutorial4 extends Activity implements SurfaceHolder.Callback { }); } + public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) { + if (fromUser == false) return; + desired_position = progress; + // If this is a local file, allow scrub seeking, this is, seek soon as the slider is moved. + if (is_local_media) nativeSetPosition(desired_position); + updateTimeWidget(); + } + + public void onStartTrackingTouch(SeekBar sb) { + nativePause(); + } + + public void onStopTrackingTouch(SeekBar sb) { + // If this is a remote file, scrub seeking is probably not going to work smoothly enough. + // Therefore, perform only the seek when the slider is released. + if (!is_local_media) nativeSetPosition(desired_position); + if (is_playing_desired) nativePlay(); + } }