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
- GStreamer iOS教程4——一个基础的播放器
- GStreamer iOS教程5——一个完整的播放器
- GStreamer播放教程01——playbin2的使用
- GStreamer播放教程03——pipeline的快捷访问
- GStreamer播放教程07——自定义playbin2的sink
- GStreamer播放教程08——视频解码的硬件加速
- GStreamer播放教程01——playbin的使用
- GStreamer播放教程03——pipeline的快捷访问
- GStreamer播放教程07——自定义playbin的sink
- GStreamer播放教程08——视频解码的硬件加速
- 【GStreamer开发】GStreamer播放教程02——字幕管理
- 【GStreamer开发】GStreamer播放教程04——既看式流
- 【GStreamer开发】GStreamer播放教程05——色彩平衡
- 【GStreamer开发】GStreamer播放教程06——可视化音频
- 【GStreamer开发】GStreamer播放教程09——数字音频传输
- 【GStreamer开发】GStreamer播放教程01——playbin2的使用
- 【GStreamer开发】GStreamer播放教程03——pipeline的快捷访问
- 【GStreamer开发】GStreamer播放教程07——自定义playbin2的sink
- Linux学习之——存储设备和分区标识及分区
- List for springRestful + openlayer
- DBCC大全集之(适用版本MS SQLServer 2008 R2)---DBCC CHECKFILEGROUP检查当前数据库中指定文件组中的所有表和索引视图的分配和结构完整性
- 关于android的JNI几点注意问题。
- 驱动开发之三:常用API简介
- GStreamer iOS教程4——一个基础的播放器
- [TED] 创建自己的ramfs (二)
- mini2440 移植usb wifi;DMA报错
- 使用C#实现网站用户登录
- 图论 次小生成树
- 使用C#登录带验证码的网站
- 豆瓣API用户图书收藏
- C# 中的 ConfigurationManager类引用方法
- Google glog 使用方法