2015. 3. 24. 10:58ㆍMultiMedia Framework/GStreamer
이번 포스팅에서는 스트림 위치 값 등을 얻기 위해 어떻게 파이프라인에 요청을 하는 지와 스트림의 위치를 어떻게 조작하는지에 대해 살펴볼 것이다.
일단 코드에 앞서 GstQuery 라는 객체가 새롭게 등장하는데, 이 GstQuery는 element나 pad에게 정보를 요청하기 위해 사용된다.
이전에는 파이프라인이 구성되면 EOS나 ERROR가 발생하기 전까지는 메인 function에서 계속 대기하는 것 위주로 설명을 하였는데, 이번 포스팅에서는 주기적으로 파이프라인을 깨워서 스트림 위치에 대한 정보를 요청하고 화면에 해당 위치를 출력하는 프로그램을 짤 것이다.
#include <gst/gst.h> | |
typedef struct _CustomData{ | |
GstElement *playbin; | |
gboolean playing; | |
gboolean terminate; | |
gboolean seek_enabled; | |
gboolean seek_done; | |
gint64 duration; | |
}CustomData; | |
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; | |
gst_init(&argc, &argv); | |
data.playbin = gst_element_factory_make("playbin", "playbin"); | |
if(!data.playbin){ | |
g_printerr("Not all elements could be created.\n"); | |
return -1; | |
} | |
g_object_set(data.playbin, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL); | |
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; | |
} | |
bus = gst_element_get_bus(data.playbin); | |
do{ | |
msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND, (GstMessageType)(GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION)); | |
if(msg != NULL){ | |
handle_message(&data, msg); | |
}else{ | |
if(data.playing){ | |
GstFormat fmt = GST_FORMAT_TIME; | |
gint64 current = -1; | |
if(!gst_element_query_position(data.playbin, fmt, ¤t)){ | |
g_printerr("Could not query current position.\n"); | |
} | |
if(!GST_CLOCK_TIME_IS_VALID(data.duration)){ | |
if(!gst_element_query_duration(data.playbin, fmt, &data.duration)){ | |
g_printerr("Could not query current duration.\n"); | |
} | |
} | |
g_print("Position %" GST_TIME_FORMAT "/ %" GST_TIME_FORMAT "\r", GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration)); | |
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, (GstSeekFlags)(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT), 30 * GST_SECOND); | |
data.seek_done = TRUE; | |
} | |
} | |
} | |
}while(!data.terminate); | |
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("End-Of-Stream reached.\n"); | |
data->terminate = TRUE; | |
break; | |
case GST_MESSAGE_DURATION: | |
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)); | |
data->playing = (new_state == GST_STATE_PLAYING); | |
if(data->playing){ | |
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: | |
g_printerr("Unexpected message received.\n"); | |
break; | |
} | |
gst_message_unref(msg); | |
} |
실행해보면 화면이 출력되고, console 창에 스트림 위치 값이 계속 변하는 것을 볼 수 있다.
typedef struct _CustomData{
GstElement *playbin;
gboolean playing;
gboolean terminate;
gboolean seek_enabled;
gboolean seek_done;
gint64 duration;
}CustomData;
이번에도 역시 CustomData 구조체를 하나 생성한다.
data.playbin = gst_element_factory_make("playbin", "playbin");
g_object_set(data.playbin, "uri", "http://docs.gstreamer.com/media/sintel_trailer-480p.webm", NULL);
그 후 playbin이라는 element를 하나 생성한 후 uri 속성 값을 설정하였다. playbin은 [gstreamer] hello world 찍기 에서 다뤄본 바가 있다.
msg = gst_bus_timed_pop_filtered(bus, 100 * GST_MSECOND, (GstMessageType)(GST_MESSAGE_STATE_CHANGED | GST_MESSAGE_ERROR | GST_MESSAGE_EOS | GST_MESSAGE_DURATION));
timeout에 대해 이번 포스팅에서 새롭게 등장하였다. 위 코드에 대해 설명하자면, 10초(100 ms) 동안 아무런 메시지(CHANGED, ERROR, EOS, DURATION)가 도착하지 않으면 NULL을 리턴한다. (이전에는 메시지가 없으면 계속 홀딩 상태를 유지했었다.)
메시지가 NULL이 아니면 handle_message를 실행하고 NULL일 경우에는 스트림 위치 값을 계산하게 된다.
GstFormat fmt = GST_FORMAT_TIME;
gint64 current = -1;
if(!gst_element_query_position(data.playbin, fmt, ¤t)){
g_printerr("Colud not query current position.\n");
}
gst_element_query_position을 통해 스크림 위치를 가져온다. 값은 0부터 스트림 길이까지 nanoseconds 형태로(GST_FORMAT_TIME) 리턴한다.
if(!GST_CLOCK_TIME_IS_VALID(data.duration)){
if(!gst_element_query_duration(data.playbin, fmt, &data.duration)){
g_printerr("Colud not query current duration.\n");
}
}
전체 스트림의 길이를 가져온다. 반복문 안에서 매번 가져올 필요 없이 GST_CLOCK_TIME_IS_VALID란 조건을 줘서 duration이 GST_CLOCK_TIME_NONE(시간이 정의되지 않았을 경우에만) 일 경우에만 전체 스트림의 길이를 가져온다.
g_print("Position %" GST_TIME_FORMAT "/ %" GST_TIME_FORMAT "\r", GST_TIME_ARGS(current), GST_TIME_ARGS(data.duration));
h:m:s 형태로 스트림 시간 데이터를 출력한다. GST_TIME_FORMAT은 아래와 같이 정의되어 있다.
#define GST_TIME_FORMAT "u:%02u:%02u.%09u"
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, (GstSeekFlags)(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_KEY_UNIT), 30 * GST_SECOND);
data.seek_done = TRUE;
}
스트림의 시작으로부터 상대 위치를 찾고자 할때 gst_element_seek_simple 함수를 사용한다.
GST_FORMAT_TIME은 명시된 위치를 나타내기 위한 시간 포멧이며, SeekFlag 옵션을 제공한다. Flag 옵션의 종류는 아래와 같다.
- GST_SEEK_FLAG_FLUSH : 현재 파이프라인 안에 존재하는 모든 데이터를 버린다. 만약 이 flag가 없다면 파이프라인 끝에서 새로운 위치가 나타나지 전까지 이전 데이터가 계속 나타날 것이다.
- GST_SEEK_FLAG_KEY_UNIY : 가장 가까운 keyframe을 찾는다. 이 옵션은 빠르게 위치를 찾지만 정확도가 떨어질 수 있다.
- GST_SEEK_FLAG_ACCURATE : 정확한 위치가 요청될 때 사용한다. 몇몇 포멧에 있어서는 느리게 동작 할 수 있다.
그리고 마지막 파라미터로 찾고자 하는 시작 위치를 지정할 수 있다. 기본적으로 nano second이기 때문에 초로 바꾸고자 할 경우에 GST_SECOND를 곱해주자.
case GST_MESSAGE_DURATION:
data->duration = GST_CLOCK_TIME_NONE;
break;
스트림의 길이가 변경되었을 때 해당 case문에 진입한다. 길이가 변경되면 data의 duration을 GST_CLOCK_TIME_NONE으로 설정해서 다시 한번 스트림의 길이를 체크할 수 있도록 한다.
if(data->playing){
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_STATE_PLAYING일 때 조건문에 진입한다. gst_query_new_seeking 함수는 스트림의 seeking 속성을 물어보기 위해서 새로 query 객체를 생성한다. 이렇게 생성된 query 객체를 gst_element_query 함수에 파라미터로 넣음으로써 playbin에 대한 seeking 속성 정보를 요청한다.
gst_query_parse_seeking 함수는 query결과에 대해 파싱을 진행한다. seeking 가능 여부, seeking 시작 위치, seeking 끝 위치를 결과로 알려준다.
마지막으로 query 객체를 해제하는 것을 잊어서는 안된다.
이번 장으로 우리는 미디어 플레이어의 seek bar를 제어할 수 있을 것이다.(희망 -_-)