Split out documentation into subfolders.
[platform/upstream/gstreamer.git] / markdown / tutorials / android / media-player.md
1 # Android tutorial 4: A basic media player
2
3 ## Goal
4
5 ![screenshot]
6
7 Enough testing with synthetic images and audio tones! This tutorial
8 finally plays actual media, streamed directly from the Internet, in your
9 Android device. It shows:
10
11   - How to keep the User Interface regularly updated with the current
12     playback position and duration
13   - How to implement a [Seek
14     Bar](http://developer.android.com/reference/android/widget/SeekBar.html)
15   - How to report the media size to adapt the display surface
16
17 It also uses the knowledge gathered in the [](tutorials/basic/index.md) regarding:
18
19   - How to use `playbin` to play any kind of media
20   - How to handle network resilience problems
21
22 ## Introduction
23
24 From the previous tutorials, we already have almost all necessary pieces
25 to build a media player. The most complex part is assembling a pipeline
26 which retrieves, decodes and displays the media, but we already know
27 that the `playbin` element can take care of all that for us. We only
28 need to replace the manual pipeline we used in
29 [](tutorials/android/video.md) with a single-element
30 `playbin` pipeline and we are good to go!
31
32 However, we can do better than. We will add a [Seek
33 Bar](http://developer.android.com/reference/android/widget/SeekBar.html),
34 with a moving thumb that will advance as our current position in the
35 media advances. We will also allow the user to drag the thumb, to jump
36 (or *seek*) to a different position.
37
38 And finally, we will make the video surface adapt to the media size, so
39 the video sink is not forced to draw black borders around the clip.
40  This also allows the Android layout to adapt more nicely to the actual
41 media content. You can still force the video surface to have a specific
42 size if you really want to.
43
44 ## A basic media player \[Java code\]
45
46 **src/com/gst\_sdk\_tutorials/tutorial\_4/Tutorial4.java**
47
48 ``` java
49 package com.gst_sdk_tutorials.tutorial_4;
50
51 import java.text.SimpleDateFormat;
52 import java.util.Date;
53 import java.util.TimeZone;
54
55 import android.app.Activity;
56 import android.os.Bundle;
57 import android.util.Log;
58 import android.view.SurfaceHolder;
59 import android.view.SurfaceView;
60 import android.view.View;
61 import android.view.View.OnClickListener;
62 import android.widget.ImageButton;
63 import android.widget.SeekBar;
64 import android.widget.SeekBar.OnSeekBarChangeListener;
65 import android.widget.TextView;
66 import android.widget.Toast;
67
68 import org.freedesktop.gstreamer.GStreamer;
69
70 public class Tutorial4 extends Activity implements SurfaceHolder.Callback, OnSeekBarChangeListener {
71     private native void nativeInit();     // Initialize native code, build pipeline, etc
72     private native void nativeFinalize(); // Destroy pipeline and shutdown native code
73     private native void nativeSetUri(String uri); // Set the URI of the media to play
74     private native void nativePlay();     // Set pipeline to PLAYING
75     private native void nativeSetPosition(int milliseconds); // Seek to the indicated position, in milliseconds
76     private native void nativePause();    // Set pipeline to PAUSED
77     private static native boolean nativeClassInit(); // Initialize native class: cache Method IDs for callbacks
78     private native void nativeSurfaceInit(Object surface); // A new surface is available
79     private native void nativeSurfaceFinalize(); // Surface about to be destroyed
80     private long native_custom_data;      // Native code will use this to keep private data
81
82     private boolean is_playing_desired;   // Whether the user asked to go to PLAYING
83     private int position;                 // Current position, reported by native code
84     private int duration;                 // Current clip duration, reported by native code
85     private boolean is_local_media;       // Whether this clip is stored locally or is being streamed
86     private int desired_position;         // Position where the users wants to seek to
87     private String mediaUri;              // URI of the clip being played
88
89     private final String defaultMediaUri = "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-368p.ogv";
90
91     // Called when the activity is first created.
92     @Override
93     public void onCreate(Bundle savedInstanceState)
94     {
95         super.onCreate(savedInstanceState);
96
97         // Initialize GStreamer and warn if it fails
98         try {
99             GStreamer.init(this);
100         } catch (Exception e) {
101             Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
102             finish();
103             return;
104         }
105
106         setContentView(R.layout.main);
107
108         ImageButton play = (ImageButton) this.findViewById(R.id.button_play);
109         play.setOnClickListener(new OnClickListener() {
110             public void onClick(View v) {
111                 is_playing_desired = true;
112                 nativePlay();
113             }
114         });
115
116         ImageButton pause = (ImageButton) this.findViewById(R.id.button_stop);
117         pause.setOnClickListener(new OnClickListener() {
118             public void onClick(View v) {
119                 is_playing_desired = false;
120                 nativePause();
121             }
122         });
123
124         SurfaceView sv = (SurfaceView) this.findViewById(R.id.surface_video);
125         SurfaceHolder sh = sv.getHolder();
126         sh.addCallback(this);
127
128         SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
129         sb.setOnSeekBarChangeListener(this);
130
131         // Retrieve our previous state, or initialize it to default values
132         if (savedInstanceState != null) {
133             is_playing_desired = savedInstanceState.getBoolean("playing");
134             position = savedInstanceState.getInt("position");
135             duration = savedInstanceState.getInt("duration");
136             mediaUri = savedInstanceState.getString("mediaUri");
137             Log.i ("GStreamer", "Activity created with saved state:");
138         } else {
139             is_playing_desired = false;
140             position = duration = 0;
141             mediaUri = defaultMediaUri;
142             Log.i ("GStreamer", "Activity created with no saved state:");
143         }
144         is_local_media = false;
145         Log.i ("GStreamer", "  playing:" + is_playing_desired + " position:" + position +
146                 " duration: " + duration + " uri: " + mediaUri);
147
148         // Start with disabled buttons, until native code is initialized
149         this.findViewById(R.id.button_play).setEnabled(false);
150         this.findViewById(R.id.button_stop).setEnabled(false);
151
152         nativeInit();
153     }
154
155     protected void onSaveInstanceState (Bundle outState) {
156         Log.d ("GStreamer", "Saving state, playing:" + is_playing_desired + " position:" + position +
157                 " duration: " + duration + " uri: " + mediaUri);
158         outState.putBoolean("playing", is_playing_desired);
159         outState.putInt("position", position);
160         outState.putInt("duration", duration);
161         outState.putString("mediaUri", mediaUri);
162     }
163
164     protected void onDestroy() {
165         nativeFinalize();
166         super.onDestroy();
167     }
168
169     // Called from native code. This sets the content of the TextView from the UI thread.
170     private void setMessage(final String message) {
171         final TextView tv = (TextView) this.findViewById(R.id.textview_message);
172         runOnUiThread (new Runnable() {
173           public void run() {
174             tv.setText(message);
175           }
176         });
177     }
178
179     // Set the URI to play, and record whether it is a local or remote file
180     private void setMediaUri() {
181         nativeSetUri (mediaUri);
182         is_local_media = mediaUri.startsWith("file://");
183     }
184
185     // Called from native code. Native code calls this once it has created its pipeline and
186     // the main loop is running, so it is ready to accept commands.
187     private void onGStreamerInitialized () {
188         Log.i ("GStreamer", "GStreamer initialized:");
189         Log.i ("GStreamer", "  playing:" + is_playing_desired + " position:" + position + " uri: " + mediaUri);
190
191         // Restore previous playing state
192         setMediaUri ();
193         nativeSetPosition (position);
194         if (is_playing_desired) {
195             nativePlay();
196         } else {
197             nativePause();
198         }
199
200         // Re-enable buttons, now that GStreamer is initialized
201         final Activity activity = this;
202         runOnUiThread(new Runnable() {
203             public void run() {
204                 activity.findViewById(R.id.button_play).setEnabled(true);
205                 activity.findViewById(R.id.button_stop).setEnabled(true);
206             }
207         });
208     }
209
210     // The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether
211     // it is an actual pipeline position or the position the user is currently dragging to.
212     private void updateTimeWidget () {
213         final TextView tv = (TextView) this.findViewById(R.id.textview_time);
214         final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
215         final int pos = sb.getProgress();
216
217         SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
218         df.setTimeZone(TimeZone.getTimeZone("UTC"));
219         final String message = df.format(new Date (pos)) + " / " + df.format(new Date (duration));
220         tv.setText(message);
221     }
222
223     // Called from native code
224     private void setCurrentPosition(final int position, final int duration) {
225         final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
226
227         // Ignore position messages from the pipeline if the seek bar is being dragged
228         if (sb.isPressed()) return;
229
230         runOnUiThread (new Runnable() {
231           public void run() {
232             sb.setMax(duration);
233             sb.setProgress(position);
234             updateTimeWidget();
235           }
236         });
237         this.position = position;
238         this.duration = duration;
239     }
240
241     static {
242         System.loadLibrary("gstreamer_android");
243         System.loadLibrary("tutorial-4");
244         nativeClassInit();
245     }
246
247     public void surfaceChanged(SurfaceHolder holder, int format, int width,
248             int height) {
249         Log.d("GStreamer", "Surface changed to format " + format + " width "
250                 + width + " height " + height);
251         nativeSurfaceInit (holder.getSurface());
252     }
253
254     public void surfaceCreated(SurfaceHolder holder) {
255         Log.d("GStreamer", "Surface created: " + holder.getSurface());
256     }
257
258     public void surfaceDestroyed(SurfaceHolder holder) {
259         Log.d("GStreamer", "Surface destroyed");
260         nativeSurfaceFinalize ();
261     }
262
263     // Called from native code when the size of the media changes or is first detected.
264     // Inform the video surface about the new size and recalculate the layout.
265     private void onMediaSizeChanged (int width, int height) {
266         Log.i ("GStreamer", "Media size changed to " + width + "x" + height);
267         final GStreamerSurfaceView gsv = (GStreamerSurfaceView) this.findViewById(R.id.surface_video);
268         gsv.media_width = width;
269         gsv.media_height = height;
270         runOnUiThread(new Runnable() {
271             public void run() {
272                 gsv.requestLayout();
273             }
274         });
275     }
276
277     // The Seek Bar thumb has moved, either because the user dragged it or we have called setProgress()
278     public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
279         if (fromUser == false) return;
280         desired_position = progress;
281         // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
282         if (is_local_media) nativeSetPosition(desired_position);
283         updateTimeWidget();
284     }
285
286     // The user started dragging the Seek Bar thumb
287     public void onStartTrackingTouch(SeekBar sb) {
288         nativePause();
289     }
290
291     // The user released the Seek Bar thumb
292     public void onStopTrackingTouch(SeekBar sb) {
293         // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
294         // Therefore, perform only the seek when the slider is released.
295         if (!is_local_media) nativeSetPosition(desired_position);
296         if (is_playing_desired) nativePlay();
297     }
298 }
299 ```
300
301 ### Supporting arbitrary media URIs
302
303 The C code provides the `nativeSetUri()` method so we can indicate the
304 URI of the media to play. Since `playbin` will be taking care of
305 retrieving the media, we can use local or remote URIs indistinctly
306 (`file://` or `http://`, for example). From Java, though, we want to
307 keep track of whether the file is local or remote, because we will not
308 offer the same functionalities. We keep track of this in the
309 `is_local_media` variable, and update it every time we change the media
310 URI:
311
312 ``` java
313 private void setMediaUri() {
314     nativeSetUri (mediaUri);
315     is_local_media = mediaUri.startsWith("file://");
316 }
317 ```
318
319 We call `setMediaUri()` in the `onGStreamerInitialized()` callback, once
320 the pipeline is ready to accept commands.
321
322 ### Reporting media size
323
324 Every time the size of the media changes (which could happen mid-stream,
325 for some kind of streams), or when it is first detected, C code calls
326 our `onMediaSizeChanged()` callback:
327
328 ``` java
329 private void onMediaSizeChanged (int width, int height) {
330     Log.i ("GStreamer", "Media size changed to " + width + "x" + height);
331     final GStreamerSurfaceView gsv = (GStreamerSurfaceView) this.findViewById(R.id.surface_video);
332     gsv.media_width = width;
333     gsv.media_height = height;
334     runOnUiThread(new Runnable() {
335         public void run() {
336             gsv.requestLayout();
337         }
338     });
339 }
340 ```
341
342 Here we simply pass the new size onto the `GStreamerSurfaceView` in
343 charge of displaying the media, and ask the Android layout to be
344 recalculated. Eventually, the `onMeasure()` method in
345 GStreamerSurfaceView will be called and the new size will be taken
346 into account. As we have already seen in
347 [](tutorials/android/a-running-pipeline.md), methods which change
348 the UI must be called from the main thread, and we are now in a
349 callback from some GStreamer internal thread. Hence, the usage of
350 [runOnUiThread()](http://developer.android.com/reference/android/app/Activity.html#runOnUiThread\(java.lang.Runnable\)).
351
352 ### Refreshing the Seek Bar
353
354 [](tutorials/basic/toolkit-integration.md)
355 has already shown how to implement a [Seek
356 Bar](http://developer.android.com/reference/android/widget/SeekBar.html) using
357 the GTK+ toolkit. The implementation on Android is very similar.
358
359 The Seek Bar accomplishes to functions: First, it moves on its own to
360 reflect the current playback position in the media. Second, it can be
361 dragged by the user to seek to a different position.
362
363 To realize the first function, C code will periodically call our
364 `setCurrentPosition()` method so we can update the position of the thumb
365 in the Seek Bar. Again we do so from the UI thread, using
366 `RunOnUiThread()`.
367
368 ``` java
369 private void setCurrentPosition(final int position, final int duration) {
370     final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
371
372     // Ignore position messages from the pipeline if the seek bar is being dragged
373     if (sb.isPressed()) return;
374
375     runOnUiThread (new Runnable() {
376       public void run() {
377         sb.setMax(duration);
378         sb.setProgress(position);
379         updateTimeWidget();
380       }
381     });
382     this.position = position;
383     this.duration = duration;
384 }
385 ```
386
387 To the left of the Seek Bar (refer to the screenshot at the top of this
388 page), there is a
389 [TextView](http://developer.android.com/reference/android/widget/TextView.html)
390 widget which we will use to display the current position and duration in
391 `HH:mm:ss / HH:mm:ss` textual format. The `updateTimeWidget()` method
392 takes care of it, and must be called every time the Seek Bar is updated:
393
394 ``` java
395 private void updateTimeWidget () {
396     final TextView tv = (TextView) this.findViewById(R.id.textview_time);
397     final SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
398     final int pos = sb.getProgress();
399
400     SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
401     df.setTimeZone(TimeZone.getTimeZone("UTC"));
402     final String message = df.format(new Date (pos)) + " / " + df.format(new Date (duration));
403     tv.setText(message);
404 }
405 ```
406
407 ### Seeking with the Seek Bar
408
409 To perform the second function of the [Seek
410 Bar](http://developer.android.com/reference/android/widget/SeekBar.html) (allowing
411 the user to seek by dragging the thumb), we implement the
412 [OnSeekBarChangeListener](http://developer.android.com/reference/android/widget/SeekBar.OnSeekBarChangeListener.html)
413 interface in the
414 Activity:
415
416 ``` java
417 public class Tutorial4 extends Activity implements SurfaceHolder.Callback, OnSeekBarChangeListener {
418 ```
419
420 And we register the Activity as the listener for the [Seek
421 Bar](http://developer.android.com/reference/android/widget/SeekBar.html)’s
422 events in the `onCreate()` method:
423
424 ``` java
425 SeekBar sb = (SeekBar) this.findViewById(R.id.seek_bar);
426 sb.setOnSeekBarChangeListener(this);
427 ```
428
429 We will now be notified of three events: When the user starts dragging
430 the thumb, every time the thumb moves and when the thumb is released by
431 the user:
432
433 ``` java
434 public void onStartTrackingTouch(SeekBar sb) {
435     nativePause();
436 }
437 ```
438
439 [onStartTrackingTouch()](http://developer.android.com/reference/android/widget/SeekBar.OnSeekBarChangeListener.html#onStartTrackingTouch\(android.widget.SeekBar\))
440 is called when the user starts dragging, and the only thing we do is
441 pause the pipeline. If the user is searching for a particular scene, we
442 do not want it to keep
443 moving.
444
445 ``` java
446 public void onProgressChanged(SeekBar sb, int progress, boolean fromUser) {
447     if (fromUser == false) return;
448     desired_position = progress;
449     // If this is a local file, allow scrub seeking, this is, seek soon as the slider is moved.
450     if (is_local_media) nativeSetPosition(desired_position);
451     updateTimeWidget();
452 }
453 ```
454
455 [onProgressChanged()](http://developer.android.com/reference/android/widget/SeekBar.OnSeekBarChangeListener.html#onProgressChanged\(android.widget.SeekBar,%20int,%20boolean\)) is
456 called every time the thumb moves, be it because the user dragged it, or
457 because we called `setProgress()` on the Seek Bar. We discard the latter
458 case with the handy `fromUser` parameter.
459
460 As the comment says, if this is a local media, we allow scrub seeking,
461 this is, we jump to the indicated position as soon as the thumb moves.
462 Otherwise, the seek will be performed when the thumb is released, and
463 the only thing we do here is update the textual time widget.
464
465 ``` java
466 public void onStopTrackingTouch(SeekBar sb) {
467     // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
468     // Therefore, perform only the seek when the slider is released.
469     if (!is_local_media) nativeSetPosition(desired_position);
470     if (is_playing_desired) nativePlay();
471 }
472 ```
473
474 Finally, [onStopTrackingTouch()](http://developer.android.com/reference/android/widget/SeekBar.OnSeekBarChangeListener.html#onStopTrackingTouch\(android.widget.SeekBar\))
475 is called when the thumb is released. We simply perform the seek
476 operation if the file was non-local, and restore the pipeline to the
477 desired playing state.
478
479 This concludes the User interface part of this tutorial. Let’s review
480 now the under-the-hood C code that allows this to work.
481
482 ## A basic media player \[C code\]
483
484 **jni/tutorial-4.c**
485
486 ``` c
487 #include <string.h>
488 #include <jni.h>
489 #include <android/log.h>
490 #include <android/native_window.h>
491 #include <android/native_window_jni.h>
492 #include <gst/gst.h>
493 #include <gst/interfaces/xoverlay.h>
494 #include <gst/video/video.h>
495 #include <pthread.h>
496
497 GST_DEBUG_CATEGORY_STATIC (debug_category);
498 #define GST_CAT_DEFAULT debug_category
499
500 /*
501  * These macros provide a way to store the native pointer to CustomData, which might be 32 or 64 bits, into
502  * a jlong, which is always 64 bits, without warnings.
503  */
504 #if GLIB_SIZEOF_VOID_P == 8
505 ## define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(*env)->GetLongField (env, thiz, fieldID)
506 ## define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)data)
507 #else
508 ## define GET_CUSTOM_DATA(env, thiz, fieldID) (CustomData *)(jint)(*env)->GetLongField (env, thiz, fieldID)
509 ## define SET_CUSTOM_DATA(env, thiz, fieldID, data) (*env)->SetLongField (env, thiz, fieldID, (jlong)(jint)data)
510 #endif
511
512 /* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably
513  * confuse some demuxers. */
514 #define SEEK_MIN_DELAY (500 * GST_MSECOND)
515
516 /* Structure to contain all our information, so we can pass it to callbacks */
517 typedef struct _CustomData {
518   jobject app;                  /* Application instance, used to call its methods. A global reference is kept. */
519   GstElement *pipeline;         /* The running pipeline */
520   GMainContext *context;        /* GLib context used to run the main loop */
521   GMainLoop *main_loop;         /* GLib main loop */
522   gboolean initialized;         /* To avoid informing the UI multiple times about the initialization */
523   ANativeWindow *native_window; /* The Android native window where video will be rendered */
524   GstState state;               /* Current pipeline state */
525   GstState target_state;        /* Desired pipeline state, to be set once buffering is complete */
526   gint64 duration;              /* Cached clip duration */
527   gint64 desired_position;      /* Position to seek to, once the pipeline is running */
528   GstClockTime last_seek_time;  /* For seeking overflow prevention (throttling) */
529   gboolean is_live;             /* Live streams do not use buffering */
530 } CustomData;
531
532 /* playbin flags */
533 typedef enum {
534   GST_PLAY_FLAG_TEXT = (1 << 2)  /* We want subtitle output */
535 } GstPlayFlags;
536
537 /* These global variables cache values which are not changing during execution */
538 static pthread_t gst_app_thread;
539 static pthread_key_t current_jni_env;
540 static JavaVM *java_vm;
541 static jfieldID custom_data_field_id;
542 static jmethodID set_message_method_id;
543 static jmethodID set_current_position_method_id;
544 static jmethodID on_gstreamer_initialized_method_id;
545 static jmethodID on_media_size_changed_method_id;
546
547 /*
548  * Private methods
549  */
550
551 /* Register this thread with the VM */
552 static JNIEnv *attach_current_thread (void) {
553   JNIEnv *env;
554   JavaVMAttachArgs args;
555
556   GST_DEBUG ("Attaching thread %p", g_thread_self ());
557   args.version = JNI_VERSION_1_4;
558   args.name = NULL;
559   args.group = NULL;
560
561   if ((*java_vm)->AttachCurrentThread (java_vm, &env, &args) < 0) {
562     GST_ERROR ("Failed to attach current thread");
563     return NULL;
564   }
565
566   return env;
567 }
568
569 /* Unregister this thread from the VM */
570 static void detach_current_thread (void *env) {
571   GST_DEBUG ("Detaching thread %p", g_thread_self ());
572   (*java_vm)->DetachCurrentThread (java_vm);
573 }
574
575 /* Retrieve the JNI environment for this thread */
576 static JNIEnv *get_jni_env (void) {
577   JNIEnv *env;
578
579   if ((env = pthread_getspecific (current_jni_env)) == NULL) {
580     env = attach_current_thread ();
581     pthread_setspecific (current_jni_env, env);
582   }
583
584   return env;
585 }
586
587 /* Change the content of the UI's TextView */
588 static void set_ui_message (const gchar *message, CustomData *data) {
589   JNIEnv *env = get_jni_env ();
590   GST_DEBUG ("Setting message to: %s", message);
591   jstring jmessage = (*env)->NewStringUTF(env, message);
592   (*env)->CallVoidMethod (env, data->app, set_message_method_id, jmessage);
593   if ((*env)->ExceptionCheck (env)) {
594     GST_ERROR ("Failed to call Java method");
595     (*env)->ExceptionClear (env);
596   }
597   (*env)->DeleteLocalRef (env, jmessage);
598 }
599
600 /* Tell the application what is the current position and clip duration */
601 static void set_current_ui_position (gint position, gint duration, CustomData *data) {
602   JNIEnv *env = get_jni_env ();
603   (*env)->CallVoidMethod (env, data->app, set_current_position_method_id, position, duration);
604   if ((*env)->ExceptionCheck (env)) {
605     GST_ERROR ("Failed to call Java method");
606     (*env)->ExceptionClear (env);
607   }
608 }
609
610 /* If we have pipeline and it is running, query the current position and clip duration and inform
611  * the application */
612 static gboolean refresh_ui (CustomData *data) {
613   GstFormat fmt = GST_FORMAT_TIME;
614   gint64 current = -1;
615   gint64 position;
616
617   /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */
618   if (!data || !data->pipeline || data->state < GST_STATE_PAUSED)
619     return TRUE;
620
621   /* If we didn't know it yet, query the stream duration */
622   if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
623     if (!gst_element_query_duration (data->pipeline, &fmt, &data->duration)) {
624       GST_WARNING ("Could not query current duration");
625     }
626   }
627
628   if (gst_element_query_position (data->pipeline, &fmt, &position)) {
629     /* Java expects these values in milliseconds, and GStreamer provides nanoseconds */
630     set_current_ui_position (position / GST_MSECOND, data->duration / GST_MSECOND, data);
631   }
632   return TRUE;
633 }
634
635 /* Forward declaration for the delayed seek callback */
636 static gboolean delayed_seek_cb (CustomData *data);
637
638 /* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for
639  * some time in the future. */
640 static void execute_seek (gint64 desired_position, CustomData *data) {
641   gint64 diff;
642
643   if (desired_position == GST_CLOCK_TIME_NONE)
644     return;
645
646   diff = gst_util_get_timestamp () - data->last_seek_time;
647
648   if (GST_CLOCK_TIME_IS_VALID (data->last_seek_time) && diff < SEEK_MIN_DELAY) {
649     /* The previous seek was too close, delay this one */
650     GSource *timeout_source;
651
652     if (data->desired_position == GST_CLOCK_TIME_NONE) {
653       /* There was no previous seek scheduled. Setup a timer for some time in the future */
654       timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
655       g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, data, NULL);
656       g_source_attach (timeout_source, data->context);
657       g_source_unref (timeout_source);
658     }
659     /* Update the desired seek position. If multiple requests are received before it is time
660      * to perform a seek, only the last one is remembered. */
661     data->desired_position = desired_position;
662     GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,
663         GST_TIME_ARGS (desired_position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
664   } else {
665     /* Perform the seek now */
666     GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (desired_position));
667     data->last_seek_time = gst_util_get_timestamp ();
668     gst_element_seek_simple (data->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, desired_position);
669     data->desired_position = GST_CLOCK_TIME_NONE;
670   }
671 }
672
673 /* Delayed seek callback. This gets called by the timer setup in the above function. */
674 static gboolean delayed_seek_cb (CustomData *data) {
675   GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (data->desired_position));
676   execute_seek (data->desired_position, data);
677   return FALSE;
678 }
679
680 /* Retrieve errors from the bus and show them on the UI */
681 static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
682   GError *err;
683   gchar *debug_info;
684   gchar *message_string;
685
686   gst_message_parse_error (msg, &err, &debug_info);
687   message_string = g_strdup_printf ("Error received from element %s: %s", GST_OBJECT_NAME (msg->src), err->message);
688   g_clear_error (&err);
689   g_free (debug_info);
690   set_ui_message (message_string, data);
691   g_free (message_string);
692   data->target_state = GST_STATE_NULL;
693   gst_element_set_state (data->pipeline, GST_STATE_NULL);
694 }
695
696 /* Called when the End Of the Stream is reached. Just move to the beginning of the media and pause. */
697 static void eos_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
698   data->target_state = GST_STATE_PAUSED;
699   data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
700   execute_seek (0, data);
701 }
702
703 /* Called when the duration of the media changes. Just mark it as unknown, so we re-query it in the next UI refresh. */
704 static void duration_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
705   data->duration = GST_CLOCK_TIME_NONE;
706 }
707
708 /* Called when buffering messages are received. We inform the UI about the current buffering level and
709  * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */
710 static void buffering_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
711   gint percent;
712
713   if (data->is_live)
714     return;
715
716   gst_message_parse_buffering (msg, &percent);
717   if (percent < 100 && data->target_state >= GST_STATE_PAUSED) {
718     gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);
719     gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
720     set_ui_message (message_string, data);
721     g_free (message_string);
722   } else if (data->target_state >= GST_STATE_PLAYING) {
723     gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
724   } else if (data->target_state >= GST_STATE_PAUSED) {
725     set_ui_message ("Buffering complete", data);
726   }
727 }
728
729 /* Called when the clock is lost */
730 static void clock_lost_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
731   if (data->target_state >= GST_STATE_PLAYING) {
732     gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
733     gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
734   }
735 }
736
737 /* Retrieve the video sink's Caps and tell the application about the media size */
738 static void check_media_size (CustomData *data) {
739   JNIEnv *env = get_jni_env ();
740   GstElement *video_sink;
741   GstPad *video_sink_pad;
742   GstCaps *caps;
743   GstVideoFormat fmt;
744   int width;
745   int height;
746
747   /* Retrieve the Caps at the entrance of the video sink */
748   g_object_get (data->pipeline, "video-sink", &video_sink, NULL);
749   video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
750   caps = gst_pad_get_negotiated_caps (video_sink_pad);
751
752   if (gst_video_format_parse_caps(caps, &fmt, &width, &height)) {
753     int par_n, par_d;
754     if (gst_video_parse_caps_pixel_aspect_ratio (caps, &par_n, &par_d)) {
755       width = width * par_n / par_d;
756     }
757     GST_DEBUG ("Media size is %dx%d, notifying application", width, height);
758
759     (*env)->CallVoidMethod (env, data->app, on_media_size_changed_method_id, (jint)width, (jint)height);
760     if ((*env)->ExceptionCheck (env)) {
761       GST_ERROR ("Failed to call Java method");
762       (*env)->ExceptionClear (env);
763     }
764   }
765
766   gst_caps_unref(caps);
767   gst_object_unref (video_sink_pad);
768   gst_object_unref(video_sink);
769 }
770
771 /* Notify UI about pipeline state changes */
772 static void state_changed_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
773   GstState old_state, new_state, pending_state;
774   gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
775   /* Only pay attention to messages coming from the pipeline, not its children */
776   if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->pipeline)) {
777     data->state = new_state;
778     gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
779     set_ui_message(message, data);
780     g_free (message);
781
782     /* The Ready to Paused state change is particularly interesting: */
783     if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
784       /* By now the sink already knows the media size */
785       check_media_size(data);
786
787       /* If there was a scheduled seek, perform it now that we have moved to the Paused state */
788       if (GST_CLOCK_TIME_IS_VALID (data->desired_position))
789         execute_seek (data->desired_position, data);
790     }
791   }
792 }
793
794 /* Check if all conditions are met to report GStreamer as initialized.
795  * These conditions will change depending on the application */
796 static void check_initialization_complete (CustomData *data) {
797   JNIEnv *env = get_jni_env ();
798   if (!data->initialized && data->native_window && data->main_loop) {
799     GST_DEBUG ("Initialization complete, notifying application. native_window:%p main_loop:%p", data->native_window, data->main_loop);
800
801     /* The main loop is running and we received a native window, inform the sink about it */
802     gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->pipeline), (guintptr)data->native_window);
803
804     (*env)->CallVoidMethod (env, data->app, on_gstreamer_initialized_method_id);
805     if ((*env)->ExceptionCheck (env)) {
806       GST_ERROR ("Failed to call Java method");
807       (*env)->ExceptionClear (env);
808     }
809     data->initialized = TRUE;
810   }
811 }
812
813 /* Main method for the native code. This is executed on its own thread. */
814 static void *app_function (void *userdata) {
815   JavaVMAttachArgs args;
816   GstBus *bus;
817   CustomData *data = (CustomData *)userdata;
818   GSource *timeout_source;
819   GSource *bus_source;
820   GError *error = NULL;
821   guint flags;
822
823   GST_DEBUG ("Creating pipeline in CustomData at %p", data);
824
825   /* Create our own GLib Main Context and make it the default one */
826   data->context = g_main_context_new ();
827   g_main_context_push_thread_default(data->context);
828
829   /* Build pipeline */
830   data->pipeline = gst_parse_launch("playbin", &error);
831   if (error) {
832     gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);
833     g_clear_error (&error);
834     set_ui_message(message, data);
835     g_free (message);
836     return NULL;
837   }
838
839   /* Disable subtitles */
840   g_object_get (data->pipeline, "flags", &flags, NULL);
841   flags &= ~GST_PLAY_FLAG_TEXT;
842   g_object_set (data->pipeline, "flags", flags, NULL);
843
844   /* Set the pipeline to READY, so it can already accept a window handle, if we have one */
845   data->target_state = GST_STATE_READY;
846   gst_element_set_state(data->pipeline, GST_STATE_READY);
847
848   /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
849   bus = gst_element_get_bus (data->pipeline);
850   bus_source = gst_bus_create_watch (bus);
851   g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func, NULL, NULL);
852   g_source_attach (bus_source, data->context);
853   g_source_unref (bus_source);
854   g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, data);
855   g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, data);
856   g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, data);
857   g_signal_connect (G_OBJECT (bus), "message::duration", (GCallback)duration_cb, data);
858   g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, data);
859   g_signal_connect (G_OBJECT (bus), "message::clock-lost", (GCallback)clock_lost_cb, data);
860   gst_object_unref (bus);
861
862   /* Register a function that GLib will call 4 times per second */
863   timeout_source = g_timeout_source_new (250);
864   g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, data, NULL);
865   g_source_attach (timeout_source, data->context);
866   g_source_unref (timeout_source);
867
868   /* Create a GLib Main Loop and set it to run */
869   GST_DEBUG ("Entering main loop... (CustomData:%p)", data);
870   data->main_loop = g_main_loop_new (data->context, FALSE);
871   check_initialization_complete (data);
872   g_main_loop_run (data->main_loop);
873   GST_DEBUG ("Exited main loop");
874   g_main_loop_unref (data->main_loop);
875   data->main_loop = NULL;
876
877   /* Free resources */
878   g_main_context_pop_thread_default(data->context);
879   g_main_context_unref (data->context);
880   data->target_state = GST_STATE_NULL;
881   gst_element_set_state (data->pipeline, GST_STATE_NULL);
882   gst_object_unref (data->pipeline);
883
884   return NULL;
885 }
886
887 /*
888  * Java Bindings
889  */
890
891 /* Instruct the native code to create its internal data structure, pipeline and thread */
892 static void gst_native_init (JNIEnv* env, jobject thiz) {
893   CustomData *data = g_new0 (CustomData, 1);
894   data->desired_position = GST_CLOCK_TIME_NONE;
895   data->last_seek_time = GST_CLOCK_TIME_NONE;
896   SET_CUSTOM_DATA (env, thiz, custom_data_field_id, data);
897   GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-4", 0, "Android tutorial 4");
898   gst_debug_set_threshold_for_name("tutorial-4", GST_LEVEL_DEBUG);
899   GST_DEBUG ("Created CustomData at %p", data);
900   data->app = (*env)->NewGlobalRef (env, thiz);
901   GST_DEBUG ("Created GlobalRef for app object at %p", data->app);
902   pthread_create (&gst_app_thread, NULL, &app_function, data);
903 }
904
905 /* Quit the main loop, remove the native thread and free resources */
906 static void gst_native_finalize (JNIEnv* env, jobject thiz) {
907   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
908   if (!data) return;
909   GST_DEBUG ("Quitting main loop...");
910   g_main_loop_quit (data->main_loop);
911   GST_DEBUG ("Waiting for thread to finish...");
912   pthread_join (gst_app_thread, NULL);
913   GST_DEBUG ("Deleting GlobalRef for app object at %p", data->app);
914   (*env)->DeleteGlobalRef (env, data->app);
915   GST_DEBUG ("Freeing CustomData at %p", data);
916   g_free (data);
917   SET_CUSTOM_DATA (env, thiz, custom_data_field_id, NULL);
918   GST_DEBUG ("Done finalizing");
919 }
920
921 /* Set playbin's URI */
922 void gst_native_set_uri (JNIEnv* env, jobject thiz, jstring uri) {
923   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
924   if (!data || !data->pipeline) return;
925   const jbyte *char_uri = (*env)->GetStringUTFChars (env, uri, NULL);
926   GST_DEBUG ("Setting URI to %s", char_uri);
927   if (data->target_state >= GST_STATE_READY)
928     gst_element_set_state (data->pipeline, GST_STATE_READY);
929   g_object_set(data->pipeline, "uri", char_uri, NULL);
930   (*env)->ReleaseStringUTFChars (env, uri, char_uri);
931   data->duration = GST_CLOCK_TIME_NONE;
932   data->is_live = (gst_element_set_state (data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL);
933 }
934
935 /* Set pipeline to PLAYING state */
936 static void gst_native_play (JNIEnv* env, jobject thiz) {
937   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
938   if (!data) return;
939   GST_DEBUG ("Setting state to PLAYING");
940   data->target_state = GST_STATE_PLAYING;
941   data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
942 }
943
944 /* Set pipeline to PAUSED state */
945 static void gst_native_pause (JNIEnv* env, jobject thiz) {
946   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
947   if (!data) return;
948   GST_DEBUG ("Setting state to PAUSED");
949   data->target_state = GST_STATE_PAUSED;
950   data->is_live = (gst_element_set_state (data->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
951 }
952
953 /* Instruct the pipeline to seek to a different position */
954 void gst_native_set_position (JNIEnv* env, jobject thiz, int milliseconds) {
955   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
956   if (!data) return;
957   gint64 desired_position = (gint64)(milliseconds * GST_MSECOND);
958   if (data->state >= GST_STATE_PAUSED) {
959     execute_seek(desired_position, data);
960   } else {
961     GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (desired_position));
962     data->desired_position = desired_position;
963   }
964 }
965
966 /* Static class initializer: retrieve method and field IDs */
967 static jboolean gst_native_class_init (JNIEnv* env, jclass klass) {
968   custom_data_field_id = (*env)->GetFieldID (env, klass, "native_custom_data", "J");
969   set_message_method_id = (*env)->GetMethodID (env, klass, "setMessage", "(Ljava/lang/String;)V");
970   set_current_position_method_id = (*env)->GetMethodID (env, klass, "setCurrentPosition", "(II)V");
971   on_gstreamer_initialized_method_id = (*env)->GetMethodID (env, klass, "onGStreamerInitialized", "()V");
972   on_media_size_changed_method_id = (*env)->GetMethodID (env, klass, "onMediaSizeChanged", "(II)V");
973
974   if (!custom_data_field_id || !set_message_method_id || !on_gstreamer_initialized_method_id ||
975       !on_media_size_changed_method_id || !set_current_position_method_id) {
976     /* We emit this message through the Android log instead of the GStreamer log because the later
977      * has not been initialized yet.
978      */
979     __android_log_print (ANDROID_LOG_ERROR, "tutorial-4", "The calling class does not implement all necessary interface methods");
980     return JNI_FALSE;
981   }
982   return JNI_TRUE;
983 }
984
985 static void gst_native_surface_init (JNIEnv *env, jobject thiz, jobject surface) {
986   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
987   if (!data) return;
988   ANativeWindow *new_native_window = ANativeWindow_fromSurface(env, surface);
989   GST_DEBUG ("Received surface %p (native window %p)", surface, new_native_window);
990
991   if (data->native_window) {
992     ANativeWindow_release (data->native_window);
993     if (data->native_window == new_native_window) {
994       GST_DEBUG ("New native window is the same as the previous one", data->native_window);
995       if (data->pipeline) {
996         gst_x_overlay_expose(GST_X_OVERLAY (data->pipeline));
997         gst_x_overlay_expose(GST_X_OVERLAY (data->pipeline));
998       }
999       return;
1000     } else {
1001       GST_DEBUG ("Released previous native window %p", data->native_window);
1002       data->initialized = FALSE;
1003     }
1004   }
1005   data->native_window = new_native_window;
1006
1007   check_initialization_complete (data);
1008 }
1009
1010 static void gst_native_surface_finalize (JNIEnv *env, jobject thiz) {
1011   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
1012   if (!data) return;
1013   GST_DEBUG ("Releasing Native Window %p", data->native_window);
1014
1015   if (data->pipeline) {
1016     gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->pipeline), (guintptr)NULL);
1017     gst_element_set_state (data->pipeline, GST_STATE_READY);
1018   }
1019
1020   ANativeWindow_release (data->native_window);
1021   data->native_window = NULL;
1022   data->initialized = FALSE;
1023 }
1024
1025 /* List of implemented native methods */
1026 static JNINativeMethod native_methods[] = {
1027   { "nativeInit", "()V", (void *) gst_native_init},
1028   { "nativeFinalize", "()V", (void *) gst_native_finalize},
1029   { "nativeSetUri", "(Ljava/lang/String;)V", (void *) gst_native_set_uri},
1030   { "nativePlay", "()V", (void *) gst_native_play},
1031   { "nativePause", "()V", (void *) gst_native_pause},
1032   { "nativeSetPosition", "(I)V", (void*) gst_native_set_position},
1033   { "nativeSurfaceInit", "(Ljava/lang/Object;)V", (void *) gst_native_surface_init},
1034   { "nativeSurfaceFinalize", "()V", (void *) gst_native_surface_finalize},
1035   { "nativeClassInit", "()Z", (void *) gst_native_class_init}
1036 };
1037
1038 /* Library initializer */
1039 jint JNI_OnLoad(JavaVM *vm, void *reserved) {
1040   JNIEnv *env = NULL;
1041
1042   java_vm = vm;
1043
1044   if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
1045     __android_log_print (ANDROID_LOG_ERROR, "tutorial-4", "Could not retrieve JNIEnv");
1046     return 0;
1047   }
1048   jclass klass = (*env)->FindClass (env, "com/gst_sdk_tutorials/tutorial_4/Tutorial4");
1049   (*env)->RegisterNatives (env, klass, native_methods, G_N_ELEMENTS(native_methods));
1050
1051   pthread_key_create (&current_jni_env, detach_current_thread);
1052
1053   return JNI_VERSION_1_4;
1054 }
1055 ```
1056
1057 ### Supporting arbitrary media URIs
1058
1059 Java code will call `gst_native_set_uri()` whenever it wants to change
1060 the playing URI (in this tutorial the URI never changes, but it could):
1061
1062 ``` c
1063 void gst_native_set_uri (JNIEnv* env, jobject thiz, jstring uri) {
1064   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
1065   if (!data || !data->pipeline) return;
1066   const jbyte *char_uri = (*env)->GetStringUTFChars (env, uri, NULL);
1067   GST_DEBUG ("Setting URI to %s", char_uri);
1068   if (data->target_state >= GST_STATE_READY)
1069     gst_element_set_state (data->pipeline, GST_STATE_READY);
1070   g_object_set(data->pipeline, "uri", char_uri, NULL);
1071   (*env)->ReleaseStringUTFChars (env, uri, char_uri);
1072   data->duration = GST_CLOCK_TIME_NONE;
1073   data->is_live = (gst_element_set_state (data->pipeline, data->target_state) == GST_STATE_CHANGE_NO_PREROLL);
1074 }
1075 ```
1076
1077 We first need to convert between the
1078 [UTF16](http://en.wikipedia.org/wiki/UTF-16) encoding used by Java and
1079 the [Modified
1080 UTF8](http://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8) used by
1081 GStreamer with
1082 [GetStringUTFChars()](http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html#wp17265)
1083 and
1084 [ReleaseStringUTFChars()](http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/functions.html#wp17294).
1085
1086 `playbin` will only care about URI changes in the READY to PAUSED state
1087 change, because the new URI might need a completely different playback
1088 pipeline (think about switching from a local Matroska file to a remote
1089 OGG file: this would require, at least, different source and demuxing
1090 elements). Thus, before passing the new URI to `playbin` we set its
1091 state to READY (if we were in PAUSED or PLAYING).
1092
1093 `playbin`’s URI is exposed as a common GObject property, so we simply
1094 set it with `g_object_set()`.
1095
1096 We then reset the clip duration, so it is re-queried later, and bring
1097 the pipeline to the playing state it had before. In this last step, we
1098 also take note of whether the new URI corresponds to a live source or
1099 not. Live sources must not use buffering (otherwise latency is
1100 introduced which is inacceptable for them), so we keep track of this
1101 information in the `is_live` variable.
1102
1103 ### Reporting media size
1104
1105 Some codecs allow the media size (width and height of the video) to
1106 change during playback. For simplicity, this tutorial assumes that they
1107 do not. Therefore, in the READY to PAUSED state change, once the Caps of
1108 the decoded media are known, we inspect them in `check_media_size()`:
1109
1110 ``` c
1111 static void check_media_size (CustomData *data) {
1112   JNIEnv *env = get_jni_env ();
1113   GstElement *video_sink;
1114   GstPad *video_sink_pad;
1115   GstCaps *caps;
1116   GstVideoFormat fmt;
1117   int width;
1118   int height;
1119
1120   /* Retrieve the Caps at the entrance of the video sink */
1121   g_object_get (data->pipeline, "video-sink", &video_sink, NULL);
1122   video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
1123   caps = gst_pad_get_negotiated_caps (video_sink_pad);
1124
1125   if (gst_video_format_parse_caps(caps, &fmt, &width, &height)) {
1126     int par_n, par_d;
1127     if (gst_video_parse_caps_pixel_aspect_ratio (caps, &par_n, &par_d)) {
1128       width = width * par_n / par_d;
1129     }
1130     GST_DEBUG ("Media size is %dx%d, notifying application", width, height);
1131
1132     (*env)->CallVoidMethod (env, data->app, on_media_size_changed_method_id, (jint)width, (jint)height);
1133     if ((*env)->ExceptionCheck (env)) {
1134       GST_ERROR ("Failed to call Java method");
1135       (*env)->ExceptionClear (env);
1136     }
1137   }
1138
1139   gst_caps_unref(caps);
1140   gst_object_unref (video_sink_pad);
1141   gst_object_unref(video_sink);
1142 }
1143 ```
1144
1145 We first retrieve the video sink element from the pipeline, using the
1146 `video-sink` property of `playbin`, and then its sink Pad. The
1147 negotiated Caps of this Pad, which we recover using
1148 `gst_pad_get_negotiated_caps()`,  are the Caps of the decoded media.
1149
1150 The helper functions `gst_video_format_parse_caps()` and
1151 `gst_video_parse_caps_pixel_aspect_ratio()` turn the Caps into
1152 manageable integers, which we pass to Java through
1153 its `onMediaSizeChanged()` callback.
1154
1155 ### Refreshing the Seek Bar
1156
1157 To keep the UI updated, a GLib timer is installed in the
1158 `app_function()` that fires 4 times per second (or every 250ms), right
1159 before entering the main loop:
1160
1161 ``` c
1162 timeout_source = g_timeout_source_new (250);
1163 g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, data, NULL);
1164 g_source_attach (timeout_source, data->context);
1165 g_source_unref (timeout_source);
1166 ```
1167
1168 Then, in the refresh\_ui method:
1169
1170 ``` c
1171 static gboolean refresh_ui (CustomData *data) {
1172   GstFormat fmt = GST_FORMAT_TIME;
1173   gint64 current = -1;
1174   gint64 position;
1175
1176   /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */
1177   if (!data || !data->pipeline || data->state < GST_STATE_PAUSED)
1178     return TRUE;
1179
1180   /* If we didn't know it yet, query the stream duration */
1181   if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
1182     if (!gst_element_query_duration (data->pipeline, &fmt, &data->duration)) {
1183       GST_WARNING ("Could not query current duration");
1184     }
1185   }
1186
1187   if (gst_element_query_position (data->pipeline, &fmt, &position)) {
1188     /* Java expects these values in milliseconds, and GStreamer provides nanoseconds */
1189     set_current_ui_position (position / GST_MSECOND, data->duration / GST_MSECOND, data);
1190   }
1191   return TRUE;
1192 }
1193 ```
1194
1195 If it is unknown, the clip duration is retrieved, as explained in
1196 [](tutorials/basic/time-management.md). The current position is
1197 retrieved next, and the UI is informed of both through its
1198 `setCurrentPosition()` callback.
1199
1200 Bear in mind that all time-related measures returned by GStreamer are in
1201 nanoseconds, whereas, for simplicity, we decided to make the UI code
1202 work in milliseconds.
1203
1204 ### Seeking with the Seek Bar
1205
1206 The Java UI code already takes care of most of the complexity of seeking
1207 by dragging the thumb of the Seek Bar. From C code, we just need to
1208 honor the calls to `nativeSetPosition()` and instruct the pipeline to
1209 jump to the indicated position.
1210
1211 There are, though, a couple of caveats. Firstly, seeks are only possible
1212 when the pipeline is in the PAUSED or PLAYING state, and we might
1213 receive seek requests before that happens. Secondly, dragging the Seek
1214 Bar can generate a very high number of seek requests in a short period
1215 of time, which is visually useless and will impair responsiveness. Let’s
1216 see how to overcome these problems.
1217
1218 #### Delayed seeks
1219
1220 In
1221 `gst_native_set_position()`:
1222
1223 ``` c
1224 void gst_native_set_position (JNIEnv* env, jobject thiz, int milliseconds) {
1225   CustomData *data = GET_CUSTOM_DATA (env, thiz, custom_data_field_id);
1226   if (!data) return;
1227   gint64 desired_position = (gint64)(milliseconds * GST_MSECOND);
1228   if (data->state >= GST_STATE_PAUSED) {
1229     execute_seek(desired_position, data);
1230   } else {
1231     GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (desired_position));
1232     data->desired_position = desired_position;
1233   }
1234 }
1235 ```
1236
1237 If we are already in the correct state for seeking, execute it right
1238 away; otherwise, store the desired position in the
1239 `desired_position` variable. Then, in the
1240 `state_changed_cb()` callback:
1241
1242 ``` c
1243 if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
1244   /* By now the sink already knows the media size */
1245   check_media_size(data);
1246
1247   /* If there was a scheduled seek, perform it now that we have moved to the Paused state */
1248   if (GST_CLOCK_TIME_IS_VALID (data->desired_position))
1249     execute_seek (data->desired_position, data);
1250  }
1251 }
1252 ```
1253
1254 Once the pipeline moves from the READY to the PAUSED state, we check if
1255 there is a pending seek operation and execute it. The
1256 `desired_position` variable is reset inside `execute_seek()`.
1257
1258 #### Seek throttling
1259
1260 A seek is potentially a lengthy operation. The demuxer (the element
1261 typically in charge of seeking) needs to estimate the appropriate byte
1262 offset inside the media file that corresponds to the time position to
1263 jump to. Then, it needs to start decoding from that point until the
1264 desired position is reached. If the initial estimate is accurate, this
1265 will not take long, but, on some container formats, or when indexing
1266 information is missing, it can take up to several seconds.
1267
1268 If a demuxer is in the process of performing a seek and receives a
1269 second one, it is up to it to finish the first one, start the second one
1270 or abort both, which is a bad thing. A simple method to avoid this issue
1271 is *throttling*, which means that we will only allow one seek every half
1272 a second (for example): after performing a seek, only the last seek
1273 request received during the next 500ms is stored, and will be honored
1274 once this period elapses.
1275
1276 To achieve this, all seek requests are routed through the
1277 `execute_seek()` method:
1278
1279 ``` c
1280 static void execute_seek (gint64 desired_position, CustomData *data) {
1281   gint64 diff;
1282
1283   if (desired_position == GST_CLOCK_TIME_NONE)
1284     return;
1285
1286   diff = gst_util_get_timestamp () - data->last_seek_time;
1287
1288   if (GST_CLOCK_TIME_IS_VALID (data->last_seek_time) && diff < SEEK_MIN_DELAY) {
1289     /* The previous seek was too close, delay this one */
1290     GSource *timeout_source;
1291
1292     if (data->desired_position == GST_CLOCK_TIME_NONE) {
1293       /* There was no previous seek scheduled. Setup a timer for some time in the future */
1294       timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
1295       g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, data, NULL);
1296       g_source_attach (timeout_source, data->context);
1297       g_source_unref (timeout_source);
1298     }
1299     /* Update the desired seek position. If multiple requests are received before it is time
1300      * to perform a seek, only the last one is remembered. */
1301     data->desired_position = desired_position;
1302     GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,
1303         GST_TIME_ARGS (desired_position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
1304   } else {
1305     /* Perform the seek now */
1306     GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (desired_position));
1307     data->last_seek_time = gst_util_get_timestamp ();
1308     gst_element_seek_simple (data->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, desired_position);
1309     data->desired_position = GST_CLOCK_TIME_NONE;
1310   }
1311 }
1312 ```
1313
1314 The time at which the last seek was performed is stored in the
1315 `last_seek_time` variable. This is wall clock time, not to be confused
1316 with the stream time carried in the media time stamps, and is obtained
1317 with `gst_util_get_timestamp()`.
1318
1319 If enough time has passed since the last seek operation, the new one is
1320 directly executed and `last_seek_time` is updated. Otherwise, the new
1321 seek is scheduled for later. If there is no previously scheduled seek, a
1322 one-shot timer is setup to trigger 500ms after the last seek operation.
1323 If another seek was already scheduled, its desired position is simply
1324 updated with the new one.
1325
1326 The one-shot timer calls `delayed_seek_cb()`, which simply calls
1327 `execute_seek()` again.
1328
1329 > ![information]
1330 > Ideally, `execute_seek()` will now find that enough time has indeed passed since the last seek and the scheduled one will proceed. It might happen, though, that after 500ms of the previous seek, and before the timer wakes up, yet another seek comes through and is executed. `delayed_seek_cb()` needs to check for this condition to avoid performing two very close seeks, and therefore calls `execute_seek()` instead of performing it itself.
1331 >
1332 > This is not a complete solution: the scheduled seek will still be executed, even though a more-recent seek has already been executed that should have cancelled it. However, it is a good tradeoff between functionality and simplicity.
1333
1334 ### Network resilience
1335
1336 [](tutorials/basic/streaming.md) has already
1337 shown how to adapt to the variable nature of the network bandwidth by
1338 using buffering. The same procedure is used here, by listening to the
1339 buffering
1340 messages:
1341
1342 ``` c
1343 g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, data);
1344 ```
1345
1346 And pausing the pipeline until buffering is complete (unless this is a
1347 live
1348 source):
1349
1350 ``` c
1351 static void buffering_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
1352   gint percent;
1353
1354   if (data->is_live)
1355     return;
1356
1357   gst_message_parse_buffering (msg, &percent);
1358   if (percent < 100 && data->target_state >= GST_STATE_PAUSED) {
1359     gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);
1360     gst_element_set_state (data->pipeline, GST_STATE_PAUSED);
1361     set_ui_message (message_string, data);
1362     g_free (message_string);
1363   } else if (data->target_state >= GST_STATE_PLAYING) {
1364     gst_element_set_state (data->pipeline, GST_STATE_PLAYING);
1365   } else if (data->target_state >= GST_STATE_PAUSED) {
1366     set_ui_message ("Buffering complete", data);
1367   }
1368 }
1369 ```
1370
1371 `target_state` is the state in which we have been instructed to set the
1372 pipeline, which might be different to the current state, because
1373 buffering forces us to go to PAUSED. Once buffering is complete we set
1374 the pipeline to the `target_state`.
1375
1376 ## A basic media player \[Android.mk\]
1377
1378 The only line worth mentioning in the makefile
1379 is `GSTREAMER_PLUGINS`:
1380
1381 **jni/Android.mk**
1382
1383 ```
1384 GSTREAMER_PLUGINS         := $(GSTREAMER_PLUGINS_CORE) $(GSTREAMER_PLUGINS_PLAYBACK) $(GSTREAMER_PLUGINS_CODECS) $(GSTREAMER_PLUGINS_NET) $(GSTREAMER_PLUGINS_SYS)
1385 ```
1386
1387 In which all plugins required for playback are loaded, because it is not
1388 known at build time what would be needed for an unspecified URI (again,
1389 in this tutorial the URI does not change, but it will in the next one).
1390
1391 ## Conclusion
1392
1393 This tutorial has shown how to embed a `playbin` pipeline into an
1394 Android application. This, effectively, turns such application into a
1395 basic media player, capable of streaming and decoding all the formats
1396 GStreamer understands. More particularly, it has shown:
1397
1398   - How to keep the User Interface regularly updated by using a timer,
1399     querying the pipeline position and calling a UI code method.
1400   - How to implement a Seek Bar which follows the current position and
1401     transforms thumb motion into reliable seek events.
1402   - How to report the media size to adapt the display surface, by
1403     reading the sink Caps at the appropriate moment and telling the UI
1404     about it.
1405
1406 The next tutorial adds the missing bits to turn the application built
1407 here into an acceptable Android media player.
1408
1409 As usual, it has been a pleasure having you here, and see you soon!
1410
1411   [screenshot]: images/tutorials/android-media-player-screenshot.png
1412   [information]: images/icons/emoticons/information.png