Split out documentation into subfolders.
[platform/upstream/gstreamer.git] / markdown / tutorials / ios / a-basic-media-player.md
1 # iOS 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 iOS 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 [Time
14     Slider](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UISlider_Class/Reference/Reference.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
25 pieces to build a media player. The most complex part is assembling a
26 pipeline which retrieves, decodes and displays the media, but we
27 already know that the `playbin` element can take care of all that for
28 us. We only need to replace the manual pipeline we used in
29 [](tutorials/ios/video.md) with a single-element `playbin` pipeline
30 and we are good to go!
31
32 However, we can do better than. We will add a [Time
33 Slider](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UISlider_Class/Reference/Reference.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 iOS 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 ## The User Interface
45
46 The User Interface from the previous tutorial is expanded again. A
47 `UISlider` has been added to the toolbar, to keep track of the current
48 position in the clip, and allow the user to change it. Also, a
49 (read-only) `UITextField` is added to show the exact clip position and
50 duration.
51
52 **VideoViewController.h**
53
54 ```
55 #import <UIKit/UIKit.h>
56 #import "GStreamerBackendDelegate.h"
57
58 @interface VideoViewController : UIViewController <GStreamerBackendDelegate> {
59     IBOutlet UILabel *message_label;
60     IBOutlet UIBarButtonItem *play_button;
61     IBOutlet UIBarButtonItem *pause_button;
62     IBOutlet UIView *video_view;
63     IBOutlet UIView *video_container_view;
64     IBOutlet NSLayoutConstraint *video_width_constraint;
65     IBOutlet NSLayoutConstraint *video_height_constraint;
66     IBOutlet UIToolbar *toolbar;
67     IBOutlet UITextField *time_label;
68     IBOutlet UISlider *time_slider;
69 }
70
71 @property (retain,nonatomic) NSString *uri;
72
73 -(IBAction) play:(id)sender;
74 -(IBAction) pause:(id)sender;
75 -(IBAction) sliderValueChanged:(id)sender;
76 -(IBAction) sliderTouchDown:(id)sender;
77 -(IBAction) sliderTouchUp:(id)sender;
78
79 /* From GStreamerBackendDelegate */
80 -(void) gstreamerInitialized;
81 -(void) gstreamerSetUIMessage:(NSString *)message;
82
83 @end
84 ```
85
86 Note how we register callbacks for some of the Actions the
87 [UISlider](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UISlider_Class/Reference/Reference.html) generates.
88 Also note that the class has been renamed from `ViewController` to
89 `VideoViewController`, since the next tutorial adds another
90 `ViewController` and we will need to differentiate.
91
92 ## The Video View Controller
93
94 The `ViewController `class manages the UI, instantiates
95 the `GStreamerBackend` and also performs some UI-related tasks on its
96 behalf:
97
98 ![](images/icons/grey_arrow_down.gif)Due to the extension of this code,
99 this view is collapsed by default. Click here to expand…
100
101 **VideoViewController.m**
102
103 ```
104 #import "VideoViewController.h"
105 #import "GStreamerBackend.h"
106 #import <UIKit/UIKit.h>
107
108 @interface VideoViewController () {
109     GStreamerBackend *gst_backend;
110     int media_width;                /* Width of the clip */
111     int media_height;               /* height of the clip */
112     Boolean dragging_slider;        /* Whether the time slider is being dragged or not */
113     Boolean is_local_media;         /* Whether this clip is stored locally or is being streamed */
114     Boolean is_playing_desired;     /* Whether the user asked to go to PLAYING */
115 }
116
117 @end
118
119 @implementation VideoViewController
120
121 @synthesize uri;
122
123 /*
124  * Private methods
125  */
126
127 /* The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether
128  * it is an actual pipeline position or the position the user is currently dragging to. */
129 - (void) updateTimeWidget
130 {
131     NSInteger position = time_slider.value / 1000;
132     NSInteger duration = time_slider.maximumValue / 1000;
133     NSString *position_txt = @" -- ";
134     NSString *duration_txt = @" -- ";
135
136     if (duration > 0) {
137         NSUInteger hours = duration / (60 * 60);
138         NSUInteger minutes = (duration / 60) % 60;
139         NSUInteger seconds = duration % 60;
140
141         duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
142     }
143     if (position > 0) {
144         NSUInteger hours = position / (60 * 60);
145         NSUInteger minutes = (position / 60) % 60;
146         NSUInteger seconds = position % 60;
147
148         position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
149     }
150
151     NSString *text = [NSString stringWithFormat:@"%@ / %@",
152                       position_txt, duration_txt];
153
154     time_label.text = text;
155 }
156
157 /*
158  * Methods from UIViewController
159  */
160
161 - (void)viewDidLoad
162 {
163     [super viewDidLoad];
164
165     play_button.enabled = FALSE;
166     pause_button.enabled = FALSE;
167
168     /* As soon as the GStreamer backend knows the real values, these ones will be replaced */
169     media_width = 320;
170     media_height = 240;
171
172     uri = @"https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-368p.ogv";
173
174     gst_backend = [[GStreamerBackend alloc] init:self videoView:video_view];
175 }
176
177 - (void)viewDidDisappear:(BOOL)animated
178 {
179     if (gst_backend)
180     {
181         [gst_backend deinit];
182     }
183 }
184
185 - (void)didReceiveMemoryWarning
186 {
187     [super didReceiveMemoryWarning];
188     // Dispose of any resources that can be recreated.
189 }
190
191 /* Called when the Play button is pressed */
192 -(IBAction) play:(id)sender
193 {
194     [gst_backend play];
195     is_playing_desired = YES;
196 }
197
198 /* Called when the Pause button is pressed */
199 -(IBAction) pause:(id)sender
200 {
201     [gst_backend pause];
202     is_playing_desired = NO;
203 }
204
205 /* Called when the time slider position has changed, either because the user dragged it or
206  * we programmatically changed its position. dragging_slider tells us which one happened */
207 - (IBAction)sliderValueChanged:(id)sender {
208     if (!dragging_slider) return;
209     // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
210     if (is_local_media)
211         [gst_backend setPosition:time_slider.value];
212     [self updateTimeWidget];
213 }
214
215 /* Called when the user starts to drag the time slider */
216 - (IBAction)sliderTouchDown:(id)sender {
217     [gst_backend pause];
218     dragging_slider = YES;
219 }
220
221 /* Called when the user stops dragging the time slider */
222 - (IBAction)sliderTouchUp:(id)sender {
223     dragging_slider = NO;
224     // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
225     // Therefore, perform only the seek when the slider is released.
226     if (!is_local_media)
227         [gst_backend setPosition:time_slider.value];
228     if (is_playing_desired)
229         [gst_backend play];
230 }
231
232 /* Called when the size of the main view has changed, so we can
233  * resize the sub-views in ways not allowed by storyboarding. */
234 - (void)viewDidLayoutSubviews
235 {
236     CGFloat view_width = video_container_view.bounds.size.width;
237     CGFloat view_height = video_container_view.bounds.size.height;
238
239     CGFloat correct_height = view_width * media_height / media_width;
240     CGFloat correct_width = view_height * media_width / media_height;
241
242     if (correct_height < view_height) {
243         video_height_constraint.constant = correct_height;
244         video_width_constraint.constant = view_width;
245     } else {
246         video_width_constraint.constant = correct_width;
247         video_height_constraint.constant = view_height;
248     }
249
250     time_slider.frame = CGRectMake(time_slider.frame.origin.x, time_slider.frame.origin.y, toolbar.frame.size.width - time_slider.frame.origin.x - 8, time_slider.frame.size.height);
251 }
252
253 /*
254  * Methods from GstreamerBackendDelegate
255  */
256
257 -(void) gstreamerInitialized
258 {
259     dispatch_async(dispatch_get_main_queue(), ^{
260         play_button.enabled = TRUE;
261         pause_button.enabled = TRUE;
262         message_label.text = @"Ready";
263         [gst_backend setUri:uri];
264         is_local_media = [uri hasPrefix:@"file://"];
265         is_playing_desired = NO;
266     });
267 }
268
269 -(void) gstreamerSetUIMessage:(NSString *)message
270 {
271     dispatch_async(dispatch_get_main_queue(), ^{
272         message_label.text = message;
273     });
274 }
275
276 -(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height
277 {
278     media_width = width;
279     media_height = height;
280     dispatch_async(dispatch_get_main_queue(), ^{
281         [self viewDidLayoutSubviews];
282         [video_view setNeedsLayout];
283         [video_view layoutIfNeeded];
284     });
285 }
286
287 -(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration
288 {
289     /* Ignore messages from the pipeline if the time sliders is being dragged */
290     if (dragging_slider) return;
291
292     dispatch_async(dispatch_get_main_queue(), ^{
293         time_slider.maximumValue = duration;
294         time_slider.value = position;
295         [self updateTimeWidget];
296     });
297 }
298
299 @end
300 ```
301
302 Supporting arbitrary media URIs
303
304 The `GStreamerBackend`  provides the `setUri()` method so we can
305 indicate the URI of the media to play. Since `playbin` will be taking
306 care of retrieving the media, we can use local or remote URIs
307 indistinctly (`file://` or `http://`, for example). From the UI code,
308 though, we want to keep track of whether the file is local or remote,
309 because we will not offer the same functionalities. We keep track of
310 this in the `is_local_media` variable, which is set when the URI is set,
311 in the `gstreamerInitialized` method:
312
313 ```
314 -(void) gstreamerInitialized
315 {
316     dispatch_async(dispatch_get_main_queue(), ^{
317         play_button.enabled = TRUE;
318         pause_button.enabled = TRUE;
319         message_label.text = @"Ready";
320         [gst_backend setUri:uri];
321         is_local_media = [uri hasPrefix:@"file://"];
322         is_playing_desired = NO;
323     });
324 }
325 ```
326
327 Reporting media size
328
329 Every time the size of the media changes (which could happen mid-stream,
330 for some kind of streams), or when it is first detected,
331 `GStreamerBackend`  calls our `mediaSizeChanged()` callback:
332
333 ```
334 -(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height
335 {
336     media_width = width;
337     media_height = height;
338     dispatch_async(dispatch_get_main_queue(), ^{
339         [self viewDidLayoutSubviews];
340         [video_view setNeedsLayout];
341         [video_view layoutIfNeeded];
342     });
343 }
344 ```
345
346 Here we simply store the new size and ask the layout to be recalculated.
347 As we have already seen in [](tutorials/ios/a-running-pipeline.md),
348 methods which change the UI must be called from the main thread, and we
349 are now in a callback from some GStreamer internal thread. Hence, the
350 usage
351 of `dispatch_async()`[.](http://developer.android.com/reference/android/app/Activity.html#runOnUiThread\(java.lang.Runnable\))
352
353 ### Refreshing the Time Slider
354
355 [](tutorials/basic/toolkit-integration.md) has
356 already shown how to implement a Seek Bar (or [Time
357 Slider](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UISlider_Class/Reference/Reference.html)
358 in this tutorial) using the GTK+ toolkit. The implementation on iOS is
359 very similar.
360
361 The Seek Bar accomplishes to functions: First, it moves on its own to
362 reflect the current playback position in the media. Second, it can be
363 dragged by the user to seek to a different position.
364
365 To realize the first function, `GStreamerBackend`  will periodically
366 call our `setCurrentPosition` method so we can update the position of
367 the thumb in the Seek Bar. Again we do so from the UI thread, using
368 `dispatch_async()`.
369
370 ```
371 -(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration
372 {
373     /* Ignore messages from the pipeline if the time sliders is being dragged */
374     if (dragging_slider) return;
375
376     dispatch_async(dispatch_get_main_queue(), ^{
377         time_slider.maximumValue = duration;
378         time_slider.value = position;
379         [self updateTimeWidget];
380     });
381 }
382 ```
383
384 Also note that if the user is currently dragging the slider (the
385 `dragging_slider` variable is explained below) we ignore
386 `setCurrentPosition` calls from `GStreamerBackend`, as they would
387 interfere with the user’s actions.
388
389 To the left of the Seek Bar (refer to the screenshot at the top of this
390 page), there is
391 a [TextField](https://developer.apple.com/library/ios/#documentation/UIKit/Reference/UITextField_Class/Reference/UITextField.html) widget
392 which we will use to display the current position and duration in
393 "`HH:mm:ss / HH:mm:ss"` textual format. The `updateTimeWidget` method
394 takes care of it, and must be called every time the Seek Bar is
395 updated:
396
397 ```
398 /* The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether
399  * it is an actual pipeline position or the position the user is currently dragging to. */
400 - (void) updateTimeWidget
401 {
402     NSInteger position = time_slider.value / 1000;
403     NSInteger duration = time_slider.maximumValue / 1000;
404     NSString *position_txt = @" -- ";
405     NSString *duration_txt = @" -- ";
406
407     if (duration > 0) {
408         NSUInteger hours = duration / (60 * 60);
409         NSUInteger minutes = (duration / 60) % 60;
410         NSUInteger seconds = duration % 60;
411
412         duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
413     }
414     if (position > 0) {
415         NSUInteger hours = position / (60 * 60);
416         NSUInteger minutes = (position / 60) % 60;
417         NSUInteger seconds = position % 60;
418
419         position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
420     }
421
422     NSString *text = [NSString stringWithFormat:@"%@ / %@",
423                       position_txt, duration_txt];
424
425     time_label.text = text;
426 }
427 ```
428
429 Seeking with the Seek Bar
430
431 To perform the second function of the Seek Bar (allowing the user to
432 seek by dragging the thumb), we register some callbacks through IBAction
433 outlets. Refer to the storyboard in this tutorial’s project to see which
434 outlets are connected. We will be notified when the user starts dragging
435 the Slider, when the Slider position changes and when the users releases
436 the Slider.
437
438 ```
439 /* Called when the user starts to drag the time slider */
440 - (IBAction)sliderTouchDown:(id)sender {
441     [gst_backend pause];
442     dragging_slider = YES;
443 }
444 ```
445
446 `sliderTouchDown` is called when the user starts dragging. Here we pause
447 the pipeline because if the user is searching for a particular scene, we
448 do not want it to keep moving. We also mark that a drag operation is in
449 progress in the
450 `dragging_slider` variable.
451
452 ```
453 /* Called when the time slider position has changed, either because the user dragged it or
454  * we programmatically changed its position. dragging_slider tells us which one happened */
455 - (IBAction)sliderValueChanged:(id)sender {
456     if (!dragging_slider) return;
457     // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.
458     if (is_local_media)
459         [gst_backend setPosition:time_slider.value];
460     [self updateTimeWidget];
461 }
462 ```
463
464 `sliderValueChanged` is called every time the Slider’s thumb moves, be
465 it because the user dragged it, or because we changed its value form the
466 program. We discard the latter case using the
467 `dragging_slider` variable.
468
469 As the comment says, if this is a local media, we allow scrub seeking,
470 this is, we jump to the indicated position as soon as the thumb moves.
471 Otherwise, the seek operation will be performed when the thumb is
472 released, and the only thing we do here is update the textual time
473 widget.
474
475 ```
476 /* Called when the user stops dragging the time slider */
477 - (IBAction)sliderTouchUp:(id)sender {
478     dragging_slider = NO;
479     // If this is a remote file, scrub seeking is probably not going to work smoothly enough.
480     // Therefore, perform only the seek when the slider is released.
481     if (!is_local_media)
482         [gst_backend setPosition:time_slider.value];
483     if (is_playing_desired)
484         [gst_backend play];
485 }
486 ```
487
488 Finally, `sliderTouchUp` is called when the thumb is released. We
489 perform the seek operation if the file was non-local, restore the
490 pipeline to the desired playing state and end the dragging operation by
491 setting `dragging_slider` to NO.
492
493 This concludes the User interface part of this tutorial. Let’s review
494 now the `GStreamerBackend`  class that allows this to work.
495
496 ## The GStreamer Backend
497
498 The `GStreamerBackend` class performs all GStreamer-related tasks and
499 offers a simplified interface to the application, which does not need to
500 deal with all the GStreamer details. When it needs to perform any UI
501 action, it does so through a delegate, which is expected to adhere to
502 the `GStreamerBackendDelegate` protocol.
503
504 **GStreamerBackend.m**
505
506 ```
507 #import "GStreamerBackend.h"
508
509 #include <gst/gst.h>
510 #include <gst/video/video.h>
511
512 GST_DEBUG_CATEGORY_STATIC (debug_category);
513 #define GST_CAT_DEFAULT debug_category
514
515 /* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably
516  * confuse some demuxers. */
517 #define SEEK_MIN_DELAY (500 * GST_MSECOND)
518
519 @interface GStreamerBackend()
520 -(void)setUIMessage:(gchar*) message;
521 -(void)app_function;
522 -(void)check_initialization_complete;
523 @end
524
525 @implementation GStreamerBackend {
526     id ui_delegate;              /* Class that we use to interact with the user interface */
527     GstElement *pipeline;        /* The running pipeline */
528     GstElement *video_sink;      /* The video sink element which receives VideoOverlay commands */
529     GMainContext *context;       /* GLib context used to run the main loop */
530     GMainLoop *main_loop;        /* GLib main loop */
531     gboolean initialized;        /* To avoid informing the UI multiple times about the initialization */
532     UIView *ui_video_view;       /* UIView that holds the video */
533     GstState state;              /* Current pipeline state */
534     GstState target_state;       /* Desired pipeline state, to be set once buffering is complete */
535     gint64 duration;             /* Cached clip duration */
536     gint64 desired_position;     /* Position to seek to, once the pipeline is running */
537     GstClockTime last_seek_time; /* For seeking overflow prevention (throttling) */
538     gboolean is_live;            /* Live streams do not use buffering */
539 }
540
541 /*
542  * Interface methods
543  */
544
545 -(id) init:(id) uiDelegate videoView:(UIView *)video_view
546 {
547     if (self = [super init])
548     {
549         self->ui_delegate = uiDelegate;
550         self->ui_video_view = video_view;
551         self->duration = GST_CLOCK_TIME_NONE;
552
553         GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-4", 0, "iOS tutorial 4");
554         gst_debug_set_threshold_for_name("tutorial-4", GST_LEVEL_DEBUG);
555
556         /* Start the bus monitoring task */
557         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
558             [self app_function];
559         });
560     }
561
562     return self;
563 }
564
565 -(void) deinit
566 {
567     if (main_loop) {
568         g_main_loop_quit(main_loop);
569     }
570 }
571
572 -(void) play
573 {
574     target_state = GST_STATE_PLAYING;
575     is_live = (gst_element_set_state (pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
576 }
577
578 -(void) pause
579 {
580     target_state = GST_STATE_PAUSED;
581     is_live = (gst_element_set_state (pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
582 }
583
584 -(void) setUri:(NSString*)uri
585 {
586     const char *char_uri = [uri UTF8String];
587     g_object_set(pipeline, "uri", char_uri, NULL);
588     GST_DEBUG ("URI set to %s", char_uri);
589 }
590
591 -(void) setPosition:(NSInteger)milliseconds
592 {
593     gint64 position = (gint64)(milliseconds * GST_MSECOND);
594     if (state >= GST_STATE_PAUSED) {
595         execute_seek(position, self);
596     } else {
597         GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));
598         self->desired_position = position;
599     }
600 }
601
602 /*
603  * Private methods
604  */
605
606 /* Change the message on the UI through the UI delegate */
607 -(void)setUIMessage:(gchar*) message
608 {
609     NSString *string = [NSString stringWithUTF8String:message];
610     if(ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)])
611     {
612         [ui_delegate gstreamerSetUIMessage:string];
613     }
614 }
615
616 /* Tell the application what is the current position and clip duration */
617 -(void) setCurrentUIPosition:(gint)pos duration:(gint)dur
618 {
619     if(ui_delegate && [ui_delegate respondsToSelector:@selector(setCurrentPosition:duration:)])
620     {
621         [ui_delegate setCurrentPosition:pos duration:dur];
622     }
623 }
624
625 /* If we have pipeline and it is running, query the current position and clip duration and inform
626  * the application */
627 static gboolean refresh_ui (GStreamerBackend *self) {
628     gint64 position;
629
630     /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */
631     if (!self || !self->pipeline || self->state < GST_STATE_PAUSED)
632         return TRUE;
633
634     /* If we didn't know it yet, query the stream duration */
635     if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {
636         gst_element_query_duration (self->pipeline, GST_FORMAT_TIME, &self->duration);
637     }
638
639     if (gst_element_query_position (self->pipeline, GST_FORMAT_TIME, &position)) {
640         /* The UI expects these values in milliseconds, and GStreamer provides nanoseconds */
641         [self setCurrentUIPosition:position / GST_MSECOND duration:self->duration / GST_MSECOND];
642     }
643     return TRUE;
644 }
645
646 /* Forward declaration for the delayed seek callback */
647 static gboolean delayed_seek_cb (GStreamerBackend *self);
648
649 /* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for
650  * some time in the future. */
651 static void execute_seek (gint64 position, GStreamerBackend *self) {
652     gint64 diff;
653
654     if (position == GST_CLOCK_TIME_NONE)
655         return;
656
657     diff = gst_util_get_timestamp () - self->last_seek_time;
658
659     if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {
660         /* The previous seek was too close, delay this one */
661         GSource *timeout_source;
662
663         if (self->desired_position == GST_CLOCK_TIME_NONE) {
664             /* There was no previous seek scheduled. Setup a timer for some time in the future */
665             timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
666             g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);
667             g_source_attach (timeout_source, self->context);
668             g_source_unref (timeout_source);
669         }
670         /* Update the desired seek position. If multiple requests are received before it is time
671          * to perform a seek, only the last one is remembered. */
672         self->desired_position = position;
673         GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,
674                    GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
675     } else {
676         /* Perform the seek now */
677         GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));
678         self->last_seek_time = gst_util_get_timestamp ();
679         gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);
680         self->desired_position = GST_CLOCK_TIME_NONE;
681     }
682 }
683
684 /* Delayed seek callback. This gets called by the timer setup in the above function. */
685 static gboolean delayed_seek_cb (GStreamerBackend *self) {
686     GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (self->desired_position));
687     execute_seek (self->desired_position, self);
688     return FALSE;
689 }
690
691 /* Retrieve errors from the bus and show them on the UI */
692 static void error_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)
693 {
694     GError *err;
695     gchar *debug_info;
696     gchar *message_string;
697
698     gst_message_parse_error (msg, &err, &debug_info);
699     message_string = g_strdup_printf ("Error received from element %s: %s", GST_OBJECT_NAME (msg->src), err->message);
700     g_clear_error (&err);
701     g_free (debug_info);
702     [self setUIMessage:message_string];
703     g_free (message_string);
704     gst_element_set_state (self->pipeline, GST_STATE_NULL);
705 }
706
707 /* Called when the End Of the Stream is reached. Just move to the beginning of the media and pause. */
708 static void eos_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
709     self->target_state = GST_STATE_PAUSED;
710     self->is_live = (gst_element_set_state (self->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
711     execute_seek (0, self);
712 }
713
714 /* Called when the duration of the media changes. Just mark it as unknown, so we re-query it in the next UI refresh. */
715 static void duration_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
716     self->duration = GST_CLOCK_TIME_NONE;
717 }
718
719 /* Called when buffering messages are received. We inform the UI about the current buffering level and
720  * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */
721 static void buffering_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
722     gint percent;
723
724     if (self->is_live)
725         return;
726
727     gst_message_parse_buffering (msg, &percent);
728     if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {
729         gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);
730         gst_element_set_state (self->pipeline, GST_STATE_PAUSED);
731         [self setUIMessage:message_string];
732         g_free (message_string);
733     } else if (self->target_state >= GST_STATE_PLAYING) {
734         gst_element_set_state (self->pipeline, GST_STATE_PLAYING);
735     } else if (self->target_state >= GST_STATE_PAUSED) {
736         [self setUIMessage:"Buffering complete"];
737     }
738 }
739
740 /* Called when the clock is lost */
741 static void clock_lost_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
742     if (self->target_state >= GST_STATE_PLAYING) {
743         gst_element_set_state (self->pipeline, GST_STATE_PAUSED);
744         gst_element_set_state (self->pipeline, GST_STATE_PLAYING);
745     }
746 }
747
748 /* Retrieve the video sink's Caps and tell the application about the media size */
749 static void check_media_size (GStreamerBackend *self) {
750     GstElement *video_sink;
751     GstPad *video_sink_pad;
752     GstCaps *caps;
753     GstVideoInfo info;
754
755     /* Retrieve the Caps at the entrance of the video sink */
756     g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
757
758     /* Do nothing if there is no video sink (this might be an audio-only clip */
759     if (!video_sink) return;
760
761     video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
762     caps = gst_pad_get_current_caps (video_sink_pad);
763
764     if (gst_video_info_from_caps(&info, caps)) {
765         info.width = info.width * info.par_n / info.par_d
766         GST_DEBUG ("Media size is %dx%d, notifying application", info.width, info.height);
767
768         if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:info.height:)])
769         {
770             [self->ui_delegate mediaSizeChanged:info.width height:info.height];
771         }
772     }
773
774     gst_caps_unref(caps);
775     gst_object_unref (video_sink_pad);
776     gst_object_unref(video_sink);
777 }
778
779 /* Notify UI about pipeline state changes */
780 static void state_changed_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)
781 {
782     GstState old_state, new_state, pending_state;
783     gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
784     /* Only pay attention to messages coming from the pipeline, not its children */
785     if (GST_MESSAGE_SRC (msg) == GST_OBJECT (self->pipeline)) {
786         self->state = new_state;
787         gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
788         [self setUIMessage:message];
789         g_free (message);
790
791         if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
792         {
793             check_media_size(self);
794
795             /* If there was a scheduled seek, perform it now that we have moved to the Paused state */
796             if (GST_CLOCK_TIME_IS_VALID (self->desired_position))
797                 execute_seek (self->desired_position, self);
798         }
799     }
800 }
801
802 /* Check if all conditions are met to report GStreamer as initialized.
803  * These conditions will change depending on the application */
804 -(void) check_initialization_complete
805 {
806     if (!initialized && main_loop) {
807         GST_DEBUG ("Initialization complete, notifying application.");
808         if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerInitialized)])
809         {
810             [ui_delegate gstreamerInitialized];
811         }
812         initialized = TRUE;
813     }
814 }
815
816 /* Main method for the bus monitoring code */
817 -(void) app_function
818 {
819     GstBus *bus;
820     GSource *timeout_source;
821     GSource *bus_source;
822     GError *error = NULL;
823
824     GST_DEBUG ("Creating pipeline");
825
826     /* Create our own GLib Main Context and make it the default one */
827     context = g_main_context_new ();
828     g_main_context_push_thread_default(context);
829
830     /* Build pipeline */
831     pipeline = gst_parse_launch("playbin", &error);
832     if (error) {
833         gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);
834         g_clear_error (&error);
835         [self setUIMessage:message];
836         g_free (message);
837         return;
838     }
839
840     /* Set the pipeline to READY, so it can already accept a window handle */
841     gst_element_set_state(pipeline, GST_STATE_READY);
842
843     video_sink = gst_bin_get_by_interface(GST_BIN(pipeline), GST_TYPE_VIDEO_OVERLAY);
844     if (!video_sink) {
845         GST_ERROR ("Could not retrieve video sink");
846         return;
847     }
848     gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(video_sink), (guintptr) (id) ui_video_view);
849
850     /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
851     bus = gst_element_get_bus (pipeline);
852     bus_source = gst_bus_create_watch (bus);
853     g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func, NULL, NULL);
854     g_source_attach (bus_source, context);
855     g_source_unref (bus_source);
856     g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, (__bridge void *)self);
857     g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, (__bridge void *)self);
858     g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);
859     g_signal_connect (G_OBJECT (bus), "message::duration", (GCallback)duration_cb, (__bridge void *)self);
860     g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);
861     g_signal_connect (G_OBJECT (bus), "message::clock-lost", (GCallback)clock_lost_cb, (__bridge void *)self);
862     gst_object_unref (bus);
863
864     /* Register a function that GLib will call 4 times per second */
865     timeout_source = g_timeout_source_new (250);
866     g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, (__bridge void *)self, NULL);
867     g_source_attach (timeout_source, context);
868     g_source_unref (timeout_source);
869
870     /* Create a GLib Main Loop and set it to run */
871     GST_DEBUG ("Entering main loop...");
872     main_loop = g_main_loop_new (context, FALSE);
873     [self check_initialization_complete];
874     g_main_loop_run (main_loop);
875     GST_DEBUG ("Exited main loop");
876     g_main_loop_unref (main_loop);
877     main_loop = NULL;
878
879     /* Free resources */
880     g_main_context_pop_thread_default(context);
881     g_main_context_unref (context);
882     gst_element_set_state (pipeline, GST_STATE_NULL);
883     gst_object_unref (pipeline);
884     pipeline = NULL;
885
886     ui_delegate = NULL;
887     ui_video_view = NULL;
888
889     return;
890 }
891
892 @end
893 ```
894
895 Supporting arbitrary media URIs
896
897 The UI code will call `setUri` whenever it wants to change the playing
898 URI (in this tutorial the URI never changes, but it does in the next
899 one):
900
901 ```
902 -(void) setUri:(NSString*)uri
903 {
904     const char *char_uri = [uri UTF8String];
905     g_object_set(pipeline, "uri", char_uri, NULL);
906     GST_DEBUG ("URI set to %s", char_uri);
907 }
908 ```
909
910 We first need to obtain a plain `char *` from within the `NSString *` we
911 get, using the `UTF8String` method.
912
913 `playbin`’s URI is exposed as a common GObject property, so we simply
914 set it with `g_object_set()`.
915
916 ### Reporting media size
917
918 Some codecs allow the media size (width and height of the video) to
919 change during playback. For simplicity, this tutorial assumes that they
920 do not. Therefore, in the READY to PAUSED state change, once the Caps of
921 the decoded media are known, we inspect them
922 in `check_media_size()`:
923
924 ```
925 /* Retrieve the video sink's Caps and tell the application about the media size */
926 static void check_media_size (GStreamerBackend *self) {
927     GstElement *video_sink;
928     GstPad *video_sink_pad;
929     GstCaps *caps;
930     GstVideoInfo info;
931
932     /* Retrieve the Caps at the entrance of the video sink */
933     g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
934
935     /* Do nothing if there is no video sink (this might be an audio-only clip */
936     if (!video_sink) return;
937
938     video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
939     caps = gst_pad_get_current_caps (video_sink_pad);
940
941     if (gst_video_info_from_caps(&info, caps)) {
942         info.width = info.width * info.par_n / info.par_d;
943         GST_DEBUG ("Media size is %dx%d, notifying application", info.width, info.height);
944
945         if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:info.height:)])
946         {
947             [self->ui_delegate mediaSizeChanged:info.width height:info.height];
948         }
949     }
950
951     gst_caps_unref(caps);
952     gst_object_unref (video_sink_pad);
953     gst_object_unref(video_sink);
954 }
955 ```
956
957 We first retrieve the video sink element from the pipeline, using
958 the `video-sink` property of `playbin`, and then its sink Pad. The
959 negotiated Caps of this Pad, which we recover using
960 `gst_pad_get_current_caps()`,  are the Caps of the decoded media.
961
962 The helper functions `gst_video_format_parse_caps()` and
963 `gst_video_parse_caps_pixel_aspect_ratio()` turn the Caps into
964 manageable integers, which we pass to the application through
965 its `mediaSizeChanged` callback.
966
967 ### Refreshing the Seek Bar
968
969 To keep the UI updated, a GLib timer is installed in
970 the `app_function` that fires 4 times per second (or every 250ms),
971 right before entering the main loop:
972
973 ```
974     /* Register a function that GLib will call 4 times per second */
975     timeout_source = g_timeout_source_new (250);
976     g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, (__bridge void *)self, NULL);
977     g_source_attach (timeout_source, context);
978     g_source_unref (timeout_source);
979 ```
980
981 Then, in the refresh\_ui
982 method:
983
984 ```
985 /* If we have pipeline and it is running, query the current position and clip duration and inform
986  * the application */
987 static gboolean refresh_ui (GStreamerBackend *self) {
988     gint64 position;
989
990     /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */
991     if (!self || !self->pipeline || self->state < GST_STATE_PAUSED)
992         return TRUE;
993
994     /* If we didn't know it yet, query the stream duration */
995     if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {
996         gst_element_query_duration (self->pipeline, GST_FORMAT_TIME, &self->duration);
997     }
998
999     if (gst_element_query_position (self->pipeline, GST_FORMAT_TIME, &position)) {
1000         /* The UI expects these values in milliseconds, and GStreamer provides nanoseconds */
1001         [self setCurrentUIPosition:position / GST_MSECOND duration:self->duration / GST_MSECOND];
1002     }
1003     return TRUE;
1004 }
1005 ```
1006
1007 If it is unknown, the clip duration is retrieved, as explained in
1008 [](tutorials/basic/time-management.md). The current position is
1009 retrieved next, and the UI is informed of both through its
1010 `setCurrentUIPosition` callback.
1011
1012 Bear in mind that all time-related measures returned by GStreamer are in
1013 nanoseconds, whereas, for simplicity, we decided to make the UI code
1014 work in milliseconds.
1015
1016 ### Seeking with the Seek Bar
1017
1018 The UI code already takes care of most of the complexity of seeking by
1019 dragging the thumb of the Seek Bar. From the `GStreamerBackend`, we just
1020 need to honor the calls to `setPosition` and instruct the pipeline to
1021 jump to the indicated position.
1022
1023 There are, though, a couple of caveats. Firstly, seeks are only possible
1024 when the pipeline is in the PAUSED or PLAYING state, and we might
1025 receive seek requests before that happens. Secondly, dragging the Seek
1026 Bar can generate a very high number of seek requests in a short period
1027 of time, which is visually useless and will impair responsiveness. Let’s
1028 see how to overcome these problems.
1029
1030 #### Delayed seeks
1031
1032 In `setPosition`:
1033
1034 ```
1035 -(void) setPosition:(NSInteger)milliseconds
1036 {
1037     gint64 position = (gint64)(milliseconds * GST_MSECOND);
1038     if (state >= GST_STATE_PAUSED) {
1039         execute_seek(position, self);
1040     } else {
1041         GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));
1042         self->desired_position = position;
1043     }
1044 }
1045 ```
1046
1047 If we are already in the correct state for seeking, execute it right
1048 away; otherwise, store the desired position in
1049 the `desired_position` variable. Then, in
1050 the `state_changed_cb()` callback:
1051
1052 ```
1053         if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
1054         {
1055             check_media_size(self);
1056
1057             /* If there was a scheduled seek, perform it now that we have moved to the Paused state */
1058             if (GST_CLOCK_TIME_IS_VALID (self->desired_position))
1059                 execute_seek (self->desired_position, self);
1060         }
1061 ```
1062
1063 Once the pipeline moves from the READY to the PAUSED state, we check if
1064 there is a pending seek operation and execute it.
1065 The `desired_position` variable is reset inside `execute_seek()`.
1066
1067 #### Seek throttling
1068
1069 A seek is potentially a lengthy operation. The demuxer (the element
1070 typically in charge of seeking) needs to estimate the appropriate byte
1071 offset inside the media file that corresponds to the time position to
1072 jump to. Then, it needs to start decoding from that point until the
1073 desired position is reached. If the initial estimate is accurate, this
1074 will not take long, but, on some container formats, or when indexing
1075 information is missing, it can take up to several seconds.
1076
1077 If a demuxer is in the process of performing a seek and receives a
1078 second one, it is up to it to finish the first one, start the second one
1079 or abort both, which is a bad thing. A simple method to avoid this issue
1080 is *throttling*, which means that we will only allow one seek every half
1081 a second (for example): after performing a seek, only the last seek
1082 request received during the next 500ms is stored, and will be honored
1083 once this period elapses.
1084
1085 To achieve this, all seek requests are routed through
1086 the `execute_seek()` method:
1087
1088 ```
1089 /* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for
1090  * some time in the future. */
1091 static void execute_seek (gint64 position, GStreamerBackend *self) {
1092     gint64 diff;
1093
1094     if (position == GST_CLOCK_TIME_NONE)
1095         return;
1096
1097     diff = gst_util_get_timestamp () - self->last_seek_time;
1098
1099     if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {
1100         /* The previous seek was too close, delay this one */
1101         GSource *timeout_source;
1102
1103         if (self->desired_position == GST_CLOCK_TIME_NONE) {
1104             /* There was no previous seek scheduled. Setup a timer for some time in the future */
1105             timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);
1106             g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);
1107             g_source_attach (timeout_source, self->context);
1108             g_source_unref (timeout_source);
1109         }
1110         /* Update the desired seek position. If multiple requests are received before it is time
1111          * to perform a seek, only the last one is remembered. */
1112         self->desired_position = position;
1113         GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,
1114                    GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));
1115     } else {
1116         /* Perform the seek now */
1117         GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));
1118         self->last_seek_time = gst_util_get_timestamp ();
1119         gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);
1120         self->desired_position = GST_CLOCK_TIME_NONE;
1121     }
1122 }
1123 ```
1124
1125 The time at which the last seek was performed is stored in
1126 the `last_seek_time` variable. This is wall clock time, not to be
1127 confused with the stream time carried in the media time stamps, and is
1128 obtained with `gst_util_get_timestamp()`.
1129
1130 If enough time has passed since the last seek operation, the new one is
1131 directly executed and `last_seek_time` is updated. Otherwise, the new
1132 seek is scheduled for later. If there is no previously scheduled seek, a
1133 one-shot timer is setup to trigger 500ms after the last seek operation.
1134 If another seek was already scheduled, its desired position is simply
1135 updated with the new one.
1136
1137 The one-shot timer calls `delayed_seek_cb()`, which simply
1138 calls `execute_seek()` again.
1139
1140 > ![information]
1141 > 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 the seek itself.
1142 >
1143 >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.
1144
1145 ###  Network resilience
1146
1147 [](tutorials/basic/streaming.md) has already
1148 shown how to adapt to the variable nature of the network bandwidth by
1149 using buffering. The same procedure is used here, by listening to the
1150 buffering
1151 messages:
1152
1153 ```
1154 g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);
1155 ```
1156
1157 And pausing the pipeline until buffering is complete (unless this is a
1158 live
1159 source):
1160
1161
1162
1163 ```
1164 /* Called when buffering messages are received. We inform the UI about the current buffering level and
1165  * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */
1166 static void buffering_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
1167     gint percent;
1168
1169     if (self->is_live)
1170         return;
1171
1172     gst_message_parse_buffering (msg, &percent);
1173     if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {
1174         gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);
1175         gst_element_set_state (self->pipeline, GST_STATE_PAUSED);
1176         [self setUIMessage:message_string];
1177         g_free (message_string);
1178     } else if (self->target_state >= GST_STATE_PLAYING) {
1179         gst_element_set_state (self->pipeline, GST_STATE_PLAYING);
1180     } else if (self->target_state >= GST_STATE_PAUSED) {
1181         [self setUIMessage:"Buffering complete"];
1182     }
1183 }
1184 ```
1185
1186 `target_state` is the state in which we have been instructed to set the
1187 pipeline, which might be different to the current state, because
1188 buffering forces us to go to PAUSED. Once buffering is complete we set
1189 the pipeline to the `target_state`.
1190
1191 ## Conclusion
1192
1193 This tutorial has shown how to embed a `playbin` pipeline into an iOS
1194 application. This, effectively, turns such application into a basic
1195 media player, capable of streaming and decoding all the formats
1196 GStreamer understands. More particularly, it has shown:
1197
1198   - How to keep the User Interface regularly updated by using a timer,
1199     querying the pipeline position and calling a UI code method.
1200   - How to implement a Seek Bar which follows the current position and
1201     transforms thumb motion into reliable seek events.
1202   - How to report the media size to adapt the display surface, by
1203     reading the sink Caps at the appropriate moment and telling the UI
1204     about it.
1205
1206 The next tutorial adds the missing bits to turn the application built
1207 here into an acceptable iOS media player.
1208
1209   [information]: images/icons/emoticons/information.png
1210   [screenshot]: images/tutorials/ios-a-basic-media-player-screenshot.png