Stream Seek

2015. 3. 24. 10:58MultiMedia 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, &current)){
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);
}
view raw gistfile1.c hosted with ❤ by GitHub

실행해보면 화면이 출력되고, 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, &current)){

    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를 제어할 수 있을 것이다.(희망 -_-)