1 # iOS tutorial 4: A basic media player
7 Enough testing with synthetic images and audio tones! This tutorial
8 finally plays actual media, streamed directly from the Internet, in your
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
17 It also uses the knowledge gathered in the [](tutorials/basic/index.md) regarding:
19 - How to use `playbin` to play any kind of media
20 - How to handle network resilience problems
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!
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.
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.
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
52 **VideoViewController.h**
55 #import <UIKit/UIKit.h>
56 #import "GStreamerBackendDelegate.h"
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;
71 @property (retain,nonatomic) NSString *uri;
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;
79 /* From GStreamerBackendDelegate */
80 -(void) gstreamerInitialized;
81 -(void) gstreamerSetUIMessage:(NSString *)message;
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.
92 ## The Video View Controller
94 The `ViewController `class manages the UI, instantiates
95 the `GStreamerBackend` and also performs some UI-related tasks on its
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…
101 **VideoViewController.m**
104 #import "VideoViewController.h"
105 #import "GStreamerBackend.h"
106 #import <UIKit/UIKit.h>
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 */
119 @implementation VideoViewController
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
131 NSInteger position = time_slider.value / 1000;
132 NSInteger duration = time_slider.maximumValue / 1000;
133 NSString *position_txt = @" -- ";
134 NSString *duration_txt = @" -- ";
137 NSUInteger hours = duration / (60 * 60);
138 NSUInteger minutes = (duration / 60) % 60;
139 NSUInteger seconds = duration % 60;
141 duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
144 NSUInteger hours = position / (60 * 60);
145 NSUInteger minutes = (position / 60) % 60;
146 NSUInteger seconds = position % 60;
148 position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
151 NSString *text = [NSString stringWithFormat:@"%@ / %@",
152 position_txt, duration_txt];
154 time_label.text = text;
158 * Methods from UIViewController
165 play_button.enabled = FALSE;
166 pause_button.enabled = FALSE;
168 /* As soon as the GStreamer backend knows the real values, these ones will be replaced */
172 uri = @"https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-368p.ogv";
174 gst_backend = [[GStreamerBackend alloc] init:self videoView:video_view];
177 - (void)viewDidDisappear:(BOOL)animated
181 [gst_backend deinit];
185 - (void)didReceiveMemoryWarning
187 [super didReceiveMemoryWarning];
188 // Dispose of any resources that can be recreated.
191 /* Called when the Play button is pressed */
192 -(IBAction) play:(id)sender
195 is_playing_desired = YES;
198 /* Called when the Pause button is pressed */
199 -(IBAction) pause:(id)sender
202 is_playing_desired = NO;
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.
211 [gst_backend setPosition:time_slider.value];
212 [self updateTimeWidget];
215 /* Called when the user starts to drag the time slider */
216 - (IBAction)sliderTouchDown:(id)sender {
218 dragging_slider = YES;
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.
227 [gst_backend setPosition:time_slider.value];
228 if (is_playing_desired)
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
236 CGFloat view_width = video_container_view.bounds.size.width;
237 CGFloat view_height = video_container_view.bounds.size.height;
239 CGFloat correct_height = view_width * media_height / media_width;
240 CGFloat correct_width = view_height * media_width / media_height;
242 if (correct_height < view_height) {
243 video_height_constraint.constant = correct_height;
244 video_width_constraint.constant = view_width;
246 video_width_constraint.constant = correct_width;
247 video_height_constraint.constant = view_height;
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);
254 * Methods from GstreamerBackendDelegate
257 -(void) gstreamerInitialized
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;
269 -(void) gstreamerSetUIMessage:(NSString *)message
271 dispatch_async(dispatch_get_main_queue(), ^{
272 message_label.text = message;
276 -(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height
279 media_height = height;
280 dispatch_async(dispatch_get_main_queue(), ^{
281 [self viewDidLayoutSubviews];
282 [video_view setNeedsLayout];
283 [video_view layoutIfNeeded];
287 -(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration
289 /* Ignore messages from the pipeline if the time sliders is being dragged */
290 if (dragging_slider) return;
292 dispatch_async(dispatch_get_main_queue(), ^{
293 time_slider.maximumValue = duration;
294 time_slider.value = position;
295 [self updateTimeWidget];
302 Supporting arbitrary media URIs
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:
314 -(void) gstreamerInitialized
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;
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:
334 -(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height
337 media_height = height;
338 dispatch_async(dispatch_get_main_queue(), ^{
339 [self viewDidLayoutSubviews];
340 [video_view setNeedsLayout];
341 [video_view layoutIfNeeded];
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
351 of `dispatch_async()`[.](http://developer.android.com/reference/android/app/Activity.html#runOnUiThread\(java.lang.Runnable\))
353 ### Refreshing the Time Slider
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
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.
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
371 -(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration
373 /* Ignore messages from the pipeline if the time sliders is being dragged */
374 if (dragging_slider) return;
376 dispatch_async(dispatch_get_main_queue(), ^{
377 time_slider.maximumValue = duration;
378 time_slider.value = position;
379 [self updateTimeWidget];
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.
389 To the left of the Seek Bar (refer to the screenshot at the top of this
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
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
402 NSInteger position = time_slider.value / 1000;
403 NSInteger duration = time_slider.maximumValue / 1000;
404 NSString *position_txt = @" -- ";
405 NSString *duration_txt = @" -- ";
408 NSUInteger hours = duration / (60 * 60);
409 NSUInteger minutes = (duration / 60) % 60;
410 NSUInteger seconds = duration % 60;
412 duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
415 NSUInteger hours = position / (60 * 60);
416 NSUInteger minutes = (position / 60) % 60;
417 NSUInteger seconds = position % 60;
419 position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];
422 NSString *text = [NSString stringWithFormat:@"%@ / %@",
423 position_txt, duration_txt];
425 time_label.text = text;
429 Seeking with the Seek Bar
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
439 /* Called when the user starts to drag the time slider */
440 - (IBAction)sliderTouchDown:(id)sender {
442 dragging_slider = YES;
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
450 `dragging_slider` variable.
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.
459 [gst_backend setPosition:time_slider.value];
460 [self updateTimeWidget];
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.
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
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.
482 [gst_backend setPosition:time_slider.value];
483 if (is_playing_desired)
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.
493 This concludes the User interface part of this tutorial. Let’s review
494 now the `GStreamerBackend` class that allows this to work.
496 ## The GStreamer Backend
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.
504 **GStreamerBackend.m**
507 #import "GStreamerBackend.h"
510 #include <gst/video/video.h>
512 GST_DEBUG_CATEGORY_STATIC (debug_category);
513 #define GST_CAT_DEFAULT debug_category
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)
519 @interface GStreamerBackend()
520 -(void)setUIMessage:(gchar*) message;
522 -(void)check_initialization_complete;
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 */
545 -(id) init:(id) uiDelegate videoView:(UIView *)video_view
547 if (self = [super init])
549 self->ui_delegate = uiDelegate;
550 self->ui_video_view = video_view;
551 self->duration = GST_CLOCK_TIME_NONE;
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);
556 /* Start the bus monitoring task */
557 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
568 g_main_loop_quit(main_loop);
574 target_state = GST_STATE_PLAYING;
575 is_live = (gst_element_set_state (pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);
580 target_state = GST_STATE_PAUSED;
581 is_live = (gst_element_set_state (pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);
584 -(void) setUri:(NSString*)uri
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);
591 -(void) setPosition:(NSInteger)milliseconds
593 gint64 position = (gint64)(milliseconds * GST_MSECOND);
594 if (state >= GST_STATE_PAUSED) {
595 execute_seek(position, self);
597 GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));
598 self->desired_position = position;
606 /* Change the message on the UI through the UI delegate */
607 -(void)setUIMessage:(gchar*) message
609 NSString *string = [NSString stringWithUTF8String:message];
610 if(ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)])
612 [ui_delegate gstreamerSetUIMessage:string];
616 /* Tell the application what is the current position and clip duration */
617 -(void) setCurrentUIPosition:(gint)pos duration:(gint)dur
619 if(ui_delegate && [ui_delegate respondsToSelector:@selector(setCurrentPosition:duration:)])
621 [ui_delegate setCurrentPosition:pos duration:dur];
625 /* If we have pipeline and it is running, query the current position and clip duration and inform
627 static gboolean refresh_ui (GStreamerBackend *self) {
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)
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);
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];
646 /* Forward declaration for the delayed seek callback */
647 static gboolean delayed_seek_cb (GStreamerBackend *self);
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) {
654 if (position == GST_CLOCK_TIME_NONE)
657 diff = gst_util_get_timestamp () - self->last_seek_time;
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;
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);
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));
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;
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);
691 /* Retrieve errors from the bus and show them on the UI */
692 static void error_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)
696 gchar *message_string;
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);
702 [self setUIMessage:message_string];
703 g_free (message_string);
704 gst_element_set_state (self->pipeline, GST_STATE_NULL);
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);
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;
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) {
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"];
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);
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;
755 /* Retrieve the Caps at the entrance of the video sink */
756 g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
758 /* Do nothing if there is no video sink (this might be an audio-only clip */
759 if (!video_sink) return;
761 video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
762 caps = gst_pad_get_current_caps (video_sink_pad);
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);
768 if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:info.height:)])
770 [self->ui_delegate mediaSizeChanged:info.width height:info.height];
774 gst_caps_unref(caps);
775 gst_object_unref (video_sink_pad);
776 gst_object_unref(video_sink);
779 /* Notify UI about pipeline state changes */
780 static void state_changed_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self)
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];
791 if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
793 check_media_size(self);
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);
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
806 if (!initialized && main_loop) {
807 GST_DEBUG ("Initialization complete, notifying application.");
808 if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerInitialized)])
810 [ui_delegate gstreamerInitialized];
816 /* Main method for the bus monitoring code */
820 GSource *timeout_source;
822 GError *error = NULL;
824 GST_DEBUG ("Creating pipeline");
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);
831 pipeline = gst_parse_launch("playbin", &error);
833 gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);
834 g_clear_error (&error);
835 [self setUIMessage:message];
840 /* Set the pipeline to READY, so it can already accept a window handle */
841 gst_element_set_state(pipeline, GST_STATE_READY);
843 video_sink = gst_bin_get_by_interface(GST_BIN(pipeline), GST_TYPE_VIDEO_OVERLAY);
845 GST_ERROR ("Could not retrieve video sink");
848 gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(video_sink), (guintptr) (id) ui_video_view);
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);
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);
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);
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);
887 ui_video_view = NULL;
895 Supporting arbitrary media URIs
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
902 -(void) setUri:(NSString*)uri
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);
910 We first need to obtain a plain `char *` from within the `NSString *` we
911 get, using the `UTF8String` method.
913 `playbin`’s URI is exposed as a common GObject property, so we simply
914 set it with `g_object_set()`.
916 ### Reporting media size
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()`:
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;
932 /* Retrieve the Caps at the entrance of the video sink */
933 g_object_get (self->pipeline, "video-sink", &video_sink, NULL);
935 /* Do nothing if there is no video sink (this might be an audio-only clip */
936 if (!video_sink) return;
938 video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
939 caps = gst_pad_get_current_caps (video_sink_pad);
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);
945 if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:info.height:)])
947 [self->ui_delegate mediaSizeChanged:info.width height:info.height];
951 gst_caps_unref(caps);
952 gst_object_unref (video_sink_pad);
953 gst_object_unref(video_sink);
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.
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.
967 ### Refreshing the Seek Bar
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:
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);
981 Then, in the refresh\_ui
985 /* If we have pipeline and it is running, query the current position and clip duration and inform
987 static gboolean refresh_ui (GStreamerBackend *self) {
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)
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);
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];
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.
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.
1016 ### Seeking with the Seek Bar
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.
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.
1035 -(void) setPosition:(NSInteger)milliseconds
1037 gint64 position = (gint64)(milliseconds * GST_MSECOND);
1038 if (state >= GST_STATE_PAUSED) {
1039 execute_seek(position, self);
1041 GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));
1042 self->desired_position = position;
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:
1053 if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)
1055 check_media_size(self);
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);
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()`.
1067 #### Seek throttling
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.
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.
1085 To achieve this, all seek requests are routed through
1086 the `execute_seek()` method:
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) {
1094 if (position == GST_CLOCK_TIME_NONE)
1097 diff = gst_util_get_timestamp () - self->last_seek_time;
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;
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);
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));
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;
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()`.
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.
1137 The one-shot timer calls `delayed_seek_cb()`, which simply
1138 calls `execute_seek()` again.
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.
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.
1145 ### Network resilience
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
1154 g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);
1157 And pausing the pipeline until buffering is complete (unless this is a
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) {
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"];
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`.
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:
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
1206 The next tutorial adds the missing bits to turn the application built
1207 here into an acceptable iOS media player.
1209 [information]: images/icons/emoticons/information.png
1210 [screenshot]: images/tutorials/ios-a-basic-media-player-screenshot.png