GStreamer官方教程系列

Basic tutorial 5: GUI toolkit integration

原文:https://gstreamer.freedesktop.org/documentation/tutorials/basic/toolkit-integration.html?gi-language=c

目标
  本教程会展示如何将GStreamer集成到GUI工具包例如GTK+。基本上,GStreamer负责媒体播放而GUI工具包负责用户交互。最有意思的部分是两个库都需要交互的地方:用GStreamer将视频输出到GTK+的窗口中并且将用户操作传回GStreamer。

  尤其是,你可以学到:

  • 如何告诉GStreamer将视频输出到一个特定的窗口而不是自己创建一个窗口。
  • 如何利用GStreamer信息来持续更新GUI。
  • 如何从多线程的GStreamer更新GUI,这个操作是大部分GUI工具所禁止的。
  • 一种只订阅你感兴趣的消息的机制,而不是提示你所有的消息。

引言
  我们将会利用GTK+工具来构建一个媒体播放器,而这些概念也同样适用于其他工具箱,例如QT。一些关于GTK+的知识可以帮助你理解这个教程。

  重点是告诉GStreamer将视频输出到我们选定的窗口上。具体的机制取决于操作系统(或者说,窗口系统),但是GStreamer提供了虚假的平台独立的抽象层。这种独立性来自于GstVideoOverlay接口,这个接口允许应用将需要接收视频的窗口的句柄告诉一个视频槽。

GObject接口
  GObject接口(GStreamer使用的)是一系列的函数,这些函数可以被元件所使用。如果可以使用,那么就是说支持这个特殊的接口。例如,视频槽通常创建输入自己的传教港口来播放视频,但是如果它们同样能够传递给一个外部的窗口,它们就可以选择实施GstVideoOverlay接口并提供指出外部窗口的函数。从应用开发者的角度而言,如果支持特定的接口,你就可以使用它并且忘记是哪种元件在使用它。此外,如果你在使用playbin,它会自动暴露一些它内部元件所支持的接口:你可以在你的playbin上直接使用你的接口函数而不必直到具体是谁在实施函数。

  另一个问题是GUI工具包通常仅仅允许通过主线程对图像组件进行操作,然而GStreamer通常有多个线程来处理不同的任务。在回调函数内部调用GTK+函数通常会失败,因为回调函数在调用线程执行操作,而调用线程可不一定是主线程。这个问题可以通过向GStreamer总线发送消息以及设置回调来解决:消息会在主线程被接收然后就可以正常地进行操作。

  最后,目前我们已经注册了一个handle_message函数每当总线中有消息时就会调用它,这会让我们解析每个消息,看看是不是我们感兴趣的消息。在本教程中,我们使用了一种不一眼的方法来为每种消息注册一个回调,所以代码更少解析也更少。

A media player in GTK+
  代码basic-tutorial-5.c如下所示。

#include <string.h>#include <gtk/gtk.h>
#include <gst/gst.h>
#include <gst/video/videooverlay.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 *playbin;           /* 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 VideoOverlay 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 GstVideoOverlay!");/* 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 playbin, which implements VideoOverlay and will forward it to the video sink */gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (data->playbin), 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->playbin, 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->playbin, 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->playbin, 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 draw_cb (GtkWidget *widget, cairo_t *cr, CustomData *data) {if (data->state < GST_STATE_PAUSED) {GtkAllocation allocation;/* 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);cairo_set_source_rgb (cr, 0, 0, 0);cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);cairo_fill (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->playbin, 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, "draw", G_CALLBACK (draw_cb), data);play_button = gtk_button_new_from_icon_name ("media-playback-start", GTK_ICON_SIZE_SMALL_TOOLBAR);g_signal_connect (G_OBJECT (play_button), "clicked", G_CALLBACK (play_cb), data);pause_button = gtk_button_new_from_icon_name ("media-playback-pause", GTK_ICON_SIZE_SMALL_TOOLBAR);g_signal_connect (G_OBJECT (pause_button), "clicked", G_CALLBACK (pause_cb), data);stop_button = gtk_button_new_from_icon_name ("media-playback-stop", GTK_ICON_SIZE_SMALL_TOOLBAR);g_signal_connect (G_OBJECT (stop_button), "clicked", G_CALLBACK (stop_cb), data);data->slider = gtk_scale_new_with_range (GTK_ORIENTATION_HORIZONTAL, 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_box_new (GTK_ORIENTATION_HORIZONTAL, 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_box_new (GTK_ORIENTATION_HORIZONTAL, 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_box_new (GTK_ORIENTATION_VERTICAL, 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) {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->playbin, GST_FORMAT_TIME, &data->duration)) {g_printerr ("Could not query current duration.\n");} 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->playbin, GST_FORMAT_TIME, &current)) {/* 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 *playbin, 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 (playbin,gst_message_new_application (GST_OBJECT (playbin),gst_structure_new_empty ("tags-changed")));
}/* 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\n", GST_OBJECT_NAME (msg->src), err->message);g_printerr ("Debugging information: %s\n", 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->playbin, 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.\n");gst_element_set_state (data->playbin, 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->playbin)) {data->state = new_state;g_print ("State set to %s\n", 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->playbin, "n-video", &n_video, NULL);g_object_get (data->playbin, "n-audio", &n_audio, NULL);g_object_get (data->playbin, "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->playbin, "get-video-tags", i, &tags);if (tags) {total_str = g_strdup_printf ("video stream %d:\n", 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\n", 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->playbin, "get-audio-tags", i, &tags);if (tags) {total_str = g_strdup_printf ("\naudio stream %d:\n", 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\n", 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\n", 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\n", 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->playbin, "get-text-tags", i, &tags);if (tags) {total_str = g_strdup_printf ("\nsubtitle stream %d:\n", 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\n", 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 (gst_message_get_structure (msg)), "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, char *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.playbin = gst_element_factory_make ("playbin", "playbin");if (!data.playbin) {g_printerr ("Not all elements could be created.\n");return -1;}/* Set the URI to play */g_object_set (data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);/* Connect to interesting signals in playbin */g_signal_connect (G_OBJECT (data.playbin), "video-tags-changed", (GCallback) tags_cb, &data);g_signal_connect (G_OBJECT (data.playbin), "audio-tags-changed", (GCallback) tags_cb, &data);g_signal_connect (G_OBJECT (data.playbin), "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.playbin);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.playbin, GST_STATE_PLAYING);if (ret == GST_STATE_CHANGE_FAILURE) {g_printerr ("Unable to set the pipeline to the playing state.\n");gst_object_unref (data.playbin);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.playbin, GST_STATE_NULL);gst_object_unref (data.playbin);return 0;
}

  Linux平台编译命令:gcc basic-tutorial-5.c -o basic-tutorial-5 `pkg-config --cflags --libs gstreamer-video-1.0 gtk±3.0 gstreamer-1.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/gdkquartzwindow.h>
#endif

  第一件值得关注的事情是我们不再是完全的平台独立的了。为了我们使用的窗口系统,我们需要包含合适的GDK头文件。好消息是,并没有那么多支持的窗口系统,所以三行就足够了:X11对Linux,Win32对Windows,Quartz对Mac OSX。

  本教程主要由回调函数组成,这些回调函数被GStreamer或者GTK+调用,所以我们来看看main函数,在这之中注册了所有的回调函数。

 */gst_init (&argc, &argv);/* Initialize our data structure */memset (&data, 0, sizeof (data));data.duration = GST_CLOCK_TIME_NONE;/* Create the elements */data.playbin = gst_element_factory_make ("playbin", "playbin");if (!data.playbin) {g_printerr ("Not all elements could be created.\n");return -1;}/* Set the URI to play */g_object_set (data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);

  标准的GStreamer初始化以及playbin管道的构建,还有GTK+的初始化。

  /* Connect to interesting signals in playbin */g_signal_connect (G_OBJECT (data.playbin), "video-tags-changed", (GCallback) tags_cb, &data);g_signal_connect (G_OBJECT (data.playbin), "audio-tags-changed", (GCallback) tags_cb, &data);g_signal_connect (G_OBJECT (data.playbin), "text-tags-changed", (GCallback) tags_cb, &data);

  我们对流中出现新标签(metadata)提示感兴趣。为了简化,我们在回调函数tags_cb将会处理所有种类的标签(视频,音频以及文本)。

/* Create the GUI */
create_ui (&data);

  所有GTK+组件创建爱你和信号注册都在这个函数里 。它仅包含了GTK相关的函数调用,所以我们会跳过它的定义。它所注册的信号传递用户的命令,就像下文所展示的那样。

  /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */bus = gst_element_get_bus (data.playbin);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);

  在 Playback tutorial 1: Playbin usage中,gst_bus_add_watch()用来注册一个接收所有发送给GStreamer总线的消息的函数。我们可以使用信号来实现更细粒度的控制,我们可以我们可以只对我们感兴趣的消息注册回调。通过调用gst_bus_add_signal_watch()我们指示总线每当接收到消息就发送信号。名为“message::detail”的信号在消息为“detail”时被触发。例如,当总线接收到EOS消息,它就发送一个名为“message::eos”的信号。

  本教程利用信号的细节来仅仅只注册我们关心的消息。如果我们呢已经注册了一个消息的信号,我们会得到每个同类消息被发送的提醒,就像gst_bus_add_watch()做的那样。

  需要牢记的是,为了让总线监视器工作(无论是gst_bus_add_watch()还是gst_bus_add_signal_watch()),必须要有GLib的Main Loop在运行。本案例中,Main Loop被隐藏在了GTK+的主循环中。

/* Register a function that GLib will call every second */
g_timeout_add_seconds (1, (GSourceFunc)refresh_ui, &data);

  在将控制权转移给GTK+之前,我们使用g_timeout_add_seconds ()注册另一个回调,这次有一个超时,它会每秒调用一次回调。我们用这个来调用refresh_ui函数以刷新GUI。

  在这之后,我们结束了设置并且可以开始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 VideoOverlay 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 GstVideoOverlay!");/* 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 playbin, which implements VideoOverlay and will forward it to the video sink */gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (data->playbin), window_handle);
}

  代码注释就足以说明。应用的生命周期到了这个时候,我们知道了GStreamer所要播放视频的窗口的句柄(无论是X11的XID,Windows的HWND,还是Quartz的NSView)。我们就是从窗口系统获取它,并通过GstVideoOverlay的接口gst_video_overlay_set_window_handle()将其传递给playbin。playbin会定位视频槽并将句柄传递给它,这样playbin就不会创建一个新的窗口而是使用句柄对应的窗口。

  这里没有更多的东西了,playbin和GstVideoOverlay将这个过程大大简化了!

/* This function is called when the PLAY button is clicked */
static void play_cb (GtkButton *button, CustomData *data) {gst_element_set_state (data->playbin, 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->playbin, 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->playbin, GST_STATE_READY);
}

  这三个小回调与GUI中的PLAY,PAUSED和STOP关联起来了。它们就是将管道设置为对应的状态。注意在STOP状态我们将管道设置为READY。我们完全可以将管道直接设置为NULL状态,但是这过渡阶段比较慢,因为某些资源(例如音频设备)会需要释放和重新获取。

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

  gtk_main_quit()最终会在main中调用gtk_main_run()以结束,在本例中,这会结束程序。在这里,我们在关闭主窗口的时候调用它,在停止管道后(为了清理干净)。

/* 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 draw_cb (GtkWidget *widget, cairo_t *cr, CustomData *data) {if (data->state < GST_STATE_PAUSED) {GtkAllocation allocation;/* 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);cairo_set_source_rgb (cr, 0, 0, 0);cairo_rectangle (cr, 0, 0, allocation.width, allocation.height);cairo_fill (cr);}return FALSE;
}

  当有数据流的时候(PAUSED和PLAYING状态)视频槽会负责刷新视频窗口的内容。其他情况下,则不会,所以我们必须自己做。本例中,我们用黑色填充窗口。

/* 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->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT,(gint64)(value * GST_SECOND));
}

  这里是一个例子,讲述了如何通过GStreamer和GTK+的写作简单地部署一个复杂的GUI元件像是滑动条。如果滑动条被拖到一个新的位置,则要告诉GStreamer通过gst_element_seek_simple()寻找位置(可见 Basic tutorial 4: Time management)。这个已经设置为其值单位为秒。

  值得关注的是,可以通过一些节流操作来获取更好的性能及响应,就是不会对每个用户的请求进行响应。由于查找操作必然会耗费一些时间,通常来说在一个查找操作后最好等待比如说半秒再允许查找。否则,当用户疯狂拖动滑动条的时候应用可能会没有反应,这会在新的查找操作进入队列前不能完成任何查找操作。

/* This function is called periodically to refresh the GUI */
static gboolean refresh_ui (CustomData *data) {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;

  这个函数会移动滑动条来反映媒体当前的位置。开始时,我们不在PLAYING状态,我们不做任何操作(位置和长度查询通常会失败)。

/* 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->playbin, GST_FORMAT_TIME, &data->duration)) {g_printerr ("Could not query current duration.\n");} 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->playbin, GST_FORMAT_TIME, &current)) {/* 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;

  我们查询当前管道的位置,并且相应地设置滑块的位置。这会触发“value-changed”信号的发送,这是我们用来发现用户拖动滑块的信号。由于我们不想让查询操作在用户没有要求的情况下发生,所以我们在这个过程中用g_signal_handler_block()取消“value-changed”的发送并用g_signal_handler_unblock()恢复。

  回调返回TRUE可以保证它在将来能够被调用。如果返回FALSE,那么计时器会被移除。

/* This function is called when new metadata is discovered in the stream */
static void tags_cb (GstElement *playbin, 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 (playbin,gst_message_new_application (GST_OBJECT (playbin),gst_structure_new_empty ("tags-changed")));
}

  这是本教程的关键点。当在媒体中发现新标签的时候就会调用该函数,在流线程中,就是说,从一个不是应用(或主)线程我们在这里想做的是更新一个GTK+组件来反映新的信息。但是GTK+不允许操作在主线程之外的线程中执行。

  解决方法是playbin向总线发送一个消息,然后返回调用线程。当合适的时候,主线程会接收这个消息并更新GTK。

  gst_element_post_message()让一个GStreamer元件向总线发送给定的消息。gst_message_new_application()创建一个APPLICATION类型的新消息。GStreaemer消息有不同的类型,而这个类型就是为应用而保留的:它会不受GStreamer影响并通过总线。类型的列表可以在GstMessageType文档中找到。

  消息可以携带额外的信息通过它们嵌入的GstStructure,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 (gst_message_get_structure (msg)), "tags-changed") == 0) {/* If the message is the "tags-changed" (only one we are currently issuing), update* the stream info GUI */analyze_streams (data);}
}

  一旦我们确认这是一个"tags-changed"消息,我们调用analyze_streams函数,这也在Playback tutorial 1: Playbin usage使用并且更详细地描述了。它主要从流中获取标签,并将其写入GUI文本组件。

  error_cb,error_cb,error_cb不值得详细说明,因为它们和之前的教程中做的差不多,不过是有了各自的函数。

  这就是全部了。本教程的代码量看起来有点吓人,但是涉及到的概念很少并且很简单。如果你看过了之前的教程并且对GTK有所了解,你会理解本教程,现在你可以看看属于你自己的媒体播放器了!

总结
  本教程展示了:

  • 如何使用gst_video_overlay_set_window_handle()实现向特定窗口输出视频。
  • 如何使用g_timeout_add_seconds ()注册一个超时回调来定时刷新GUI。
  • 如何通过用gst_element_post_message()和应用消息来向主线程中传递信息。
  • 怎么用gst_bus_add_signal_watch()来发送信号使得我们能够被提示感兴趣的消息,并且使用消息细节从所有消息种类中识别我们需要的消息。

GStreamer官方教程系列——Basic tutorial 5: GUI toolkit integration相关推荐

  1. 帝国cms【官方教程系列教程一】 首页模板制作

    帝国cms首页模板是指网站首页的模板.(/index.html) 修改首页模板 1.登录后台,单击"模板"菜单,选择"首页模板"子菜单,进入修改首页模板界面: ...

  2. 《SPARK官方教程系列》(标贝科技)

    1-概述 Apache Spark是一个用于大规模数据处理的统一分析引擎,它在Java.Scala.Python和R中的提供了高级api,以及一个支持通用执行图[general execution g ...

  3. GStreamer Tutorial 中文翻译:Basic tutorial 3: Dynamic pipelines

    GStreamer Tutorial 3中文翻译 文章目录 GStreamer Tutorial 3中文翻译 前言 [Basic tutorial 3: Dynamic pipelines](http ...

  4. socket 获取回传信息_Luat系列官方教程5:Socket代码详解

    文章篇幅较长,代码部分建议横屏查看,或在PC端打开本文链接.文末依然为爱学习的你准备了专属福利~ TCP和UDP除了在Lua代码声明时有一些不同,其他地方完全一样,所以下面的代码将以TCP长连接的数据 ...

  5. 【Flask】官方教程(Tutorial)-part2:蓝图-视图、模板、静态文件

    前序文章: 官方教程(Tutorial)-part1:项目布局.应用程序设置.定义和访问数据库 蓝图-视图 视图函数是您为响应应用程序请求而编写的代码.Flask 使用模式将传入的请求 URL 与应该 ...

  6. 【Flask】官方教程(Tutorial)-part4(完结)测试覆盖度、部署到生产、持续开发

    前序文章: 官方教程(Tutorial)-part1:项目布局.应用程序设置.定义和访问数据库 官方教程(Tutorial)-part2:蓝图-视图.模板.静态文件 官方教程(Tutorial)-pa ...

  7. Caffe官方教程翻译(1):LeNet MNIST Tutorial

    前言 最近打算重新跟着官方教程学习一下caffe,顺便也自己翻译了一下官方的文档.自己也做了一些标注,都用斜体标记出来了.中间可能额外还加了自己遇到的问题或是运行结果之类的.欢迎交流指正,拒绝喷子! ...

  8. 《SteamVR2.2.0官方教程(一)》(Yanlz+Unity+XR+VR+AR+MR+SteamVR+Valve+Oculus+Tutorials+Interaction+立钻哥哥++ok++)

    <SteamVR2.2.0官方教程> <SteamVR2.2.0官方教程> 版本 作者 参与者 完成日期 备注 SteamVR2.2.0_Tutorials_V01_1.0 严 ...

  9. TensorFlow2.0 Guide官方教程 学习笔记17 -‘Using the SavedModel format‘

    本笔记参照TensorFlow官方教程,主要是对'Save a model-Training checkpoints'教程内容翻译和内容结构编排,原文链接:Using the SavedModel f ...

最新文章

  1. 第1关:学习-用循环和数组实现输入某年某月某日,判断这一天一年的第几天
  2. 【干货】优秀的移动客户端 Web App设计,让用户体验飞起来
  3. 【CyberSecurityLearning 47】PHP 数组
  4. ubuntu安装python库_Ubuntu18.04一次性升级Python所有库的方法步骤
  5. matlab学习:人脸识别之LBP (Local Binary Pattern)
  6. DB2的ErrorCode
  7. ✨Synchronized底层实现---偏向锁
  8. bootstrap获取弹框数据_execl基础-分类汇总与数据有效性验证
  9. 阿里云服务器上单机部署大数据开发环境(hadoop2.6-cdh5.8.0系列)
  10. Linux环境搭建 | 全能终端神器——MobaXterm
  11. VMware vSphere Client5.0与 Windows8不再有问题,解决VMware 5.0 客户端提示VMRC控制台的连接已断开
  12. 开发日记(01) - uni-app 使用等宽字体对齐数字宽度
  13. mfc使用matlab绘图,mfc调用matlab绘图
  14. 软件可维护性测试方法,软件可维护性
  15. 零基础学习3d建模需要多久?
  16. 【Pandas实战】1000部流行电影数据分析
  17. ReentrantReadWriteLock、StampedLock
  18. 设置浏览器为单进程模式
  19. 手机版本android升级包下载,ColorOS8.0升级包下载安装-oppo手机系统ColorOS8.0正式版升级包下载 安卓版 v1.0- 游娱下载站...
  20. 【Spark NLP】第 8 章:使用 Keras 进行序列建模

热门文章

  1. 团体程序设计天梯赛-练习集 1-2 打台球(5 分)
  2. Android9.0 程序锁实现
  3. R语言 计算 赫芬达尔−赫希曼指数(HHI)
  4. d196g服务器无响应,幻影路由器D196G手机怎么设置? | 192路由网
  5. 2021-2027全球与中国私人游艇市场现状及未来发展趋势
  6. Vue富文本编辑器vue-quill-editor-使用-bug问题-教程
  7. python编程是什么-Python编程
  8. apex库安装,解决amp not installed 报错( ModuleNotFoundError: No module named ‘apex‘)
  9. 用echarts 中的地图来显示区域分布情况(vue),包括地理数据来源生成的制作注意事项
  10. dispatch_source