나는 매일 틈날때마다 네이버 웹툰이나 시리즈의 무협/판타지 소설을 즐겨 읽는 사람이다. 반면에 영양가 있어 보이는 책들과는 상당한 거리감을 가진 사람이기도 하다. 하지만 최근에 친구 준호가 있어보이는 책을 읽는 모습을 보며 자극을 받아 나도 편식하지 않고 골고루 독서 해야겠다고 생각했다. 그런 와중 집에서 굴러다니던 이 책을 우연히 발견해 읽게되었다. 하버드는 커녕 미국땅도 못 밟아본 내겐 제목이 참 있어보였다.
이책은 먼저 하버드와 기타 몇몇 예시를 들어 글쓰기의 중요성에 대해 어필한다. 그리고 글쓰기 비법이라는 것을 알려준다.
말하고자 하는 주제, 의견을 먼저 쓰고 그 후엔 그 주제를 뒷받침하는 근거와 예시, 사례 등을 작성한 뒤 마지막으로 다시 한 번 말하고자 하는 바를 언급하여 강조한다.
이게 비법이다. 학창시절 열심히공부하지 않은 나도 어디서 들어본 내용이다. 비법이라기엔 뻔한 이 문장에 저자는 오레오맵이라는 이름을 붙혔다. OREO는 Opinion, Reason, Example, Opinion의 앞문자를 따 축약한 것이다. 맵이란 이를 정형화해서 글을 쓰다 삼천포로 빠지지 말자는 의도로 붙힌 것 같다.
물론 뻔한 방법이라고 해서 가치가 없진 않다. 내가 직접 말하지 못하고 들었을 때 안다고 느끼는 지식은 내 몸이 실천하여 체화하지 못한 죽은 지식이라는 얘기를 어디선가 들었다. ㅋㅋ 동감하는 바다. 이런 기회에 한 번 더 듣고 새겨 실천할 수 있다면 그것만으로 유익한 책이 될 것이다. 실천을 안하는게 문제지만.
"나는 그래도 이해가 안 간다. 이게 그리 쉬운 일이라면...... 왜 다른 문파는 그런 수련을 하지 않았다는 거냐?" "사형은 하루에 삼분의 이를 공부하고, 남은 시간에 몸가짐을 바르게 하며, 부모를 진심으로 봉양하고, 약자를 기만하지 않으며, 재물을 탐하지 않고, 위로는 예의를 다하되, 아래로는 존중을 잃지 않고, 친구를 진심으로 대하며, 나라에는 충성을 다하면서 살 수 있어?" "......못 하지." "왜 못 해? 그것만 지키면 군자가 되는데."
(시리즈 화산귀환 102화 영원히 잊지 못할 날을 만들어... 33%)
그외에도 읽는 사람이 누구인지 생각해서 공감되는 글쓰기를 하라거나 매일 조금씩 글쓰기 연습을 하라는 등 추가로 몇가지 글쓰기 조언을 해준다. 예문을 들어 설명하기도 하는데 예문이 조금 작위적이라 설명하고자 하는 내용에 집중이 잘 안됐다. 저자가 글쓰기 수업도 한다는 것 같던데 학생이 제출한 글쓰기 과제중 잘써진 글을 예시로 들었다면 더 좋았을 것 같다. 대체로 영양가 있는 글이었다. 이책 덕분에 조금이나마 글쓰기에흥미도 생겼다.
이 책의 제목은 150년 하버드 글쓰기 비법이다. 하지만, 책에서 말하는 비법이 하버드의 글쓰기 비법은 아닌 것 같다. 책에서도 오레오맵이 미국 초등학교 글쓰기 수업에서 힌트를 얻어 만든 방법이라고 밝힌다. 저자의 이력을 찾아봐도 경희대만 나오고 하버드는 안나온다. 그런데 왜 제목은 저렇게 지었을까? 아무 생각없이 영양가 있는 책 좀 읽어보자고 책을 잡은 나같은 사람이 아니라 제목만 곧이 곧대로 믿고 하버드 글쓰기 비법을 기대한 사람이라면 실망할 수도 있을 것 같다.
- 스트림(stream)의 위치(position), 길이(duration) 정보를 어떻게 얻는지?
-다른 position으로 어떻게 이동하는지(seeking)?
l 소개
"GstQuery"를 사용하면 element나 pad의 정보를 가져올 수 있다. 먼저, pipeline이 seeking을 지원하는지 확인한다. live stream처럼 seeking을 지원하지 않는 source도 있다. seeking을 지원한다면 미디어를 재생하고 10초가 지난 후에 다른 시간대로 이동해본다.
l Seeking example
역시나, “basic-tutorial-4.c”파일을 만들고 아래 코드를 넣어보자.
#pragmawarning(disable: 4819) #include<gst/gst.h> /* Structure to contain all our information, so we can pass it around */ typedefstruct_CustomData { GstElement* playbin;/* Our one and only element */ gboolean playing;/* Are we in the PLAYING state? */ gboolean terminate;/* Should we terminate execution? */ gboolean seek_enabled; /* Is seeking enabled for this media? */ gboolean seek_done;/* Have we performed the seek already? */ gint64 duration;/* How long does this media last, in nanoseconds */ } CustomData; /* Forward definition of the message processing function */ staticvoid handle_message(CustomData* data, GstMessage* msg); int main(intargc, char* argv[]) { CustomData data; GstBus* bus; GstMessage* msg; GstStateChangeReturn ret; data.playing = FALSE; data.terminate = FALSE; data.seek_enabled = FALSE; data.seek_done = FALSE; data.duration = GST_CLOCK_TIME_NONE; /* Initialize GStreamer */ gst_init(&argc, &argv); /* Create the elements */ data.playbin = gst_element_factory_make("playbin", "playbin"); if (!data.playbin) { g_printerr("Not all elements could be created.\n"); return -1; } /* Set the URI to play */ g_object_set(data.playbin, "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL); /* Start playing */ ret = gst_element_set_state(data.playbin, 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.playbin); return -1; } /* Listen to the bus */ bus = gst_element_get_bus(data.playbin); do { msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION); /* Parse message */ if (msg != NULL) { handle_message(&data, msg); } else { /* We got no message, this means the timeout expired */ if (data.playing) { gint64 current = -1; /* Query the current position of the stream */ if (!gst_element_query_position(data.playbin, GST_FORMAT_TIME, ¤t)) { g_printerr("Could not query current position.\n"); } /* If we didn't know it yet, query the stream duration */ if (!GST_CLOCK_TIME_IS_VALID(data.duration)) { if (!gst_element_query_duration(data.playbin, GST_FORMAT_TIME, &data.duration)) { g_printerr("Could not query current duration.\n"); } } /* Print current position and total duration */ g_print("Position %"GST_TIME_FORMAT" / %"GST_TIME_FORMAT"\r", GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration)); /* If seeking is enabled, we have not done it yet, and the time is right, seek */ if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) { g_print("\nReached 10s, performing seek...\n"); gst_element_seek_simple(data.playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND); data.seek_done = TRUE; } } } } while (!data.terminate); /* Free resources */ gst_object_unref(bus); gst_element_set_state(data.playbin, GST_STATE_NULL); gst_object_unref(data.playbin); return 0; } staticvoid handle_message(CustomData* data, GstMessage* msg) { GError* err; gchar* debug_info; switch (GST_MESSAGE_TYPE(msg)) { caseGST_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); data->terminate = TRUE; break; caseGST_MESSAGE_EOS: g_print("\nEnd-Of-Stream reached.\n"); data->terminate = TRUE; break; caseGST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break; caseGST_MESSAGE_STATE_CHANGED: { GstState old_state, new_state, pending_state; gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data->playbin)) { g_print("Pipeline state changed from %s to %s:\n", gst_element_state_get_name(old_state), gst_element_state_get_name(new_state)); /* Remember whether we are in the PLAYING state or not */ data->playing = (new_state == GST_STATE_PLAYING); if (data->playing) { /* We just moved to PLAYING. Check if seeking is possible */ GstQuery* query; gint64 start, end; query = gst_query_new_seeking(GST_FORMAT_TIME); if (gst_element_query(data->playbin, query)) { gst_query_parse_seeking(query, NULL, &data->seek_enabled, &start, &end); if (data->seek_enabled) { g_print("Seeking is ENABLED from %"GST_TIME_FORMAT" to %"GST_TIME_FORMAT"\n", GST_TIME_ARGS(start), GST_TIME_ARGS(end)); } else { g_print("Seeking is DISABLED for this stream.\n"); } } else { g_printerr("Seeking query failed."); } gst_query_unref(query); } } } break; default: /* We should not reach here */ g_printerr("Unexpected message received.\n"); break; } gst_message_unref(msg); }
l 상세 설명
/* Structure to contain all our information, so we can pass it around */ typedefstruct_CustomData { GstElement* playbin;/* Our one and only element */ gboolean playing;/* Are we in the PLAYING state? */ gboolean terminate;/* Should we terminate execution? */ gboolean seek_enabled; /* Is seeking enabled for this media? */ gboolean seek_done;/* Have we performed the seek already? */ gint64 duration;/* How long does this media last, in nanoseconds */ } CustomData; /* Forward definition of the message processing function */ staticvoid handle_message(CustomData* data, GstMessage* msg);
다른 함수로 데이터를 전달하기 좋게 구조체를 정의한다. 이 예제에서는 메시지 핸들링 코드를 별도 함수 “handle_message()”로 옮겼다. 코드가 갈수록 길어지기 때문이다.
그리고나서 “playbin” 엘리먼트 하나로 파이프라인을 구성한다. 이미 Basic tutorial 1: Hello world! 에서 써봤다. “playbin” 엘리먼트는 그 자체로 하나의 파이프라인이다. 이후 “playbin”의 URI 속성이나 pipeline의 재생상태 변경같은 설정을 한다.
전에는 “gst_bus_timed_pop_filtered()” 함수에 timeout 인자를 넣지 않았다. 그경우 메시지를 받을 때까지 함수가 반환되지 않는다(block). 이번엔 100ms의 timeout을 주었다. 따라서, 메시지를 못받아도 100ms가 지나면 함수는 NULL을 반환한다. 이런 점을 이용해 UI를 업데이트할 것이다.
Timeout 값이 “GstClockTime” 형이어야 한다는 점에 주의한다. 기본 단위는 나노 세컨드(ns)다. 다른 단위의 시간은 “GST_SECOND”나“GST_MSECOND” 같은 매크로에 시간값을 곱해서 사용한다.
만약 timeout 안에 메시지를 받는다면 “handle_message()” 함수를 수행할 것이고 그렇지 않으면:
l User interface refreshing
/* We got no message, this means the timeout expired */ if (data.playing) {
pipeline이 “PLAYING” 상태라면 화면을 갱신한다. 재생상태가 아니라면 아무것도 하지 않는다. 어차피 재생중이 아니면 대부분의 query는 실패한다.
대략적으로 초당 10번 정도 위 구문을 수행할 것이다. 이는 UI를 업데이트하기에 괜찮은 비율이다. 현재 미디어의 position을 화면에 출력할 것이다. 여기서 pipeline에 query하는 법을 배울 수 있다. 아래에서 볼 내용이지만 position 및 duration은 꽤나 일반적인 query임으로 “GstElement”는 이를 더 쉽게 얻을 수 있는 방법을 제공한다.
/* Query the current position of the stream */ if (!gst_element_query_position(data.playbin, GST_FORMAT_TIME, ¤t)) { g_printerr("Could not query current position.\n"); }
“gst_element_query_position()” 함수는 query 객체를 다루는 부분을 감추고 사용자에게 결과를 직접적으로 제공한다.
/* If we didn't know it yet, query the stream duration */ if (!GST_CLOCK_TIME_IS_VALID(data.duration)) { if (!gst_element_query_duration(data.playbin, GST_FORMAT_TIME, &data.duration)) { g_printerr("Could not query current duration.\n"); } }
“gst_element_query_duration()” 함수를 사용해서는 duration. 즉, stream의 길이를 알 수 있다.
/* Print current position and total duration */ g_print("Position %"GST_TIME_FORMAT" / %"GST_TIME_FORMAT"\r", GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration));
GStreamer에서 제공하는 사용자 친화적 시간 표현 방법 “GST_TIME_FORMAT”, “GST_TIME_ARGS” 매크로의 사용법을 확인한다.
/* If seeking is enabled, we have not done it yet, and the time is right, seek */ if (data.seek_enabled && !data.seek_done && current > 10 * GST_SECOND) { g_print("\nReached 10s, performing seek...\n"); gst_element_seek_simple(data.playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT, 30 * GST_SECOND); data.seek_done = TRUE; }
“gst_element_seek_simple()” 함수를 사용해서 간단하게 seeking을 수행한다. Seeking 동작의 많은 복잡한 부분이 이 함수안에 감춰져 있어 쉽게 사용할 수 있다.
인자를 확인해보자:
“GST_FORMAT_TIME”은 우리가 시간을 조정하려 한다는 것을 알린다. 다른 format은 다른 type을 사용한다.
다음으로는 “GstSeekFlags”의 가장 일반적인 flags를 확인한다:
“GST_SEEK_FLAG_FLUSH”: seek 하기 전에 pipeline에 있던 모든 데이터를 버린다. pipeline이 화면에 표시할 새 데이터를 가져오는 동안 잠시 pause된다. 이 flag를 넣지 않으면 pipeline 끝에 새 position이 나타날 때까지 stale”(오래된) 데이터가 표시될 수 있다.
“GST_SEEK_FLAG_KEY_UNIT”: 대부분의 인코딩한 비디오 stream은 키 프레임이라고 불리는 특정한 프레임으로만 위치 변경이 가능하다. 이 flag를 사용하면 가장 가까운 키 프레임으로 이동하고 해당 위치의 데이터가 없어도 일단 즉시 화면을 출력한다(화면이 버벅일 수 있다). 이 flag를 사용하지 않아도 pipeline이 내부적으로 가장 가까운 키 프레임으로 이동한다(다른 대안은 없다). 하지만 요청한 위치의 데이터가 도달하기까지 화면이 멈출 것이다. 후자의 방법이 더 정확하지만 더 오래 걸릴 수 있다.
“GST_SEEK_FLAG_ACCURATE”: 일부 미디어는 충분한 indexing 정보를 제공하지 않는다. 즉, 위치 이동에 시간이 많이 소요된다. 이경우, GStreamer가 seek할 위치를 대충 추정한다. 대부분의 경우에 잘 동작한다. 만약 정확도가 원하는 것보다 떨어진다면 이 flag를 사용한다. 하지만, Seek 위치를 계산하는데 매우 오래 걸릴 수 있다는데 주의한다.
마침내 seek할 위치를 요청했다. “GST_FORMAT_TIME”을 사용했기 때문에 넣은 값은 ns로 여겨지며 간단하게 초단위로 사용하기 위해 “GST_SECOND”의 곱을 사용했다.
l Message Pump
“handle_message()” 함수는 bus를 통해 받는 모든 메시지를 처리한다. “ERROR”, “EOS” 메시지 처리는 이전 튜토리얼과 같음으로 설명을 생략한다.
caseGST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break;
이 메시지는 stream의 duration이 변할 때마다 전달된다. 여기서는 변수의 값을 간단하게 유효하지 않은 값으로 설정하고 나중에 다시 query할 수 있도록 한다.
caseGST_MESSAGE_STATE_CHANGED: { GstState old_state, new_state, pending_state; gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state); if (GST_MESSAGE_SRC(msg) == GST_OBJECT(data->playbin)) { g_print("Pipeline state changed from %s to %s:\n", gst_element_state_get_name(old_state), gst_element_state_get_name(new_state)); /* Remember whether we are in the PLAYING state or not */ data->playing = (new_state == GST_STATE_PLAYING);
Seek나 time 관련 query는 일반적으로 “PAUSE”나 “PLAYING” 상태일 때만 유효한 값을 얻어올 수 있다. 해당 상태일 때 모든 element가 정보를 얻고 스스로 설정할 수 있기 때문이다. “playing” 변수를 사용해서 pipeline이 “PLAYING” 상태인지 여부를 추적하고 있다. “PLAYING” 상태에 진입하면 pipeline이 현재 stream에서 seek가 가능한지 query할 것이다.
if (data->playing) { /* We just moved to PLAYING. Check if seeking is possible */ GstQuery* query; gint64 start, end; query = gst_query_new_seeking(GST_FORMAT_TIME); if (gst_element_query(data->playbin, query)) { gst_query_parse_seeking(query, NULL, &data->seek_enabled, &start, &end); if (data->seek_enabled) { g_print("Seeking is ENABLED from %"GST_TIME_FORMAT" to %"GST_TIME_FORMAT"\n", GST_TIME_ARGS(start), GST_TIME_ARGS(end)); } else { g_print("Seeking is DISABLED for this stream.\n"); } } else { g_printerr("Seeking query failed."); } gst_query_unref(query); }
“gst_query_new_seeking()” 함수는 seeking type으로 “GST_FORMAT_TIME”을 받아서 새로운 query 객체를 생성한다. 이는 우리가 원하는 시간대로 이동하고자 함을 나타낸다. “GST_FORMAT_BYTES”을 사용하면 source내 특정 바이트 위치로 이동할 수 있다, 하지만 일반적으로는 덜 유용한 방법이다.
query 객체는 “gst_element_query()” 함수를 통해 pipeline에 전달된다. 결과는 query에 저장되어 “gst_query_parse_seeking()” 함수로 쉽게 검색할 수 있다. 이는 seeking이 가능한지 여부와 seeking이 가능할 경우 그 범위를 반환한다.
query가 끝난 후 자원 해제를 잊지 않도록 유의한다.
이게 끝이다. 이제 미디어의 현재 위치에 따라 진행바 UI를 주기적으로 업데이트할 수 있고 진행바를 움직여서 원하는 위치로 이동할 수 있는 플레이어 앱을 만들 수 있다.
l 결론
이 튜토리얼에서 다음을 배웠다.
-“GstQuery”를 사용해서 pipeline에게서 정보를 query하는 방법?
-“gst_element_query_position()”과 “gst_element_query_duration()”을 사용해서 position나 duration 같은 일반적인 정보를 얻는 방법?
-“gst_element_seek_simple()”을 사용해서 stream의 임의 위치로 이동하는 방법?
-이런 작업을 할 수 있는 pipeline 상태는?
다음 튜토리얼에서는 Graphical User Interface toolkit과 GStreamer를 함께 사용하는 방법을 배운다.
이번엔 파이프라인을 완벽하게 구현하지 않고 재생 상태로 바꾼다. 하지만 문제는 없다. 추가적인 조치를 하지 않으면 데이터가 파이프라인 끝에 도달한뒤 에러 메시지와 함께 멈출 것이다. 물론, 추가적인 조치를 취할 것이다…
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 파일을 만들고 아래 코드를 복붙한다.
#pragmawarning(disable: 4819) #include<gst/gst.h> // structure to contain all out information, so we can pass it to callbacks typedefstruct_CustomData { GstElement* pipeline; GstElement* source; GstElement* convert; GstElement* resample; GstElement* sink; } CustomData; // hanlder for the pad-added signal staticvoid pad_added_handler(GstElement* src, GstPad* pad, CustomData* data); int main(intargc, 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)) { caseGST_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; caseGST_MESSAGE_EOS: g_printerr("***** End of Stream\n"); terminate = TRUE; break; caseGST_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; } staticvoid 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; constgchar* 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 typedefstruct_CustomData { GstElement* pipeline; GstElement* source; GstElement* convert; GstElement* resample; GstElement* sink; } CustomData;
지금까지는 필요한 모든 정보를 지역 변수로 만들어 사용했다(기본적으로 GstElement의 포인터). 이제부턴 콜백 함수를 사용한다. 더 쉽게 다루기 위해 모든 데이터를 구조체에 넣어 그룹 지을 것이다.
// hanlder for the pad-added signal staticvoid pad_added_handler(GstElement* src, GstPad* pad, CustomData* data);
평소처럼 엘리먼트를 만들었다. "uridecodebin"은 URI를 가지고 여러 스트림으로 바꿀 것이다. 이를 위해 필요한 모든 엘리먼트를 내부에서 객체화한다(소스, 디먹서, 디코더 등). “playbin”이 하는 일의 절반만 한다. 이는 디먹서를 포함해서 처음에는 소스 패드를 사용할 수가 없고 후에 상황을 보고 연결해야 하기 때문이다.
오디오 디코더에서 만든 포맷이 오디오 싱크에서 요구하는 포맷이 아닐 수 있는데 “audioconvert”가 다른 오디오 포맷 간에 일치가 되게끔 변환을 해준다.
오디오 디코더 출력의 샘플레이트를 오디오 싱크에서 지원하지 않는 경우 “audioresample”은 서로 다른 오디오 샘플레이트(sample rates)를 변환하여 일치시킨다.
“autoaudiosink”는 이전 튜토리얼에서 본 “autovideosink”와 비슷하다. 단지 오디오를 위한 싱크라는 것만 다르다. 이 싱크가 오디오 스트림을 오디오 카드로 보낸다.
이전 튜토리얼에서 했던 것처럼 속성을(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” 시그널을 발생시킬 것이다. 이 때, 설정해둔 콜백이 호출된다.
“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(파이프라인)에 들어있는 엘리먼트 소유여야한다.
원하는 오디오 패드가 추가되면 오디오를 처리하는 파이프라인의 나머지 부분이 연결되고 ERROR나 EOS가 발생할 때까지 재생하게 된다. 그러나 상태의 개념을 설명하면서 이 튜토리얼의 내용을 약간 더 꼬아보겠다.
l GStreamer States
상태에 관해선 이미 전에 언급한 적이 있다. 파이프라인을 “PLAYING”상태로 변경하기 전까진 재생되지 않는다는 언급을 하면서였다. 여기서 상태에 관한 의미와 나머지 설명을 하도록 한다. GStreamer에는 4가지 상태가 있다.
NULL
엘리먼트의 초기 상태
READY
“PAUSED”로 변할 준비가 된 상태
PAUSED
엘리먼트가 일시 중지된 상태 데이터를 받거나 처리할 준비가 된 상태. 하지만 싱크 엘리먼트는 오직 버퍼 하나 분량의 데이터만 받고 중단된다.
PLAYING
재생 상태, 시간과 데이터가 흐른다.
서로 인접한 상태로만 변경할 수 있다. 다시 말해 NULL에서 PLAYING으로는 바꿀 수 없고 중간에 READY와 PAUSED를 거쳐야한다. 그럼에도 파이프라인을 PLAYING으로 변경하면 GStreamer가 중간에서 알아서 변경해준다.
caseGST_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”을 사용한다.
Qt Quick 2는 전용 scene graph(이하 sg)를 사용하며 그래픽 API(OpenGL ES, OpenGL, Vulkan, Metal, Direct 3D 등)가 sg를 순회하면서 그리게 된다. QPainter나 그와 유사한 기존의 imperative painting systems 대신 sg를 사용하면 프레임 사이에 scene을 유지할 수 있으며 rendering 전에 필요한 primitives 값을 알 수 있다. 이는 rendering의 많은 부분을 최적화한다.
예를 들어, user-interface에 10개 아이템이 있고 각각이 background color, icon, text를 가지는 상황에서 기존 drawing techniques를 사용하면 30번의 draw call이 필요하다. 하지만, sg의 경우 render를 위한 primitives를 재구성(reorganize)하기 때문에 background color, icons, text별로 각각 한 번씩, 단 3번의 draw call로 render할 수 있다. 이와 같은 batching과 state change reduction을 통해 performance를 증가시킬 수 있다.
Sg는 Qt Quick 2.0과 밀접하게 연관되어 있으며 Qt Quick 2.0없이 sg만 홀로 사용하지 않는다. Sg는 QQuickWindow클래스에 의해 관리되고 그려진다. Custom item types은 sg에 custom graphical primitives를 추가할 수 있으며 QQuickItem::updatePaintNode() 함수에서 추가한다.
Sg는 scene을 그래픽적으로 표현한 것으로 모든 아이템을 그리기에 충분한 정보를 가지는 독립적 구조이다. 한 번 구성되면 sg내 각 아이템의 state와 상관없이 그릴 수 있다. 대다수의 플랫폼에서 sg는 전용 render thread에서 그려지며 그동안 GUI thread는 다음 frame의 state를 준비한다.
Note: 이 페이지에서 표시하는 정보는 대부분 Qt Quick Scene graph의 기본 동작에 관한 것이다. 다른 sg를 porting하여 사용할 경우 적용되지 않는 개념이 있을 수 있다. 다른 sg adaptations에 대한 정보는 Scene Graph Adaptations에서 확인할 수 있다.
l Qt Quick Scene Graph Structure
Sg는 미리 정의된 node types으로 구성되며 각각은 나름의 전용 목적으로 사용된다. Sg를 scene graph라고 부르지만 더 정확한 명칭은 node tree이다. QML에서 sg는 QQuickItem types으로 구성되며 내부적으로 scene은 renderer에 의해 처리된다. Nodes 자체는 active drawing code나 paint() 함수를 가지지 않는다.
대게 node tree의 node는 기존 Qt Quick QML types을 사용하지만 users가 직접 custom content를 가지는 complete subtrees를 추가할 수도 있다.
l Nodes
users에게 가장 중요한 nodes는 QSGGeometryNode다. 이 node는 geometry와 material을 구현하여 custom graphics를 정의하는데 사용한다. geometry는 graphical primitive의 shape과 mesh를 정의하며 QSGGeometry를 사용하여 구현한다. Line, rectangle, polygon, 많은 끊어진 rectangles, 복잡한 3D mesh 등이 있다. material은 shape의 pixel을 어떻게 채울지를 정의한다.
Node 하나는 많은 children을 가질 수 있고 geometry nodes는 child-order로 그려진다. (children 뒤에 parent)
Note: child-order로 그려진다는 것은 renderer가 실제로 뭘 먼저 그리는지 순서를 말하는 것이 아니다. 단지 visual output의 z-order를 보장할 뿐이다.
-사용할 수 있는 nodes:
nQSGClipNode - sg에서 clipping 기능을 구현
nQSGGeometryNode - sg에서 모든 visual한 item이 사용
nQSGNode – 모든 nodes의 base class
nQSGOpacityNode – nodes의 투명도를 변경하기 위해 사용
nQSGTransformNode – transformations를 구현
Sg에 custom nodes를 추가할 때는 QQuickItem::updatePaintNode() 함수를 override하고QQuickItem::ItemHasContents flag를 설정한다.
Warning: native graphics(OpenGL, Vulkan, Metal 등)의 작업(operations)과 sg와의 상호 작용(interaction)은 render thread에서만(exclusively) 수행해야 한다. 가장 중요한 규칙은 QQuickItem::-udatePaintNode() 함수 안에서 “QSG” 접두사가 붙은 classes만 사용하는 것이다.
Nodes는 vitual QSGNode::preprocess() 함수를 가진다. 이 함수는 sg가 그려지기 전에 호출된다. Node를 상속받은 subclasses는 QSGNode::UsePreprocess flag를 설정하고 QSGNode::preprocess() 함수를 override하여 custom node의 final preparation을 할 수 있다.
l Node Ownership
Nodes의 소유권은 생성자 또는, sg에서 QSGNode::OwnedByParent flag를 설정함으로써 명시적으로 설정한다. 소유권을 설정하면 sg가 GUI thread 밖에 있을 때 쉽게 메모리 정리를 할 수 있으므로 소유권을 설정하는 것을 권장한다.
l Materials
Material은 QSGGeometryNode 내부의 geometry를 채우는 방법을 정의한다. Graphics shaders를 encapsulates하며 충분한 유연성을 제공한다. 하지만 대부분의 Qt Quick items는 solid color, texture같은 매우 간단한 basic materials를 사용한다.
QML item type에 custom shading을 적용하고자 하는 사람은 ShaderEffect type을 사용해서 QML에서 직접 적용시킬 수 있다.
-material 클래스 목록:
nQSGFlatColorMaterial – geometry를 단일 색상으로 rendering하는 편리한 방법
nQSGMaterial – shader program을 위해 rendering state를 encapsulates함
nQSGMaterialRhiShader
nQSGMaterialShader - renderer에서 opengl shader program을 표현
Sg API는 low-level이며 편의성 보다는 성능에 초점을 맞췄기 때문에 가장 간단하게 구현해도 처음부터 custom geometries와 materials를 구현하려면 적지 않은 양의 코드를 작성해야 한다. 이런 이유로, 일반적인 custom nodes를 쉽게 만들 수 있도록 몇몇 유용한 클래스를 제공한다.
ØQSGSimpleRectNode – QSGGeometryNode의 subclass로 rectangular geometry와 solid color material을 가진다.
ØQSGSimpleTextureNode – QSGGeometryNode의 subclass로 rectangular geometry와 texture material을 가진다.
l Scene Graph and Rendering
Sg의 rendering은 QQuickWindow클래스 내부에서 수행되며 이에 접근할 수 있는 public API는 없다. 대신 user가 rendering pipeline에 접근할 수 있도록 몇몇 places를 제공한다. 이 위치에서 custom sg content를 추가하거나 직접 graphics API를 호출하여 원하는 rendering commands를 삽입할 수 있다. 이는 sg가 사용한다. 위치는 render loop에서 정의한다.
사용 가능한 render loop variants는 [ “basic”, “windows”, “threaded” ] 3개가 있다. 이중 “basic"과 “windows”는 single-thread이고 “threaded”는 sg rendering을 위한 전용 thread를 따로 가진다. Qt는 현재 사용하는 플랫폼과 그래픽 드라이버에 기반해서 적합한 loop를 선택한다. 결과가 만족스럽지 않는 경우엔 QSG_RENDER_LOOP 환경 변수를 사용해서 원하는 loop의 사용을 강제할 수 있다. 어떤 render loop가 사용 중인지 확인하려면 qt.scenegraph.general logging category를 enable 한다.
Note: “threaded”와 “windows” render loop는 graphics API 구현에 따라 throttling 관련 설정이 달라진다. 예를 들어, OpenGL의 경우 swap interval을 1로 요청한다. 어떤 그래픽 드라이버는 사용자가 qt의 요청을 무시하고 다른 설정을 적용할 수 있도록 허용하기도 한다. Swap buffers와 같은 throttling 관련 작업에서 blocking을 하지 않으면 render loop가 CPU를 100% 사용할 것이다. 만약 system이 vsync-based throttling을 지원하지 않는다면 basic render loop를 사용하기를 권한다.
l Threaded Render Loop (“threaded”)
대부분의 경우 sg rendering은 전용 render thread에서 수행시킨다. 이는 multi-core processors의 병렬성을 향상시키고 지연 시간을 잘 활용할 수 있는 방법이다. 뚜렷하게 성능을 향상시킬 수 있지만 sg와 interaction을 할 시간, 장소에 제약이 생긴다.
다음은 threaded render loop와 OpenGL을 사용하여 하나의 frame을 어떻게 그리는지에 대한 간단한 개요이다. 각 단계는 다른 graphics APIs에서도 동일하다(OpenGL context 부분 제외).
1.사용자 입력이나 animation의 결과로 QML scene에 변경사항이 생기고 QQuickItem::update() 함수가 호출된다. 새로운 frame을 준비하도록 render thread에 event를 전달한다.
2.Render thread가 새 frame을 그리기 위해 준비한다. GUI thread block을 initiates한다.
3.Render thread가 새 frame을 그리기 위해 준비하는 동안 GUI thread는 QQuickItem::update-Polish() 함수를 호출하여 items을 render하기 전 final touch-up한다.
4.GUI thread가 block된다.
5.QQuickWindow::beforeSynchronizing() signal을 발생시킨다. App은 Qt::DirectConnection으로 해당 signal과 slots을 연결하여 QQuickItem::updatePaintNode()함수가 호출되기 전에 필요한 준비를 할 수 있다. (DirectConnection으로 연결할 경우 slots은 signaling thread에서 수행된다.)
6.이전 frame이후 변경된 모든 items에서 QQuickItem::updatePaintNode()함수를 호출하여 sg과 QML state를 동기화한다.
7.GUI thread block이 해제된다.
8.Sg가 render된다
A.QQuickWindow::beforeRendering() signal을 발생시킨다. App은 Qt::DirectConnection으로 signal slots을 연결해서 custom graphics API calls을 수행할 수 있다. 이 때 그리는 내용은 QML scene 아래에(z-order) 그려진다.
B.QSGNode::UsePreprocess가 설정된 items의 QSGNode::preprocess() 함수를 호출한다.
C.Renderer가 nodes를 처리한다.
D.Renderer는 states를 생성하고 사용중인 graphics API로 draw calls를 기록한다.
E.QQuickWindow::afterRendering() signal을 발생시킨다. App은 Qt::DirectConnection으로 signal slots을 연결해서 custom graphics API calls을 수행할 수 있다. 이 때 그리는 내용은 QML scene 위에(z-order) 그려진다.
F.이 단계가 되면 frame이 완성된다. opengl의 경우 buffers를 교체하고 다른 graphic API의 경우(Vulkan, Metal) 현재 명령을 기록하고 command buffers를 graphics queue에 제출한다. 이후 QQuickWindow::frameSwapped() signal을 발생시킨다.
9.Sg가 render되는 동안 GUI thread는 advance animations를 수행하거나 events를 처리하는 등의 할 일을 한다.
현재 windows(opengl32.dll), linux(Mesa llvmpipe 제외), macOS, Metal, mobile platforms, Embedded Linux(EGLFS), 기타 Vulkan등에서 threaded renderer를 default로 사용하고 있다. (추후 변경될 수 있다.)
두 점 A, B를 잇는 하나의 선분위에 점 A에서 점 B로 이동하는 점 M0가 있다. 이 때, 점 M0가 A에서 출발하여 B에 도착하기 까지의 진행도를 0에서 1사이의 값을 가지는 변수 t로 설정한다. M0가 A위에 있을 때 t=0일 것이며 B를 향해 움직임에 따라 t의 값이 증가하여 마침내 B에 도달했을 때 t=1이 될 것이다. 이 때 점 M0의 이동 궤적을 1차 Bezier curves라고 한다.
이 상황에서 점 C를 추가하고 B와 잇는다. 마찬가지로 점 B에서 점 C를 향해 이동하는 점 M1이 있고 이동함에 따른 정도를 동일한 변수 t로 표기하면 M0와 M1이 동시에 출발하고 도착하게 된다. 그 과정에서 M0와 M1을 잇는 선분을 그릴 수 있고 그 선분을 동일하게 t의 정도로 이동하는 점 N0를 그릴 수 있다.
이 점 N0가 이동하는 궤적을 2차 Bezier curves라고 한다. 마찬가지로 계속 점을 추가하여 3차, 4차 Bezier curves를 만들 수 있다.
코드에서 보이듯 gst_element_factory_make() 함수를 사용해서 새 엘리먼트를 생성한다. 첫 번째 함수 인자는 생성할 엘리먼트의 타입이다. (Basic tutorial 14: Handy elements에서 몇까지 주로 사용하는 엘리먼트 타입을 다룬다. Basic tutorial 10: GStreamer tools에선 사용할 수 있는 모든 엘리먼트 목록을 확인할 수 있다.) 두 번째 인자는 생성한 엘리먼트 객체에 부여하고자 하는 이름이다. 엘리먼트의 이름을 지음으로써 포인터를 따로 저장하지 않은 엘리먼트를 나중에 찾을 수 있다. (또한, 보다 의미 있는 디버그 출력을 확인할 수 있다.) NULL을 입력할 경우엔 GStreamer가 임의로 유일한 이름을 지어준다.
이 예제에선 “videotestsrc”와 “autovideosink” 두 엘리먼트를 만들었다. 필터 엘리먼트는 만들지 않았다. 따라서 파이프라인은 다음과 같다.
videotestsrc는 소스 엘리먼트로(데이터를 생산한다.) 테스트 비디오 패턴을 만든다. 이 엘리먼트는 디버깅이나 테스트 용도로 유용하게 사용된다.
autovideosink는 싱크 엘리먼트이다.(데이터를 소비한다.) 전달받은 이미지를 윈도우에 표시한다. 운영체제에 따라서 다양한 종류의 비디오 싱크가 있는데 autovideosink는 자동으로 가장 적절한 것을 찾아서 객체화한다. 따라서 어떤 비디오 싱크를 생성할 지 고민해서 구체적으로 명시할 필요가 없고 플랫폼에 독립적인 코드를 작성할 수 있다.
l 파이프라인 생성
/* create the empty pipeline */ pipeline = gst_pipeline_new("test-pipeline");
GStreamer의 모든 엘리먼트는 사용되기 전에 반드시 하나의 파이프라인에 포함되어야 한다. 왜냐하면, 파이프라인이 시간 조절이나 메시지 관련 함수를 관리하기 때문이다. 예제에선 파이프라인을 만들기 위해 gst_pipeline_new()함수를 사용했다.
/* build the pipeline */ gst_bin_add_many(GST_BIN(pipeline), source, sink, NULL); if (gst_element_link(source, sink) != TRUE) { g_printerr("Elements could not be linked.\n"); gst_object_unref(pipeline); return -1; }
파이프라인은 bin의 일종이다. bin은 다른 엘리먼트를 포함하는 엘리먼트이다. 때문에 bin에 사용할 수 있는 모든 함수는 파이프라인에도 사용할 수 있다.
예제의 경우에 gst_bin_add_many()함수가 파이프라인에 엘리먼트를 넣는데 사용되었다. 이 함수는 추가할 엘리먼트의 목록을 인자로 받고 마지막에 NULL을 받는다. 각 엘리먼트는 gst_bin_add()함수를 써서 따로따로 추가할 수도 있다.
하지만 파이프라인에 추가한 엘리먼트들이 아직까지 서로 연결된 것은 아니다. gst_element_link()함수를 사용해서 엘리먼트들을 서로 연결한다. 이 함수는 첫 번째 인자는 소스 엘리먼트이고 두 번째 인자는 싱크 엘리먼트이다. 인자의 순서는 중요하다. 데이터의 흐름이 소스 엘리먼트에서 싱크 엘리먼트 방향으로 결정되기 때문이다. 동일한 bin에 들어있는 엘리먼트끼리만 연결될 수 있다는데 주의한다. 연결하기 전에 파이프라인에 엘리먼트들을 추가해야한다.
l 속성
GStreamer의 모든 엘리먼트는 GObject의 일종이다. GObject는 속성(property) 기능을 제공한다.
대부분의 엘리먼트는 속성을 가지며 엘리먼트의 행동을 바꾸기 위해 속성 값을 바꿀 수 있다. (값을 쓸 수 있는;writable 속성) 또는 속성을 통해 엘리먼트의 내부 상태 값을 읽을 수도 있다. (읽을 수 있는;readable 속성)
속성을 읽는 데는 g_object_get()함수를 사용하고 쓰는 데는 g_object_set()함수를 쓴다.
g_object_set()함수는 속성 이름, 값 쌍의 리스트를 인자로 받고 마지막에 NULL을 인자로 받는다. 덕분에 복수개의 속성을 한 번의 함수 호출로 설정할 수 있다.
속성을 다루는 함수들이 “g_” 접두사를 사용하는 이유이다.
예제에서 사용예를 찾아보면 아래와 같다.
/* modify the source's properties */ g_object_set(source, "pattern", 0, NULL);
위 구문은 videotestsrc 엘리먼트의 “pattern” 속성 값을 0으로 바꾸고 있다. 엘리먼트가 출력하는테스트 비디오의 타입을 제어하는 속성이다. 한 번 다른 값으로 바꿔도 보자.
엘리먼트가 제공하는 모든 속성의 이름과 변경 가능한 값의 범위 등 정보는 gst-inspect-1.0 도구를 써서 찾을 수 있다. 또는, 사용할 엘리먼트의 문서를 보고 찾을 수도 있다.
l 에러 확인
이제까지 전체 파이프라인을 수동으로 구성하고 설정하였다. 남은 부분은 이전 튜토리얼과 비슷하다. 하지만 에러 확인하는 부분이 약간 추가되었다.
/* start playing */ ret = gst_element_set_state(pipeline, GST_STATE_PLAYING); if (ret == GST_STATE_CHANGE_FAILURE) { g_printerr("Unable to set the pipeline to the playing state.\n"); return -1; }
이전과 같이 gst_element_set_state()함수를 호출했다. 하지만, 이번엔 에러 확인을 위해 반환 값을 추가로 확인한다. 상태를 변경하는건 전용 프로세스에서 수행된다. 보다 자세한 내용은 Basic tutorial 3: Dynamic pipelines를 참조한다.
/* wait until error or eos */ bus = gst_element_get_bus(pipeline); msg = gst_bus_timed_pop_filtered(bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS); /* 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); break; case GST_MESSAGE_EOS: g_print("End-Of-Stream reached.\n"); break; default: /* we should not reach here because we only asked for errors and eos */ g_printerr("Unexpected message received.\n"); break; } gst_message_unref(msg); }
gst_bus_timed_pop_filtered() 함수는 실행 동안 대기했다가 종료되면 GstMessage를 반환한다. 우리는 gst_bus_timed_pop_filtered() 함수를 호출할 때 EOS나 Error가 발생하면 함수가 반환되도록 요청했다. 따라서, 반환된 값이 둘 중 무엇인지 확인하고 화면에 메시지를 출력할 필요가 있다. (아마 실제 앱에선 단순 출력이 아니라 더 복잡한 처리를 하게 될 것이다.)
GstMessage는 다양한 목적으로 사용할 수 있는 구조체이다. 어떤 종류의 정보든지 전달할 수 있다. 다행히도 GStreamer에서 GstMessage를 파싱할 수 있도록 몇 가지 함수를 제공한다.
예제에서는 GST_MESSAGE_TYPE()을 사용해서 GstMessage가 에러를 포함하고 있는지 확인하고 gst_message_parse_error() 함수를 사용한다. 이 함수는 GLib의 GError 구조체와 디버깅에 유용한 문자열을 반환한다. 사용되었다는 걸 알 수 있다. 이후에 이 GError 구조체와 문자열을 어떻게 사용하고 해제하는지 코드를 살펴보자.
l 버스
이즈음해서 GStreamer 버스를 살펴볼 필요가 있다. 버스의 객체는 엘리먼트에서 발생한 GstMessages를 앱으로 전달하는 역할을 한다. 발생 순서대로, 앱의 쓰레드로 전달한다. 마지막 언급한 부분이 중요하다. 실제 미디어의 스트리밍은 앱과 다른 쓰레드에서 수행된다.
메시지는 gst_bus_timed_pop_filtered() 함수를 통해 버스로부터 동기적으로 추출할 수 있다. 이와 비슷하게 시그널을 사용하면 비동기적으로 추출할 수 있다(다음 튜토리얼에서 다룬다). 앱은 항상 버스를 관찰하여 미디어 재생 중 에러나 재생관련 이슈가 발생했는지 확인해야한다.
나머지 코드는 사용한 객체를 정리하는 코드로 이전 튜토리얼의 예제와 동일하다.
l 연습
만약 연습이 필요하다고 느낀다면 다음을 수행한다: 비디오 필터 엘리먼트를 위 예제의 소스와 싱크 사이에 추가한다. 필터 엘리먼트는 “vertigotv”를 사용하도록 한다. 엘리먼트를 생성하고 파이프라인에 추가하고 다른 엘리먼트와 연결해야한다.
사용하는 플랫폼과 설치된 플러그인에 따라 “negotiation” 에러가 날 수도 있다. 이는 싱크가 필터의 출력을 이해하지 못하기 때문이다(negotiation에 대한 자세한 내용은 Basic tutorial 6: Media formats and Pad Capabilities를 참조한다). 이경우 “videoconvert” 필터를 “vertigotv” 엘리먼트 이후에 추가한다(결과적으로 4개 엘리먼트로 파이프라인을 구성한다.).
l 결론
이 튜토리얼에서 확인한 내용:
-엘리먼트를 만든 방법은? gst_element_factory_make()
-빈 파이프라인을 만든 방법은? gst_pipeline_new()
-어떻게 파이프라인에 엘리먼트를 추가하였는가? gst_bin_add_many()
-어떻게 엘리먼트들을 서로 연결했는가? gst_element_link()
예제를 실행하면 왼쪽과 같이 테스트 비디오가 나옵니다. 연습대로 필터를 추가하면 오른쪽과 같이 테스트 비디오에 약간 흐림 효과가 들어가서 나옵니다.
소프트웨어 라이브러리를 이해하는데 화면에 “Hello World”를 출력하는 것 보다 좋은 예제는 없다. 하지만 우리는 멀티미디어 프레임워크를 다루기 때문에 Hello World를 출력하는 대신 샘플 비디오 하나를 재생할 것이다.
아래 코드의 양만 보고 지레 겁먹지 않기를 바란다: 동작과 관련한 코드는 단 4줄이며 나머지는 정리 코드이다. C언어에서는 항상 코드가 길어진다.
l Hello World
basic-tutorial-1.c 파일을 만들고 아래 코드를 복사 붙여넣기 한다.
#include <gst/gst.h> int main (int argc, char *argv[]) { GstElement *pipeline; GstBus *bus; GstMessage *msg; /* Initialize GStreamer */ gst_init (&argc, &argv); /* Build the pipeline */ pipeline = gst_parse_launch ("playbin uri=https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm", NULL); /* Start playing */ gst_element_set_state (pipeline, GST_STATE_PLAYING); /* Wait until error or EOS */ bus = gst_element_get_bus (pipeline); msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS); /* See next tutorial for proper error message handling/parsing */ if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) { g_error ("An error occurred! Re-run with the GST_DEBUG=*:WARN environment " "variable set for more details."); } /* Free resources */ gst_message_unref (msg); gst_object_unref (bus); gst_element_set_state (pipeline, GST_STATE_NULL); gst_object_unref (pipeline); return 0; }
위 코드 파일을 컴파일한다. 성공적으로 빌드가 되면 실행파일을 실행한다. 팝업 윈도우가 뜨고 인터넷으로부터 전송되는 비디오가 오디오와 함께 출력될 것이다.
-필요한 라이브러리: gstreamer-1.0
위 tutorial은 윈도우 하나를 열고 영상을 출력한다. 미디어는 인터넷에서 가져온다. 그래서 인터넷 연결 상태에 따라 윈도우가 늦게 나타날 수 있다. 또한, 예제 코드에서는 지연 시간을(latency) 관리(buffering)하지 않는다. 때문에 연결상태가 안 좋은 경우 영상이 멈출 수 있다. 이 issue를 해결하려면 Basic tutorial 12: Streaming을 확인한다.
위 위 구문은 이번 튜토리얼의 핵심이자 키포인트 두 가지를 제시한다: gst_parse_launch(), playbin.
l gst_parse_launch
GStreamer는 멀티 미디어의 흐름을 다루는 프레임워크이다. 미디어는 “source” 엘리먼트에서(생산자) “sink” 엘리먼트로(소비자) 이동한다(to down). 그 과정에서 여러가지 중간 엘리먼트를 지나오며 각 엘리먼트에서는 데이터를 조작할 수 있다. 연결된 엘리먼트의 묶음을 “pipeline”이라고 한다.
보통은 여러 개의 엘리먼트를 수동으로 붙여서 파이프라인을 만들지만 고급 기능이 필요 없고 파이프라인이 간단하다면 shortcut으로 “gst_parse_launch()”함수를 사용할 수 있다.
이 함수는 파이프라인을 표현한 문자열을 입력으로 받아서 실제 파이프라인을 생성하며 굉장히 유용하다. 사실, 이 함수는 너무 유용해서 관련하여 만들어진 tool도 있다. 계속해서 튜토리얼을 진행함에 따라 이 tool을 매우 잘 알게 될 것이다. (gst-launch-1.0 도구와 그 문법에 대한 상세 설명은 Basic tutorial 10: GStreamer tools를 참조한다.)
l playbin
그래서, 예제에선 어떤 종류의 파이프라인이 gst_parse_launch()을 통해 만들어질까? 여기서 두 번째 키포인트를 살펴본다: 우리는 playbin이라고 불리는 엘리먼트 하나로 구성된 파이프라인을 만들고 있다.
playbin은 특별한 엘리먼트로 source, sink 둘의 역할을 모두 한다. 자체로 하나의 파이프라인을 구성할 수 있다. 내부적으로 필요한 모든 엘리먼트를 생성하고 연결하여 미디어를 재생시킨다.
수동 파이프라인과 다르게 매우 자세한 제어는 할 수 없지만 충분한 customization도 제공한다.
이 예제에서 우리는 오직 하나의 파라미터만 playbin에 전달하고 있다. 재생시키고자 하는 미디어의 URI이다. URI을 바꿔보자. “https://”인지 “file://”인지에 따라 playbin이 적절한 source를 만들 것이다.
URI를 잘못 입력했거나 파일이 존재하지 않거나 plug-in을 빠뜨리는 등 실수할 수 있는데 GStreamer는 이에 대해 몇 가지 알림 시스템도 제공한다.
/* Start playing */ gst_element_set_state (pipeline, GST_STATE_PLAYING);
이 구문에는 또 다른 개념이 숨어있다. “state”이다. 모든 GStreamer 엘리먼트는 상태와 관련이 있다. DVD 플레이어에서 재생/중지 버튼을 생각해보면 어느 정도 알 수 있다. 아직까지는 이것만 알아도 충분하다. 생성한 파이프라인의 상태를 PLAYING으로 설정하지 않는 한 미디어는 재생되지 않는다.
위 구문에서 gst_element_set_state()함수는 파이프라인을 PLAYING 상태로 설정하여 미디어를 재생한다.
/* Wait until error or EOS */ bus = gst_element_get_bus (pipeline); msg = gst_bus_timed_pop_filtered (bus, GST_CLOCK_TIME_NONE, GST_MESSAGE_ERROR | GST_MESSAGE_EOS);
이 구문은 에러가 발생하거나 미디어가 끝날 때까지 대기한다. gst_element_get_bus()함수는 파이프라인의 버스를 찾는다. gst_bus_timed_pop_filtered()함수는 버스를 통해 에러나 EOS가 반환될 때까지 block된다. Bus에 관해서는 Basic tutorial 2: GStreamer concepts를 참조한다.
이제 끝났다. 앞으로는 GStreamer가 모든 것을 관리한다. 미디어가 끝나거나 에러가 발생할 때(윈도우를 닫거나 인터넷이 끊겼을 때) 실행이 끝난다. 앱은 콘솔창에서 Ctrl+C를 눌러서 언제든지 끌 수 있다.
l 정리
앱이 끝나기 전에 몇 가지 정리해야 할 것이 있다.
/* See next tutorial for proper error message handling/parsing */ if (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) { g_error ("An error occurred! Re-run with the GST_DEBUG=*:WARN environment " "variable set for more details."); }
항상 사용하는 함수에 대한 문서를 읽어라. 이를 통해 objects를 사용한 후 자원 해제를 해야 하는지 알 수 있다.
예제의 경우 gst_bus_timed_pop_filtered()함수는 message를 반환한다. 이는 gst_message_unref()함수로 자원 해제되어야 한다.
gst_element_get_bus()함수는 bus 객체에 대한 참조를 가져왔다. 이 역시 gst_object_unref()함수로 자원을 해제해야 한다. 파이프라인을 NULL 상태로 설정하기 전에 모든 할당한 자원을 해제해야 한다. 마지막으로 파이프라인을 해제한다.
l 결론
첫 번째 예제가 끝났다. 이 예제의 간결성으로 이 프레임워크가 얼마나 강력한지 알 수 있었기를 바란다.
간단히 이번 예제에서 배운 것을 요약한다.
-어떻게 GStreamer를 초기화 하는가? gst_init()
-어떻게 빠르게 문자 표현으로 파이프라인을 만들 수 있나? gst_parse_launch()
-어떻게 playbin을 사용해서 자동으로 재생하는 파이프라인을 만들 수 있나?
-어떻게 GStreamer가 재생을 시작하도록 만들 수 있나? gst_element_set_state()
-어떻게 GStreamer가 모든 것을 진행하는 동안 프로그램을 유지시킬 수 있나? gst_element_get_bus() & gst_bus_timed_pop_filtered()
다음 튜토리얼은 계속해서 더 많은 기본 GStreamer 엘리먼트를 소개한다. 그리고 어떻게 수동으로 파이프라인을 구성하는지 배울 것이다.
이 전 글에서 gstreamer 관련 설치 후 단순히 복붙해서 돌려봤던 예제입니다. 아직 잘 모르지만 일단 예제 분석한 것만으로 판단했을 때 확실히 튜토리얼에서 말하는 것처럼 고수준으로 구현되어 있는 것 같습니다. 몇 줄 끄적이니 영상이 나오네요... 이런 걸 어떻게 만들었는지.. 대단합니다.
이 튜토리얼은 아직까지는 C언어 버전만 쓰였다. 따라서, C를 알아야 한다. C가 객체 지향 언어는 아니지만 GStreamer 프레임워크는 GObject를 사용하므로 객체지향 개념에 대한 지식이 약간이라도 있으면 좋다. GObject, Glib 라이브러리에 대한 지식은 필수는 아니지만 물론 알면 좋다.
l 소스코드
모든 튜토리얼은 self-contained 프로젝트이며 전체 코드를 공개한다. 코드 snippets은 해석 글과 전체 코드와 함께 제공한다. (makefiles 또는 project files 같은 다른 필요한 파일들도 같이 제공한다.) GStreamer 설치 페이지에서 배포한다.
l GObject, GLib에 대한 간단한 설명
GStreamer는 GObject(객체 지향적인 코딩을 위해), GLib(공통 알고리즘을 위해) 라이브러리를 기반하여 작성되었다. 즉, 앞으로 위 라이브러리의 함수들을 사용하게 될 것이다. 물론 튜토리얼을 진행하는데 이 라이브러리에 대한 깊은 지식은 필요하지 않지만 알면 확실히 좋다.
l 문서
GObject, GLib의 래퍼런스 가이드와 GStreamer 문서를 참조한다.
l 구조
튜토리얼은 여러 파트로 구성된다.
-Basic tutorials: 아래 튜토리얼을 이해하기 위해 필요한 일반적인 주제 설명
-Playback tutorials: 미디어 재생 애플리케이션을 작성하기 위해 필요한 모든 것
runtime은 gstreamer 바이너리 프로그램을 설치하는 것이고 develop이 라이브러리인 것으로 보입니다. 저는 둘다 받았습니다. 프로그램의 경우 사용하시려면 환경변수를 등록하는 것이 좋습니다.
다운로드 후 내용입니다.
bin 폴더에는 동적 라이브러리(*.dll),
lib 폴더에는 정적 라이브러리(*.lib),
include 폴더에는 헤더 파일(*.h)가 들어있습니다.
일단은 visual studio를 설치했음으로 hello world를 한 번 돌려보았습니다.
..만 위와 같은 에러가 발생했습니다.
폭풍 검색후 위 그림처럼 솔루션 탐색기에서 프로젝트를 선택한 후 "프로젝트 속성" - "VC++ 디렉터리" 메뉴에서 "포함 디렉터리" 항목과 "라이브러리 디렉터리"에 각각 "C:\Program Files (x86)\Windows Kits\10\..." 경로를 추가하였습니다.
추가로, 일단 저는 범용적인 이름의 라이브러리를 넣다보니 됐습니다만... 필요한 라이브러리가 무엇인지 어떻게 알 수 있을까요? 라는 궁금증이 생겼습니다.
일단 documents에서 함수-라이브러리 관계를 찾는 것이 1번일 것입니다. 그런데, 물론 제가 못 찾는거겠지만... 못찾겠습니다... 방법을 생각하다가 *.lib 파일을 vim으로 열어보니(물론 바이너리지만) 함수 이름이 text로 들어가는 것을 확인했습니다. 즉, 2번 방법으로는 grep 또는 그와 유사한 것으로 lib 경로에서 함수명으로 검색해서 함수가 들어있는 library를 찾을 수 있을 것 같습니다. (아래는 wt; windows terminal(ubuntu)에서 grep을 사용한 결과입니다.)
찾아보니 powershell에서도 select-string(sls)라는 것을 통해 찾을 수 있을 것 같습니다.
아래에서 merge와 rebase에 대해 알아본다. 각 동그라미는 하나의 commit을 뜻한다. 즉, 변경 사항이다. *이 붙어 있는 branch가 현재 선택한 branch임으로 이를 주의 깊게 본다.
* merge
merge의 뜻은 '병합하다, 하나로 합친다.' 이다. 위의 변경은 "git merge bugFix" 명령을 수행한 결과이다. merge나 rebase를 사용할 때 어느 branch가 새로 만들어진 commit을 가리키게 되는지 헷갈릴 때가 있는데 현재 branch를 주어로 하여 명령이 수행된다는 것을 기억하면 헷갈릴일이 없다. *을 보고 현재 branch가 main임을 알 수 있다. C2, C3 두 commit의 변경 사항을 모두 반영한 새로운 commit C4를 생성하고 현재 branch인 main이 가리키도록 한다.
* rebase
merge와 유사하다. 위의 변경은 "git rebase main" 을 수행한 결과이다. 현재 branch는 bugFix이다. 현재 branch인 bugFix가 가리키는 commit C2의 base를 main이 가리키는 C3으로 변경(rebase)한다.
merge, rebase 모두 서로 다른 두 commit의 내용을 합친 새 commit을 생성한다는 점에서 동일한 효과를 가진다. 변경 후의 그림을 보면 rebase는 마치 branch를 여러 개로 분기하지 않고 하나의 branch에서 commit을 계속 수행한 것처럼 보인다. 한 commit의 base를 다른 commit 아래로 변경한다는 개념 때문인데 덕분에 git history가 더 깔끔해 보인다.