您的位置:首页 > 移动开发 > IOS开发

GStreamer iOS教程2 —— 运行pipeline

2013-12-24 15:29 405 查看
1. 目标

在Basic和Playback的教程中(注:另外两个教程),GStreamer可以和GLib的主循环完美的集成起来,这样能用一种简单的方法同时监控pipeline的操作和UI的处理。而在不支持GLib的iOS和Android平台上就必须小心对于pipeline的操作——不能阻塞住UI。

这份教程讲述了:

如何把GStreamer的相关处理代码放到其他的线程(DispatchQueue),保持UI仍然保留在主线程(MainDispatchQueue)
在ObjC的UI代码中如何和GStreamer的C代码通信

2. 介绍
当由UI界面的时候,如果应用等待GStreamer的回传消息然后进行UI的处理是非常悲催的。通常的做法是(用GTK+toolkit做例子哈)让GMainLoop(GLib)来处理收到的event,无论是UI的还是GStreamer发出的。
悲催的是这个方法不适合其他非基于GLib的图形系统(比如iOS的GocoaTouch框架),我们的方法是在另一个线程里面简单的调用GMainLoop,确保不会阻塞UI主线程。
这个教程还会指出几个ObjC和C互相调用时需要注意的几个地方。
下面的代码使用的pipeline仅仅使用了audiotestsrc和autoaudiosink两个element。UI上包含两个按钮,用来设置pipeline的状态PLAYING/ PAUSED。还有一个Label用来显示一些信息(错误信息或状态改变)。

3. UI
这个界面底下包含了一个工具条,工具条上放了Play和Pause两个按钮。工具条上方就是一个Label,用来显示GStreamer的信息。本次教程就包含这么多内容,后面的教程会在这个基础上逐渐增加内容。
ViewController.m

#import "ViewController.h"
#import "GStreamerBackend.h"
#import <UIKit/UIKit.h>

@interface ViewController () {
GStreamerBackend *gst_backend;
}

@end

@implementation ViewController

/*
* Methods from UIViewController
*/

- (void)viewDidLoad
{
[super viewDidLoad];

play_button.enabled = FALSE;
pause_button.enabled = FALSE;

gst_backend = [[GStreamerBackend alloc] init:self];
}

- (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];
}

/* Called when the Pause button is pressed */
-(IBAction) pause:(id)sender
{
[gst_backend pause];
}

/*
* Methods from GstreamerBackendDelegate
*/

-(void) gstreamerInitialized
{
dispatch_async(dispatch_get_main_queue(), ^{
play_button.enabled = TRUE;
pause_button.enabled = TRUE;
message_label.text = @"Ready";
});
}

-(void) gstreamerSetUIMessage:(NSString *)message
{
dispatch_async(dispatch_get_main_queue(), ^{
message_label.text = message;
});
}

@end

在这个类里面包含了一个GStreamerBackend的实例,
@interface ViewController () {
GStreamerBackend *gst_backend;
}
在viewDidLoad方法里面创建并调用了自定义的init方法
- (void)viewDidLoad
{
[super viewDidLoad];

play_button.enabled = FALSE;
pause_button.enabled = FALSE;

gst_backend = [[GStreamerBackend alloc] init:self];
}
这个自定义的init方法必须传入一个对象作为UI的delegate(本例是使用了self)

在viewDidLoad的时候,Play/Pause两个按钮都是不能使用的,直到GStreamerBackend通知初始化结束为止。

/* Called when the Play button is pressed */
-(IBAction) play:(id)sender
{
[gst_backend play];
}

/* Called when the Pause button is pressed */
-(IBAction) pause:(id)sender
{
[gst_backend pause];
}
      在用户按下Play/Pause按钮时,上面的方法会被调用。我们看到仅仅就是简单的调用了GStreamerBackend里面对应的方法。



-(void) gstreamerInitialized
{
dispatch_async(dispatch_get_main_queue(), ^{
play_button.enabled = TRUE;
pause_button.enabled = TRUE;
message_label.text = @"Ready";
});
}
gstreamerInitialized方法是定义在GStreamerBackendDelegate协议里面的,用来标识后台已经准备好,可以接受命令了。在这个例子中我们把Play/Pause按钮激活,并显示“Ready”信息。这个方法不是在UI线程里面运行的,所以要用dispatch_async()方法把UI的内容封起来。
-(void) gstreamerSetUIMessage:(NSString *)message
{
dispatch_async(dispatch_get_main_queue(), ^{
message_label.text = message;
});
}
gstreamerSetUIMessage:方法同样是定义在GStreamerBackendDelegate协议里面的,当后台有消息通知UI的时候会调用这个方法。在这个例子里面消息会拷贝到界面的Label控件上,当然,也同样要用dispatch_async()方法来封装。

4. GStreamer后端
GStreamerBackend类处理了所有和GStreamer相关的内容,并给应用提供了一个简单的接口。这个接口并不需要实现所有的GStreamer的细节,当需要引起UI的变化的时候,调用GSreamerBackendDelegate协议来解决。
GStreamerBackend.m

#import "GStreamerBackend.h"

#include <gst/gst.h>

GST_DEBUG_CATEGORY_STATIC (debug_category);
#define GST_CAT_DEFAULT debug_category

@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 */
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 */
}

/*
* Interface methods
*/

-(id) init:(id) uiDelegate
{
if (self = [super init])
{
self->ui_delegate = uiDelegate;

GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-2", 0, "iOS tutorial 2");
gst_debug_set_threshold_for_name("tutorial-2", 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) dealloc
{
if (pipeline) {
GST_DEBUG("Setting the pipeline to NULL");
gst_element_set_state(pipeline, GST_STATE_NULL);
gst_object_unref(pipeline);
pipeline = NULL;
}
}

-(void) play
{
if(gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
[self setUIMessage:"Failed to set pipeline to playing"];
}
}

-(void) pause
{
if(gst_element_set_state(pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
[self setUIMessage:"Failed to set pipeline to paused"];
}
}

/*
* 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];
}
}

/* 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);
}

/* 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)) {
gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
[self setUIMessage:message];
g_free (message);
}
}

/* 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 *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("audiotestsrc ! audioconvert ! audioresample ! autoaudiosink", &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;
}

/* 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::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);
gst_object_unref (bus);

/* 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);

return;
}

@end
其中,接口方法是:
-(id) init:(id) uiDelegate
{
if (self = [super init])
{
self->ui_delegate = uiDelegate;

GST_DEBUG_CATEGORY_INIT (debug_category, "tutorial-2", 0, "iOS tutorial 2");
gst_debug_set_threshold_for_name("tutorial-2", GST_LEVEL_DEBUG);

/* Start the bus monitoring task */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self app_function];
});
}

return self;
}
这个init方法通过调用[super init]来生成实例,保存delegate的对象用来做UI互动,接着调用app_function并运行在一个独立的并发的线程里面,app_function会一直监听GStreamer总线,看看有没有应用需要处理的消息或者警告发生。
init:方法同样注册了一个新的GStreamer调试类别并设置了吐出的信息的等级,我们就可以在Xcode里面看到打印信息了。

-(void) dealloc
{
if (pipeline) {
GST_DEBUG("Setting the pipeline to NULL");
gst_element_set_state(pipeline, GST_STATE_NULL);
gst_object_unref(pipeline);
pipeline = NULL;
}
}
dealloc方法把pipeline置成NULL状态并释放它。

-(void) play
{
if(gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
[self setUIMessage:"Failed to set pipeline to playing"];
}
}

-(void) pause
{
if(gst_element_set_state(pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
[self setUIMessage:"Failed to set pipeline to paused"];
}
}
play/pause方法仅仅简单的设置pipeline状态的该变,并在出错的时候通知UI
下面是文件中几个私有方法:
-(void)setUIMessage:(gchar*) message
{
NSString *string = [NSString stringWithUTF8String:message];
if(ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)])
{
[ui_delegate gstreamerSetUIMessage:string];
}
}
setUIMessage:方法是把GStreamer使用的C的字符串转变成NSString*字符串,然后调用GStreamerBackendProtocal协议里面的gstreamerSetUIMessage:方法来在屏幕上显示出来。
因为这个方法是optional的,所以需要用respondsToSelector来判一下是否存在。

/* 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);
}

/* 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)) {
gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
[self setUIMessage:message];
g_free (message);
}
}
error_cb()和state_changed_cb()是注册的两个回调,分别在GStreamer出错和状态变化的时候被调用。这两个回调的目的是当事件发生时能通知到用户。
这两个回调函数在Base的教程中出现了多次,实现起来也除了下面2点之外基本一致:一个是消息使用私有方法setUIMessage:来传递到UI;第二个是要调用setUIMessage:就需要一个GStreamerBackend的实例,通过callback的userdata来传递,这个在下面讨论app_function里回调的注册时可以看到
-(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;
}
}
check_initialization_complete()方法确认满足所有的条件之后通知UI后台GStreamer准备完成。在这个教程里面这个条件非常简单,仅仅是主循环存在并且没有通知过UI。后续的教程这里会更加复杂。
绝大部分的GStreamer的行为都是在app_function里面实现的,这些代码和android的教程几乎一致。

/* Create our own GLib Main Context and make it the default one */
context = g_main_context_new ();
g_main_context_push_thread_default(context);

这里第一次创建了一个GLib的上下文,使用g_main_context_new(),然后用g_main_context_push_thread_default()来创建了一个线程

/* Build pipeline */
pipeline = gst_parse_launch("audiotestsrc ! audioconvert ! audioresample ! autoaudiosink", &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;
}
这里用gst_arse_launch()方法很轻易的创建了一个pipeline。在这个教程里面仅仅audiotestsrc和autoaudiosink两个element需要完成适配。

/* 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::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);
gst_object_unref (bus);
这几行创建了一个总线信号的监视器,并和设置了需要监视的信号,这些和Basic教程里面的做法也是一致的。这个监视器我们这里是一步一步创建的,并非调用gst_bus_add_signal_watch()来创建,这样可以看清如何使用GLib的一些内容。这里需要指出的是使用了__bridge来把一个ObjC的对象指针转换成C语言里面的指针。

这里我们不需要太担心对象的所有权的转移问题,因为在回调里面userdata会把这个指针带回来,重新转换成GStreamerBackend的对象指针

/* 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;
最后,主循环创建并开始运行,在进入主循环之前,我们调用了check_initialization_complete()方法。主循环会一直运行,直到退出为止。

这篇教程有点长了,主要是需要讲清楚一系列的基础内容。在这个基础之上,后面的会比较短一些,并且会只关注新的内容。

5. 结论

这篇教程主要讲述了:

使用DispatchQueue如何让GStreamer的代码单独运行在子线程中
如何在ObjC的UI代码和GStreamer的C代码中传递对象

在这篇教程里面的方法,象check_initialization_complete()和app_function(),init:,play;,pause:,gstreamerInitialized:和setUIMessage:等接口后续会简单修改一下继续使用,所以最好熟悉一下。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: