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을 소스 코드가 위치한 디렉토리가 아닌 빌드를 지원하는 프로젝트로 구성하는 방법에 대해서 기술한다.


GStreamer User Book #2

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

GStreamer 구동 환경

실제로 GStreamer는 멀티 플랫폼을 지향하는 미디어 프레임워크이다. 대부분의 리눅스 배포본에 기본적으로 탑재해있으며, Android와 iOS와 같은 모바일 플랫폼을 위한 SDK를 제공하고 있다. 또한 MacOS X와 Windows에서도 사용이 가능하다.

하지만 이러한 멀티 플랫폼 지원은 GStreamer보다는 뒤에 논의할 GLib에서 각 OS의 의존적인 부분을 추상화하여 제공하기 때문에 가능한 것이고, 이러한 특성 때문에 태생적으로 리눅스에서 가장 편리하게 사용할수 있다. 유닉스 기반의 운영체제인 MacOS X도 리눅스와 유사하게 혹은 리눅스 환경 대비 큰 노력을 들이지 않고도 편리하게 사용할 수있다.

Windows에서는 MinGW(Minialist GNU for Windows)를 이용하여 리눅스에서 사용하던 GNU 도구와 컴파일러를 기반으로 빌드한 SDK를 사용하였으나, GStreamer 버전 1.16부터는 MSVC 기반으로 빌드한 SDK도 같이 제공하고 있다. 하지만 이는 GStreamer SDK이전에 MinGW나 MSVC에 대해 먼저 익숙하다는 전제가 필요하고, 그렇기 때문에 GStreamer를 이용하는 애플리케이션 작성법을 익히기 위한 출발점으로 Windows 환경은 권장하지 않는다. 그렇지만 GStreamer를 구동하는 환경을 정확히 제어하고, GStreamer가 제공하는 라이브러리와 플러그인을 어떻게 애플리케이션과 연결할 것인지를 이해하고 있다면, Windows 환경에서 GStreamer를 이용하는 것 자체가 어려운 일은 아닐 것이다.

본 서적에서는 리눅스 혹은 MacOS X를 사용하는 것을 가정하고 예제를 작성하고 있다.

Release version과 ABI version

리눅스를 연습하기 위한 환경으로 선택했다면, 리눅스 배포본에서 제공하는 패키지 명칭 때문에 GStreamer의 버전 체계에 의문이 생길 수 있다. gstreamer0.10 혹은 gstreamer1.0으로 명명된 패키지들을 볼 수 있는데, 이때 사용하는 숫자는 ABI(Application Binary Interface)에 대한 호환성을 표기하는 버전 체계이다. ABI 버전이 같은 경우에는, 라이브러리와 바이너리에 대한 하위 호환성을 유지한다는 뜻이며, 같은 ABI 버전 내에서 릴리즈 버전을 운영하는 형태로 진행된다. 즉, 현재 릴리즈 버전이 1.16.x이라면, 1.14.x 버전에서 빌드된 플러그인을 새롭게 빌드하지 않고도 사용이 가능하도록 하지만, 0.10.x 버전의 바이너리는 사용하지 못한다. 이러한 호환성을 강조하기 위하여 GStreamer는 각 라이브러리, 플러그인, CLI 도구들에 ABI 버전을 명시하는 정책을 사용하고 있다.

GStreamer 설치하기

최신 동향이나 논의되고 있는 기능들을 활용하고자 한다면 git 저장소에서 소스 코드를 가져와서 빌드하는 것이 맞겠지만, 개발 중인 코드들은 가끔씩 사용자를 당황하게 할때가 많이 있다. 최근에는 CI(Continuous Integration)의 발달로 인해서 master 브랜치에 제출된 코드에 대한 검사를 철저히 하기는 하지만, 어디까지나 빌드와 단위 기능에 대한 테스트까지 수행될 뿐 논리적인 혹은 런타임 오류까지 확인하지 못하기에 특별한 이유가 없다면 Pre-built 패키지를 사용하는 것을 권장한다.

권장 환경

  • GStreamer 1.10 버전 이상
  • Linux
    • 우분투의 경우 18.04 이후
    • 데비안의 경우 stretch
    • CenOS 7 이후
  • Mac OS X (HomeBrew 사용 권장)

Linux

대표적으로 많이 사용되는 우분투, 데비안 같은 경우에는 gstreamer1.0 이라는 패키지 명칭으로 제공되고 있다. 다만 주의할 점은 헤더와 디버깅 심볼들은 별도의 패키지로 제공되기 때문에 GStreamer 기반 사용자 애플리케이션을 작성하기 위해서는 반드시 개발용 패키지를 설치해야한다. 편의상 우분투 패키지 명칭을 통해서 살펴본다면 다음과 같은 패키지들이 필수이다.

  • 런타임 라이브러리 및 실행 파일
    • libgstreamer1.0-0
    • libgstreamer-plugins-base1.0-0
    • libgstreamer-plugins-bad1.0-0
    • gstreamer1.0-plugins-base
    • gstreamer1.0-plugins-base-apps
    • gstreamer1.0-plugins-good
    • gstreamer1.0-plugins-ugly
    • gstreamer1.0-plugins-bad
    • gstreamer1.0-libav
    • gstreamer1.0-tools
  • 개발용 헤더 파일
    • libgstreamer1.0-dev
    • libgstreamer-plugins-base1.0-dev
    • libgstreamer-plugins-good1.0-dev
    • libgstreamer-plugins-bad1.0-dev
  • 디버깅용 심볼 파일
    • libgstreamer1.0-0-dbg
    • gstreamer1.0-plugins-base-dbg
    • gstreamer1.0-plugins-good-dbg
    • gstreamer1.0-plugins-ugly-dbg
    • gstreamer1.0-plugins-bad-dbg
    • gstreamer1.0-libav-dbg

특히 이름이 -dbg로 끝나는 패키지들은 사용자 애플리케이션을 작성하는 도중 gdb와 같은 디버깅 도구로 내부 스택을 추적할 때 반드시 필요하기 때문에 Pre-built 패키지로 개발 환경을 구축할 때에는 반드시 함께 설치해야한다. 런타임, 개발, 디버깅이라는 용도에 따른 패키지를 구분하는 규칙은 다른 리눅스 배포본에서도 유사하다.

MacOS X

MacOS X는 HomeBrew(https://brew.sh/)라는 도구를 통해서 GStreamer를 설치할 수 있다.

$ brew install \
    gstreamer \
    gst-plugins-base \
    gst-plugins-good \
    gst-plugins-ubly \
    gst-plugins-bad \
    gst-libav

GStreamer CLIs

GStreamer가 기본적으로 제공하는 CLI 도구들은 애플리케이션을 작성하기 전에 사용 환경을 점검하고 사용하고자 하는 GStreamer Pipeline 구조를 시뮬레이션 하기 위한 용도로 사용할 수 있다.

gst-inspect-1.0

GStreamer의 상태를 점검하고 개별 플러그인과 Element에 대한 정보를 알려주는 역할을 수행한다.

$ gst-inspect-1.0
osxaudio:  osxaudiodeviceprovider (GstDeviceProviderFactory)
osxaudio:  osxaudiosrc: Audio Source (OSX)
osxaudio:  osxaudiosink: Audio Sink (OSX)
replaygain:  rgvolume: ReplayGain volume
replaygain:  rglimiter: ReplayGain limiter
... (중략) ...

아무런 인자없이 수행을 하게 되면, 현재 환경에서 사용할 수 있는 플러그인 목록을 출력한다. 개별 플러그인을 살펴보고 싶다면, 단순히 해당 플러그인의 이름을 인자로 사용하면 플러그인을 구성하고 있는 Element 리스트를 출력한다.

다음은 coreelements라는 이름의 플러그인 정보를 살펴본 예이다.

$ gst-inspect-1.0 coreelements
Plugin Details:
  Name                     coreelements
  Description              GStreamer core elements
  Filename                 /usr/local/lib/gstreamer-1.0/libgstcoreelements.so
  Version                  1.16.0
  License                  LGPL
  Source module            gstreamer
  Source release date      2019-04-19
  Binary package           GStreamer source release
  Origin URL               Unknown package origin

  capsfilter: CapsFilter
  concat: Concat
  dataurisrc: data: URI source element
  downloadbuffer: DownloadBuffer
  fakesrc: Fake Source
  fakesink: Fake Sink
  fdsrc: Filedescriptor Source
  fdsink: Filedescriptor Sink
  filesrc: File Source
  funnel: Funnel pipe fitting
  identity: Identity
  input-selector: Input selector
  output-selector: Output selector
  queue: Queue
  queue2: Queue 2
  filesink: File Sink
  tee: Tee pipe fitting
  typefind: TypeFind
  multiqueue: MultiQueue
  valve: Valve element
  streamiddemux: Streamid Demux

  21 features:
  +-- 21 elements

플러그인의 이름이 인자로 주어진 경우에는 부수적인 정보들을 확인할 수 있다. 여기서 개발자에게 중요한 것은 절대 경로로 표시된 해당 플러그인의 위치(Filename)와 버전 정보, 그리고 이 플러그인이 속한 모듈의 이름(Binary package)이다. 플러그인의 경로를 통해서 해당 플러그만 삭제하거나 업데이트를 하는 용도로도 사용할 수 있으며, 소속 모듈의 이름을 통해서 레퍼런스를 찾아보기가 쉽기 때문이다.

또한 하단에는 이 플러그인이 가지고 있는 최종 Element 들에 대한 정보를 가지고 있는데, coreelements의 경우에는 21개의 Element를 가지고 있음을 알수 있다. 각 개별 Element의 정보를 확인하기 위해서는 플러그인의 경우와 동일하게 해당 Element의 이름을 인자로 사용하면 된다.

다음은 identity Element에 대한 정보를 확인한 결과이다. 해당 Element의 속성을 표시해주는 것을 확인할 수 있는데, Element에 대한 설명외에도 개발자를 위한 직접적인 정보를 구성하고 있다. 이번 장에서는 각 항목들이 어떻게 구분되고 있는지 살펴보고 생략된 항목들은 이후 챕터에서 상세하게 다룰 예정이다.

$ gst-inspect-1.0 identity
Factory Details:
  Rank                     none (0)
  Long-name                Identity
  Klass                    Generic
  Description              Pass data without modification
  Author                   Erik Walthinsen <omega@cse.ogi.edu>

Plugin Details:
  Name                     coreelements
  Description              GStreamer core elements
  Filename                 /usr/local/lib/gstreamer-1.0/libgstcoreelements.so
  Version                  1.16.0
  License                  LGPL
  Source module            gstreamer
  Source release date      2019-04-19
  Binary package           GStreamer source release
  Origin URL               Unknown package origin

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstBaseTransform
                         +----GstIdentity

Pad Templates:
...(중략)...

Pads:
...(중략)...

Element Properties:
...(중략)...

Element Signals:
...(중략)...

GObject를 최상위 부모로 표현한 트리는 해당 Element가 어떠한 역할을 하는지 유추할 수 있게 하는 가장 간단한 정보 중에 하나이다. identity의 경우 GstBaseTransform을 상속하고 있음을 알수 있고, 이는 입력과 출력이 있다는 것으로 해석할 수 있다.

Pad Templates는 이 Element가 생성하는 Pad들이 어떤 스키마를 가지고 생성하는지 알려주는 역할을 하며, Pads 항목은 Pad를 생성할때 사용하는 이름 규칙과 어떠한 Pad template과 관련이 있는지를 알려주는 역할을 한다. 이 정보를 이용하여 Element 간 연결이 가능한지를 확인할 수 있고 gst-launch-1.0을 이용하여 Pipeline을 어떻게 구성할시 시뮬레이션을 할수 있다.

Element Properties는 Element가 가지고 있는 속성 값을 표현하며, 해당 속성의 타입, 읽기/쓰기 여부, 사용할 수 있는 값의 범위 등을 별도의 API를 사용하지 않고도 알수 있게 한다.

Element Signals는 Element에서 외부로 어떠한 이벤트를 보내는지 혹은 외부에서 어떻게 Element 내부의 함수를 호출할 수 있는지를 기술하고 있으며, Signal의 경우 제공하지 않는 Element도 많이 존재한다.

gst-inspect-1.0의 또 다른 숨겨진 기능 중에 하나는 플러그인 파일을 직접 분석하는 기능이다. 예를들어 coreelements는 위 정보에 따르면 /usr/local/lib/gstreamer-1.0/libgstcoreelements.so 파일로 제공되고 있는데, 해당 파일을 직접 읽어서 정보를 출력해주는 역할을 한다.

$ gst-inspect-1.0 /usr/local/lib/gstreamer-1.0/libgstcoreelements.so

위 명령의 결과는 gst-inspect-1.0 corelements를 사용했을 때와 동일한데, 내부적으로는 플러그인 파일 자체를 읽어서 정보를 표시해주는 것이기 때문에, 다른 경로에 설치된 동일한 이름의 플러그인을 제거한다던가, 실수로 동일한 이름으로 정의된 Element를 찾아야 하는 경우에 유용하게 사용할 수 있다.

또한 gst-inspect-1.0은 제공된 파일이 GStreamer 플러그인이 아닌지를 판별하는데 사용할수도 있다.

$ gst-inspect-1.0 /usr/local/lib/libgstreamer-1.0.0.dylib
Could not load plugin file: File "/usr/local/lib/libgstreamer-1.0.0.dylib" is not a GStreamer plugin

의외로 GStreamer를 이용한 애플리케이션 개발 프로젝트가 길어지는 경우에 이런 문제는 자주 발생하게 된다. 주요 원인 중에 하나는 이전 버전의 플러그인과 현재 버전의 플러그인의 동작성을 테스트하면서 Pre-built 패키지들이 설치된 공간을 편의상 패키지 관리자가 아닌 수동으로 교체하고 복원하는 것을 잊어버리는 경우인데, 그런 경우 대게 시스템이 꼬였다라고 하며 OS부터 다시 설치하는 것으로 마무리 짖곤 한다. 물론 이러한 문제는 GStreamer 환경 변수를 정교하게 조작하여 애초부터 방지할수는 있겠지만, 이미 문제가 발생한 경우, 단순한 플러그인 파일 중복이나 버전 호환성 문제 때문이라면 gst-inspect-1.0 도구로 해결할 확률이 매우 높다.

gst-launch-1.0

GStreamer는 Pipeline 구조를 통해서 데이터를 전달하고 가공하는 역할을 수행한다. 애플리케이션을 제작하기 위해서는 Pipeline 내에 적절한 Element를 배치하는 작업을 먼저 수행하게 되는데, 구상한 전체 Pipeline을 테스트하거나, Pipeline 내의 일부 구간에 대해서 동작을 확인하는 용도로 사용할 수 있는 도구이다.

다음은 gst-launch-1.0으로 w3.org에서 제공하는 영상을 playbin을 통해서 재생하는 명령이다. playbin은 GStreamer에서 Playback을 지원하기 위하여 작성한 Element로 내부에서는 소스 타입부터 화면출력까지 현재 시스템에서 사용할 수 있는 최적의 Element를 찾아 Pipeline을 구성하는 기능을 수행한다.

$ gst-launch-1.0 playbin uri=https://media.w3.org/2010/05/sintel/trailer.mp4
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Got context from element 'source': gst.soup.session=context, session=(SoupSession)NULL, force=(boolean)false;
Got context from element 'sink': gst.gl.GLDisplay=context, gst.gl.GLDisplay=(GstGLDisplay)"\(GstGLDisplayCocoa\)\ gldisplaycocoa0";
Redistribute latency...
Got context from element 'playsink': gst.gl.GLDisplay=context, gst.gl.GLDisplay=(GstGLDisplay)"\(GstGLDisplayCocoa\)\ gldisplaycocoa0";
Redistribute latency...
Prerolled, waiting for buffering to finish...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstAudioSinkClock

playbin 재생 화면 캡쳐

playbin은 GStreamer에서 제공하는 Element 중에도 State of the Art로 여겨질만큼 많은 노력의 결정체이다. 실제로 위에 주어진 미디어를 재생하기 위하여 다음과 같이 복잡한 Pipeline을 자동으로 생성하고있다.

playbin dot graph

gst-launch-1.0의 막강한 기능은 Element를 임의로 나열할 수 있다는 것이다. 다음은 테스트용 비디오 소스를 통해서 화면으로 출력하는 간단한 Pipeline이다.

$ gst-launch-1.0 videotestsrc ! autovideosink

위 Pipeline에 약간의 기능을 추가하여, 화면 크기를 조절하고, 재생 시간 정보를 표시하려면 다음과 같은 Pipeline을 사용할 수 있다.

$ gst-launch-1.0 \
    videotestsrc ! video/x-raw,width=320,height=240 ! \
    timeoverlay halignment=center valignment=center font-desc="Sans, 32" ! \
    autovideosink

위의 예제를 변형하여 화면 출력과 동시에 레코딩을 수행한다면 tee Element를 이용하여 스트림을 분기할 수 있다.

gst-launch-1.0 \
     videotestsrc ! video/x-raw,width=320,height=240 ! \
     timeoverlay halignment=center valignment=center font-desc="Sans, 32" ! \
     tee name=t \
     t. ! queue ! autovideosink \
     t. ! queue ! theoraenc bitrate=256 ! oggmux ! filesink location=test.ogg

위 Pipeline을 구동하면, 윈도우에 보이는 화면그대로 test.ogg 파일로 저장되어있음을 알 수 있고, 또다시 gst-launch-1.0 playbin uri=file://<절대경로>/test.ogg 를 이용하여 재생이 가능함을 확인할 수 있다.

gst-typefind-1.0

미디어 정보를 처리하는데 있어서 가장 어려운 부분중에 하나는 다양한 코덱과 다양한 포맷의 파일들을 다룰 수 있어야 한다는 것이다. GStreamer는 파일의 헤더 혹은 바이트 패턴을 분석해서 주어진 파일의 포맷을 추측하는 기능을 제공하는데, gst-typefind-1.0을 이용하여 해당 기능을 사용해볼 수 있다.

도구 자체로는 특별한 입력이 필요없기 때문에, 로컬에 준비된 미디어 파일을 인자로 사용하여 테스트 해볼 수 있다. 본 예제에서는 앞서 소개했던 w3c의 HTML5 Video Events and API 테스트 페이지에서 사용하고 있는 컨텐츠를 미리 다운로드 하여 사용하는 것으로 가정하였다.

$ wget https://media.w3.org/2010/05/sintel/trailer.mp4

gst-typefind-1.0에 파일 경로를 입력하여 실행하면 다음과 같이 주어진 파일의 포맷과 속성을 결과로 얻을 수 있다.

$ gst-typefind-1.0 ./trailer.mp4
./trailer.mp4 - video/quicktime, variant=(string)iso

파일의 확장자(여기서는 .mp4)를 이용하여 파일 포맷을 추정하는 것이 가장 간단하겠지만, GStreamer는 앞서 기술한 것처럼 내부 바이트 패턴을 분석하기 때문에 파일의 이름을 변경한다하더라도 동일한 결과를 얻을 수 있다.

$ mv trailer.mp4 dummy
$ gst-typefind-1.0 ./dummy
./dummy - video/quicktime, variant=(string)iso

사실 gst-typefind-1.0core 모듈의 typefind Element를 활용한 아주 간단한 Pipeline을 활용한 도구이기 때문에, 동일한 정보를 앞서 기술한 gst-launch-1.0의 디버깅 메시지를 통하여 얻을 수 있다. 타입 정보는 typefind Element 내부에 디버그 메시지 레벨 4에서 출력하기 때문에 --gst-debug=typefind:4를 인자로 주어 메시지를 확인할 수 있다.

$ gst-launch-1.0 \
    filesink location=./dummy ! typefind ! fakesink \
    --gst-debug=typefind:4
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
0:00:00.042075000 18827 0x7f9c40019b70 INFO                typefind gsttypefindelement.c:850:gst_type_find_get_extension:<typefindelement0> could not find uri extension in file:///Users/justinkim/git/gst-build/dummy
0:00:00.048977000 18827 0x7f9c40019b70 INFO                typefind gsttypefindelement.c:181:gst_type_find_element_have_type:<typefindelement0> found caps video/quicktime, variant=(string)iso, probability=100
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
Got EOS from element "pipeline0".
Execution ended after 0:00:00.004914000
Setting pipeline to PAUSED ...
Setting pipeline to READY ...
Setting pipeline to NULL ...
Freeing pipeline ...

그렇다면, typefind Element가 인식할 수 있는 파일 타입은 얼마나 될까? 엄밀히 말하자면, 사용자가 원하는 모든 포맷을 인식할 수 있다. 내부적으로는 typefind Element는 GstTypeFindFactory의 정의에 맞게 구현된 함수를 등록하여 호출하는 방식을 취하고 있기 때문에 현재 GStreamer가 인식하지 못하는 패턴일지라도 GStreamer 모듈을 수정하지 않고도 사용자 애플리케이션에서 몇몇 함수를 정의하고 등록하는 것만으로 typefind가 사용자가 정의한 파일을 인식할 수 있게 된다. 이에 대해서는 이후 GStreamer 애플리케이션을 작성하는 예제에서 상세하게 다룰 예정이다.

gst-discoverer-1.0

gst-typefind-1.0은 해당 파일의 포맷을 분석하는 역할에 그쳤다면, gst-discoverer-1.0은 미디어 정보를 추출하는 역할을 한다. 내부적으로 Pipeline을 구성하여 정보를 추출하고 있으며, 비디오와 오디오 그리고 부수적인 데이터 정보까지 분석하기 때문에, uridecodebin을 사용하고 있다. 이 때문에 gst-plugins-base 모듈에서 구현하고 있고, gst-typefind-1.0과 다르게 URI를 통해서 소스를 제공할 수 있다.

$ gst-discoverer-1.0 https://media.w3.org/2010/05/sintel/trailer.mp4
Analyzing https://media.w3.org/2010/05/sintel/trailer.mp4
Done discovering https://media.w3.org/2010/05/sintel/trailer.mp4

Topology:
  container: Quicktime
    audio: MPEG-4 AAC
    video: H.264 (High Profile)

Properties:
  Duration: 0:00:52.209000000
  Seekable: yes
  Live: no
  Tags:
      audio codec: MPEG-4 AAC audio
      maximum bitrate: 128000
      datetime: 1970-01-01T00:00:00Z
      title: Sintel Trailer
      artist: Durian Open Movie Team
      copyright: (c) copyright Blender Foundation | durian.blender.org
      description: Trailer for the Sintel open movie project
      encoder: Lavf52.62.0
      container format: ISO MP4/M4A
      video codec: H.264 / AVC
      bitrate: 535929

또한 미디어 파일의 정보를 추출하기 위해서 gst-discoverer-1.0은 재생이 가능한 온전한 Pipeline을 사용한다. Demuxer와 Decoder를 모두 사용하게 되며, 미디어 파일에 얻을 수 있는 메타데이터들을 수집하여 출력하고 있다. 다만 영상과 음성 출력을 목적으로 하는 것이 아니기 때문에 디코딩을 수행한 이후 발생한 데이터를 fakesink를 이용하여 버리게 된다.

다음은 실제로 위에 사용한 영상 파일을 분석하는데 사용한 Pipeline을 캡쳐한 이미지이다.

discoverer dot graph

Pipeline 내부에서 사용하는 정보들을 추출하는 것이기 때문에, gst-launch-1.0을 이용해서도 동일한 정보들을 추출할 수 있다. -t 옵션을 사용하게 되면 메타데이터를 출력하게 되는데, gst-discoverer-1.0이 출력하는 정보와 동일함을 알수 있다. 아래의 예제는 gst-discoverer-1.0이 사용하는 동일한 Pipeline을 이용하여 gst-launch-1.0에서 실행한 경우이다.

아래 예제에서 볼 수 있듯이 메타데이터(tag)는 중복해서 나타날 수 있고, gst-launch-1.0 명령 자체에는 동적으로 트랙을 구성하는 기능이 없기 때문에, 미디어 파일의 트랙 정보를 알고 있는 경우에만 Pipeline을 구성할 수 있다.

$ gst-launch-1.0 -t \
    uridecodebin uri=https://media.w3.org/2010/05/sintel/trailer.mp4 name=u \
    u. ! queue ! fakesink \
    u. ! queue ! fakesink
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Got context from element 'source': gst.soup.session=context, session=(SoupSession)NULL, force=(boolean)false;
Redistribute latency...
FOUND TAG      : found by element "fakesink1".
     audio codec: MPEG-4 AAC audio
 maximum bitrate: 128000
FOUND TAG      : found by element "fakesink1".
        datetime: 1970-01-01 00:00:00 (UTC)
           title: Sintel Trailer
          artist: Durian Open Movie Team
       copyright: (c) copyright Blender Foundation | durian.blender.org
     description: Trailer for the Sintel open movie project
         encoder: Lavf52.62.0
container format: ISO MP4/M4A
FOUND TAG      : found by element "fakesink1".
     audio codec: MPEG-4 AAC audio
 maximum bitrate: 128000
 ... (중략) ...

GStreamer User Book #1

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

GStreamer 소개

GStreamer는 미디어 정보를 처리하기 위한 소프트웨어 집합, 즉 미디어프레임워크이며, 가장 신사적인 오픈 소스 프로젝트 커뮤니티이기도 하다. 미디어 프레임워크로서 GStreamer는 사용자 애플리케이션이 영상, 음성 및 데이터를 제한된 컴퓨팅 자원과 주어진 시간 내에서 처리할 수 있도록 도와주는 역할을 수행한다. 이미 개발된 표준안과 기술들에 대해서는 소프트웨어적인 신뢰성을 제공하고,새로운 표준과 기술들에 대해서는 빠르게 적용하고 최적화할 수 있는 기본 토대를 마련해주는 것을 목표로 하고 있다.

GStreamer의 시작은 매우 단순하지만 소프트웨어 프레임워크들이 해결하고자 하는 공통적인 문제인, 코드의 재 사용성과 추상화된 계층 구조에 대한 요구 사항으로 부터 출발한다. 1999년 이전의 상황으로 잠시 돌아가서 생각해본다면, MP3 파일을 재생하는 사용자 애플리케이션 작성을 위해서는 MP3 디코더부터 구현해야만 했다. 성공적으로 MP3 재생기를 구현하였다고 하더라도, 다른 애플리케이션으로 확장을 하기 위하여 디코더 부분만을 재사용한다거나, 새로운 코덱을 지원하도록 기능을 추가한다던가 하는 작업은 온전히 소프트웨어를 구현하는 엔지니어의 역량에 의존할 수 밖에 없는 상황이었다.

그 당시에 사용할 수 있는, 재사용이 가능한 디코더 라이브러리, 혹은 미디어 재생기와 같은 프로그램은 1996년부터 사용가능 했던 Microsoft의 DirectShow나 그 이전에는 1991년부터 Apple이 공급한 QuickTime 정도에 지나지 않았으며, 특히 리눅스와 같은 환경에서 사용가능한 미디어 정보 처리를 위한 소프트웨어 라이브러리 혹은 애플리케이션은 전무한 상태였다. 당시의 설계자들이 해결하고자 했던 또다른 문제는 단순한 기능적인 요구사항뿐 아니라, 지속적인 발전이 가능한 라이센스 모델을 찾는 것이었다. VLC의 경우 GPL을 사용하여 프로젝트 공헌자들의 아이디어를 다시 오픈 소스 커뮤니티로 강제로 환원하는 정책을 선택하였으며, GStreamer는 그와 달리, 산업에서 사용할 때 보다 자유로운 LGPL을 채택함으로써 임베디드 시스템에서도 사용이 가능한 미디어프레임워크로 발전하게 되었다.

미디어프레임워크 주요 릴리즈 연도

GStreamer 구성 요소

GStreamer를 간단히 정의한다면, 플러그인들로 구성된 모듈의 집합이라고 할 수 있다. 일반적으로 인식하고 있는 모듈과 플러그인의 정의와 약간 다를 수는 있으나, GStreamer 커뮤니티에서는 모듈을 소스 코드 저장소로 정의하고 있고, 각 저장소는 기능을 모아둔 라이브러리와 플러그인으로 구성되어있다. 기능을 정의하기 위한 단위는 Element이며 하나의 기능을 구현하기 위한 기본 단위는 Element와 Pad이다. 각 Element는 다수의 Pad를 생성할 수 있고, Pad의 속성에 따라서 Element를 연결할 수 있는지가 결정된다.

각 Element를 연결하여 정보의 흐름을 제어하는 아이디어는 DirectShow에서 차용하였으나, GStreamer 프로젝트는 C 언어 기반으로 작성된 완전히 다른 미디어 프레임워크이다. 조금 더 덧붙이자면 형식적으로는 C 언어 기반으로 작성되었다고 말할 수 있으나, 정작 내부 구조는 너무나 철저하게도 객체지향적이기 때문에, 명확한 정의는 아니라고 할 수 있다. 이는 GStreamer의 기본 단위가 GObject에서 파생되었기 때문이며, 보충 설명을 위하여 연재의 한 챕터를 GObject를 설명하는데 할애하려고 한다.

GstObject 클래스 계층 구조

GStreamer 모듈

플러그인으로 나뉘어진 각 기능들을 전략적으로 관리하기 위하여 GStreamer는 다수의 모듈, 즉 저장소를 관리하고 있다. 사실 GStreamer SDK를 이용하여 애플리케이션을 작성한다고 하면 모듈 내부의 소스 코드들을 직접 분석할 필요는 없다. 그러나 단위 기능이나 Element에 대한 테스트와 예제를 tests 디렉토리에서 찾을 수 있기 때문에 코드 작성중에 올바른 사용 방법 혹은 불확실성에 대한 확신을 얻고자 한다면 SDK를 사용한다고 하더라도 저장소 코드는 직접 살펴보는 것이 검색 엔진을 통한 질의 보다 효율적이다.

gstreamer

커뮤니티에서는 core 라고 부르기도 하며, GStreamer를 구동하기 위한 필수 도구와 라이브러리를 제공한다. 특히 gst-launch, gst-inspect 처럼, CLI로 제공되는 도구들은 GStreamer의 기본적인 동작과 디버깅 기법을 이해하는데 매우 큰 도움이 된다.

gst-plugins-base

이름에서 유추할 수 있는 것처럼, 기본 라이브러리와 플러그인을 제공한다. 기능들에 대한 동작성이 철저하게 검토된 플러그인들을 관리하는 공간이며, base 모듈 내에서 버그 혹은 다른 어떠한 이슈가 발견된다면, 매우 즉각적으로 대응을 하고 있다. OpenGL을 통한 렌더링, CPU 기반의 오디오 및 비디오 처리 기능을 담당하는 Element들을 포함하고 있으며 gst-play 와 같은 High-Level Playback 기능을 사용할 수 있는 CLI를 제공하는 공간이기도 하다.

gst-plugins-good

사용자 애플리케이션을 작성하는 경우, 보통은 core, base, good 이 세 모듈을 기반으로 작성할 수 있다. 특히 임베디드 시스템에 탑재하는 경우 발생할 수 있는 라이센스의 충돌이나 의도치 않는 정책들에 대해서 비교적 자유로운 코드들이 good 모듈에 탑재된다. 기능 상으로도 누구나 좋다고 인정하는 수준의 동작성, 코드 상의 무결성 등이 검토된 라이브러리와 플러그인들이 위치한다. RTSP와 관련된 Element들이 대표적이며, V4L2 (Video for Linux) API를 이용한 기능들도 이곳에 있다.

gst-plugins-ugly

이제 이 모듈부터는 상용 소프트웨어를 제작하는데 주의가 필요하다고 보면 되는 공간이다. 모듈의 이름이 ugly이지만, 소스 코드의 품질이나 기능 동작 상태에 관련된 명칭은 아니며, 대체적으로 기능적으로는 잘 동작하지만 라이센스에 대해서 검토가 필요할 수도 있다는 정도의 의미로 해석하는 것이 좋다. RealMedia나 ASF에 대한 지원이 대표적인 기능이며, 다른 모듈과 비교하여 활동량이 적은 공간이다.

gst-plugins-bad

GStreamer 컨트리뷰터가 되고 싶지만 시작점을 찾기 어렵다거나, 새로운 플러그인을 제안하고 제작하는 것을 고려하고 있다거나, 최신 동향을 분석하기 위해서는 절대적으로 익숙해져야하는 모듈이 bad이다. ugly라는 모듈명을 정의할때 사용한 뉘앙스처럼, bad 또한 무언가 나쁜것이 있는 공간이다라고 해석해서는 안된다. 기능적으로 동작하고 검증되었지만 누군가 더 사용해서 유즈 케이스를 확보하고 싶다거나, 자신이 설계한 기능에 대한 여러 사용자들의 동의를 얻기 위한다던가, 아니면 라이센스에 대해서 검토가 이루어지지 않은 코드들에 대한 공간으로 인식하는 것이 바람직하다. 그렇기 때문에 새롭게 제작한 플러그인이 있다면 이 모듈을 타겟으로 제출하면 된다. 대표적으로 안드로이드와 iOS를 지원하는 라이브러리와 Element가 이 공간에 위치한다.

기타 저장소

core, base, good, ugly, bad는 GStreamer를 구성하는 모듈이자 저장소이지만, GStreamer 프로젝트는 이외에도 다수의 저장소로 구성되어 있다. 특정한 프로그래밍 언어를 지원하기 위한 바인딩 프로젝트와 SDK 빌드를 위한 저장소, 그리고 레퍼런스로 사용할 수 있는 예제들이 별도의 저장소에서 운영되고 있다.

  • 프로그래밍 언어 바인딩 - gst-python, gstreamer-rs, gstreamer-sharp
  • 코덱과 필터 - gst-libav
  • RTSP 서버 - gst-rtsp-server
  • 레퍼런스 애플리케이션 - gst-examples
  • SDK 빌드 - cerbero
  • GStreamer 모듈 빌드 - gst-build

GStreamer 아키텍쳐

벌써 GStreamer에 대한 필수 구성 요소들에 대한 이야기들을 많이 했지만, 정작 그들간의 관계와 어떻게 미디어 정보를 처리할 수 있는지에 대한 이야기는 아직 하지 못했다. GStreamer 아키텍쳐를 계층별 흐름대로 설명하자면, 오브젝트 단위로 구성된 (Element로 구성된) 기능들을 플러그인으로 만들고, 그 플러그인들을 런타임에 동적으로 사용할 수 있도록 하는 기능을 가진 core 프레임워크 위에 애플리케이션을 만들 수 있다.

앞서 잠깐 언급한 CLI 도구들도 GStreamer를 사용한 애플리케이션이며, 다른 형태의 미디어 처리기를 제작할 때에도 GStreamer 프레임워크를 사용할 수 있다. 다만 어떠한 플러그인을 사용할지, 어떠한 Element를 사용할지는 전적으로 GStreamer core가 담당하게 된다. 좀더 프로그래머의 언어로 설명을 하자면, 헤더 파일을 통해서 접근하고 링크를 만들 수 있는 함수는 core까지이며, 각각의 플러그인이나 Element에는 함수 단위의 접근이 불가능하다. 뒤에서 좀더 자세하게 다루겠지만, Element 단위의 접근을 하기 위해서는 Property나 Singal이 유일한 경로이며, 이 또한 Element가 명시적으로 제공하지 않는한 애플리케이션 계층에서 Element를 직접적으로 제어할 수 있는 방법은 없다. 또한 core는 가장 중요한 개념인 Pipeline을 관리할 수 있는 기능을 포함하며, 애플리케이션과 Pipeline 간의 대화가 가능한 채널인 Bus를 제공한다. 즉, core는 각 플러그인과 Element에 대한 접근을 은폐하고 추상화된 제어함수를 제공하게 되는 것이다.

GStreamer 아키텍쳐

플러그인 계층은 특수한 형태로 제작된 라이브러리의 집합이다. GStreamer는 기본적으로 런타임에 관리가 가능한 동적 로딩 방식으로 플러그인을 관리하고 있으며, 이 플러그인들은 파일 시스템 기준으로 본다면 특수하게 제작된 동적 라이브러리들이다. 그렇기 때문에 제작된 플러그인은 반드시 소스코드로 배포될 필요는 없고, 플러그인 제작자가 의도한다면 바이너리 형태의 배포가 가능하다. 이러한 플러그인 로딩 방식은 지적 재산권을 보호하기 위한 방편으로도 사용할 수 있다. 실제로 많은 SoC 벤더들이 하드웨어 코덱이나 보안을 강화한 플러그인을 배포할때 바이너리 배포 방식을 사용하고 있다.