GStreamer iOS教程4——一个基础的播放器

来源:互联网 发布:c语言中数组的定义 编辑:程序博客网 时间:2024/05/18 01:33

1. 目标

      本教程最终会在你的iOS设备上播放一个在Internet上的流媒体。它展示了:

  • UI上如何进行刷新
  • 如何实现时间进度条
  • 如何获得媒体的尺寸并适配显示层
      本教程同样需要在Basic教程的前继内容,包括playbin2如何播放媒体和如何处理网速不稳定的问题。

2. 介绍
      在上一篇教程里,我们已经实现了几乎所有的播放器所需要的元素。其中最重要的是实现一个能检索,解码和显示的管道,很幸运的是playbin2是集这些能力于一身的一个element。所以我们仅仅需要手工把上一篇教程中的一个element改成playbin2就可以了。
      但这样还是不够的,我们还会加入一个进度条,用来指示当前播放的位置,并且用户可以使用这个进度条进行播放内容的跳跃。
      最后,我们还要根据媒体的尺寸来适配视频大小,让video sink不在周围填充黑色。

3. UI
      本教程的UI是上一篇教程的扩展。在工具条上增加了一个UISlider来指示当前播放的位置,用户也可以操作这个UISlider来实现内容的跳跃。我们还增加了一个UITextField来显示当前播放时间和总时间。
      VideoViewController.h
#import <UIKit/UIKit.h>#import "GStreamerBackendDelegate.h"@interface VideoViewController : UIViewController <GStreamerBackendDelegate> {    IBOutlet UILabel *message_label;    IBOutlet UIBarButtonItem *play_button;    IBOutlet UIBarButtonItem *pause_button;    IBOutlet UIView *video_view;    IBOutlet UIView *video_container_view;    IBOutlet NSLayoutConstraint *video_width_constraint;    IBOutlet NSLayoutConstraint *video_height_constraint;    IBOutlet UIToolbar *toolbar;    IBOutlet UITextField *time_label;    IBOutlet UISlider *time_slider;}@property (retain,nonatomic) NSString *uri;-(IBAction) play:(id)sender;-(IBAction) pause:(id)sender;-(IBAction) sliderValueChanged:(id)sender;-(IBAction) sliderTouchDown:(id)sender;-(IBAction) sliderTouchUp:(id)sender;/* From GStreamerBackendDelegate */-(void) gstreamerInitialized;-(void) gstreamerSetUIMessage:(NSString *)message;@end
      请注意,我们在这里注册了一些UISlider动作的回调,并且把文件名从ViewController改成了VideoViewController。

4. Video View Controller
      和前面一样,ViewController类是处理所有UI相关内容的,并实例化一个GStreamerBackend对象,这个对象会处理GStreamer里面和UI相关的一些内容。
      VideoViewController.m
#import "VideoViewController.h"#import "GStreamerBackend.h"#import <UIKit/UIKit.h>@interface VideoViewController () {    GStreamerBackend *gst_backend;    int media_width;                /* Width of the clip */    int media_height;               /* height of the clip */    Boolean dragging_slider;        /* Whether the time slider is being dragged or not */    Boolean is_local_media;         /* Whether this clip is stored locally or is being streamed */    Boolean is_playing_desired;     /* Whether the user asked to go to PLAYING */}@end@implementation VideoViewController@synthesize uri;/* * Private methods *//* The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether * it is an actual pipeline position or the position the user is currently dragging to. */- (void) updateTimeWidget{    NSInteger position = time_slider.value / 1000;    NSInteger duration = time_slider.maximumValue / 1000;    NSString *position_txt = @" -- ";    NSString *duration_txt = @" -- ";    if (duration > 0) {        NSUInteger hours = duration / (60 * 60);        NSUInteger minutes = (duration / 60) % 60;        NSUInteger seconds = duration % 60;        duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];    }    if (position > 0) {        NSUInteger hours = position / (60 * 60);        NSUInteger minutes = (position / 60) % 60;        NSUInteger seconds = position % 60;        position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];    }    NSString *text = [NSString stringWithFormat:@"%@ / %@",                      position_txt, duration_txt];    time_label.text = text;}/* * Methods from UIViewController */- (void)viewDidLoad{    [super viewDidLoad];        play_button.enabled = FALSE;    pause_button.enabled = FALSE;        /* As soon as the GStreamer backend knows the real values, these ones will be replaced */    media_width = 320;    media_height = 240;    uri = @"http://docs.gstreamer.com/media/sintel_trailer-368p.ogv";    gst_backend = [[GStreamerBackend alloc] init:self videoView:video_view];}- (void)viewDidDisappear:(BOOL)animated{    if (gst_backend)    {        [gst_backend deinit];    }}- (void)didReceiveMemoryWarning{    [super didReceiveMemoryWarning];    // Dispose of any resources that can be recreated.}/* Called when the Play button is pressed */-(IBAction) play:(id)sender{    [gst_backend play];    is_playing_desired = YES;}/* Called when the Pause button is pressed */-(IBAction) pause:(id)sender{    [gst_backend pause];    is_playing_desired = NO;}/* Called when the time slider position has changed, either because the user dragged it or * we programmatically changed its position. dragging_slider tells us which one happened */- (IBAction)sliderValueChanged:(id)sender {    if (!dragging_slider) return;    // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.    if (is_local_media)        [gst_backend setPosition:time_slider.value];    [self updateTimeWidget];}/* Called when the user starts to drag the time slider */- (IBAction)sliderTouchDown:(id)sender {    [gst_backend pause];    dragging_slider = YES;}/* Called when the user stops dragging the time slider */- (IBAction)sliderTouchUp:(id)sender {    dragging_slider = NO;    // If this is a remote file, scrub seeking is probably not going to work smoothly enough.    // Therefore, perform only the seek when the slider is released.    if (!is_local_media)        [gst_backend setPosition:time_slider.value];    if (is_playing_desired)        [gst_backend play];}/* Called when the size of the main view has changed, so we can * resize the sub-views in ways not allowed by storyboarding. */- (void)viewDidLayoutSubviews{    CGFloat view_width = video_container_view.bounds.size.width;    CGFloat view_height = video_container_view.bounds.size.height;    CGFloat correct_height = view_width * media_height / media_width;    CGFloat correct_width = view_height * media_width / media_height;    if (correct_height < view_height) {        video_height_constraint.constant = correct_height;        video_width_constraint.constant = view_width;    } else {        video_width_constraint.constant = correct_width;        video_height_constraint.constant = view_height;    }    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);}/* * Methods from GstreamerBackendDelegate */-(void) gstreamerInitialized{    dispatch_async(dispatch_get_main_queue(), ^{        play_button.enabled = TRUE;        pause_button.enabled = TRUE;        message_label.text = @"Ready";        [gst_backend setUri:uri];        is_local_media = [uri hasPrefix:@"file://"];        is_playing_desired = NO;    });}-(void) gstreamerSetUIMessage:(NSString *)message{    dispatch_async(dispatch_get_main_queue(), ^{        message_label.text = message;    });}-(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height{    media_width = width;    media_height = height;    dispatch_async(dispatch_get_main_queue(), ^{        [self viewDidLayoutSubviews];        [video_view setNeedsLayout];        [video_view layoutIfNeeded];    });}-(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration{    /* Ignore messages from the pipeline if the time sliders is being dragged */    if (dragging_slider) return;    dispatch_async(dispatch_get_main_queue(), ^{        time_slider.maximumValue = duration;        time_slider.value = position;        [self updateTimeWidget];    });}@end
      +支持任意媒体的URI
      GStreamerBackend类提供setURI()方法来指定URI地址。对于playbin2来说,本地URI或者远程URI都是支持的(区别是用file://还是用http://)。对于UI来说,本地URI还是远程URI毕竟还是有所不同的,所以is_local_media这个变量来跟踪记录。
-(void) gstreamerInitialized{    dispatch_async(dispatch_get_main_queue(), ^{        play_button.enabled = TRUE;        pause_button.enabled = TRUE;        message_label.text = @"Ready";        [gst_backend setUri:uri];        is_local_media = [uri hasPrefix:@"file://"];        is_playing_desired = NO;    });}

      +获得媒体尺寸
       在第一次检测出媒体尺寸或者每次尺寸的变化时,会调用mediaSizeChanged()回调。
-(void) mediaSizeChanged:(NSInteger)width height:(NSInteger)height{    media_width = width;    media_height = height;    dispatch_async(dispatch_get_main_queue(), ^{        [self viewDidLayoutSubviews];        [video_view setNeedsLayout];        [video_view layoutIfNeeded];    });}

       这里我们仅仅简单的存储尺寸并重新计算布局。但就像在iOS教程2中提到的那样,对UI的操作都需要在主线程中,而这段代码的上下文是在GStreamerBackend线程中,所以需要用dispatch_async()来转一下。
      +刷新进度条
      在Basic教程5中已经演示了如何增加一个Seekbar了,在iOS中就表现为UISlider了,但实现还是非常相似的。
      UISlider的功能有2个:指示当前的进度和实现拖放功能
      要实现第一个功能,GStreamerBackend需要定时的调用setCurrentPosition方法,这样我们就有足够的信息来更新UI了——当然,还是需要用dispatch_async()方法。
-(void) setCurrentPosition:(NSInteger)position duration:(NSInteger)duration{    /* Ignore messages from the pipeline if the time sliders is being dragged */    if (dragging_slider) return;    dispatch_async(dispatch_get_main_queue(), ^{        time_slider.maximumValue = duration;        time_slider.value = position;        [self updateTimeWidget];    });}

      需要注意的是如果用户当前正在拖动UISlider,我们就会暂时不响应setCurrentPosition。
      在UISlider旁边是一个TextField控件,用来显示当前播放时间和总时间,采用HH:mm:ss的格式来显示。updateTimeWidget方法会对这个控件进行刷新。
/* The text widget acts as an slave for the seek bar, so it reflects what the seek bar shows, whether * it is an actual pipeline position or the position the user is currently dragging to. */- (void) updateTimeWidget{    NSInteger position = time_slider.value / 1000;    NSInteger duration = time_slider.maximumValue / 1000;    NSString *position_txt = @" -- ";    NSString *duration_txt = @" -- ";    if (duration > 0) {        NSUInteger hours = duration / (60 * 60);        NSUInteger minutes = (duration / 60) % 60;        NSUInteger seconds = duration % 60;        duration_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];    }    if (position > 0) {        NSUInteger hours = position / (60 * 60);        NSUInteger minutes = (position / 60) % 60;        NSUInteger seconds = position % 60;        position_txt = [NSString stringWithFormat:@"%02u:%02u:%02u", hours, minutes, seconds];    }    NSString *text = [NSString stringWithFormat:@"%@ / %@",                      position_txt, duration_txt];    time_label.text = text;}

      +实现seek功能
      要实现seek功能,需要在story board上注册一些IBAction的回调,这样用户开始拖动UISlider、Slider值变化、用户放开Slider这些时候我们就能收到回调了。
/* Called when the user starts to drag the time slider */- (IBAction)sliderTouchDown:(id)sender {    [gst_backend pause];    dragging_slider = YES;}
      sliderTouchDown方法时用户开始拖动UISlider时调用的,我们会暂停pipeline,因为在用户没有确定播放点之前,继续播放也没什么意思(这点应取决于UI的设计)。同时用dragging_slider变量来记录我们正在操作UISlider。
/* Called when the time slider position has changed, either because the user dragged it or * we programmatically changed its position. dragging_slider tells us which one happened */- (IBAction)sliderValueChanged:(id)sender {    if (!dragging_slider) return;    // If this is a local file, allow scrub seeking, this is, seek as soon as the slider is moved.    if (is_local_media)        [gst_backend setPosition:time_slider.value];    [self updateTimeWidget];}

      sliderValueChanged方法在拖动过程中会反复被调用。
      就像在注释中描写的一样,如果是本地URI,那么马上就可以进行seek,反之,则需要等到拖放操作结束才能确定,这里仅仅更新一下时间。
/* Called when the user stops dragging the time slider */- (IBAction)sliderTouchUp:(id)sender {    dragging_slider = NO;    // If this is a remote file, scrub seeking is probably not going to work smoothly enough.    // Therefore, perform only the seek when the slider is released.    if (!is_local_media)        [gst_backend setPosition:time_slider.value];    if (is_playing_desired)        [gst_backend play];}

      用户停止拖放时,slideTouchUp会被调用。这时,如果是远程URI就可以进行seek动作了,pipeline也重新进入播放状态,dragging_slider也设置成NO。

5. GStreamer Backend
      GStreamerBackend类实现所有和GStreamer相关的内容,并给应用提供简单的接口,屏蔽掉一些实现细节。可以功过GStreamerBackendDelegate来实现一些UI动作。
GStreamerBackend.m
#import "GStreamerBackend.h"#include <gst/gst.h>#include <gst/interfaces/xoverlay.h>#include <gst/video/video.h>GST_DEBUG_CATEGORY_STATIC (debug_category);#define GST_CAT_DEFAULT debug_category/* Do not allow seeks to be performed closer than this distance. It is visually useless, and will probably * confuse some demuxers. */#define SEEK_MIN_DELAY (500 * GST_MSECOND)@interface GStreamerBackend()-(void)setUIMessage:(gchar*) message;-(void)app_function;-(void)check_initialization_complete;@end@implementation GStreamerBackend {    id ui_delegate;              /* Class that we use to interact with the user interface */    GstElement *pipeline;        /* The running pipeline */    GstElement *video_sink;      /* The video sink element which receives XOverlay commands */    GMainContext *context;       /* GLib context used to run the main loop */    GMainLoop *main_loop;        /* GLib main loop */    gboolean initialized;        /* To avoid informing the UI multiple times about the initialization */    UIView *ui_video_view;       /* UIView that holds the video */    GstState state;              /* Current pipeline state */    GstState target_state;       /* Desired pipeline state, to be set once buffering is complete */    gint64 duration;             /* Cached clip duration */    gint64 desired_position;     /* Position to seek to, once the pipeline is running */    GstClockTime last_seek_time; /* For seeking overflow prevention (throttling) */    gboolean is_live;            /* Live streams do not use buffering */}/* * Interface methods */-(id) init:(id) uiDelegate videoView:(UIView *)video_view{    if (self = [super init])    {        self->ui_delegate = uiDelegate;        self->ui_video_view = video_view;        self->duration = GST_CLOCK_TIME_NONE;        GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-4", 0, "iOS tutorial 4");        gst_debug_set_threshold_for_name("tutorial-4", GST_LEVEL_DEBUG);        /* Start the bus monitoring task */        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{            [self app_function];        });    }    return self;}-(void) deinit{    if (main_loop) {        g_main_loop_quit(main_loop);    }}-(void) play{    target_state = GST_STATE_PLAYING;    is_live = (gst_element_set_state (pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_NO_PREROLL);}-(void) pause{    target_state = GST_STATE_PAUSED;    is_live = (gst_element_set_state (pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);}-(void) setUri:(NSString*)uri{    const char *char_uri = [uri UTF8String];    g_object_set(pipeline, "uri", char_uri, NULL);    GST_DEBUG ("URI set to %s", char_uri);}-(void) setPosition:(NSInteger)milliseconds{    gint64 position = (gint64)(milliseconds * GST_MSECOND);    if (state >= GST_STATE_PAUSED) {        execute_seek(position, self);    } else {        GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));        self->desired_position = position;    }}/* * Private methods *//* Change the message on the UI through the UI delegate */-(void)setUIMessage:(gchar*) message{    NSString *string = [NSString stringWithUTF8String:message];    if(ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)])    {        [ui_delegate gstreamerSetUIMessage:string];    }}/* Tell the application what is the current position and clip duration */-(void) setCurrentUIPosition:(gint)pos duration:(gint)dur{    if(ui_delegate && [ui_delegate respondsToSelector:@selector(setCurrentPosition:duration:)])    {        [ui_delegate setCurrentPosition:pos duration:dur];    }}/* If we have pipeline and it is running, query the current position and clip duration and inform * the application */static gboolean refresh_ui (GStreamerBackend *self) {    GstFormat fmt = GST_FORMAT_TIME;    gint64 position;    /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */    if (!self || !self->pipeline || self->state < GST_STATE_PAUSED)        return TRUE;    /* If we didn't know it yet, query the stream duration */    if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {        gst_element_query_duration (self->pipeline, &fmt, &self->duration);    }    if (gst_element_query_position (self->pipeline, &fmt, &position)) {        /* The UI expects these values in milliseconds, and GStreamer provides nanoseconds */        [self setCurrentUIPosition:position / GST_MSECOND duration:self->duration / GST_MSECOND];    }    return TRUE;}/* Forward declaration for the delayed seek callback */static gboolean delayed_seek_cb (GStreamerBackend *self);/* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for * some time in the future. */static void execute_seek (gint64 position, GStreamerBackend *self) {    gint64 diff;    if (position == GST_CLOCK_TIME_NONE)        return;    diff = gst_util_get_timestamp () - self->last_seek_time;    if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {        /* The previous seek was too close, delay this one */        GSource *timeout_source;        if (self->desired_position == GST_CLOCK_TIME_NONE) {            /* There was no previous seek scheduled. Setup a timer for some time in the future */            timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);            g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);            g_source_attach (timeout_source, self->context);            g_source_unref (timeout_source);        }        /* Update the desired seek position. If multiple requests are received before it is time         * to perform a seek, only the last one is remembered. */        self->desired_position = position;        GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,                   GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));    } else {        /* Perform the seek now */        GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));        self->last_seek_time = gst_util_get_timestamp ();        gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);        self->desired_position = GST_CLOCK_TIME_NONE;    }}/* Delayed seek callback. This gets called by the timer setup in the above function. */static gboolean delayed_seek_cb (GStreamerBackend *self) {    GST_DEBUG ("Doing delayed seek to %" GST_TIME_FORMAT, GST_TIME_ARGS (self->desired_position));    execute_seek (self->desired_position, self);    return FALSE;}/* Retrieve errors from the bus and show them on the UI */static void error_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self){    GError *err;    gchar *debug_info;    gchar *message_string;        gst_message_parse_error (msg, &err, &debug_info);    message_string = g_strdup_printf ("Error received from element %s: %s", GST_OBJECT_NAME (msg->src), err->message);    g_clear_error (&err);    g_free (debug_info);    [self setUIMessage:message_string];    g_free (message_string);    gst_element_set_state (self->pipeline, GST_STATE_NULL);}/* Called when the End Of the Stream is reached. Just move to the beginning of the media and pause. */static void eos_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {    self->target_state = GST_STATE_PAUSED;    self->is_live = (gst_element_set_state (self->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_NO_PREROLL);    execute_seek (0, self);}/* Called when the duration of the media changes. Just mark it as unknown, so we re-query it in the next UI refresh. */static void duration_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {    self->duration = GST_CLOCK_TIME_NONE;}/* Called when buffering messages are received. We inform the UI about the current buffering level and * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */static void buffering_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {    gint percent;    if (self->is_live)        return;    gst_message_parse_buffering (msg, &percent);    if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {        gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);        gst_element_set_state (self->pipeline, GST_STATE_PAUSED);        [self setUIMessage:message_string];        g_free (message_string);    } else if (self->target_state >= GST_STATE_PLAYING) {        gst_element_set_state (self->pipeline, GST_STATE_PLAYING);    } else if (self->target_state >= GST_STATE_PAUSED) {        [self setUIMessage:"Buffering complete"];    }}/* Called when the clock is lost */static void clock_lost_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {    if (self->target_state >= GST_STATE_PLAYING) {        gst_element_set_state (self->pipeline, GST_STATE_PAUSED);        gst_element_set_state (self->pipeline, GST_STATE_PLAYING);    }}/* Retrieve the video sink's Caps and tell the application about the media size */static void check_media_size (GStreamerBackend *self) {    GstElement *video_sink;    GstPad *video_sink_pad;    GstCaps *caps;    GstVideoFormat fmt;    int width;    int height;    /* Retrieve the Caps at the entrance of the video sink */    g_object_get (self->pipeline, "video-sink", &video_sink, NULL);    /* Do nothing if there is no video sink (this might be an audio-only clip */    if (!video_sink) return;    video_sink_pad = gst_element_get_static_pad (video_sink, "sink");    caps = gst_pad_get_negotiated_caps (video_sink_pad);    if (gst_video_format_parse_caps(caps, &fmt, &width, &height)) {        int par_n, par_d;        if (gst_video_parse_caps_pixel_aspect_ratio (caps, &par_n, &par_d)) {            width = width * par_n / par_d;        }        GST_DEBUG ("Media size is %dx%d, notifying application", width, height);        if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:height:)])        {            [self->ui_delegate mediaSizeChanged:width height:height];        }    }    gst_caps_unref(caps);    gst_object_unref (video_sink_pad);    gst_object_unref(video_sink);}/* Notify UI about pipeline state changes */static void state_changed_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self){    GstState old_state, new_state, pending_state;    gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);    /* Only pay attention to messages coming from the pipeline, not its children */    if (GST_MESSAGE_SRC (msg) == GST_OBJECT (self->pipeline)) {        self->state = new_state;        gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));        [self setUIMessage:message];        g_free (message);        if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)        {            check_media_size(self);            /* If there was a scheduled seek, perform it now that we have moved to the Paused state */            if (GST_CLOCK_TIME_IS_VALID (self->desired_position))                execute_seek (self->desired_position, self);        }    }}/* Check if all conditions are met to report GStreamer as initialized. * These conditions will change depending on the application */-(void) check_initialization_complete{    if (!initialized && main_loop) {        GST_DEBUG ("Initialization complete, notifying application.");        if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerInitialized)])        {            [ui_delegate gstreamerInitialized];        }        initialized = TRUE;    }}/* Main method for the bus monitoring code */-(void) app_function{    GstBus *bus;    GSource *timeout_source;    GSource *bus_source;    GError *error = NULL;    GST_DEBUG ("Creating pipeline");    /* Create our own GLib Main Context and make it the default one */    context = g_main_context_new ();    g_main_context_push_thread_default(context);        /* Build pipeline */    pipeline = gst_parse_launch("playbin2", &error);    if (error) {        gchar *message = g_strdup_printf("Unable to build pipeline: %s", error->message);        g_clear_error (&error);        [self setUIMessage:message];        g_free (message);        return;    }    /* Set the pipeline to READY, so it can already accept a window handle */    gst_element_set_state(pipeline, GST_STATE_READY);        video_sink = gst_bin_get_by_interface(GST_BIN(pipeline), GST_TYPE_X_OVERLAY);    if (!video_sink) {        GST_ERROR ("Could not retrieve video sink");        return;    }    gst_x_overlay_set_window_handle(GST_X_OVERLAY(video_sink), (guintptr) (id) ui_video_view);    /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */    bus = gst_element_get_bus (pipeline);    bus_source = gst_bus_create_watch (bus);    g_source_set_callback (bus_source, (GSourceFunc) gst_bus_async_signal_func, NULL, NULL);    g_source_attach (bus_source, context);    g_source_unref (bus_source);    g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, (__bridge void *)self);    g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, (__bridge void *)self);    g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);    g_signal_connect (G_OBJECT (bus), "message::duration", (GCallback)duration_cb, (__bridge void *)self);    g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);    g_signal_connect (G_OBJECT (bus), "message::clock-lost", (GCallback)clock_lost_cb, (__bridge void *)self);    gst_object_unref (bus);    /* Register a function that GLib will call 4 times per second */    timeout_source = g_timeout_source_new (250);    g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, (__bridge void *)self, NULL);    g_source_attach (timeout_source, context);    g_source_unref (timeout_source);    /* Create a GLib Main Loop and set it to run */    GST_DEBUG ("Entering main loop...");    main_loop = g_main_loop_new (context, FALSE);    [self check_initialization_complete];    g_main_loop_run (main_loop);    GST_DEBUG ("Exited main loop");    g_main_loop_unref (main_loop);    main_loop = NULL;        /* Free resources */    g_main_context_pop_thread_default(context);    g_main_context_unref (context);    gst_element_set_state (pipeline, GST_STATE_NULL);    gst_object_unref (pipeline);    pipeline = NULL;        ui_delegate = NULL;    ui_video_view = NULL;    return;}@end
      +支持任意媒体的URI
       在UI代码里,通过调用setURI方法来更换播放的URI。
-(void) setUri:(NSString*)uri{    const char *char_uri = [uri UTF8String];    g_object_set(pipeline, "uri", char_uri, NULL);    GST_DEBUG ("URI set to %s", char_uri);}

       我们需要一个C语言中的char*指针,所以使用了NSString*的UTF8String方法转换了一下。
       因为继承自GObject,playbin2的URI属性同样可以用g_object_set方法来设置。
      +获得媒体尺寸
       有些解码器支持媒体在播放的时候改变尺寸大小,我们在这里先不考虑这种比较复杂的情况。而且,当READY/PAUSE状态切换时,一旦获得了解码的Caps,就可以调用check_media_size()了。
/* Retrieve the video sink's Caps and tell the application about the media size */static void check_media_size (GStreamerBackend *self) {    GstElement *video_sink;    GstPad *video_sink_pad;    GstCaps *caps;    GstVideoFormat fmt;    int width;    int height;    /* Retrieve the Caps at the entrance of the video sink */    g_object_get (self->pipeline, "video-sink", &video_sink, NULL);    /* Do nothing if there is no video sink (this might be an audio-only clip */    if (!video_sink) return;    video_sink_pad = gst_element_get_static_pad (video_sink, "sink");    caps = gst_pad_get_negotiated_caps (video_sink_pad);    if (gst_video_format_parse_caps(caps, &fmt, &width, &height)) {        int par_n, par_d;        if (gst_video_parse_caps_pixel_aspect_ratio (caps, &par_n, &par_d)) {            width = width * par_n / par_d;        }        GST_DEBUG ("Media size is %dx%d, notifying application", width, height);        if (self->ui_delegate && [self->ui_delegate respondsToSelector:@selector(mediaSizeChanged:height:)])        {            [self->ui_delegate mediaSizeChanged:width height:height];        }    }    gst_caps_unref(caps);    gst_object_unref (video_sink_pad);    gst_object_unref(video_sink);}

       我们首先去获得pipeline里面的video sink element。这可以通过playbin2的video-sink属性,然后获得sink Pad。然后调用gst_pad_get_negotiated_caps()方法来获得这个Pad的协商过的Caps。
       通过gst_video_format_parse_caps()和gst_video_parse_caps_pixel_aspect_ratio()可以把Caps转换成一个整数,这样在mediaSizeChanged()里面我们就可以传给应用了。
      +刷新进度条
       为了让UI正常刷新,所以在app_function里面启动了一个GLib的定时器,定时250ms。
    /* Register a function that GLib will call 4 times per second */    timeout_source = g_timeout_source_new (250);    g_source_set_callback (timeout_source, (GSourceFunc)refresh_ui, (__bridge void *)self, NULL);    g_source_attach (timeout_source, context);    g_source_unref (timeout_source);

        然后调用refresh_ui方法。
/* If we have pipeline and it is running, query the current position and clip duration and inform * the application */static gboolean refresh_ui (GStreamerBackend *self) {    GstFormat fmt = GST_FORMAT_TIME;    gint64 position;    /* We do not want to update anything unless we have a working pipeline in the PAUSED or PLAYING state */    if (!self || !self->pipeline || self->state < GST_STATE_PAUSED)        return TRUE;    /* If we didn't know it yet, query the stream duration */    if (!GST_CLOCK_TIME_IS_VALID (self->duration)) {        gst_element_query_duration (self->pipeline, &fmt, &self->duration);    }    if (gst_element_query_position (self->pipeline, &fmt, &position)) {        /* The UI expects these values in milliseconds, and GStreamer provides nanoseconds */        [self setCurrentUIPosition:position / GST_MSECOND duration:self->duration / GST_MSECOND];    }    return TRUE;}

       如果视频的总时间还不知道,这里会先取一次总时间,然后再取当前位置,并在setCurrentUIPosition方法里面把2个参数都传出去。
       请注意,所有GStreamer传回的都是时间单位都是纳秒,我们显示一般精确到毫秒就可以了,这里需要简单换算一下。
      +拖放进度条
      UI基本处理了所有的拖动上的复杂的计算,在GStreamerBackend,我们仅仅简单的调用setPosition让pipeline跳到那个位置即可。
      当然,也有一些需要注意的地方。首先,仅当pipeline在PAUSED或PLAYING状态才能拖放;其次,拖放可能在短时间发出大量的seek请求,这会带来很大的压力。请注意代码上是如何克服这些的。
      延迟搜索:
      在setPosition方法里面
-(void) setPosition:(NSInteger)milliseconds{    gint64 position = (gint64)(milliseconds * GST_MSECOND);    if (state >= GST_STATE_PAUSED) {        execute_seek(position, self);    } else {        GST_DEBUG ("Scheduling seek to %" GST_TIME_FORMAT " for later", GST_TIME_ARGS (position));        self->desired_position = position;    }}

      如果我们在可以seek的状态,直接运行;如果目前不能seek,那么用desired_position来记录下希望seek的位置。然后在state_changed_cb里面处理。
        if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED)        {            check_media_size(self);            /* If there was a scheduled seek, perform it now that we have moved to the Paused state */            if (GST_CLOCK_TIME_IS_VALID (self->desired_position))                execute_seek (self->desired_position, self);        }

      一旦pipeline从READY状态迁移到PAUSE状态,会检查是否还有没响应的seek,然后调用execute_seek 。
      seek限制:
      seek实际上是一个冗长的操作。demuxer需要估算大约的字节偏移量,然后开始decode,直到正确的位置为止。如果估计的表准确,这个过程可能较短,但在一些容器的格式下,可能会需要几秒钟的时间。
      如果一个dumuxer在处理一个seek时来了一个新的seek。这个时候dumuxer会做什么不同的dumuxer是不同的,肯呢噶是结束第一个开始第二个,也可能是两个都会出错。所以我们设定了一个最短时间,在这个时间内不允许开始一个新的seek(本例是0.5秒)。
      为了实现这个目的,所有的seek请求都会通过execute_seek来发出。
/* Perform seek, if we are not too close to the previous seek. Otherwise, schedule the seek for * some time in the future. */static void execute_seek (gint64 position, GStreamerBackend *self) {    gint64 diff;    if (position == GST_CLOCK_TIME_NONE)        return;    diff = gst_util_get_timestamp () - self->last_seek_time;    if (GST_CLOCK_TIME_IS_VALID (self->last_seek_time) && diff < SEEK_MIN_DELAY) {        /* The previous seek was too close, delay this one */        GSource *timeout_source;        if (self->desired_position == GST_CLOCK_TIME_NONE) {            /* There was no previous seek scheduled. Setup a timer for some time in the future */            timeout_source = g_timeout_source_new ((SEEK_MIN_DELAY - diff) / GST_MSECOND);            g_source_set_callback (timeout_source, (GSourceFunc)delayed_seek_cb, (__bridge void *)self, NULL);            g_source_attach (timeout_source, self->context);            g_source_unref (timeout_source);        }        /* Update the desired seek position. If multiple requests are received before it is time         * to perform a seek, only the last one is remembered. */        self->desired_position = position;        GST_DEBUG ("Throttling seek to %" GST_TIME_FORMAT ", will be in %" GST_TIME_FORMAT,                   GST_TIME_ARGS (position), GST_TIME_ARGS (SEEK_MIN_DELAY - diff));    } else {        /* Perform the seek now */        GST_DEBUG ("Seeking to %" GST_TIME_FORMAT, GST_TIME_ARGS (position));        self->last_seek_time = gst_util_get_timestamp ();        gst_element_seek_simple (self->pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, position);        self->desired_position = GST_CLOCK_TIME_NONE;    }}

      上次搜索的时间会存放在last_seek_time变量里面。如果上次搜索之后已经过了足够多的时间,那么就直接开始搜索;否则就需要等待。如果前面没有在等待的seek请求,那么就等待上次搜索之后的冷却时间到达即可,反之,则丢弃前面的seek请求,把desired_position设成现在的位置。
      +网络的不稳定
      在Basic的教程12里面已经 展示了如果使用Buffer根据网络带宽来适配。本例使用同样的流程,监听Buffer的消息:
g_signal_connect (G_OBJECT (bus), "message::buffering", (GCallback)buffering_cb, (__bridge void *)self);

      暂停pipeline直到Buffer结束(当然仅在远程URI的情况下)  
/* Called when buffering messages are received. We inform the UI about the current buffering level and * keep the pipeline paused until 100% buffering is reached. At that point, set the desired state. */static void buffering_cb (GstBus *bus, GstMessage *msg, GStreamerBackend *self) {    gint percent;    if (self->is_live)        return;    gst_message_parse_buffering (msg, &percent);    if (percent < 100 && self->target_state >= GST_STATE_PAUSED) {        gchar * message_string = g_strdup_printf ("Buffering %d%%", percent);        gst_element_set_state (self->pipeline, GST_STATE_PAUSED);        [self setUIMessage:message_string];        g_free (message_string);    } else if (self->target_state >= GST_STATE_PLAYING) {        gst_element_set_state (self->pipeline, GST_STATE_PLAYING);    } else if (self->target_state >= GST_STATE_PAUSED) {        [self setUIMessage:"Buffering complete"];    }}

      其中target_state是pipeline的状态,这个可能和当前状态不一致,因为buffering会强制进入到PAUSED状态。一旦buffering结束后,我们会把pipeline置成target_state。

6.结论
      本教程演示了如何在iOS里面嵌入一个playbin2的pipeline。这样,只要GStreamer能识别的格式,这个应用都能播放,也就成了一个最简单的播放器。

0 0