【Gstreamer 18】播放教程2:字幕管理

发布于:2025-03-26 ⋅ 阅读:(24) ⋅ 点赞:(0)

官网地址

Playback tutorial 2: Subtitle management

一、目标

     本教程与上一个教程非常相似,但这次我们将使用字幕流而不是音频流。通过本教程,我们将学习:

  • 如何选择字幕流
  • 如何添加外部字幕
  • 如何自定义字幕的字体

二、介绍

       我们已经知道(从上一个教程中),容器文件可以包含多个音频和视频流,并且我们可以通过更改 playbin 的 current-audio 或 current-video 属性轻松地在它们之间切换。切换字幕同样简单。

        值得注意的是,就像音频和视频一样,playbin 会负责为字幕选择合适的解码器,而 GStreamer 的插件结构使得添加对新格式的支持就像复制文件一样简单。这一切对应用程序开发者来说都是透明的。

        除了嵌入在容器中的字幕,playbin 还提供了从外部 URI 添加额外字幕流的功能。

        本教程将打开一个已经包含 5 个字幕流的文件,并添加另一个来自外部文件的字幕流(用于希腊语)。

三、带字幕的多语言播放器

        将以下代码复制到一个名为 playback-tutorial-2.c 的文本文件中(或在 GStreamer 安装目录中找到它)。

playback-tutorial-2.c

#include <stdio.h>
#include <gst/gst.h>

/* Structure to contain all our information, so we can pass it around */
typedef struct _CustomData {
  GstElement *playbin;  /* Our one and only element */

  gint n_video;          /* Number of embedded video streams */
  gint n_audio;          /* Number of embedded audio streams */
  gint n_text;           /* Number of embedded subtitle streams */

  gint current_video;    /* Currently playing video stream */
  gint current_audio;    /* Currently playing audio stream */
  gint current_text;     /* Currently playing subtitle stream */

  GMainLoop *main_loop;  /* GLib's Main Loop */
} CustomData;

/* playbin flags */
typedef enum {
  GST_PLAY_FLAG_VIDEO         = (1 << 0), /* We want video output */
  GST_PLAY_FLAG_AUDIO         = (1 << 1), /* We want audio output */
  GST_PLAY_FLAG_TEXT          = (1 << 2)  /* We want subtitle output */
} GstPlayFlags;

/* Forward definition for the message and keyboard processing functions */
static gboolean handle_message (GstBus *bus, GstMessage *msg, CustomData *data);
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data);

int main(int argc, char *argv[]) {
  CustomData data;
  GstBus *bus;
  GstStateChangeReturn ret;
  gint flags;
  GIOChannel *io_stdin;

  /* Initialize GStreamer */
  gst_init (&argc, &argv);

  /* 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://gstreamer.freedesktop.org/data/media/sintel_trailer-480p.ogv", NULL);

  /* Set the subtitle URI to play and some font description */
  g_object_set (data.playbin, "suburi", "https://gstreamer.freedesktop.org/data/media/sintel_trailer_gr.srt", NULL);
  g_object_set (data.playbin, "subtitle-font-desc", "Sans, 18", NULL);

  /* Set flags to show Audio, Video and Subtitles */
  g_object_get (data.playbin, "flags", &flags, NULL);
  flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
  g_object_set (data.playbin, "flags", flags, NULL);

  /* Add a bus watch, so we get notified when a message arrives */
  bus = gst_element_get_bus (data.playbin);
  gst_bus_add_watch (bus, (GstBusFunc)handle_message, &data);

  /* Add a keyboard watch so we get notified of keystrokes */
#ifdef G_OS_WIN32
  io_stdin = g_io_channel_win32_new_fd (fileno (stdin));
#else
  io_stdin = g_io_channel_unix_new (fileno (stdin));
#endif
  g_io_add_watch (io_stdin, G_IO_IN, (GIOFunc)handle_keyboard, &data);

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

  /* Create a GLib Main Loop and set it to run */
  data.main_loop = g_main_loop_new (NULL, FALSE);
  g_main_loop_run (data.main_loop);

  /* Free resources */
  g_main_loop_unref (data.main_loop);
  g_io_channel_unref (io_stdin);
  gst_object_unref (bus);
  gst_element_set_state (data.playbin, GST_STATE_NULL);
  gst_object_unref (data.playbin);
  return 0;
}

/* Extract some metadata from the streams and print it on the screen */
static void analyze_streams (CustomData *data) {
  gint i;
  GstTagList *tags;
  gchar *str;
  guint rate;

  /* Read some properties */
  g_object_get (data->playbin, "n-video", &data->n_video, NULL);
  g_object_get (data->playbin, "n-audio", &data->n_audio, NULL);
  g_object_get (data->playbin, "n-text", &data->n_text, NULL);

  g_print ("%d video stream(s), %d audio stream(s), %d text stream(s)\n",
    data->n_video, data->n_audio, data->n_text);

  g_print ("\n");
  for (i = 0; i < data->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) {
      g_print ("video stream %d:\n", i);
      gst_tag_list_get_string (tags, GST_TAG_VIDEO_CODEC, &str);
      g_print ("  codec: %s\n", str ? str : "unknown");
      g_free (str);
      gst_tag_list_free (tags);
    }
  }

  g_print ("\n");
  for (i = 0; i < data->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) {
      g_print ("audio stream %d:\n", i);
      if (gst_tag_list_get_string (tags, GST_TAG_AUDIO_CODEC, &str)) {
        g_print ("  codec: %s\n", str);
        g_free (str);
      }
      if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
        g_print ("  language: %s\n", str);
        g_free (str);
      }
      if (gst_tag_list_get_uint (tags, GST_TAG_BITRATE, &rate)) {
        g_print ("  bitrate: %d\n", rate);
      }
      gst_tag_list_free (tags);
    }
  }

  g_print ("\n");
  for (i = 0; i < data->n_text; i++) {
    tags = NULL;
    /* Retrieve the stream's subtitle tags */
    g_print ("subtitle stream %d:\n", i);
    g_signal_emit_by_name (data->playbin, "get-text-tags", i, &tags);
    if (tags) {
      if (gst_tag_list_get_string (tags, GST_TAG_LANGUAGE_CODE, &str)) {
        g_print ("  language: %s\n", str);
        g_free (str);
      }
      gst_tag_list_free (tags);
    } else {
      g_print ("  no tags found\n");
    }
  }

  g_object_get (data->playbin, "current-video", &data->current_video, NULL);
  g_object_get (data->playbin, "current-audio", &data->current_audio, NULL);
  g_object_get (data->playbin, "current-text", &data->current_text, NULL);

  g_print ("\n");
  g_print ("Currently playing video stream %d, audio stream %d and subtitle stream %d\n",
      data->current_video, data->current_audio, data->current_text);
  g_print ("Type any number and hit ENTER to select a different subtitle stream\n");
}

/* Process messages from GStreamer */
static gboolean handle_message (GstBus *bus, GstMessage *msg, CustomData *data) {
  GError *err;
  gchar *debug_info;

  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_ERROR:
      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);
      g_main_loop_quit (data->main_loop);
      break;
    case GST_MESSAGE_EOS:
      g_print ("End-Of-Stream reached.\n");
      g_main_loop_quit (data->main_loop);
      break;
    case GST_MESSAGE_STATE_CHANGED: {
      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)) {
        if (new_state == GST_STATE_PLAYING) {
          /* Once we are in the playing state, analyze the streams */
          analyze_streams (data);
        }
      }
    } break;
  }

  /* We want to keep receiving messages */
  return TRUE;
}

/* Process keyboard input */
static gboolean handle_keyboard (GIOChannel *source, GIOCondition cond, CustomData *data) {
  gchar *str = NULL;

  if (g_io_channel_read_line (source, &str, NULL, NULL, NULL) == G_IO_STATUS_NORMAL) {
    int index = atoi (str);
    if (index < 0 || index >= data->n_text) {
      g_printerr ("Index out of bounds\n");
    } else {
      /* If the input was a valid subtitle stream index, set the current subtitle stream */
      g_print ("Setting current subtitle stream to %d\n", index);
      g_object_set (data->playbin, "current-text", index, NULL);
    }
  }
  g_free (str);
  return TRUE;
}

Linux 安装库(Install GStreamer on Ubuntu or Debian)

apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio

其他的系统看官网教程。

Installing on Mac OS X

执行编译

gcc playback-tutorial-2.c -o playback-tutorial-2 `pkg-config --cflags --libs gstreamer-1.0`

必需的库:gstreamer-1.0

四、教程解析

       本教程是从 播放教程1:Playbin使用 复制而来,并做了一些修改。因此,我们只回顾修改的部分。

/* Set the subtitle URI to play and some font description */
g_object_set (data.playbin, "suburi", "https://gstreamer.freedesktop.org/data/media/sintel_trailer_gr.srt", NULL);
g_object_set (data.playbin, "subtitle-font-desc", "Sans, 18", NULL);

       在设置媒体 URI 之后,我们设置了 suburi 属性,该属性指向包含字幕流的文件。在本例中,媒体文件已经包含多个字幕流,因此通过 suburi 提供的字幕流会被添加到列表中,并成为当前选中的字幕流。

      需要注意的是,关于字幕流的元数据(例如语言)存储在容器文件中,因此,未嵌入容器的字幕流将没有元数据。运行本教程时,你会发现第一个字幕流没有语言标签。

      subtitle-font-desc 属性允许指定用于渲染字幕的字体。由于 Pango 是用于渲染字体的库,你可以查阅其文档以了解如何指定字体,特别是 pango-font-description-from-string 函数。

       简而言之,字符串表示的格式为 [FAMILY-LIST] [STYLE-OPTIONS] [SIZE],其中: FAMILY-LIST 是一个以逗号分隔的字体家族列表,可选以逗号结尾。 STYLE_OPTIONS 是一个以空格分隔的单词列表,每个单词描述一种样式、变体、粗细或拉伸。 SIZE 是一个十进制数字(以磅为单位的字号)。例如,以下都是有效的字符串表示:

  • sans bold 12
  • serif, monospace bold italic condensed 16
  • normal 10

      常见的字体家族有:Normal、Sans、Serif 和 Monospace。

      可用的样式有:Normal(字体直立)、Oblique(字体倾斜,但为罗马风格)、Italic(字体倾斜,为斜体风格)。

      可用的粗细有:Ultra-Light、Light、Normal、Bold、Ultra-Bold、Heavy。

      可用的变体有:Normal、Small_Caps(用小写字符替换为较小的大写字符变体)。

      可用的拉伸样式有:Ultra-Condensed、Extra-Condensed、Condensed、Semi-Condensed、Normal、Semi-Expanded、Expanded、Extra-Expanded、Ultra-Expanded。

/* Set flags to show Audio, Video and Subtitles */
g_object_get (data.playbin, "flags", &flags, NULL);
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
g_object_set (data.playbin, "flags", flags, NULL);

       我们设置了 flags 属性以允许显示音频、视频和文本(字幕)。

       本教程的其余部分与 播放教程1:Playbin使用 相同,只是键盘输入改为更改 current-text 属性,而不是 current-audio。和之前一样,请记住,流切换不是立即生效的,因为管道中有大量信息需要到达末端,新的流才会显示出来。

五、结论

本教程展示了如何处理来自 playbin 的字幕,无论它们是嵌入在容器中还是位于不同的文件中:

  • 使用 current-text 和 n-text 属性选择字幕。
  • 使用 suburi 属性选择外部字幕文件。
  • 使用 subtitle-font-desc 属性自定义字幕的外观。

下一个播放教程将展示如何更改播放速度。 请记住,本页附件中应包含教程的完整源代码以及构建所需的任何辅助文件。