GStreamer Basic tutorial 5 GUI Toolkit Integration

"GStreamer 基本教程5: GUI工具包集成"

Posted by Stephen on March 17, 2022

前言

本文是GStreamer学习笔记,也可以看成是对原文的意译。

这些教程描述了理解其余教程所需的GStreamer主题。

GStreamer教程:

基础教程 : GStreamer 介绍

基础教程 1: Hello world!

基础教程 2: GStreamer 概念

基础教程 3: 动态管道

基础教程 4: 时间管理

基础教程 5: GUI工具包集成

基础教程 6: 媒体格式和pad功能

基础教程 7: 多线程和Pad可用性

基础教程 8: 管道短路操作

基础教程 9: 媒体信息收集

基础教程 10: GStreamer工具

基础教程 11: 调试工具

基础教程 12: 流媒体

基础教程 13: 播放速度

基础教程 14: 有用的元素

基础教程 16: 特定平台元素

环境

系统环境

Distributor ID:	Ubuntu
Description:	Ubuntu 18.04.4 LTS
Release:	18.04
Codename:	bionic
Linux version :       5.3.0-46-generic ( buildd@lcy01-amd64-013 ) 
Gcc version:         7.5.0  ( Ubuntu 7.5.0-3ubuntu1~18.04 )

软件信息

version : 	
     GStreamer-1.0

正文

1. 目标

本教程展示了如何将 GStreamer 集成到Gstreamer ToolKit GTK+等图形用户界面 (GUI) 工具包中。基本上,GStreamer 负责媒体播放,而 GUI 工具包处理用户交互。最有趣的部分是两个库必须交互的部分:指示 GStreamer 将视频输出到 GTK+ 窗口和将用户操作转发到 GStreamer。

特别是,您将学习:

  • 如何告诉 GStreamer 将视频输出到特定窗口(而不是创建自己的窗口)。
  • 如何使用来自 GStreamer 的信息不断刷新 GUI。
  • 如何从 GStreamer 的多个线程更新 GUI,这是大多数 GUI 工具包上禁止的操作。
  • 一种仅订阅您感兴趣的消息的机制,而不是收到所有消息的通知。

2. 介绍

我们将使用 GTK+工具包构建媒体播放器,但这些概念适用于其他工具包,例如Qt对GTK+有最低限度的了解将有助于理解本教程。

要点是告诉 GStreamer 将视频输出到我们选择的窗口。具体机制取决于操作系统(或者更确切地说,取决于窗口系统),但 GStreamer 提供了一个抽象层以实现平台独立性。这种独立性来自GstVideoOverlay接口,它允许应用程序告诉视频接收器应该接收渲染的窗口的处理程序。

GObject 接口

GObject 接口(GStreamer使用)是元素可以实现的一组功能。如果支持,则表示支持该特定接口。例如,视频接收器通常会创建自己的窗口来显示视频,但如果它们也能够渲染到外部窗口,则可以选择实现GstVideoOverlay接口并提供指定此外部窗口的函数。从应用程序开发人员的角度来看,如果支持某个接口,您可以使用它而忘记实现它的元素类型。而且,如果你在使用playbin,它会自动暴露其内部元素所支持的一些接口:你可以直接在上面使用你的接口函数,playbin而不用知道是谁在实现它们!

另一个问题是 GUI 工具包通常只允许通过主(或应用程序)线程来操作图形“小部件”,而 GStreamer 通常会产生多个线程来处理不同的任务。从回调中调用GTK+函数通常会失败,因为回调在调用线程中执行,而调用线程不需要是主线程。这个问题可以通过在回调中在 GStreamer 总线上发布消息来解决:消息将被主线程接收,然后做出相应的反应。

最后,到目前为止,我们已经注册了一个handle_message函数,每次总线上出现消息时都会调用该函数,这迫使我们解析每条消息以查看它是否对我们感兴趣。在本教程中,使用了一种不同的方法,为每种消息注册一个回调,因此解析和整体代码更少。

3. GTK+中的媒体播发器

编写一个非常简单的基于 playbin 的媒体播放器,这次是带有 GUI 的!

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 position, 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;
   }

编译运行

   gcc basic-tutorial-5.c -o basic-tutorial-5 `pkg-config --cflags --libs gstreamer-video-1.0 gtk+-3.0 gstreamer-1.0`
   ./basic-tutorial-5

本教程打开一个 GTK+ 窗口并显示一部电影,并附带音频。媒体是从 Internet 获取的,因此窗口可能需要几秒钟才能出现,具体取决于您的连接速度。窗口有一些 GTK+ 按钮来暂停、停止和播放电影,还有一个滑块来显示流的当前位置,可以拖动它来改变它。此外,有关流的信息显示在窗口右边缘的列上。

请记住,没有延迟管理(缓冲),因此在连接速度较慢的情况下,电影可能会在几秒钟后停止。了解基础教程 12: 流媒体如何解决此问题。

所需库:gstreamer-video-1.0 gtk+-3.0 gstreamer-1.0

4. 演练

关于本教程的结构,我们将不再使用前向函数定义:函数将在使用之前定义。此外,为了解释清楚,呈现代码片段的顺序并不总是与程序顺序相匹配。使用行号来定位完整代码中的片段。

   #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 头文件。幸运的是,支持的窗口系统并不多,因此这三行通常就足够了:Linux 的 X11、Windows 的 Win32 和 Mac OSX 的 Quartz。 本教程主要由回调函数组成,将从 GStreamer 或 GTK+ 调用,因此让我们回顾一下main注册所有这些回调的函数

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

标准 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);

我们有兴趣在流中出现新标签(元数据)时收到通知。为简单起见,我们将从同一个回调中处理各种标签(视频、音频和文本)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);

播放教程 1:Playbin 使用中,gst_bus_add_watch()用于注册一个函数,该函数接收发布到 GStreamer 总线的每条消息。我们可以通过使用信号来实现更精细的粒度,这允许我们只注册我们感兴趣的消息。通过调用gst_bus_add_signal_watch()我们指示总线在每次接收到消息时发出一个信号。该信号的名称为message::detailwheredetail是触发信号发射的消息。例如,当总线接收到 EOS 消息时,它会发出一个名为 的信号message::eos

本教程使用Signals’s details 仅注册我们关心的消息。如果我们已经注册到message信号,我们会收到每条消息的通知,就像 gst_bus_add_watch()会做的那样。

请记住,为了使总线查看工作(无论是 gst_bus_add_watch()还是 gst_bus_add_signal_watch()),必须GLib运行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’s XID、 Window’s HWND还是 Quartz’s NSView)。我们只需从窗口系统中检索它playbinGstVideoOverlay使用 gst_video_overlay_set_window_handle(). 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、PAUSE 和 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()最终将调用 to gtk_main_run() inmain来终止,在这种情况下,结束程序。在这里,我们在主窗口关闭时调用它,在停止管道之后(只是为了整洁)。

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

当有数据流(PAUSEDPLAYING状态)时,视频接收器负责刷新视频窗口的内容。然而,在其他情况下,它不会,所以我们必须这样做。在这个例子中,我们只是用一个黑色矩形填充窗口。

   /* 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()(如基础教程 4:时间管理中所示)。滑块已设置,因此其值表示秒。

值得一提的是,通过进行一些限制可以获得一些性能(和响应能力),即不响应每个用户的搜索请求。由于查找操作必然需要一些时间,因此通常最好在一次查找后等待半秒(例如)然后再允许另一次查找。否则,如果用户疯狂地拖动滑块,应用程序可能看起来没有响应,这将不允许任何搜索在新的排队之前完成。

   /* 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 position, 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,我们用它来了解用户何时拖动滑块。由于我们不希望搜索发生,除非用户请求它们,因此我们在此操作期间使用和 禁用value-changed信号发射。g_signal_handler_block()g_signal_handler_unblock()

从这个函数返回TRUE将在未来继续调用它。如果我们 return FALSE,计时器将被删除。

这是本教程的重点之一。当在媒体中找到新标签时,将调用此函数,来自流线程,即来自应用程序(或主)线程以外的线程。我们在这里要做的是更新一个 GTK+ 小部件以反映这一新信息,但GTK+ 不允许从除主线程之外的线程进行操作

解决方案是playbin在总线上发布消息并返回调用线程。在适当的时候,主线程将获取此消息并更新 GTK。

gst_element_post_message()使 GStreamer 元素将给定消息发布到总线。创建该类型gst_message_new_application()的新消息。APPLICATIONGStreamer 消息有不同的类型,并且这种特定类型是保留给应用程序的:它将通过不受 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 (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函数,该函数也在播放教程 1:Playbin 使用中使用,并且在那里更详细。它基本上从流中恢复标签并将它们写入 GUI 的文本小部件中。

和并不真正值得解释,因为它们与之前所有教程中的相同,但现在来自它们自己的error_cb功能eos_cbstate_changed_cb

就是这样!本教程中的代码量可能看起来令人生畏,但所需的概念很少而且很容易。如果您已经按照前面的教程进行操作,并且对 GTK 有一点了解,那么您可能已经明白这个现在可以享受您自己的媒体播放器了!

5. 练习

如果此媒体播放器对您来说不够好,请尝试将显示有关流的信息的文本小部件更改为适当的列表视图(或树视图)。然后,当用户选择不同的流时,让 GStreamer 切换流!要切换流,您需要阅读播放教程 1:Playbin 使用

6. 结论

本教程学习了:

  • 如何使用gst_video_overlay_set_window_handle()
  • 如何通过注册超时回调来定时刷新GUI g_timeout_add_seconds()
  • 如何通过总线将信息通过应用程序消息传递给主线程gst_element_post_message()
  • gst_bus_add_signal_watch()如何通过使总线发出信号并使用信号详细信息区分所有消息类型来仅通知感兴趣的消息。

这允许您使用适当的图形用户界面构建一个有点完整的媒体播放器。

后记