The 2nd page of Hwangsaeul project

The 2nd page of Hwangsaeul project

It’s been almost a year and half since Jakub posted the installation document of Hwangsaeul (a.k.a H8L) project. The project members are still rush to develop the project for the various use cases. Although the H8L project is originally designed to support the massive deployment of video surveillance cameras which enable SRT transmission, we found that there is much more potential when we switch the domain to UAV(Unmanned Areial Viehicle) and UGV(Unmanned Ground Vehicle). Therefore, we had to change the overall shape of H8L to optimize for the new targets.

Deprecation of Messages Queue

The major architecture change for ​the second version of H8L is to remove the dependency on message queue by the deprecation of ​Chamge​. Although the module is good for exchanging complex messages among service components, it is an overkill architecture for real-time video streaming that focuses on video quality and ultra-low latency. In addition, there are lots of message queue based platform so we decided not to keep developing duplicated effort. Instead, we re-designed that each component provides D-Bus API and local settings. Since we removed this component, unfortunately, ABI compatibility with the previous version will not be guaranteed while this project is on-going.

Gaeul: Umbrella repository for streaming agents

Another change is done on the Gaeul. In the previous version, the module was in charge of edge streaming only. However, from the second version, it becomes a set of streaming agents; video source, relay, and even protocol conversion including transcoding. By analogy from the meaning of the word, it would be a very natural change for this module to handle the stream of data and video because Gaeul means babbling brook in Korean.

New Architecture

In new H8L, there are three major repositories; Gaeul, Hwangsae, and Gaeguli.

  • Hwangsae: SRT relay library
  • Gaeguli: Video stream source library
  • Gaeul: A set of video streaming services

Since Gaeul provides all of streaming agents, it will always require Hwangsae and Gaeguli depending on the feature it uses.

System Overview

Running H8L

Nightly builds are available as binary packages for Ubuntu 20.04.

$ sudo add-apt-repository ppa:hwangsaeul/nightly
$ sudo apt-get update

Note that the PPA provides important packages; libsrt and gst-plugins-bad. Since Ubuntu 20.04 provides slightly old version of those packages, some features like stream-id and SRT option strings are not supported. SRT(=1.4.2) and gst-plugins-bad with some patches that will be provided by newer version of GStreamer should be installed before using H8L features. Using PPA may be more convenient to test and have experience of H8L than building from scratch. Surely, some enhancement patches of SRT and GStreamer are already submitted to the upstream and most of them are landed onto master branch or ready for landing. If you’d like to check the patches, you can refer to our forked repositories; libsrt and gst-plugins-bad.

Source

The first agent of Gaeul is gaeul2-source-agent that is nomally simlilar to stream generator(or simply called as encoder).

$ sudo apt-get install gaeul2-source-agent

Settings

Before running the source agent, we need to set configurations to specify video parameters and SRT options. The settings are consist of two parts; one main configuration and multiple channel configurations.

Main Configuration

This configuration file is an entry point that provide capture device informations to the source agent. For the stream authentication, Stream ID is mandatory for H8L’s source agent and uid will be used for stream-id prefix for channels.

channel-configs option takes a list of absolute file paths that are channel configurations.

[org/hwangsaeul/Gaeul2/Source]
uid="device0"
channel-configs=["/etc/gaeul2/conf.d/ch0.ini", "/etc/gaeul2/conf.d/ch1.ini"]

Channel Configuration

This sub-configuration describes what type of SRT stream will be sent from which video capture device. In theory, the source agent can have an infinite number of channel configurations, but it will be limited by hardware performance. Normally, it shouldn’t exceed 2 channels encoding for 4K 30fps, or equivalent video encoding parameters.

The below is an example of a channel configuration.

[org/hwangsaeul/Gaeul2/Source/Channel]
name="channel0"
source="v4l2src"
device="/dev/video0"
bitrate=2000000
fps=30
codec="x264"
bitrate-control="CBR"
resolution="1920x1080"
target-uri="srt://ip.address:port/?mode=caller"
passphrase=""
pbkeylen="AES-128"
prefer-hw-decoding=false
record-location="/somewhare/recording-location"

With uid of a main configuration, name option will be used to compose a stream-id for this channel stream. if device0 is given for the uid and channel0 is for the name, this video channel will have device0_channel0 stream id.

Running

Now, it’s time to run the source agent. If the configurations are provided correctly, user can choose D-Bus mode; session, system and none.

If none is used, the agent will not try to acquire d-bus name. Regardless --dbus-type, it will send stream or trying to connect to the given target-uri of a channel configuration until SRT stream receiver is ready.

$ gaeul2-source-agent -c /etc/gaeul2/gaeul.ini --dbus-type=none

Debugging

If it is suspicious that the source agent sends a stream or not, the easiest way to check is to get log messages. Since all of H8L components follows GLib and GStreamer conventions, it shows log messages by setting two major environmental variables.

$ export G_MESSAGES_DEBUG=all
$ export GST_DEBUG=*:3
$ gaeul2-source-agent -c /etc/gaeul2/gaeul.ini --dbus-type=none

Then, now user can get log messages on their console.

Relay

The relay agent plays a role to distribute SRT stream from source to users. It provides stream authentication option to distinguish SRT session by stream-id. Surely, the authentication option can be disabled when it requires the compatiblity with the legacy SRT equipment or software.

Settings

Unlike the source agent, the relay agent has a main configuration only.

[org/hwangsaeul/Gaeul2/Relay]
uid="relay0"
sink-port=50010
source-port=60010
sink-latency=125
source-latency=125
external-ip="xxx.xxx.xxx.xxx"
authentication=true
  • uid: a unique id, it is used for identifying each relay service.
  • sink-port: a network port to be connected from a source agent.
  • source-port: a network port to be connected from a user or video stream consumer.
  • sink-latency, source-latency: SRT latency value for each connection.
  • external-ip: usually, it is used for VM in cloud service that has an external address.
  • authentication: an option to inteprete SRT streamid or not.

Running

If a configuration file is ready, it can be run with the below command. In a relay service, D-Bus API should be enabled to use a stream authentication feature. Here, it assumes that it uses session-wide D-Bus API.

$ gaeul2-relay-agent -c /etc/gaeul2/gaeul.ini --dbus-type=session

SRT Stream authentication

The relay is relatively simple because of SRT-nature; content agnostic. However, it plays very important role as a live stream distributor. In current implmentation, the relay agent supports only SRT’s live mode.

Stream Authentication in Relay

The relay provides a whitelist-based stream authentication mechanism. That means, the agent should know streamid before attempting to connect. Otherwise, the agent will reject stream connection.

To allow a connection from source agent, the stream-id of source agent must be registered. Here, it assumed that the source agent uses device0_channel0 as its stream-id.

$ busctl call \
    org.hwangsaeul.Gaeul2.Relay \
    /org/hwangsaeul/Gaeul2/Relay \
    org.hwangsaeul.Gaeul2.Relay \
    AddSinkToken "s" "device0_chanel0"

Then, if a user who has the unique id, admin0, wants to get a video stream of the source agent, device0_channel0, a source token should be registered too.

$ busctl call \
    org.hwangsaeul.Gaeul2.Relay \
    /org/hwangsaeul/Gaeul2/Relay \
    org.hwangsaeul.Gaeul2.Relay \
    AddSourceToken "ss" "admin0" "device0_channel0"

Playing SRT stream via Hwangsae

Currently, there are few video players that support SRT and its streamid.

  • VLC Nightly build (>= 4.0)
  • GStreamer (>= 1.18)
  • SRT Play Pro (iOS only)

If you are using GStreamer, you can test playing with the below command.

$ gst-launch-1.0 \
    srtsrc uri="srt://relay.ip:port?mode=caller" streamid="#\!::u=admin0,r=device0_channel0" ! \
    queue ! decodebin ! autovideosink

See also

H8L is not all about streaming video over SRT. It is actually designed to overcome unpredictable network in two major concerns. The first is how to stream high-quality video over high bandwidth capable network, and the second is to prevent video stuttering at a discernable level even if the quality is dropped. SRT dealt with the first issue very well by congestion control, but the second issue is complicated. Without media processing and network status prediction, it will be difficult to find solution. Here, we sugguest network adaptive streaming of H8L.


GstCaps small TIP: How to handle GstFeatures

문자열에서 GstCapsFeatures를 포함한 GstCaps 다루기

Nvidia 계열의 임베디드 보드에서 GStreamer Pipeline을 사용하는 경우 다음과 같은 예제를 볼수 있습니다.

 $ gst-launch-1.0 nvcamerasrc ! video/x-raw(memory:NVMM),width=1920,height=1080 ! nvvidconv ! autovidoesink

memory:NVMM은 Zero-copy 기능을 활성화하기 위해서 GstCaps에 부가적으로 제공하는 메타정보(GstCapsFeatures)이나, API를 사용하여 GstCaps를 생성할 때는 GstCapsFeatures를 직접 다룰 필요가 있습니다.

이와 관련하여 주로 접하게되는 Assertion Error는 다음과 같습니다.

GStreamer-CRITICAL **: gst_structure_new_empty: assertion 'gst_structure_validate_name (name)' failed
convFilterCaps could not be created. Exiting.

위 에러는 대게 gst_caps_new_simple() 함수를 이용하여 video/x-raw(memory:NVMM)을 처리할 것으로 기대한 경우에 볼 수 있습니다. 이는 gst_caps_from_string("video/x-raw(memory:NVMM)", NULL)이 동작한 것과 같은 방식으로 처리될 것으로 예상하고 사용한 경우에 볼 수 있는 에러이며, 메타정보를 정확하기 다루기 위해서는 다음의 코드처럼 GstCapsFeatures 객체를 별도로 생성하여 처리해야합니다.

/**
 * gcc -o caps_features `pkg-config --cflags --libs gstreamer-1.0` caps_features.c
 */

int
main (int argc, char **argv)
{
  GstCaps *a_caps, *b_caps;
  GstCapsFeatures *a_f, *b_f;

  gst_init (&argc, &argv);

  a_caps = gst_caps_from_string ("video/x-raw(memory:NVMM)");
  a_f = gst_caps_get_features (a_caps, 0);

  b_caps = gst_caps_new_simple ("video/x-raw", NULL);
  b_f = gst_caps_features_new ("memory:NVMM", NULL);

  gst_caps_set_features_simple (b_caps, b_f);

  g_print ("caps objects are %s\n", gst_caps_is_equal (a_caps,
          b_caps) ? "equal" : "*not* equal");

  gst_caps_unref (a_caps);
  gst_caps_unref (b_caps);

  return 0;
}

GStreamer User Book #3

본 연재는 SK Telecom의 후원으로 진행하는 “책책책 책을 만듭시다!” 프로그램으로 기획되었으며, 연재 종료 후 도서로 출간될 예정입니다. 연재 내용 중에 저자의 주관적 의견이 노출되는 경우, 해당 의견은 SK Telecom의 의견이 아님을 미리 밝힙니다.

GStreamer 사용을 위해 준비해야하는 것들

앞서 간단히 언급하였던 것처럼, GStreamer는 GLib 2.0 Object Model, 즉 GObject를 기반으로 하는 객체 지향 구조로 설계되었다. 그렇기 때문에 GStreamer의 기본 단위인 Element를 사용하기 위해서는, GObject를 이해하고 이와 더불어서 GLib에서 제공하는 기능들에 익숙해지는 것은 필수적이라 할수 있다.

GLib과 GObject를 설명하려면 먼저 GNOME 프로젝트를 언급할 수 밖에 없다. 일반적으로 GNOME 데스크탑과 GNOME 프로젝트를 구분하지 않고 사용하기 때문에, GNOME은 리눅스용 데스트탑 환경을 제공하기 위한 프로젝트로 알려져 있기도 하지만, 오픈 소스 소프트웨어의 시각에서 보면 리눅스 커널이 구동을 하는 시점부터 화려한 GUI를 제공하는 데스크탑 환경까지 필요한 모든 소프트웨어적인 기반을 제공하고 있는 거대한 플랫폼이기도 하다.

GNOME 프로젝트는 이러한 플랫폼을 완성하기까지 필요한 핵심 기능과 자료구조, 탐색 알고리즘, 파일 입출력, 네트워크 등 전반적인 기능을 제공하는 다수의 프로젝트들의 집합이라고 할 수 있고, 그중에 GLib은 핵심적인 기능들을 제공하는 라이브러리라고 할 수 있다. 엄밀히 말하자면, GObject는 GLib 라이브러리 내에서 제공하는 기능 중에 하나이며, GLib 라이브러리가 함수 수준의 기능을 제공하고 있다면, 이를 좀 더 유연하고 재사용하기 편리한 구조로 제공하기 위해서 고민한 결과가 객체 지향 모델인 GObject가 되는 것이다.

사실 GObject의 개념과 작성하는 방법에 대해서는 GObject Reference Manual에 상세하게 기술 되어 있고 최신 버전에 대한 보다 자세한 정보를 얻을 수 있다. 그러나 본 서적에서는 GStreamer 애플리케이션을 작성하기 위해 혹은 GObject에 대한 보다 쉬운 접근 방법을 제공하고자 보다 간결하게 설명을 하고자 한다.

필요한 개발 패키지 설치

리눅스와 Mac OS X에서는 pkg-config를 이용하여 헤더와 라이브러리의 위치를 확인할 수 있다. GObject의 pkg-config 모듈 이름은 gobject-2.0으로 다음 명령을 이용하여 설치된 라이브러리의 버전을 확인할 수 있다.

$ pkg-config --modversion gobject-2.0
2.60.4

만약 위 명령을 통해서 다음과 같이 버전 정보대신 패키지 정보를 찾을 수 없다고 한다면, 개발용 패키지가 설치되지 않은 것이므로 우분투 리눅스의 경우에는 libglib2.0-dev 패키지를 설치하고, Mac OS X의 경우라면 glib을 설치하면 된다.

$ pkg-config --modversion gobject-2.0
Perhaps you should add the directory containing `gobject-2.0.pc'
to the PKG_CONFIG_PATH environment variable
No package 'gobject-2.0' found

GObject 작성하기

C 언어를 처음 배우고 어느정도 프로그래밍을 할수 있다고 생각할 무렵에 공통적으로 묻는 질문 중의 하나가 어떤 기준으로 소스 파일을 나누어서 함수들을 배치해야 올바른 선택인가 일 것이다. 개발자의 선호도나 프로젝트 진행 중에 시행착오를 통해서 .c 파일을 기능별로 나누게 되는데, GObject를 이용하여 소스를 작성하는 경우에는 이름 규칙과 소스 파일 생성 규칙을 제안하고 있어, 어느 정도 이 문제에 대한 가이드를 제공해주고 있다. 이에 대한 GObject의 공식적인 가이드는 GType Conventions를 통해서 제공되고 있으며, 주요 규칙은 다음과 같다.

  • 타입 혹은 객체의 이름은 3글자 이상의 영어 대소문자 및 _로 해야한다.
  • 함수 이름은 객체 이름을 포함하여 정의한다. 예, object_method
  • 네임스페이스 혼동을 방지하기 위해서 prefix를 사용한다.
  • PREFIX_TYPE_OBJECT 매크로를 반드시 정의한다.
  • G_DECLARE_FINAL_TYPE 혹은 G_DECLARE_DERIVABLE_TYPE를 이용하여 객체를 정의한다. (GLib 2.44 이상 사용하는 경우)

이러한 규칙을 실제 적용하여 객체를 작성해 보기 위해서 본 예제에서는 가상의 프로젝트와 객체 이름을 다음과 같이 정의하기로 한다. 이때 대소문자에 유의하여야 하며, 대소문자의 역할이 어떻게 코드로 연결되는지를 살펴보는 것이 이번 챕터의 목적이다.

  • 프로젝트명: Helloworld
  • 프로젝트 약어: HLW
  • 객체 이름: HelloApp

또한 정의한 객체는 다음과 같은 멤버 변수와 함수를 가지고 있는 것으로 정의한다.

App 클래스 다이어그램

또한 HelloApp 클래스의 hello 함수는 문자열을 입력받아, 멤버 변수 name에 설정된 이름과 조합을 하여 두번째 인자에 기록하고, 조합한 문자의 길이를 리턴하는 기능으로 정의한다.

이렇게 정의한 프로젝트와 객체 정보를 이용하여 GObject의 권고안을 따른다면, 다음과 같은 디렉토리와 파일 구조를 가지게 된다.

  helloworld
    +- helloworld
      +- hello-app.c
      +- hello-app.h

헤더 파일 구성하기

GObject를 사용할때 이러한 이름 규칙을 특별히 강요하지는 않지만, 이름 규칙을 따르게 되면 미리 정의된 매크로를 사용하여 GObject를 구성하기 위해 필요한 함수를 일일이 나열하지 않아도 되는 편리함이 있다.

먼저 위에서 정의한 HelloApp 클래스를 헤더파일을 이용하여 구성한다면, 다음과 같이 정의할 수 있다.

#include <glib-object.h>

#ifndef __HLW_HELLO_APP_H__
#define __HLW_HELLO_APP_H__

G_BEGIN_DECLS

#define HLW_TYPE_HELLO_APP (hlw_hello_app_get_type ())
G_DECLARE_FINAL_TYPE (HlwHelloApp, hlw_hello_app, HLW, HELLO_APP, GObject)

HlwHelloApp     *hlw_hello_app_new      (void);
gint             hlw_hello_app_hello    (HlwHelloApp *self,
                                         const gchar *msg,
                                         gchar       *hello_msg);

G_END_DECLS

#endif /* __HLW_HELLO_APP_H__ */

아마도 GLib을 경험하지 않고 위 코드를 처음 본 독자라면, 매크로의 난무로 인하여 당황할 수 있겠지만, 위의 코드는 GObject를 기반으로 코드를 작성할때에는 가장 기본이 되는 매크로들로만 이루어진 지극히 사실적인 코드이다.

#ifndef __HLW_HELLO_APP_H__의 경우에는 일반적으로 헤더 파일의 중첩 사용을 방지하기 위하여 정의하는 매크로일뿐이지만, 프로젝트 내에서 혹은 다른 프로젝트와 우연히라도 중복되지 않도록 하기 위하여 프로젝트명의 약어(prefix)와 클래스명을 이용하여 정의할 것을 권고하고 있다.

또한 모든 명칭에 CamelCase를 사용하는 경우, 각 대문자를 음절로 간주하고 소문자로 변환하는 경우 _를 추가하는 규칙을 사용하고 있다. 위 코드에서 클래스 이름이 HelloApp이기 때문에 여기에 해당 규칙을 적용하여 소문자로 변환하면 hello_app으로 정의하게 된다.

G_BEGIN_DECLSG_END_DECLS의 경우에는 컴파일러 호환성을 위하여 GLib에서 정의한 매크로이다. C 컴파일러의 경우에는 아무런 역할을 하지 않고, C++ 컴파일러를 사용하는 경우 extern "C" { } 구문으로 변환되는 매크로이다. 사실 이 책의 코드는 모두 C 컴파일러를 사용하기 때문에 필요없는 선언일 수 있으나, 실제 프로젝트에 사용하는 경우에 혹시나 모르는 컴파일러 호환성 확보를 위하여 헤더의 시작과 끝에 정의하는 것을 습관화하는 것도 나쁘지는 않다.

클래스를 정의할 때 각 클래스를 구분하는 기준은 내부적으로 GType을 이용하여 구분하게 되며 그 값은 prefix_object_name_get_type 으로 정의한 함수를 통해서 얻어올 수 있을것으로 가정하며, 해당 함수의 구현은 .c에서 정의하게 된다. 이러한 함수는 매크로에서 사용되고는 있으나, 컴파일 단계 중 매크로를 해석하는 시점에는 함수 원형을 정의하지 않고 있기 때문에 직접 함수 명칭을 사용할수는 없다. 그래서 #define HLW_TYPE_HELLO_APP (hlw_hello_app_get_type ())와 같이 정의하여, 프리프로세서에 의해서 온전한 코드가 만들어질 수 있도록 정의하여야한다.

G_DECLARE_FINAL_TYPEBoilerplate 매크로이며, 다음과 같은 코드를 이름 규칙을 이용하여 생성하는 역할을 한다.

typedef struct _HlwHelloApp HlwHelloApp;

#define HLW_IS_HELLO_APP(obj)           (G_TYPE_CHECK_INSTANCE_TYPE((obj),HLW_TYPE_HELLO_APP)
#define HLW_IS_HELLO_APP_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE((klass),HLW_TYPE_HELLO_APP)
#define HLW_HELLO_APP_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS((obj), HLW_TYPE_HELLO_APP, HlwHelloAppClass))
#define HLW_HELLO_APP(obj)              (G_TYPE_CHECK_INSTANCE_CAST((obj), HLW_TYPE_HELLO_APP, HlwHelloApp))
#define HLW_HELLO_APP_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST((klass), HLW_TYPE_HELLO_APP, HlwHelloAppClass)
#define HLW_HELLO_APP_CAST(obj)         ((HlwHelloApp *)(obj))

struct _HlwHelloAppClass {
    GObjectClass    parent_class;
}

GType hlw_hello_app_get_type (void);

G_DECLARE_FINAL_TYPE가 없다면, 위에 나열한 코드를 객체를 정의할 때마다 기록해주어야한다. 유감스럽게도 위 매크로를 정의할때 이름 규칙을 준수하지 않는다면, 컴파일 에러가 아닌 주로 런타임 에러가 발생하게 되고 의도치 않은 버그로 이어질 수 있기 때문에 이러한 매크로의 도움을 받는 것이 좋다.

G_DECLARE_FINAL_TYPE외에도 G_DECLARE_DERIVABLE_TYPE 매크로도 존재한다. 두 매크로의 차이점은 C++이나 JAVA에서 클래스를 선언할때 final 키워드의 사용 여부와 동일하다. 다만 C 언어의 특성상 컴파일된 바이너리의 호환성에서 차이가 발생한다. G_DECLARE_FINAL_TYPE으로 선언한 클래스를 추후 G_DECLARE_DERIVABLE_TYPE로 변경하게 되더라도 ABI(Application Binary Interface) 호환성은 유지되나 반대의 경우에는 바이너리간 호환이 되지 않는 문제가 있다.

그렇기 때문에 GObject를 이용하여 클래스를 정의하는 경우 특별한 이유가 없다면 G_DECLARE_FINAL_TYPE을 사용하도록 권고 하고 있다.

한가지 주의할점은 위에서 언급한것처럼 G_DECLARE_FINAL_TYPEG_DECLARE_DERIVABLE_TYPE와 같은 Boilerplate 매크로는 GLib 2.44 버전 이후에 등장하였기 때문에, 이전 버전의 GLib과 호환성을 고려한다면, 어쩔수 없이 위에 제시한 매크로들을 기록해주는 수 밖에 없다. GStreamer 경우 그 이전 버전의 GLib을 이용하여 작성해왔고 호환성을 유지하고 있기 때문에 위 매크로를 를 사용하지 않고 있다.

소스 파일 구성하기

위에서 준비한 hello-app.h를 기반으로하는 hello-app.c는 다음과 같이 구성할 수 있다. 다른 객체지향 언어에서 보는 클래스의 개념처럼 struct 정의하고 내부에 멤버를 구성한다. 본 예제에서는 gchar * 타입의 name 멤버를 선언하였다.

#include "hello-app.h"

struct _HlwHelloApp {
    GObject      parent;

    gchar       *name;
};

G_DEFINE_TYPE (HlwHelloApp, hlw_hello_app, G_TYPE_OBJECT)

HlwHelloApp *
hlw_hello_app_new (void)
{
    return g_object_new(HLW_TYPE_HELLO_APP, NULL);
}

gint
hlw_hello_app_hello (HlwHelloApp *self, const gchar *msg, gchar *hello_msg)
{
    return g_sprintf(hello_msg, "%s!, %s", self->name, msg);
}

위 소스 코드는 일단 헤더에서 노출된 함수에 대한 내용을 채운것에 지나지 않는다. 이대로라면 객체를 구성하기 위한 Contructor가 없기 때문에 때문에 빌드가 되지 않는다. HlwHelloApp이 온전한 객체가 되기 위해서는 G_DEFINE_TYPE 매크로에서 이름 규칙을 사용하여 정의한 함수를 구성해서 온전한 객체가 되도록 해야한다.

static void
hlw_hello_app_class_init (HlwHelloAppClass *klass)
{
}

static void
hlw_hello_app_init (HlwHelloApp *self)
{
}

C 언어의 한계 때문에 객체와 객체의 클래스를 초기화를 하는 코드는 매크로와 이름 규칙을 이용하여 정의할 수 밖에 없었지만, 위 초기화 함수를 진입점으로 하여 가상 함수를 오버라이드할 수 있게 된다. 실제로 GObject의 Destructor는 prefix_object_class_init 함수에서 인자로 전달받은 klass 구조체에 정의된 함수 포인터를 설정하는 방식으로 오버라이딩을 구현하고 있다. 위 예제에 적용하면 다음과 같이 hlw_hello_app_dispose를 정의하고 사용할 수 있다.

static void
hlw_hello_app_dispose (GObject *object)
{
  HlwHelloApp *self = HLW_HELLO_APP (object);
  
  g_clear_pointer (&self->name, g_free);
  
  G_OBJECT_CLASS (hlw_hello_app_parent_class)->dispose (object);
}

static void
hlw_hello_app_class_init (HlwHelloAppClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  
  object_class->dispose = hlw_hello_app_dispose;
}

Contructor와 Destructor

객체 인스턴스를 생성하거나 제거할 때에는 g_object_new()g_object_unref() 함수를 이용할 수 있다. 소스 파일에서 정의한 hlw_hello_app_new()를 사용하지 않고 g_object_new()를 사용해서 객체를 생성할 수도 있지만, GObject를 만들때 객체 생성을 위한 함수를 명시해주는 것을 권고하고 있다.

Destructor의 경우에는 앞서 설명한대로 가상 함수 오버라이딩을 통해서 처리할 수 있다. GObject는 객체 생성과 소멸을 내부의 레퍼런스 카운터를 이용하여 처리하게 되는데, g_object_new() 함수로 생성된 함수의 레퍼런스 카운터는 g_object_ref() 함수를 이용하여 증가하고, g_object_unref()를 통하여 감소하게 된다.

g_object_unref()를 호출하여 레퍼런스 카운터가 0이 되었을때는 더이상 해당 객체를 사용하지 않는 것으로 판단하고 인스턴스를 정리하는 절차를 진행하게 되며, 이때 호출되는 것이 GObjectClass에 정의된 dispose() 함수이다.

레퍼런스 카운터에 의해서 객체의 라이프사이틀을 관리하기 때문에 GOject의 객체의 소멸에 관여하는 절차는 조금 복잡한데, 해당 절차를 좀더 명시적으로 관리하기 위하여 GObjectClassdispose()finalize() 두 함수를 정의하고 있다. 앞서 기술한대로 dispose()는 레퍼런스 카운터가 0가 되었을때 호출되는 함수이고, finalize()는 객체가 소멸할때 호출되는 함수이다. 호출 순서는 dispose()가 호출 된 다음에 finalize()가 호출되며, 두 순서가 변경되거나 무시하는 경우는 없다.

그런 이유 때문에 실질적으로 사용할때 dispose()finalize()를 구분없이 사용하는 경우가 많지만, dispose()의 경우 레퍼런스 카운터 값이 트리거가 되기 때문에 멀티 스레드 환경이라면 여러번 호출되는 경우가 발생할 수 있다. 반면에 finalize()는 단 한번만 호출된다.

dispose()가 여러번 호출될때를 방지하기 위해서 본 예제에서는 멤버 변수인 name의 메모리를 해제하는데 g_clear_pointer()를 이용하고 있다. 이는 다음 코드와 동일한 역할을 하여, name의 double free를 방지한다.

  if (self->name != NULL) {
    g_free (self->name);
    self->name = NULL;
  }

또한 dispose()finalize() 함수 모두 부모 클래스에서 정의하고 할당하고 있는 자원을 해제할 수 있도록 명시적으로 호출해주어야한다. 규칙은 해당 객체의 자원 해제를 모두 처리한 후, 부모 클래스의 가상 함수를 반드시 호출하도록 하는 것(chain up)이다. 이때 부모 클래스가 해당 가상 함수를 구현했는가 검사할 필요없이 바로 사용할 수 있다.

    G_OBJECT_CLASS (hlw_hello_app_parent_class)->dispose (object);

위 구문에서 hlw_hello_app_parent_class의 등장으로 의아할 수 있으나, 이는 G_DEFINE_TYPE 매크로에서 정의한 것으로 본 예제에서는 부모 클래스가 GObject이므로 GObjectClass 타입을 갖게 된다.

빌드하기

hello-app.hhello-app.c를 빌드하기 위해서는 pkg-config의 도움으로 필요한 헤더와 라이브러리 이름을 사용할 수 있다.

 $ cc -o hello-app.o -c hello-app.c `pkg-config --cflags gobject-2.0`

Meson Build

프로젝트를 빌드하는 고전적인 방법으로는 Makefile을 떠올릴수 있다. 이를 좀더 체계화한것이 GNU Autotools이며, GStreamer는 이 도구를 이용하여 발전해왔다. 그러나 최적화를 위해 추가된 Autotools 매크로는 소스코드만큼이나 복잡해지고 종종 소스코드 디버깅과 별개로 빌드 스크립트와 매크로를 디버깅하는 상황을 연출하곤 한다. 그렇기 때문에 오픈 소스 커뮤니티에서는 이를 대체하려는 시도가 있었고, 각 프로젝트와 오픈 소스 그룹간의 목적에 따라 극적으로 합의한 도구가 MesonBuild이다. 메이슨 혹은 메종으로 읽히는데, 주요 공헌자들이 프랑스어를 모국어로 사용해서 대게 메종으로 읽는다.

GNU Autotools나 CMake 대비 Meson의 가장 큰 이점은, 병렬 빌드에 있어서 기존 빌드 시간을 대폭 단축한다는 것에있다. GStreamer 모듈 코드를 일반적인 랩탑에서 전부 빌드한다고 하면 Autotools를 이용할 경우 한 두시간에 걸치는 작업이 되겠지만, Meson을 이용할 경우 수 십분 정도로 대폭 단축된다. 또한 프로그래머 입장에서 보면, python3 구문에 익숙한 경우 큰 거부감 없이 meson 스크립트를 작성할 수 있어 빌드 스크립트 자체를 디버깅하기 위해 학습해야는 시간이 별도로 필요하지 않다는 정도일 것이다.

Meson 설치하기

Meson은 python3를 기반으로 하기 때문에, 리눅스 배포본에 설치 되어 있지 않더라도 pip3를 이용하여 최신 버전을 이용할 수 있다. Meson은 빌드 스크립트를 읽어서, 사용하는 backend에 맞는 빌드 환경을 구성하는 역할을한다. 리눅스 혹은 Mac OSX에서는 Ninja를 backend로 사용하기 때문에 meson과 ninja를 같이 설치해주어야한다.

 $ pip3 install --user meson
 $ pip3 install --user ninja

GStreamer는 1.16 버전부터 meson을 이용한 빌드를 지원하며, 전체 소스코드를 빌드하기 위해서는 Repository Aggregator 프로젝트인 gst-build를 사용할 수 있다.

 $ git clone https://gitlab.freedesktop.org/gstreamer/gst-build.git
 $ cd gst-build
 $ meson build
 $ ninja -C build

간단한 프로젝트 만들기

이번 챕터에서는 이전에 정의한 helloworld-app을 소스 코드가 위치한 디렉토리가 아닌 빌드를 지원하는 프로젝트로 구성하는 방법에 대해서 기술한다.