目标
本教程展示了如何在GStreamer集成一个GUI(比如:GTK+)。最基本的原则是GStreamer处理多媒体的播放而GUI处理和用户的交互。
在这个教程里面,我们可以学到:
如何告诉GStreamer输出视频到一个window
如何持续的刷新GUI
在GStreamer多线程时如何保持UI的更新
一个仅发送给你订阅的消息而不是所有消息的机制
介绍
我们下面就用GTK+这样一个GUI工具来些一个播放器,但基本概念是可以推广到其它工具的(比如QT)。如果你对GTK+有一定的了解有助于理解本教程。
最重要的是告诉GStreamer把视频输出到哪个window,而这个是依赖于操作系统的,好在GStreamer提供了一个平台无关的抽象层。这个抽象层称为XOverlay,它让应用可以告诉一个视频输出到哪个window进行渲染。
另一个问题是GUI工具通常只能用主线程来进行绘制,但GStreamer通常使用多线程来处理不同的任务。在回调函数中调用GTK+的函数通常会失败,因为回调函数是在调用线程运行的,往往不是主线程。这个问题的解决方法是给GStreamer发送一个消息——消息的接收是在主线程。
在现在为止,我们仅仅注册了一个handle_message函数,每次总线上有消息出现是这个函数会呗调用。这个函数中我们会解析每个消息,看是否是我们关注的。本教程我们采用另一种方法:给每一个消息注册一个回调函数,这样解析代码会少一点。
GTK+实现的播放器
我们使用playbin2实现一个很简单的播放器,但这次,我们有GUI!
- #include <string.h>
- #include <gtk/gtk.h>
- #include <gst/gst.h>
- #include <gst/interfaces/xoverlay.h>
- #include <gdk/gdk.h>
- #if defined (GDK_WINDOWING_X11)
- #include <gdk/gdkx.h>
- #elif defined (GDK_WINDOWING_WIN32)
- #include <gdk/gdkwin32.h>
- #elif defined (GDK_WINDOWING_QUARTZ)
- #include <gdk/gdkquartz.h>
- #endif
- /* Structure to contain all our information, so we can pass it around */
- typedef struct _CustomData {
- GstElement *playbin2; /* Our one and only pipeline */
- GtkWidget *slider; /* Slider widget to keep track of current position */
- GtkWidget *streams_list; /* Text widget to display info about the streams */
- gulong slider_update_signal_id; /* Signal ID for the slider update signal */
- GstState state; /* Current state of the pipeline */
- gint64 duration; /* Duration of the clip, in nanoseconds */
- } CustomData;
- /* This function is called when the GUI toolkit creates the physical window that will hold the video.
- * At this point we can retrieve its handler (which has a different meaning depending on the windowing system)
- * and pass it to GStreamer through the XOverlay interface. */
- static void realize_cb (GtkWidget *widget, CustomData *data) {
- GdkWindow *window = gtk_widget_get_window (widget);
- guintptr window_handle;
- if (!gdk_window_ensure_native (window))
- g_error ("Couldn't create native window needed for GstXOverlay!");
- /* Retrieve window handler from GDK */
- #if defined (GDK_WINDOWING_WIN32)
- window_handle = (guintptr)GDK_WINDOW_HWND (window);
- #elif defined (GDK_WINDOWING_QUARTZ)
- window_handle = gdk_quartz_window_get_nsview (window);
- #elif defined (GDK_WINDOWING_X11)
- window_handle = GDK_WINDOW_XID (window);
- #endif
- /* Pass it to playbin2, which implements XOverlay and will forward it to the video sink */
- gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->playbin2), window_handle);
- }
- /* This function is called when the PLAY button is clicked */
- static void play_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_PLAYING);
- }
- /* This function is called when the PAUSE button is clicked */
- static void pause_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_PAUSED);
- }
- /* This function is called when the STOP button is clicked */
- static void stop_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_READY);
- }
- /* This function is called when the main window is closed */
- static void delete_event_cb (GtkWidget *widget, GdkEvent *event, CustomData *data) {
- stop_cb (NULL, data);
- gtk_main_quit ();
- }
- /* This function is called everytime the video window needs to be redrawn (due to damage/exposure,
- * rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise,
- * we simply draw a black rectangle to avoid garbage showing up. */
- static gboolean expose_cb (GtkWidget *widget, GdkEventExpose *event, CustomData *data) {
- if (data->state < GST_STATE_PAUSED) {
- GtkAllocation allocation;
- GdkWindow *window = gtk_widget_get_window (widget);
- cairo_t *cr;
- /* Cairo is a 2D graphics library which we use here to clean the video window.
- * It is used by GStreamer for other reasons, so it will always be available to us. */
- gtk_widget_get_allocation (widget, &allocation);
- cr = gdk_cairo_create (window);
- cairo_set_source_rgb (cr, 0, 0, 0);
- cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);
- cairo_fill (cr);
- cairo_destroy (cr);
- }
- return FALSE;
- }
- /* This function is called when the slider changes its position. We perform a seek to the
- * new position here. */
- static void slider_cb (GtkRange *range, CustomData *data) {
- gdouble value = gtk_range_get_value (GTK_RANGE (data->slider));
- gst_element_seek_simple (data->playbin2, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
- (gint64)(value * GST_SECOND));
- }
- /* This creates all the GTK+ widgets that compose our application, and registers the callbacks */
- static void create_ui (CustomData *data) {
- GtkWidget *main_window; /* The uppermost window, containing all other windows */
- GtkWidget *video_window; /* The drawing area where the video will be shown */
- GtkWidget *main_box; /* VBox to hold main_hbox and the controls */
- GtkWidget *main_hbox; /* HBox to hold the video_window and the stream info text widget */
- GtkWidget *controls; /* HBox to hold the buttons and the slider */
- GtkWidget *play_button, *pause_button, *stop_button; /* Buttons */
- main_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
- g_signal_connect (G_OBJECT (main_window), "delete-event", G_CALLBACK (delete_event_cb), data);
- video_window = gtk_drawing_area_new ();
- gtk_widget_set_double_buffered (video_window, FALSE);
- g_signal_connect (video_window, "realize", G_CALLBACK (realize_cb), data);
- g_signal_connect (video_window, "expose_event", G_CALLBACK (expose_cb), data);
- play_button = gtk_button_new_from_stock (GTK_STOCK_MEDIA_PLAY);
- g_signal_connect (G_OBJECT (play_button), "clicked", G_CALLBACK (play_cb), data);
- pause_button = gtk_button_new_from_stock (GTK_STOCK_MEDIA_PAUSE);
- g_signal_connect (G_OBJECT (pause_button), "clicked", G_CALLBACK (pause_cb), data);
- stop_button = gtk_button_new_from_stock (GTK_STOCK_MEDIA_STOP);
- g_signal_connect (G_OBJECT (stop_button), "clicked", G_CALLBACK (stop_cb), data);
- data->slider = gtk_hscale_new_with_range (0, 100, 1);
- gtk_scale_set_draw_value (GTK_SCALE (data->slider), 0);
- data->slider_update_signal_id = g_signal_connect (G_OBJECT (data->slider), "value-changed", G_CALLBACK (slider_cb), data);
- data->streams_list = gtk_text_view_new ();
- gtk_text_view_set_editable (GTK_TEXT_VIEW (data->streams_list), FALSE);
- controls = gtk_hbox_new (FALSE, 0);
- gtk_box_pack_start (GTK_BOX (controls), play_button, FALSE, FALSE, 2);
- gtk_box_pack_start (GTK_BOX (controls), pause_button, FALSE, FALSE, 2);
- gtk_box_pack_start (GTK_BOX (controls), stop_button, FALSE, FALSE, 2);
- gtk_box_pack_start (GTK_BOX (controls), data->slider, TRUE, TRUE, 2);
- main_hbox = gtk_hbox_new (FALSE, 0);
- gtk_box_pack_start (GTK_BOX (main_hbox), video_window, TRUE, TRUE, 0);
- gtk_box_pack_start (GTK_BOX (main_hbox), data->streams_list, FALSE, FALSE, 2);
- main_box = gtk_vbox_new (FALSE, 0);
- gtk_box_pack_start (GTK_BOX (main_box), main_hbox, TRUE, TRUE, 0);
- gtk_box_pack_start (GTK_BOX (main_box), controls, FALSE, FALSE, 0);
- gtk_container_add (GTK_CONTAINER (main_window), main_box);
- gtk_window_set_default_size (GTK_WINDOW (main_window), 640, 480);
- gtk_widget_show_all (main_window);
- }
- /* This function is called periodically to refresh the GUI */
- static gboolean refresh_ui (CustomData *data) {
- GstFormat fmt = GST_FORMAT_TIME;
- gint64 current = -1;
- /* We do not want to update anything unless we are in the PAUSED or PLAYING states */
- if (data->state < GST_STATE_PAUSED)
- return TRUE;
- /* If we didn't know it yet, query the stream duration */
- if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
- if (!gst_element_query_duration (data->playbin2, &fmt, &data->duration)) {
- g_printerr ("Could not query current duration. ");
- } else {
- /* Set the range of the slider to the clip duration, in SECONDS */
- gtk_range_set_range (GTK_RANGE (data->slider), 0, (gdouble)data->duration / GST_SECOND);
- }
- }
- if (gst_element_query_position (data->playbin2, &fmt, ¤t)) {
- /* Block the "value-changed" signal, so the slider_cb function is not called
- * (which would trigger a seek the user has not requested) */
- g_signal_handler_block (data->slider, data->slider_update_signal_id);
- /* Set the position of the slider to the current pipeline positoin, in SECONDS */
- gtk_range_set_value (GTK_RANGE (data->slider), (gdouble)current / GST_SECOND);
- /* Re-enable the signal */
- g_signal_handler_unblock (data->slider, data->slider_update_signal_id);
- }
- return TRUE;
- }
- /* This function is called when new metadata is discovered in the stream */
- static void tags_cb (GstElement *playbin2, gint stream, CustomData *data) {
- /* We are possibly in a GStreamer working thread, so we notify the main
- * thread of this event through a message in the bus */
- gst_element_post_message (playbin2,
- gst_message_new_application (GST_OBJECT (playbin2),
- gst_structure_new ("tags-changed", NULL)));
- }
- /* This function is called when an error message is posted on the bus */
- static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
- GError *err;
- gchar *debug_info;
- /* Print error details on the screen */
- gst_message_parse_error (msg, &err, &debug_info);
- g_printerr ("Error received from element %s: %s ", GST_OBJECT_NAME (msg->src), err->message);
- g_printerr ("Debugging information: %s ", debug_info ? debug_info : "none");
- g_clear_error (&err);
- g_free (debug_info);
- /* Set the pipeline to READY (which stops playback) */
- gst_element_set_state (data->playbin2, GST_STATE_READY);
- }
- /* This function is called when an End-Of-Stream message is posted on the bus.
- * We just set the pipeline to READY (which stops playback) */
- static void eos_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
- g_print ("End-Of-Stream reached. ");
- gst_element_set_state (data->playbin2, GST_STATE_READY);
- }
- /* This function is called when the pipeline changes states. We use it to
- * keep track of the current state. */
- static void state_changed_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
- GstState old_state, new_state, pending_state;
- gst_message_parse_state_changed (msg, &old_state, &new_state, &pending_state);
- if (GST_MESSAGE_SRC (msg) == GST_OBJECT (data->playbin2)) {
- data->state = new_state;
- g_print ("State set to %s ", gst_element_state_get_name (new_state));
- if (old_state == GST_STATE_READY && new_state == GST_STATE_PAUSED) {
- /* For extra responsiveness, we refresh the GUI as soon as we reach the PAUSED state */
- refresh_ui (data);
- }
- }
- }
- /* Extract metadata from all the streams and write it to the text widget in the GUI */
- static void analyze_streams (CustomData *data) {
- gint i;
- GstTagList *tags;
- gchar *str, *total_str;
- guint rate;
- gint n_video, n_audio, n_text;
- GtkTextBuffer *text;
- /* Clean current contents of the widget */
- text = gtk_text_view_get_buffer (GTK_TEXT_VIEW (data->streams_list));
- gtk_text_buffer_set_text (text, "", -1);
- /* Read some properties */
- g_object_get (data->playbin2, "n-video", &n_video, NULL);
- g_object_get (data->playbin2, "n-audio", &n_audio, NULL);
- g_object_get (data->playbin2, "n-text", &n_text, NULL);
- for (i = 0; i < n_video; i++) {
- tags = NULL;
- /* Retrieve the stream's video tags */
- g_signal_emit_by_name (data->playbin2, "get-video-tags", i, &tags);
- if (tags) {
- total_str = g_strdup_printf ("video stream %d: ", i);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- gst_tag_list_get_string (tags, GST_TAG_VIDEO_CODEC, &str);
- total_str = g_strdup_printf (" codec: %s ", str ? str : "unknown");
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- g_free (str);
- gst_tag_list_free (tags);
- }
- }
- for (i = 0; i < n_audio; i++) {
- tags = NULL;
- /* Retrieve the stream's audio tags */
- g_signal_emit_by_name (data->playbin2, "get-audio-tags", i, &tags);
- if (tags) {
- total_str = g_strdup_printf (" audio stream %d: ", i);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- if (gst_tag_list_get_string (tags, GST_TAG_AUDIO_CODEC, &str)) {
- total_str = g_strdup_printf (" codec: %s ", str);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- g_free (str);
- }
- if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
- total_str = g_strdup_printf (" language: %s ", str);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- g_free (str);
- }
- if (gst_tag_list_get_uint (tags, GST_TAG_BITRATE, &rate)) {
- total_str = g_strdup_printf (" bitrate: %d ", rate);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- }
- gst_tag_list_free (tags);
- }
- }
- for (i = 0; i < n_text; i++) {
- tags = NULL;
- /* Retrieve the stream's subtitle tags */
- g_signal_emit_by_name (data->playbin2, "get-text-tags", i, &tags);
- if (tags) {
- total_str = g_strdup_printf (" subtitle stream %d: ", i);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
- total_str = g_strdup_printf (" language: %s ", str);
- gtk_text_buffer_insert_at_cursor (text, total_str, -1);
- g_free (total_str);
- g_free (str);
- }
- gst_tag_list_free (tags);
- }
- }
- }
- /* This function is called when an "application" message is posted on the bus.
- * Here we retrieve the message posted by the tags_cb callback */
- static void application_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
- if (g_strcmp0 (gst_structure_get_name (msg->structure), "tags-changed") == 0) {
- /* If the message is the "tags-changed" (only one we are currently issuing), update
- * the stream info GUI */
- analyze_streams (data);
- }
- }
- int main(int argc, charchar *argv[]) {
- CustomData data;
- GstStateChangeReturn ret;
- GstBus *bus;
- /* Initialize GTK */
- gtk_init (&argc, &argv);
- /* Initialize GStreamer */
- gst_init (&argc, &argv);
- /* Initialize our data structure */
- memset (&data, 0, sizeof (data));
- data.duration = GST_CLOCK_TIME_NONE;
- /* Create the elements */
- data.playbin2 = gst_element_factory_make ("playbin2", "playbin2");
- if (!data.playbin2) {
- g_printerr ("Not all elements could be created. ");
- return -1;
- }
- /* Set the URI to play */
- g_object_set (data.playbin2, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);
- /* Connect to interesting signals in playbin2 */
- g_signal_connect (G_OBJECT (data.playbin2), "video-tags-changed", (GCallback) tags_cb, &data);
- g_signal_connect (G_OBJECT (data.playbin2), "audio-tags-changed", (GCallback) tags_cb, &data);
- g_signal_connect (G_OBJECT (data.playbin2), "text-tags-changed", (GCallback) tags_cb, &data);
- /* Create the GUI */
- create_ui (&data);
- /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
- bus = gst_element_get_bus (data.playbin2);
- gst_bus_add_signal_watch (bus);
- g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::application", (GCallback)application_cb, &data);
- gst_object_unref (bus);
- /* Start playing */
- ret = gst_element_set_state (data.playbin2, GST_STATE_PLAYING);
- if (ret == GST_STATE_CHANGE_FAILURE) {
- g_printerr ("Unable to set the pipeline to the playing state. ");
- gst_object_unref (data.playbin2);
- return -1;
- }
- /* Register a function that GLib will call every second */
- g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
- /* Start the GTK main loop. We will not regain control until gtk_main_quit is called. */
- gtk_main ();
- /* Free resources */
- gst_element_set_state (data.playbin2, GST_STATE_NULL);
- gst_object_unref (data.playbin2);
- return 0;
- }
工作流程
不管本教程的数据结构是怎么样的,我们都不再打算继续使用前向引用的方式。但是,作为显示声明时,代码的顺序并不是一成不变的。
- #include <gdk/gdk.h>
- #if defined (GDK_WINDOWING_X11)
- #include <gdk/gdkx.h>
- #elif defined (GDK_WINDOWING_WIN32)
- #include <gdk/gdkwin32.h>
- #elif defined (GDK_WINDOWING_QUARTZ)
- #include <gdk/gdkquartz.h>
- #endif
本教程绝大部分都是在实现由GStreamer或者GTK+调用的回调函数,这些回调函数都在main函数中被注册进去的。
- int main(int argc, charchar *argv[]) {
- CustomData data;
- GstStateChangeReturn ret;
- GstBus *bus;
- /* Initialize GTK */
- gtk_init (&argc, &argv);
- /* Initialize GStreamer */
- gst_init (&argc, &argv);
- /* Initialize our data structure */
- memset (&data, 0, sizeof (data));
- data.duration = GST_CLOCK_TIME_NONE;
- /* Create the elements */
- data.playbin2 = gst_element_factory_make ("playbin2", "playbin2");
- if (!data.playbin2) {
- g_printerr ("Not all elements could be created. ");
- return -1;
- }
- /* Set the URI to play */
- g_object_set (data.playbin2, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);
- /* Connect to interesting signals in playbin2 */
- g_signal_connect (G_OBJECT (data.playbin2), "video-tags-changed", (GCallback) tags_cb, &data);
- g_signal_connect (G_OBJECT (data.playbin2), "audio-tags-changed", (GCallback) tags_cb, &data);
- g_signal_connect (G_OBJECT (data.playbin2), "text-tags-changed", (GCallback) tags_cb, &data);
- /* Create the GUI */
- create_ui (&data);
- /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
- bus = gst_element_get_bus (data.playbin2);
- gst_bus_add_signal_watch (bus);
- g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::eos", (GCallback)eos_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::state-changed", (GCallback)state_changed_cb, &data);
- g_signal_connect (G_OBJECT (bus), "message::application", (GCallback)application_cb, &data);
- gst_object_unref (bus);
本教程使用注册信号的方法来保证只有我们关注的消息才会调用回调,如果我们注册message信号,那么每个消息都会调用回调,就和使用gst_bus_add_watch()方法一样。
请记住,要监视总线的工作(使用gst_bus_add_watch()方法或者gst_bus_add_signal_watch()方法),必须保证GLib的主循环在运行。在本教程中,GTK+的主循环隐含了这个部分。
- /* Register a function that GLib will call every second */
- g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);
在这些之后,我们完成了设置,然后就可以开始GTK+的主循环了。当我们关注的事件发生时,我们会再次拿到控制权。让我们看一下这些回调吧,每个回调有不同的签名(这里签名是输入参数和返回值),你可以在文档中查到。
- /* This function is called when the GUI toolkit creates the physical window that will hold the video.
- * At this point we can retrieve its handler (which has a different meaning depending on the windowing system)
- * and pass it to GStreamer through the XOverlay interface. */
- static void realize_cb (GtkWidget *widget, CustomData *data) {
- GdkWindow *window = gtk_widget_get_window (widget);
- guintptr window_handle;
- if (!gdk_window_ensure_native (window))
- g_error ("Couldn't create native window needed for GstXOverlay!");
- /* Retrieve window handler from GDK */
- #if defined (GDK_WINDOWING_WIN32)
- window_handle = (guintptr)GDK_WINDOW_HWND (window);
- #elif defined (GDK_WINDOWING_QUARTZ)
- window_handle = gdk_quartz_window_get_nsview (window);
- #elif defined (GDK_WINDOWING_X11)
- window_handle = GDK_WINDOW_XID (window);
- #endif
- /* Pass it to playbin2, which implements XOverlay and will forward it to the video sink */
- gst_x_overlay_set_window_handle (GST_X_OVERLAY (data->playbin2), window_handle);
- }
playbin2和XOverlay让这个过程变得很简单,没有太多需要解释的地方。
- /* This function is called when the PLAY button is clicked */
- static void play_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_PLAYING);
- }
- /* This function is called when the PAUSE button is clicked */
- static void pause_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_PAUSED);
- }
- /* This function is called when the STOP button is clicked */
- static void stop_cb (GtkButton *button, CustomData *data) {
- gst_element_set_state (data->playbin2, GST_STATE_READY);
- }
- /* This function is called when the main window is closed */
- static void delete_event_cb (GtkWidget *widget, GdkEvent *event, CustomData *data) {
- stop_cb (NULL, data);
- gtk_main_quit ();
- }
- /* This function is called everytime the video window needs to be redrawn (due to damage/exposure,
- * rescaling, etc). GStreamer takes care of this in the PAUSED and PLAYING states, otherwise,
- * we simply draw a black rectangle to avoid garbage showing up. */
- static gboolean expose_cb (GtkWidget *widget, GdkEventExpose *event, CustomData *data) {
- if (data->state < GST_STATE_PAUSED) {
- GtkAllocation allocation;
- GdkWindow *window = gtk_widget_get_window (widget);
- cairo_t *cr;
- /* Cairo is a 2D graphics library which we use here to clean the video window.
- * It is used by GStreamer for other reasons, so it will always be available to us. */
- gtk_widget_get_allocation (widget, &allocation);
- cr = gdk_cairo_create (window);
- cairo_set_source_rgb (cr, 0, 0, 0);
- cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);
- cairo_fill (cr);
- cairo_destroy (cr);
- }
- return FALSE;
- }
- /* This function is called when the slider changes its position. We perform a seek to the
- * new position here. */
- static void slider_cb (GtkRange *range, CustomData *data) {
- gdouble value = gtk_range_get_value (GTK_RANGE (data->slider));
- gst_element_seek_simple (data->playbin2, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,
- (gint64)(value * GST_SECOND));
- }
因为搜索需要花费一点时间,所以常常在搜索结束后隔半秒才可以开始下一次搜索。否则,应用在用户疯狂地拖动的时候会表现的几乎没有响应。
- /* This function is called periodically to refresh the GUI */
- static gboolean refresh_ui (CustomData *data) {
- GstFormat fmt = GST_FORMAT_TIME;
- gint64 current = -1;
- /* We do not want to update anything unless we are in the PAUSED or PLAYING states */
- if (data->state < GST_STATE_PAUSED)
- return TRUE;
- /* If we didn't know it yet, query the stream duration */
- if (!GST_CLOCK_TIME_IS_VALID (data->duration)) {
- if (!gst_element_query_duration (data->playbin2, &fmt, &data->duration)) {
- g_printerr ("Could not query current duration. ");
- } else {
- /* Set the range of the slider to the clip duration, in SECONDS */
- gtk_range_set_range (GTK_RANGE (data->slider), 0, (gdouble)data->duration / GST_SECOND);
- }
- }
如果我们不知道播放总时间,我们就再查询一次,这样我们才能设置滑块的距离。
- if (gst_element_query_position (data->playbin2, &fmt, ¤t)) {
- /* Block the "value-changed" signal, so the slider_cb function is not called
- * (which would trigger a seek the user has not requested) */
- g_signal_handler_block (data->slider, data->slider_update_signal_id);
- /* Set the position of the slider to the current pipeline positoin, in SECONDS */
- gtk_range_set_value (GTK_RANGE (data->slider), (gdouble)current / GST_SECOND);
- /* Re-enable the signal */
- g_signal_handler_unblock (data->slider, data->slider_update_signal_id);
- }
- return TRUE;
如果返回TRUE,那么定时器继续工作,下次还会调这个回调函数;如果返回FALSE,那么关闭定时器。
- /* This function is called when new metadata is discovered in the stream */
- static void tags_cb (GstElement *playbin2, gint stream, CustomData *data) {
- /* We are possibly in a GStreamer working thread, so we notify the main
- * thread of this event through a message in the bus */
- gst_element_post_message (playbin2,
- gst_message_new_application (GST_OBJECT (playbin2),
- gst_structure_new ("tags-changed", NULL)));
- }
解决方法是让playbin2发出一个消息然后回到调用线程,这样,主线程会收到消息然后刷新GTK。
gst_element_post_message()方法让一个GStreamer element发送一个特定消息到总线上。gst_message_new_application()创建一个新的APPLICATION类型的消息。GStreamer有不同类型的消息,而这个特定的消息是预留给应用的,它会不受影响的通过总线。在GstMessageType文档里面有所有类型的列表。
消息可以通过他们自己的GstStructure来携带一些信息。在这里,我们用gst_structure_new来创建了一个新的数据结构并起名tags-changed,避免和其他的消息混淆。
一旦切换到主线程,总线会接受到消息,并发出message:application信号,这个信号会调用application_cb回调函数:
- /* This function is called when an "application" message is posted on the bus.
- * Here we retrieve the message posted by the tags_cb callback */
- static void application_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
- if (g_strcmp0 (gst_structure_get_name (msg->structure), "tags-changed") == 0) {
- /* If the message is the "tags-changed" (only one we are currently issuing), update
- * the stream info GUI */
- analyze_streams (data);
- }
- }
至于error_cb、eos_cb和state_changed_cb因为和前面的教程一样,所以不拿出来详细解释了。
本教程的代码看上去有点令人生畏,但是概念比较少而且容易理解。如果你看过前面的教程并对GTK有一定的了解,你就会比较容易的理解本教程还可以写一个自己的播放器!
下面给张播放器的截图: