Basic tutorial 3: Dynamic pipelines

l 목표

이전 튜토리얼에서 설명하지 않은 GStreamer 기본 개념의 나머지 부분을 설명한다. 파이프라인 구성에 필요한 정보가 부족할 경우 대기했다가 정보가 충분해졌을 때 파이프라인을 구성할 수 있다.

이번 튜토리얼에선 Playback tutorials를 시작하기위해 미리 알아야할 내용을 배울 것이다. 내용은 다음과 같다:

-       엘리먼트를 연결할 때 어떻게 더 잘 컨트롤할 수 있나?

-       관심있는 이벤트를 받고 제시간에 처리하는 방법?

-       엘리먼트의 다양한 상태엔 어떤 것들이 있나?

l 소개

이번엔 파이프라인을 완벽하게 구현하지 않고 재생 상태로 바꾼다. 하지만 문제는 없다. 추가적인 조치를 하지 않으면 데이터가 파이프라인 끝에 도달한뒤 에러 메시지와 함께 멈출 것이다. 물론, 추가적인 조치를 취할 것이다

multiplexed(오디오와 비디오가 함께 저장되어 있는) 파일을(container) 열 것이다. 컨테이너 형식에는 Matroska(MKV), Quick Time(QT, MOV), Ogg, Advanced Systems Format(ASF, WMV, WMA) 등이 있다. 이런 컨테이너를 여는 엘리먼트를 디먹서"demuxers"라고 부른다.

만약 컨테이너에 streams이 여럿 있다면(예를 들어, 비디오1개, 오디오 트랙 2개) 디먹서는 각 데이터 스트림을 분리하고 각각을 다른 출력 포트로 보낼 것이다. 이런 방법으로 파이프라인에 다른 타입의 데이터를 다루는 브랜치"branches"가 여럿 생긴다.

GStreamer 엘리먼트 간의 소통에 사용되는 포트는 패드“pads”(GstPad)라고 부른다. 엘리먼트로 데이터가 들어가는 통로를 싱크 패드라고 하고 엘리먼트에서 데이터가 나가는 통로를 소스 패드라고 한다. 당연하게도 소스 엘리먼트는 소스 패드만 가지고 있고 싱크 엘리먼트는 싱크 패드만 가지며 필터 엘리먼트는 둘 모두를 가진다.

디먹서는 먹스된 데이터의 입력용 싱크 패드를 한 개 가지며 컨테이너에서 분리한 스트림 한 개마다 소스 패드를 하나 씩 가진다.

아래는 디먹서와 두 개 브랜치를 가지는 파이프라인의 도식화이다. 두 브랜치 중 하나는 오디오용이며 다른 하나는 비디오용이다. 참고로 아래 그림 속 파이프라인은 이번 튜토리얼의 예제가 아니다.

디먹서를 사용할 때 어려운 점은 소스가 어떤 스트림을 얼마나 가지고 있는지 알기 전까지 아무것도 할 수 없다는 점이다. , 디먹서는 다른 엘리먼트와 연결하기 위한 소스 패드가 하나도 없는 상태에서 시작한다. 따라서 파이프라인은 디먹서에서 끝난다.

해결책은 소스에서 디먹서까지 파이프라인을 구성하고 일단 동작(play) 시키는 것이다. 디먹서가 데이터를 받아서 컨테이너에 들어있는 스트림의 종류와 갯수를 알면 그때 소스 패드를 생성한다. 이때가 새로 추가된 소스 패드를 연결하고 파이프라인 구성을 마칠 때이다.

이 예제에서는 보다 쉽게하기 위해 비디오 패드를 무시하고 오디오 패드만 연결할 것이다.

l Dynamic Hello World

basic-tutorial-3.c 파일을 만들고 아래 코드를 복붙한다.

#pragma warning(disable: 4819)
#include <gst/gst.h>
 
// structure to contain all out information, so we can pass it to callbacks
typedef struct _CustomData {
        GstElement* pipeline;
        GstElement* source;
        GstElement* convert;
        GstElement* resample;
        GstElement* sink;
} CustomData;
 
// hanlder for the pad-added signal
static void pad_added_handler(GstElement* src, GstPad* pad, CustomData* data);
 
int main(int argc, char** argv) {
 
        CustomData data;
        GstBus* bus;
        GstMessage* msg;
        GstStateChangeReturn ret;
        gboolean terminate = FALSE;
 
        // initialize
        gst_init(&argc, &argv);
 
        // create elements
        data.source = gst_element_factory_make("uridecodebin", "source");
        data.convert = gst_element_factory_make("audioconvert", "convert");
        data.resample = gst_element_factory_make("audioresample", "resample");
        data.sink = gst_element_factory_make("autoaudiosink", "sink");
        data.pipeline = gst_pipeline_new("test-pipeline");
        if (!data.pipeline || !data.source || !data.convert || !data.resample || !data.sink) {
               g_printerr("elements creation failed\n");
               return -1;
        }
 
        // build the pipeline. note that we are not linking the source at this point. we will do it later
        gst_bin_add_many(GST_BIN(data.pipeline), data.source, data.convert, data.resample, data.sink, NULL);
        if (!gst_element_link_many(data.convert, data.resample, data.sink, NULL)) {
               g_printerr("elements linking failed\n");
               gst_object_unref(data.pipeline);
               return -1;
        }
 
        // set the uri to play
        g_object_set(data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
 
        // connect to the pad-added signal
        g_signal_connect(data.source, "pad-added", G_CALLBACK(pad_added_handler), &data);
 
        // start playing
        ret = gst_element_set_state(data.pipeline, 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.pipeline);
               return -1;
        }
 
        // listen to the bus
        bus = gst_element_get_bus(data.pipeline);
        do {
               msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_STATE_CHANGED);
 
               // parse message
               if (msg != NULL) {
                       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);
                              terminate = TRUE;
                              break;
                       case GST_MESSAGE_EOS:
                              g_printerr("***** End of Stream\n");
                              terminate = TRUE;
                              break;
                       case GST_MESSAGE_STATE_CHANGED:
                              // we are only interested in state-changed messages from the pipeline
                              if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data.pipeline)) {
                                      GstState old_state, new_state, pending_state;
                                      gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
                                      g_print("Pipeline state changed from %s to %s:\n",
                                              gst_element_state_get_name(old_state), gst_element_state_get_name(new_state));
                              }
                              break;
                       default:
                              g_printerr("***** Unexpected Message received.\n");
                              break;
                       }
               }
               gst_message_unref(msg);
        } while (!terminate);
 
        // free resources
        gst_object_unref(bus);
        gst_element_set_state(data.pipeline, GST_STATE_NULL);
        gst_object_unref(data.pipeline);
        return 0;
}
 
static void pad_added_handler(GstElement* src, GstPad* new_pad, CustomData* data) {
        GstPad* sink_pad = gst_element_get_static_pad(data->convert, "sink");
        GstPadLinkReturn ret;
        GstCaps* new_pad_caps = NULL;
        GstStructure* new_pad_struct = NULL;
        const gchar* new_pad_type = NULL;
 
        g_print("Received new pad '%s' from '%s':\n", GST_PAD_NAME(new_pad), GST_ELEMENT_NAME(src));
 
        if (gst_pad_is_linked(sink_pad)) {
               g_print("We are already linked. Ignoring.\n");
               goto exit;
        }
 
        new_pad_caps = gst_pad_get_current_caps(new_pad);
        new_pad_struct = gst_caps_get_structure(new_pad_caps, 0);
        new_pad_type = gst_structure_get_name(new_pad_struct);
        if (!g_str_has_prefix(new_pad_type, "audio/x-raw")) {
               g_print("Type is '%s' but link failed.\n", new_pad_type);
               goto exit;
        }
 
        ret = gst_pad_link(new_pad, sink_pad);
        if (GST_PAD_LINK_FAILED(ret)) {
               g_print("Type is '%s' but link failed.\n", new_pad_type);
        }
        else {
               g_print("Link succeeded (type: '%s').\n", new_pad_type);
        }
exit:
        if (new_pad_caps != NULL)
               gst_caps_unref(new_pad_caps);
        gst_object_unref(sink_pad);
}

l 상세 설명

// structure to contain all out information, so we can pass it to callbacks
typedef struct _CustomData {
        GstElement* pipeline;
        GstElement* source;
        GstElement* convert;
        GstElement* resample;
        GstElement* sink;
} CustomData;
 

지금까지는 필요한 모든 정보를 지역 변수로 만들어 사용했다(기본적으로 GstElement의 포인터). 이제부턴 콜백 함수를 사용한다. 더 쉽게 다루기 위해 모든 데이터를 구조체에 넣어 그룹 지을 것이다.

// hanlder for the pad-added signal
static void pad_added_handler(GstElement* src, GstPad* pad, CustomData* data);
 

위 구문은 밑에서 사용하기 전에 미리 전방 선언을 한 것이다.

        // create elements
        data.source = gst_element_factory_make("uridecodebin", "source");
        data.convert = gst_element_factory_make("audioconvert", "convert");
        data.resample = gst_element_factory_make("audioresample", "resample");
        data.sink = gst_element_factory_make("autoaudiosink", "sink");
 

평소처럼 엘리먼트를 만들었다. "uridecodebin" URI를 가지고 여러 스트림으로 바꿀 것이다. 이를 위해 필요한 모든 엘리먼트를 내부에서 객체화한다(소스, 디먹서, 디코더 등). “playbin”이 하는 일의 절반만 한다. 이는 디먹서를 포함해서 처음에는 소스 패드를 사용할 수가 없고 후에 상황을 보고 연결해야 하기 때문이다.

오디오 디코더에서 만든 포맷이 오디오 싱크에서 요구하는 포맷이 아닐 수 있는데 “audioconvert”가 다른 오디오 포맷 간에 일치가 되게끔 변환을 해준다.

오디오 디코더 출력의 샘플레이트를 오디오 싱크에서 지원하지 않는 경우 “audioresample”은 서로 다른 오디오 샘플레이트(sample rates)를 변환하여 일치시킨다.

“autoaudiosink”는 이전 튜토리얼에서 본 “autovideosink”와 비슷하다. 단지 오디오를 위한 싱크라는 것만 다르다. 이 싱크가 오디오 스트림을 오디오 카드로 보낸다.

        if (!gst_element_link_many(data.convert, data.resample, data.sink, NULL)) {
               g_printerr("elements linking failed\n");
               gst_object_unref(data.pipeline);
               return -1;
        }
 

위에서 컨버터, 리샘플, 싱크를 연결한다. 하지만 소스는 연결하지 않는다. 이 시점에는 가지고 있는 소스 패드가 없기 때문이다. 소스와 컨버터는 일단 연결하지 않고 놔둔다.

        // set the uri to play
        g_object_set(data.source, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL);
 

이전 튜토리얼에서 했던 것처럼 속성을(property) 사용해서 재생할 파일의 URI를 설정한다.

l Signals

        // connect to the pad-added signal
        g_signal_connect(data.source, "pad-added", G_CALLBACK(pad_added_handler), &data);
 

“GSignals” GStreamer내에서 상당히 중요하다. 뭔가 알아야할 일이 발생했을 때 개발자가 알 수 있도록 한다(한마디로 콜백). 시그널은 이름으로 식별한다. GObject 각각은 자신만의 시그널을 가진다.

위 구문에서 우리는 “uridecodebin” 소스의 “pad-added” 시그널을 연결한다. 이를 위해 “g_signal_connect()” 함수를 사용한다. 인자로는 시그널 발생시 호출할 콜백 함수와 데이터 포인터를 넘긴다. 이 데이터 포인터는 GStreamer가 건드리지 않으며 오직 콜백에서 사용할 목적으로 사용자가 원하는 포인터를 넣는다. 여기에선 “CustomData” 구조체의 포인터를 전달했다.

GstElement가 발생시키는 시그널은 GStreamer 문서에서 찾을 수 있고 또는, “gst-inspect-1.0” 도구를 사용해서 찾을 수도 있다. 도구 관련 설명은 Basic tutorial 10: GStreamer tools를 참조한다.

이제 준비가 끝났다. 파이프라인을 “PLAYING” 상태로 변경하고 이전 튜토리얼처럼 버스를 통해 관심있는 메시지를 관찰한다(ERROR, EOS ).

l The callback

소스 엘리먼트가 충분한 정보를 얻었다면 소스 패드를 생성하고 “pad-added” 시그널을 발생시킬 것이다. 이 때, 설정해둔 콜백이 호출된다.

static void pad_added_handler(GstElement* src, GstPad* new_pad, CustomData* data) {
 

“src”는 시그널을 발생시킨 GstElement이다. 이 예제에선 연결한 시그널이 하나뿐임으로 당연히“uridecodebin”일 것이다. 첫번째 인자는 항상 시그널을 발생시킨 객체다.

“new_pad”GstPad“src” 엘리먼트에 추가된 패드이다. 보통 이 패드가 우리가 연결할 패드다.

“data”는 시그널을 연결할 때 넣었던 포인터다. 이 예제에선 “CustomData”의 포인터이다.

        GstPad* sink_pad = gst_element_get_static_pad(data->convert, "sink");
 

“CustomData”에서 컨버터 엘리먼트를 가져오고 “gst_element_get_static_pad()”를 사용해 싱크 패드를 찾아낸다. 이 패드가 “new_pad”와 연결할 패드다. 이전 튜토리얼에서는 한 엘리먼트와 그에 대응하는 엘리먼트를 연결했고 GStreamer가 내부에서 적절한 패드를 선택했다. 이번엔 우리가 직접 패드를 연결한다.

        if (gst_pad_is_linked(sink_pad)) {
               g_print("We are already linked. Ignoring.\n");
               goto exit;
        }
 

“uridecodebin”은 필요한 만큼 많은 패드를 만들 수 있다. 그리고 패드를 생성할 때마다 콜백이 호출될 것이다. 위의 구문은 한 번 연결한 패드에 다시 연결 시도하는 것을 방지한다.

        new_pad_caps = gst_pad_get_current_caps(new_pad);
        new_pad_struct = gst_caps_get_structure(new_pad_caps, 0);
        new_pad_type = gst_structure_get_name(new_pad_struct);
        if (!g_str_has_prefix(new_pad_type, "audio/x-raw")) {
               g_print("Type is '%s' but link failed.\n", new_pad_type);
               goto exit;
        }
 

새로운 패드가 출력할 데이터의 타입을 확인한다. 오디오 데이터를 생산하는 패드에만 관심이 있기 때문이다. 위에서 이미 오디오를 처리할 파이프라인의 일부분을 만들었다(“audioconvert”“audioresample”“autoaudiosink”에 연결했었다). 비디오 같은 데이터와 관련된 패드는 연결하지 않을 것이다.

“gst_pad_get_current_caps()”는 패드의 현재 caps(capabilities) 가져온다(현재 출력하는 데이터의 종류 의미). 이 정보는 “GstCaps” 구조체에 들어있다. 패드가 지원하는 모든 종류의 캡스는 “gst_pad_query_caps()”로 찾을 수 있다. 패드 하나가 많은 캡스를 제공한다. 각각은 서로 다르다. 그리고 GstCaps는 많은 GstStructure을 가진다. 패드의 현재 캡스는 항상 하나의 GstStructure을 가진다. 이는 미디어 포맷 한 개를 나타낸다. 만약 현재 캡스가 없다면 NULL을 반환한다.

예제의 경우 패드가 오디오 캡스 하나만 가진다고 알고 있기 때문에 “gst_caps_get_structure”를 사용하여 첫 번째 GstStructure를 가져온다.

“gst_structure_get_name()”를 사용하여 structure의 이름을 찾는다. 이게 포맷에 대한 주요 설명이다(실제 미디어 타입).

만약 이름이 “audio/x-raw”가 아니라면 이건 디코드한 오디오 패드가 아니다. 이름이 일치하는 경우에 연결을 시도한다.

        ret = gst_pad_link(new_pad, sink_pad);
        if (GST_PAD_LINK_FAILED(ret)) {
               g_print("Type is '%s' but link failed.\n", new_pad_type);
        }
        else {
               g_print("Link succeeded (type: '%s').\n", new_pad_type);
        }
 

“gst_pad_link()”는 두 패드를 연결한다. “gst_element_link()”처럼 연결하는 두 패드는 상대적으로 하나는 소스가 되고 다른 하나는 싱크가 된다. 두 패드는 동일한 bin(파이프라인)에 들어있는 엘리먼트 소유여야한다.

원하는 오디오 패드가 추가되면 오디오를 처리하는 파이프라인의 나머지 부분이 연결되고 ERROREOS가 발생할 때까지 재생하게 된다. 그러나 상태의 개념을 설명하면서 이 튜토리얼의 내용을 약간 더 꼬아보겠다.

l GStreamer States

상태에 관해선 이미 전에 언급한 적이 있다. 파이프라인을 “PLAYING”상태로 변경하기 전까진 재생되지 않는다는 언급을 하면서였다. 여기서 상태에 관한 의미와 나머지 설명을 하도록 한다. GStreamer에는 4가지 상태가 있다.

NULL 엘리먼트의 초기 상태
READY “PAUSED”로 변할 준비가 된 상태
PAUSED 엘리먼트가 일시 중지된 상태 데이터를 받거나 처리할 준비가 된 상태. 하지만 싱크 엘리먼트는 오직 버퍼 하나 분량의 데이터만 받고 중단된다.
PLAYING 재생 상태, 시간과 데이터가 흐른다.

서로 인접한 상태로만 변경할 수 있다. 다시 말해 NULL에서 PLAYING으로는 바꿀 수 없고 중간에 READYPAUSED를 거쳐야한다. 그럼에도 파이프라인을 PLAYING으로 변경하면 GStreamer가 중간에서 알아서 변경해준다.

                       case GST_MESSAGE_STATE_CHANGED:
                              // we are only interested in state-changed messages from the pipeline
                              if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data.pipeline)) {
                                      GstState old_state, new_state, pending_state;
                                      gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
                                      g_print("Pipeline state changed from %s to %s:\n",
                                              gst_element_state_get_name(old_state), gst_element_state_get_name(new_state));
                              }
                              break;
 

상태 변경과 관련된 버스 메시지를 듣는 코드이다. 상태 변화와 관련한 로그를 화면에 출력하고 있다. 모든 엘리먼트는 자신의 현재 상태와 관련해 메시지를 보낸다. 그래서 오직 파이프라인에서 보낸 메시지만 듣도록 필터링했다.

대부분의 앱은 재생을 위해 PLAYING, 일시 중지를 위해 PAUSE, 프로그램 종료 후 모든 자원을 해제하기 위해 NULL로 상태 변경하는 것에 대해서만 생각하면 된다.

l 연습

동적으로 패드를 연결하는 것은 많은 프로그래머에게 어려운 주제였다. 이 주제를 마스터했다는 것을 증명해보자. “autovideosink”를 객체화하고(아마도 앞에는 "videoconvert"가 있어야 할 것이다.) 패드가 나타났을 때 디먹서와 연결하도록 한다. *힌트: 이미 비디오 패드 포맷을 화면에 출력한적이 있다.

이제 “Basic tutorial 1: Hello world!”와 동일한 비디오가 나와야한다. 해당 튜토리얼에선 “playbin”을 사용했었다. 디먹싱과 패드 연결 등 모든 것을 자동으로 해주는 유용한 엘리먼트이다. Playback tutorials에서는 대부분 “playbin”을 사용한다.

"uridecodebin" - "videoconvert" - "autovideosink"

uri: "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm"

l 결론

이번 튜토리얼에서 배운 내용:

-       어떻게 GSignals를 사용해서 이벤트 발생을 알아챌 수 있는가?

-       어떻게 부모 엘리먼트 대신 GstPad를 직접 사용해서 연결할 수 있는가?

-       다양한 GStreamer 엘리먼트의 상태

또한 동적으로 파이프라인을 구성하기 위해 위의 지식을 결합했다. 프로그램 시작 때 정의하지 않고 미디어 관련 정보가 생겼을 때 생성, 연결하는 방법을 사용했다.

계속해서 기본 튜토리얼을 공부하며 Basic tutorial 4: Time management에서 시간과 관련된 동작에 대해 배우거나 Playback tutorials로 넘어가서 playbin 엘리먼트에 대해 더 자세히 배울 수 있다.

+ Recent posts