Graphics And Media/GStreamer

Getting started GStreamer : Dynamic pipeline

kahuz 2021. 5. 28. 19:36

Getting started GStremaer 는 freedesktop의 gstreamer 예제에 대해 번역 및 참고하여 개발해보았을때 필요하다고 느꼈던 내용 들을 아주 살짝 추가/제거하여 작성했습니다.  https://gstreamer.freedesktop.org/documentation/tutorials/basic

 

3) 동적 파이프라인

목표

이번 예제에서는 프로그램 시작 부분에 모놀리 식 파이프라인을 정의하는 대신 정보를 사용할 수 있게 되면 파이프라인을 즉시 구축할 수 있는 방법에 대해 서술합니다 ( 개념 정의 새로할 것... )

이 과정을 마치면 playback 튜토리얼을 시작하는데 필요한 지식을 갖게 됩니다. 이번 예제에서 다룰 내용은 아래와 같습니다.

  • 엘레멘트를 연결할 때 더 세밀한 제어를 하는 방법
  • 제 시간에 대응할 수 있도록 이벤트에 대한 알림을 받는 방법
  • 엘레멘트가 가질 수 있는 다양한 상태

 

예제

앞에서 보았던 내용들은 파이프 라인이 재생 상태로 설정되기 전에 GStreamer가 완전히 빌드되지 않았습니다. 따라서 추가 조치를 취하지 않으면 데이터가 파이프라인 끝에 도달하고 파이프라인이 오류 메세지를 생성하고 중지될 수 있는 상태였습니다. 따라서 우리는 예제를 통해 이에 대한 추가 조치를 취할 것입니다.

본 예제에서는 multiplexed or muxed) 된 파일을 사용할 것입니다. 즉, 오디오와 비디오가 container 파일에 함께 저장된 파일을 사용하게 됩니다. 이때, container를 여는 엘레멘트를 demuxers라고 합니다. 컨테이너는 다음과 같은 종류가 있습니다.

  • Matroska (MKV), Quick Time (QT, MOV), Ogg, ASF, WMV, WMA

컨테이너에 여러 스트림 ( ex : 비디오 1개 , 오디오 트랙 2개 ) 이 포함된 경우 디먹싱 장치(demuxing device)는 이를 분리하고 다른 출력 포트를 통해 출력합니다. 이러한 방식으로 파이프라인에서 서로 다른 유형의 데이터를 처리하는 서로 다른 분기를 만들 수 있습니다.

GStreamer 엘레멘트가 서로 통신하는 포트를 패드(GstPad)라고 합니다. 데이터가 엘레멘트에 들어가는 sink pad와 데이터가 엘레멘트를 빠져나가는 src pad가 있습니다. 이름에서 알 수 있듯이 src element에는 src pad만 포함되고 sink element에는 sink pad만 포함되며 필터 엘레멘트에는 둘 다 포함할 수 있습니다.

Fig 1. 패드가 포함된 GStreamer 엘레멘트

Demuxer에는 다중화 된 데이터를 전달받는 sink pad 1개와 container에 포함된 각 스트림의 src pad들이 포함됩니다. 즉, 아래 그림과 같이 오디오와 비디오 스트림이 각각 하나, 총 두개일 경우 src pad는 2개로 구성됩니다.

Fig 2. 두 개의 소스 패드를 가지는 디먹서

 

여기에 demuxer와 두 개의 분기가 포함 된 단순화 된 파이프라인이 있습니다. 하나는 오디오 용이고 다른 하나는 비디오 용입니다. 참고로 이 예제에서 빌드 될 파이프라인과는 다른 파이프라인입니다.

Fig 3. 두 개의 분기를 가지는 디먹서

예제

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

이전 예제에서는 Gstreamer 관련 정보들을 모두 지역 변수로 관리했습니다. 이번 예제에서는 데이터 관리를 용이하게 하기 위해 구조체 변수를 통해 관리하도록 하였습니다.

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

위 함수는 예제 코드에서 pad-added 시그널이 발생했을때 호출될 함수입니다.

/* Create the 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( input data )를 raw 오디오 혹은 raw 비디오 스트림으로 변환하기 위해 필요한 모든 엘레멘트 ( src, demuxer, decoder)를 내부적으로 인스턴스화 합니다. uridecodebin에는 demuxer가 포함되므로 src pad는 초기화되지 않으며, 즉시 연결되어야합니다 ( demuxer? )

audioconvert는 오디오 디코더에 생성된 형식이 오디오 싱크에 설정된 형식과 같지 않을 수 있으므로, 모든 플랫폼에서 작동하는지 확인하여 서로 다른 오디오 형식간의 변환에 유용합니다.

audioresample은 오디오 디코더에서 생성된 오디오 샘플 속도가 오디오 싱크가 지원하는 것과 같지 않을 수 있으므로 모든 플랫폼에서 작동하는지 확인하면서 서로 다른 오디오 샘플 속도를 맞추는데 유용합니다.

autoaudiosink는 이전 예제에서 사용한 audovideosink와 같은 역할을 합니다. 오디오 스트림을 오디오 카드로 렌더링합니다.

if (!gst_element_link_many (data.convert, data.resample, data.sink, NULL)) {
  g_printerr ("Elements could not be linked.\n");
  gst_object_unref (data.pipeline);
  return -1;
}

위 코드에서 source 엘레멘트를 제외한 convert, resample, sink엘레멘트를 연결합니다. source엘레멘트는 src pad를 생성하여 연결할 것이기에 이 시점에서 연결하지 않습니다.

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

그리고 이전과 같이 소스 엘레멘트에 재생할 데이터를 설정해줍니다.

자, 이제 src pad를 제외한 엘레멘트들을 생성하고 연결해주었습니다. 다음은 src pad를 생성하고 src pad를 호출할 call-back 함수를 정의할겁니다.

/* Connect to the pad-added signal */
/*g_signal_connect(instance, detailed_signal, c_handler, data)*/
g_signal_connect (data.source, "pad-added", G_CALLBACK (pad_added_handler), &data);

GSignal는 Gstreamer에서 중요한 요소입니다. 이름 그대로 GSignal은 특정한 일이 발생했을 때 콜백을 통해 알림을 받을 수 있는 요소입니다. GSignalGObject에 등록되어 고유한 이름(ex:pad-added)으로 식별됩니다.

위 코드는 data.source , (uridecodebin) 객체에 pad-added라는 이름의 시그널을 생성하여 시그널이 발생할 때 마다 pad-added-handler함수를 호출하여 data , (Struct CustomData)를 전달하고 있습니다. 각 시그널에 대한 자세한 내용을 알고 싶다면 Gstreamer.freedesktop.org의 GstElement/Signals 를 참고하시기 바랍니다.

다음은 "pad-added"에 대한 콜백 함수입니다.

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

src는 시그널을 전달받은 엘레멘트입니다. 예제에서는 data.source, 즉 uridecodebin입니다. new_padsrc엘레멘트에 추가할 pad 객체입니다. data는 시그널 발생 시 전달받기로 등록한 data를 의미합니다.

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

pad_added_handler 콜백 함수가 호출되면 gst_element_get_static_pad함수를 이용해 data->convert로부터 sink 패드를 가져옵니다.

/* If our converter is already linked, we have nothing to do here */
if (gst_pad_is_linked (sink_pad)) {
  g_print ("We are already linked. Ignoring.\n");
  goto exit;
}

uridecodebin은 원하는 만큼의 패드를 만들 수 있으며, 각 패드에 대해 이 콜백이 호출됩니다. 위 코드는 이미 연결된 패드에 대해 새로운 패드를 연결하지 못하게하고 있습니다.

/* Check the new pad's type */
new_pad_caps = gst_pad_get_current_caps (new_pad, NULL);
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 ("It has type '%s' which is not raw audio. Ignoring.\n", new_pad_type);
  goto exit;
}

본 예제는 오디오만을 생성하고 있으므로 new_pad가 출력할 데이터 유형을 확인해주어야 합니다. 이전 코드에서 오디오를 처리하는 파이프라인(audioresample, audoaudiosink, audioconvert)를 만들었으며, 비디오에 대한 처리는 하지 않았으므로 비디오 생성 패드는 만들 수 없습니다.

gst_pad_get_current_caps는 GstCaps 구조에 래핑 된 패드의 현재 기능을 반환합니다. 패드가 지원할 수 있는 모든 기능에 대해서는 gst_pad_query_caps로 조회 가능합니다.

각 패드는 많은 기능을 제공할 수 있으므로, GstCaps는 각각 다른 기능을 나타내는 여러 개의 GstStructure를 가질 수 있습니다.

본 예제에서는 항상 단일 GstStructure를 가지며, 단일 미디어 형식을 나타내거나 현재 기능(GstCaps)가 없는 경우 NULL이 반환됩니다. 따라서 예제에서는 원하는 패드가 하나의 기능(audio)만을 가지고 있다는 것을 알고 있기에 gst_caps_get_structure()로 첫번째 GstStructure를 가져옵니다.

그 후gst_structre_get_name을 사용하여 형식의 주요 설명이 포함된 구조의 이름을 복구합니다,

/* Attempt the link */
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와 마찬가지로 src에서 sink로 연결되어야하며, 두 패드는 동일한 bin 혹은 pipeline에 있는 엘레멘트를 가지고 있어야합니다.

이제 모든 준비가 끝났습니다. 프로그램이 올바르게 실행되고 설정된 패드가 나타나면 오디오 처리 파이프라인에 연결되고, ERROR 또는 EOS까지 프로그램이 실행됩니다. 또한, 아래 코드를 통해 파이프라인의 상태가 변경되는 상황을 캐치할 수 있습니다.

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;

gst_bus_timed_pop_filteredGST_MESSAGE_STATE_CHANGED를 등록함으로써 상태 변경이 일어났을 때 위 코드를 통해 상태 정보를 얻어올 수 있게 추가된 모습이다.

나아가기

앞서 본 예제를 통해 src로부터 전달받은 파일의 오디오 재생을 확인해보았다. 추가로 오디오가 아닌 비디오 정보를 전달받아 재생하도록 해보자.

    /* Create the 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, audioconvert, audioresample, 그리고 autoaudiosink이다.

나아가기의 목적은 예제에서 오디오 데이터를 읽어오던 것을 비디오 데이터로 전환하는 것이기에 읽어올 데이터 정보를 가지는 uridecodebin은 수정할 필요가 없다. 또한 audioresample의 경우 오디오 샘플링과 관련된 엘레멘트이기에 과감히 삭제해주도록한다. 그 후 audioconvertvideoconvert로, autoaudiosinkautovideosink로 변경해주도록 한다.

    /* Create the elements */
    data.source = gst_element_factory_make ("uridecodebin", "source");
    data.convert = gst_element_factory_make ("videoconvert", "convert");
    //data.resample = gst_element_factory_make ("audioresample", "resample");
    data.sink = gst_element_factory_make ("autovideosink", "sink");

이후 코드에선 엘레멘트를 연결하는 부분의 사용하지 않는 data.resample만 연결을 제거해주면 된다.

//if (!data.pipeline || !data.source || !data.convert  || !data.resample || !data.sink) {
if (!data.pipeline || !data.source || !data.convert  || !data.sink) {    
    g_printerr ("Not all elements could be created.\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);
gst_bin_add_many (GST_BIN (data.pipeline), data.source, data.convert,  data.sink, NULL);
//if (!gst_element_link_many (data.convert, data.resample, data.sink, NULL)) {
if (!gst_element_link_many (data.convert, data.sink, NULL)) {
    g_printerr ("Elements could not be linked.\n");
    gst_object_unref (data.pipeline);
    return -1;     
}   

 

다음으로 콜백 함수를 수정해주도록 한다.

기존 예제의 콜백함수는 오디오와 관련된 패드에 대한 정의 및 활성화를 하고 있다. 따라서 우리는 오디오 패드를 비디오 패드로 전환할 필요가 있는 것이다.

이를 위해 패드의 타입을 정의하는 부분을 아래와 같이 수정해보자.

/* Check the new pad's type */
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")) {
if (!g_str_has_prefix (new_pad_type, "video")) {
	g_print ("It has type '%s' which is not video. Ignoring.\n", new_pad_type);
    goto exit;
}

모든 수정이 완료되었다면 프로젝트를 빌드하고 실행해보도록 한다. 실행 결과를 통해 정말 간단하게 오디오 출력에서 비디오 출력으로 전환된 것을 알 수 있다.

결론

이번 예제를 통해 아래 내용을 배울 수 있었다.

  • GSignals를 사용하여 이벤트 알림을 받는 방법
  • GstPadas를 연결하는 방법
  • GStreamer 엘레멘트의 다양한 상태 확인

다음 예제에서는 이전 예제에서 사용한 playbin을 활용하는 다양한 방법들이 소개된 재생(Playback) 예제 과정을 실습할 것이다. - 가 freedesktop 의 실습 내용이지만 위 내용들을 파악했다면 기초를 다 한 것이나 다름 없기에 다른 내용들을 다룰 예정이다.