Basic tutorial 4: Time management
l 목표
이번 튜토리얼에선 GStreamer의 시간 관련 기능 사용법을 배운다. 특히:
- 스트림(stream)의 위치(position), 길이(duration) 정보를 어떻게 얻는지?
- 다른 position으로 어떻게 이동하는지(seeking)?
l 소개
"GstQuery"를 사용하면 element나 pad의 정보를 가져올 수 있다. 먼저, pipeline이 seeking을 지원하는지 확인한다. live stream처럼 seeking을 지원하지 않는 source도 있다. seeking을 지원한다면 미디어를 재생하고 10초가 지난 후에 다른 시간대로 이동해본다.
l Seeking example
역시나, “basic-tutorial-4.c”파일을 만들고 아래 코드를 넣어보자.
#pragma warning(disable: 4819) #include <gst/gst.h> /* Structure to contain all our information, so we can pass it around */ typedef struct _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 */ static void handle_message(CustomData* data, GstMessage* msg); int main(int argc, 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; } static void handle_message(CustomData* data, GstMessage* msg) { 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); data->terminate = TRUE; break; case GST_MESSAGE_EOS: g_print("\nEnd-Of-Stream reached.\n"); data->terminate = TRUE; break; case GST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break; case GST_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 */ typedef struct _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 */ static void handle_message(CustomData* data, GstMessage* msg); |
다른 함수로 데이터를 전달하기 좋게 구조체를 정의한다. 이 예제에서는 메시지 핸들링 코드를 별도 함수 “handle_message()”로 옮겼다. 코드가 갈수록 길어지기 때문이다.
그리고나서 “playbin” 엘리먼트 하나로 파이프라인을 구성한다. 이미 Basic tutorial 1: Hello world! 에서 써봤다. “playbin” 엘리먼트는 그 자체로 하나의 파이프라인이다. 이후 “playbin”의 URI 속성이나 pipeline의 재생상태 변경같은 설정을 한다.
msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND, GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION); |
전에는 “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” 메시지 처리는 이전 튜토리얼과 같음으로 설명을 생략한다.
case GST_MESSAGE_DURATION: /* The duration has changed, mark the current one as invalid */ data->duration = GST_CLOCK_TIME_NONE; break; |
이 메시지는 stream의 duration이 변할 때마다 전달된다. 여기서는 변수의 값을 간단하게 유효하지 않은 값으로 설정하고 나중에 다시 query할 수 있도록 한다.
case GST_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를 함께 사용하는 방법을 배운다.
'개발 > c' 카테고리의 다른 글
[번역][gstreamer] basic tutorial 3: Dynamic pipelines (0) | 2022.01.28 |
---|---|
[번역][gstreamer] basic tutorial 2: Manual Hello world! (0) | 2022.01.18 |
[번역][gstreamer] basic tutorial 1: Hello world! (0) | 2022.01.11 |
[번역][gstreamer] Tutorials을 시작하며... (0) | 2022.01.10 |
[gstreamer] windows에서 설치 및 예제 빌드 (0) | 2022.01.04 |