gstreamerでビデオ画像にテキストを動的に表示する

# 概要
[gstreamer](https://gstreamer.freedesktop.org/)はマルチメディアのデータを取り扱うオープンソースのソフトです。ビデオ編集等のアプリが手軽に作れます。gstreamerには各種用途に対応した沢山のプラグインが用意されています。[textoverlay](https://gstreamer.freedesktop.org/documentation/pango/textoverlay.html?gi-language=c)はそのひとつで、ビデオ画面にテキストを表示することができます。
textoverlayのパラメータでtext="Room A"とすると、ビデオ画像に常時Room Aという文字が表示されますので、そのビデオ画像が何なのかとかの表示に使えます。また、[subparse](https://gstreamer.freedesktop.org/documentation/subparse/subparse.html?gi-language=c)と組み合わせるとビデオ画像に字幕を表示することができます。subparseが字幕データからテキストストリームを作成してタイミング情報とともにtextoverlayに渡すことで字幕がビデオ画像に組み込まれるといった仕組みになっているようです。
この投稿にはこのtextoverlayを用いてビデオ画像にテキストを動的に表示することを試みた結果を記述しています。最初に結論を申し上げますと、疑問がありながらも出来ましたといった感じです。疑問が残った部分はこれから勉強をしながら克服してゆくつもりです。
# パイプラインの構成
さて、ビデオ画像に動的にテキストストリームを組み込むとなるとsubparseにあたる機能が必要です。それを開発するとなると結構大掛かりな作業になってしまいます。簡単に出来る方法がないだろうかと探していると[appsrc](https://gstreamer.freedesktop.org/documentation/app/appsrc.html?gi-language=c)がありました。
appsrcはパイプラインの入力となるソースをプログラムで作り出すプラグインです。そのサンプルコードがgstreamerの「[Basic tutorial 8](https://gstreamer.freedesktop.org/documentation/tutorials/basic/short-cutting-the-pipeline.html?gi-language=c)」にあります。Basic tutorial 8ではサウンドストリームを生成していますが、それを参考にしてテキストストリームを生成する方法に変更してみました。テキストストリームのもとになるデータはテキストファイルとしました。つまり、ビデオを表示中にテキストファイルを変更するとその時から内容が画像に表示されるという仕組みです。

パイプラインは次の図のような構成になっています。
![DynamicTextOverlay - pipeline.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2736323/d357bf53-ecd0-2c92-d796-45826da33ac2.jpeg)
# テスト環境
筆者のテスト環境は以下の通りです。
ハード
- Thinkpad X60（CPU:T2300 1.67GHz RAM:1GB HDD:5400PRM 3.0Gb/sec）

ソフト
- Debian Version 11.3
- gstreamer version 1.18.4
- Debian GLIBC 2.31-13+deb11u3
# ソース
ソースコードをご覧下さい。
```
#include <gst/gst.h>
#include <string.h>
#include <stdio.h>

#define MSG_SIZE 50       // ビデオ画面に表示する文字数

/* Structure to contain all our information, so we can pass it to callbacks */
typedef struct _CustomData {
  GstElement *pipeline;
  GstElement *source1;    // エレメント：appsrc
  GstElement *source2;    // エレメント：videotestsrc
  GstElement *filter1;    // エレメント：textoverlay
  GstElement *filter2;    // エレメント：capsfilter
  GstElement *convert;    // エレメント：videoconvert
  GstElement *sink;       // エレメント：ximagesink

  guint64 num_samples;   /* Number of samples generated so far (for timestamp generation) */
  gint num_called;        // start_feed が呼ばれた回数
  gchar *umsg;            // ビデオ画面に表示する文字

  guint sourceid;        /* To control the GSource */

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

/* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
 * The idle handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
 * and is removed when appsrc has enough data (enough-data signal).
 */
static gboolean push_data (CustomData *data) {
  GstBuffer *buffer;
  GstFlowReturn ret;
  int i;
  GstMapInfo map;
  gchar *usermsg;
  gint  *msgsize;

  gint num_samples = MSG_SIZE;    // 画面に表示する文字のサイズ

  /* Create a new empty buffer */
  buffer = gst_buffer_new_and_alloc (MSG_SIZE);    // バッファーサイズ = 表示する文字のサイズ

  /* Set its timestamp and duration */
  GST_BUFFER_TIMESTAMP (buffer) = gst_util_uint64_scale (data->num_samples, GST_SECOND, 100000);    // これは適当です
  GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale (num_samples, GST_SECOND, 100000);    // これも適当です

  gst_buffer_map (buffer, &map, GST_MAP_WRITE);    // バッファをマッピング

  /* Put user message on buffer */
  usermsg = (gchar *)map.data;
  for (i = 0; i < num_samples; i++) {
    usermsg[i] = data->umsg[i];    // 文字情報をバッファに書き込む
  }

  /* release the buffer memory */
  gst_buffer_unmap (buffer, &map);
  /* count up samples */
  data->num_samples += num_samples;

  /* Push the buffer into the appsrc */
  g_signal_emit_by_name (data->source1, "push-buffer", buffer, &ret);

  /* Free the buffer now that we are done with it */
  gst_buffer_unref (buffer);

  if (ret != GST_FLOW_OK) {
    /* We got some error, stop sending data */
    return FALSE;
  }

  return TRUE;
}

/* This signal callback triggers when appsrc needs data. Here, we add an idle handler
 * to the mainloop to start pushing data into the appsrc */
static void start_feed (GstElement *source, guint size, CustomData *data) {
  if (data->sourceid == 0) {

// 表示する文字データをファイル「message.txt」から読み込む
    FILE *fp;
    char fname[] = "message.txt";
    char str1[MSG_SIZE+8];    // 何故か8文字分を加えなければならない。これをしないと文字化けが発生する
    char str2[MSG_SIZE+8] = "file read error.";    // ファイル読み込みでエラー発生時に表示する文字
    int i, j;
    fp = fopen(fname, "r");
    if(fp != NULL) {
      str2[0] = '\0';
      fgets(str1, MSG_SIZE, fp);
      i = strlen(str1);
      j = 0;
      for (i=0; i<MSG_SIZE; i++)  {
        if (str1[i] == '\0' || j == 1) {
          str2[i] = ' ';
          j = 1;
        } else {
          str2[i] = str1[i];
        }
      }
    }
    fclose(fp);

// 表示する文字をセット
    str2[MSG_SIZE+1] = '\0';
    data->umsg = str2;

    data->num_called++;
    g_print ("num_called=%d\n", data->num_called);    // デバッグ用メッセージ
    g_print ("data:%s\n", data->umsg);  // *
    g_print ("Start feeding\n");    // *
    data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
  }
}

/* This callback triggers when appsrc has enough data and we can stop sending.
 * We remove the idle handler from the mainloop */
static void stop_feed (GstElement *source, CustomData *data) {
  if (data->sourceid != 0) {
    g_print ("Stop feeding\n");
    g_source_remove (data->sourceid);
    data->sourceid = 0;
  }
}

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

  g_main_loop_quit (data->main_loop);
}
//************************
//*****     main     *****
//************************
int main(int argc, char *argv[]) {
  CustomData data;
  GstBus *bus;
  GstCaps *caps;
  GstCaps *text_caps;
  GstMessage *msg;
  GstStateChangeReturn ret;
  gboolean terminate = FALSE;

  gchar *usermsg;    // ビデオ画像に表示するメッセージの実際のエリア

  /* Initialize custom data structure */
  memset (&data, 0, sizeof (data));

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

  /* Create the elements */
  data.source1  = gst_element_factory_make ("appsrc", "source1");
  data.source2  = gst_element_factory_make ("videotestsrc", "source2");
  data.filter1 = gst_element_factory_make ("textoverlay", "filter2");
  data.filter2 = gst_element_factory_make ("capsfilter", "filter3");
  data.convert = gst_element_factory_make ("videoconvert", "convert");
  data.sink    = gst_element_factory_make ("ximagesink", "sink");

  /* Create the empty pipeline */
  data.pipeline = gst_pipeline_new ("test-pipeline");

  if (!data.pipeline || !data.source1 || !data.source2 || !data.filter1 || !data.filter2 || !data.convert || !data.sink) {
    g_printerr ("Not all elements could be created.\n");
    return -1;
  }

  // videotestsrcのcapsを作成
  caps = gst_caps_new_simple("video/x-raw", "width", G_TYPE_INT, 640, "height", G_TYPE_INT, 480, NULL);
  g_object_set (G_OBJECT (data.filter2), "caps", caps, NULL);
  gst_caps_unref (caps);

  /* Configure appsrc */
  text_caps = gst_caps_new_simple("text/x-raw", "format", G_TYPE_STRING, "utf8", NULL);
  g_object_set (data.source1, "caps", text_caps, "format", GST_FORMAT_TIME, NULL);
  g_signal_connect (data.source1, "need-data", G_CALLBACK (start_feed), &data);
  g_signal_connect (data.source1, "enough-data", G_CALLBACK (stop_feed), &data);

  // 全てのエレメントをパイプラインに加える
  gst_bin_add_many (GST_BIN (data.pipeline), data.source1, data.source2, data.filter1, data.filter2, data.convert, data.sink, NULL);

  // 先ずはテスト画像の読み込みから表示までをリンク。
  if (!gst_element_link_many (data.source2, data.filter1, data.filter2, data.convert, data.sink, NULL)) {
    g_printerr ("Elements 1 could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  // 次にappsrcとtextoverlayをリンク。
  if (!gst_element_link_many (data.source1, data.filter1, NULL)) {
    g_printerr ("Elements 2 could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;
  }

  g_object_set (data.source2, "pattern", 0, NULL);    // テスト用ビデオのパターンを設定

  /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
  bus = gst_element_get_bus (data.pipeline);
  gst_bus_add_signal_watch (bus);
  g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
  gst_object_unref (bus);

  /* Start playing */
  ret = gst_element_set_state (data.pipeline, GST_STATE_PLAYING);

  /* 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 */
  gst_element_set_state (data.pipeline, GST_STATE_NULL);
  gst_object_unref (data.pipeline);

  return 0;
}
```
コンパイルのコマンドは次の通りです。xxxxxxにはソースを保存したファイル名（プログラム名）を入れて下さい。
gcc xxxxxx.c -o xxxxxx `pkg-config --cflags --libs gstreamer-1.0`
# ソースの説明
先に記述をしましたが、このプログラムはgstreamerの「Basic tutorial 8」を基に作成しました。ソース中の「/* */」のコメントはもともとBasic tutorial 8にあったものです。「//」で始まるコメントは筆者が追加しました。
このプログラムではテキストファイル「message.txt」を読み込みその中のテキストをtextoverlayに渡しています。テキストファイルから読み取った文字はおよそ45文字がビデオ画像に表示されます。textoverlayには都度渡していますので、ファイルの内容を書き換えるとその直後から表示がテキストファイルのものに置き換わります。テキストファイルはutf-8で作成して下さい。ビデオの画面に日本語も表示されます。
#### コールバック関数について
gstreamerのプログラムでは先ず各種の設定を全て行います。そして「play」の号令をかけるとその後は基本的に何もする必要がありません。何かあった場合にはsignalが発生しますのでプログラムでは予めその処理ルーチンを用意しておきます。それがcall back関数です。このプログラムでは[appsrc](https://gstreamer.freedesktop.org/documentation/app/appsrc.html?gi-language=c)のために二つのコールバック関数を定義しています。「start_feed」と「stop_feed」です。それぞれ「need-data」と「enough-data」に対応しています。
need-dataが発生するとstart_feedが呼ばれます。start_feedはテキストをファイル「message.txt」から読み取ってバッファーにセットします。stop_feedでは特に何もしていません。
「push_data」はstart_feedからよばれるfunctionです。ここで実際にバッファにテキストをセットしています。じつはこの辺りに分からないことがあります。次の「問題点」で説明をします。
# 問題点
テキストストリームの構成が良く分かりません。textoverlayに渡す情報はテキストと時刻そして時間です。テキストは「gst_buffer_map」で、時刻と時間は「GST_BUFFER_TIMESTAMP」と「GST_BUFFER_DURATION」で渡しているのだと思いますが、この時刻と時間の指定が全く分かりません。現在指定しているGST_BUFFER_TIMESTAMPとGST_BUFFER_DURATIONはいろいろと試しまして具合の良い値を採用しただけです。パソコンの機種やらソフトのバージョンが変わると具合が悪くなるかもしれません。

もう一つの問題点は表示の文字数です。「#define MSG_SIZE 50」からお分かりのようにファイルからは50文字読み込んでバッファにセットしているのですが、どうも45文字ほどしか表示してくれません。残りはゴミが表示されてしまいます。仕方が無いのでコーディングでは「MSG_SIZE+8」のような小賢しい真似をしています。

# 最後に
上記のふたつの問題につきまして、どなたかお分かりになる方がいらっしゃいましたら是非お教え下さい。筆者もgstreamerは初心者ですので今後も勉強を重ねて何とか解決をしたいと思っております。

ここに掲載したソースコードは半分以上がgstreamerのチュートリアルのコードを写したものです。従いましてライセンスはgstreamerに準するとして下さい。