This article is a Korean translated version of this. All right reserved to original author.
Gstreamer Dynamic Pipelines
오랜 기간 동안 다루어지고 있는 또다른 Gstreamer 관련 주제 중 하나는 Application에서 어떻게 pipeline을 동적으로 구성할 수 있느냐는 것입니다.
이 내용은 pipeline을 중지 하지 않고 playing 상태에서 pipeline 내의 각 element이 재연결을 실행하는 것을 말합니다.
이러한 동작을 어떻게 할 수 있는지 설명해보도록 하겠습니다. 그렇다고 해서 여기서 기본적이거나 단순하 경우만 살펴보려고 하는 것은 아닙니다.
demuxer나 decodebin에서 state가 PLAYING으로 설정되었을때 pad를 생성하고 연결하는 방법과 같은 것은 제 예제 코드도 이야기를 하고 있습니다만, 이러한 것을 설명하는 문서들은 이미 충분히 많이 있습니다.
또한 여기의 두 예제는 몇 버그 사항 때문에 Gstreamer 1.2.3 혹은 그 이상에서만 동작이 가능합니다.
The Theory
pipeline을 동적 구성하는 것에 대해서 어려운 점은 무엇일까요? 왜 pipeline이 동작하고 있지 않을때처럼 각 element와 그 pad들을 아무때나 재연결할 수는 없는 것일까요?
여러분의 집에 있는 배수관을 하나의 예로 생각해보세요. 배수로중에 무언가를 변경하려고 한다면, 관로를 따라 아무것도 흐르지 않을 때 하는 것이 좋을 것입니다. 그렇지 않으면 큰 문제가 생기겠죠.
Pad Probes
Gstreamer는 이 문제를 pad probe를 통해서 다루고 있습니다. Pad Probe는 특정 조건을 만나면 호출되는 callback을 등록할 수 있게 되어있습니다.
이러한 조건들은 flag type으로 표현되어 있습니다.
- GST_PAD_PROBE_TYPE_BUFFER: buffer가 도착했을 때
- GST_PAD_PROBE_TYPE_QUERY_UPSTREAM: upstream query를 만났을 때
- GST_PAD_PROBE_TYPE_IDLE
- GST_PAD_PROBE_TYPE_BLOCK
The callback
probe callback은 특정 조건을 만났을 때 호출이 됩니다. callback이 호출 되었을 때 어떠한 조건이었는가, 어떠한 데이터가 있었는가는 전달된 information structure를 통해서 알 수 있습니다. 이 information structure에는 buffer, event, query 등이 들어있게 됩니다.
callback에서 얻은 데이터들은 단순히 읽을 수 있는 것 뿐이아니라, 변경하는 것도 가능합니다.
callback 안에서 무언가 작업을 완료했다면, 그에 맞는 리턴 타입으로 돌려줘야합니다.
데이터가 전달될 필요가 있다면 GST_PAD_PROBE_PASS, drop 해야하는 경우라면 GST_PAD_PROBE_DROP, 데이터를 전달하면서 probe를 제거하려면 GST_PAD_PROBE_REMOVE, 기본값으로는 GST_PAD_PROBE_OK를 사용할 수 있으며, 추후에 더 확장될 예정입니다.
callback은 임의의 thread로부터 호출 될 수 있다는 것에 주의해야합니다. 이를 호출한 thread가 main application thread라는 것을 보장하지는 않습니다.
모든 serialized event, buffer, query는 이와 관련된 streaming thread로 부터 호출 될 것입니다.
또한 callback은 여러번 호출 될 수 있다는 것을 명심해야합니다. GST_PAD_PROBE_REMOVE를 리턴하려고할때에도 다른 thread에 의해서 해당 callback이 호 출 될 수도 있습니다.
Blocking Types
blocking 타입에 대한 조건은 매우 흥미로운 주제입니다. blocking 타입을 사용하지 않으면 probe callback은 단순 알람의 기능으로 사용할 수 있지만, 이 글의 주제와는 맞지 않습니다.
blocking 타입으로 지정되어 호출된 probe는 pad를 block 상태로 만듭니다. 이는 probe가 제거되거나 GST_PAD_PROBE_PSS가 리턴되지 않는 한 probe 조건과 맞는 데이터가 더이상 통과하지 못한다는 것을 의미합니다. 이는 조건에 맞지 않는 데이터들은 callback을 아무런 조작없이 통과할 수 있다는 것을 의미합니다
특히 GST_PAD_PROBE_TYPE_DATA_BOTH가 지정된경우에는 데이터의 흐름이 차단되며, downstream 방향의 pad는 안전하게 재 연결작업을 할 수 있다는 의미가 됩니다. pipeline의 한 부분에 대해 연결을 재설정할 수 있기 위해서는 해당 포인트의 데이터흐름이 더이상 없다는 것을 확신할 수 있어야 합니다.
GST_PAD_PROBE_TYPE_IDLE 의 경우에는 pad가 idle이 되는 상태에서 호출 됩니다. 즉 현재 아무런 데이터가 흐르지 않는다는 것이됩니다. 이는 gst_pad_add_probe가 호출 되는 즉시 발생할 수 있고, gst_pad_add_probe를 호출한 thread로 부터 바로 불리게 됩니다. 또는 다음번에 도착하는 buffer, event, query들이 처리된 이후에 호출 될 수도 있습니다.
GST_PAD_PROBE_TYPE_BLOCK은 데이터를 전송하기 전에 pad를 block하고 호출 됩니다. 이는 pad의 기능을 pending하고 block하고 있는 동안에 buffer, event, query를 조작할 수 있는 기능을 제공합니다.
GST_PAD_PROBE_TYPE_BLOCK의 주요 기능은 현재 pending된 data를 가져올 수 있다는 것이고, GST_PAD_PROBE_TYPE_IDLE의 경우는 즉시 호출이 보장된다는 점이 주요 기능입니다. (이후 데이터가 도착하는지 여부와는 관계없이 호출됩니다) gs t_pad_add_probe를 호출한 thread로부터 직접 불리게 된다는 점이 단점이기도 합니다.
사용하는 방법에 따라서 이 두가지는 적절하게 선택되어야 합니다.
이제 예제코드를 보도록하겠습니다.
Example 1: Inserting & removing a filter
이 예제에서는 decodebin과 navseek element와 연결된 videosink를 이용할 것입니다. 이는 커서 키를 이용해서 지원되는 비디오 파일을 보고 seek을 수행할 수 있도록 도와줄 것입니다.
매 5초 간격으로 video effect filter가 sink 앞에 추가 되었다가 제거될 것입니다. 이 작업은 playback을 멈추거나 seek 때문에 화면이 정지하거나 하는 상황없이 진행됩니다.
setting up everything
main 함수에서 pipeline을 만들고 모든 부분을 연결했습니다. 그리고 GstElement::pad-added 시그널도 연결해둔뒤에 mainloop을 실행했습니다.
pad-added callback에서는 decodebin 이 생성한 첫번재 video pad를 converter와 video sink 에 연결합니다. 또한 매 5초마다 filter를 추가 혹은 제거하기 위한 timout callback을 등록했습니다.
이 시점 이후부터 pipeline은 PLAYING으로 전환되고 비디오가 출력될 것입니다.
The insertion/removal of the filter
timeout callback은 매우 심심한 함수 입니다. gst_pad_add_probe를 호출하여 IDLE probe를 연결하는 일밖에는 하지 않습니다. 그리고 동시 호출로 부터 보호하기 위해서 여기에 몇가지 변수를 초기화해 두었습니다.
IDLE probe를 사용한 이유는 callback이 불렸을 때 데이터에는 관심이 없고, callback이 가능하면 즉시 호출되기를 원하기 때문입니다.
이제 실질적인 filter의 추가와 제거는 probe callback에서 수행됩니다. 이부분이 사실상 우리가 관심있어하는 부분입니다.
이제 atomic operation으로 이전에 호출된적이 있는지 먼저 검사를 하고, 추가와 제거를 실행합니다. 두 가지 경우 모두, 모든 element가 각자의 pad 연결이 잘되어있고 적절한 state로 전이 되어있음을 확실히 해두어야 합니다.
또한 filter 앞쪽에 video convert를 넣어두어서 decoder의 출력물을 filter가 잘 사용할 수 있게 해주어야 합니다.
이것이 알고자하는 모든 경우였습니다. 조금 더 복잡한 경우는 gst-plugins-base에 있습니다.
주요 차이점은 BLOCK probe를 사용한 것이고, 교체를 하기 전에 EOS event를 비워 냈다(drain)는 점입니다.
filter 앞에 추가된 BLOCK probe에 의해서 EOS event가 filter에 전송되었고, 뒤쪽에 있는 다른 callback에 의해서 처리되고 있음을 알 수 있습니다.
probe 에는 EOS event가 수신될때까지 모든 데이터가 전송되며, EOS event를 수신하였을 때 filter만 제거하면 됩니다.
이렇게 하는 경우는 filter 내부에 버퍼를 가지고 있는 경우도 있기 때문입니다.
IDLE 대신에 BLOCK probe를 여기서 사용하는 이유는 잠재적으로 application의 main thread로 부터 EOS event를 받은 것처럼 되고, 이는 EOS event가 다른 probe에 도달하여 filter를 제거할 수 있을 때까지 block 되기 때문입니다.
Example 2: Adding & removing sinks
두번째 예제는 decodebin을 통해서 video를 재생하되, 매 3초마다 무작위로 video sink를 추가하거나 제거하는 것입니다.이는 tee element를 통해서 video stream을 복제하는 과정이 들어있습니다.
Setting up everything
main 함수에서 pipline을 설정하고 모든 부분을 이미 연결해 두었습니다. 그리고 GstElement::pad-added 시그널을 decodebin에 등록하고 mainloop을 실행 합니다. 이는 이전 예제와 동일하며, 아직은 sink를 여기에 추가하지는 않았습니다.
pad-added callback 에서 decodebin과 tee element를 연결하고 첫번째 srcpad를 tee로 부터 가져옵니다. 그리고나서 첫번째 sink 에 연결합니다.
첫번째 sink는 fakesink(sync=TRUE) element로 이는 항상 사용될 것입니다.
이는 화면상에 현재 보여지는 것이 없다고 하더라도 비디오 재생은 항상 진행된다는 것을 보장해줍니다. 최종적으로 3초마다 호출되는 타이머 callback을 등록합니다.
Addition of sinks
timout callback 에서 random number를 발생해서 sink를 연결할지 제거할지를 결정하게 됩니다.
timeout callback을 통해 새로운 sink를 추가하는 것으로 모든 작업은 진행됩니다. 현재 데이터가 흐르고 있지 않기 때문에 pad probe 없이 main thread로 부터 이작업은 완료됩니다.
새로운 tee의 srcpad는 여기서 만들어지고, 만약 tee가 어떠한 데이터를 새로 만들어진 pad로 전송한다면 해당 데이터는 drop 될 것입니다.
새로운 sink를 추가 하기 위해서 tee에서 srcpad를 요청했고 이것을 queue, video converter, sink에 연결합니다. 그리고 모든 state를 동기화 하게 됩니 다. 여기에서 추가한 sink를 잊지 말아야 합니다.
모든 tee srcpad 뒤에 있는 queue element는 필수입니다. 만약 queue가 없다면 tee는 lockup 될 것입니다. (tee의 srcpad는 단일 thread로 부터 동작되기 때문입니다)
Removal of sinks
sink의 제거는 조금 복잡합니다. 데이터가 흐르고 있는 상태이기 때문에 관련된 pad를 block 해야합니다.이렇게 하기 위해서 IDLE probe를 추가하고, callback에서 unlink와 sink의 detroy를 수행하게 됩니다.또 다시 callback이 다른 thread를 통해서 여러번 호출 되는 상황으로 부터 보호가기 위해, sink info structure를 통해 실제 제거하려고 하는 sink의 정 보를 가져오게 됩니다.
여기서 g_free를 gst_pad_add_probe의 detroy notify를 통해 전달하는 것에 주의하세요. 그리고 callback안에서 직접 memory free를 수행하면 안됩니다. 이것은 매우 필수적인 것으로 callback은 여전히 sink를 release 한 이후에도 호출될 수 있기 때문입니다. 그렇게 되면 이미 free된 영역에 접근하는 형태 가 되어버립니다.
이 내용이 동적인 pipeline 구성을 어떻게 구현할 수 있는지에 대해서 이해할 수 있도록 도움이 되었으면 좋겠습니다.
제공된 예제를 통해서 쉽게 확장할 수 있고, 실제 그리고 더 복잡한 유즈 케이스에도 적용할 수 있을 것입니다. 이러한 컨셉은 모든 경우에 있어서 동일 합니다.