GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 07
- 제 7 장 멀티미디어 즐기기
멀티미디어 즐기기
멀티미디어 기능은 GNOME의 가장 강력한 장점에 속한다. 이러한 기능은 개발자들이 쉽게 멀티미디어 내용을 표시하도록 다양한 API를 제공한다. 구현할 수 있는 광범위한 범위의 애플리케이션 아이디어를 제시하고, 단순한 오디오/비디오 변환 툴, 음악이나 비디오 스트림 재생기, CCTV 감시, 모든 기능을 갖춘 교육 애플리케이션을 예로 들 수 있겠다.
이번 장에서는 GStreamer API를 이용해 오디오와 비디오의 내용을 표시함으로써 GStreamer의 기본적인 사용을 살펴보도록 하겠다. 그 내용을 구체적으로 소개하자면 다음과 같다.
- GStreamer 개념
- 오디오와 비디오 재생하기
- 필터를 스트림에 적용하기
이제 이 주제들을 자세히 살펴보도록 하자.
필요한 패키지
이번 장에서는 MPEG 코드라는 소프트웨어를 사용하는데, 기본 Linux 배포판 보관소에서 무료로 이용할 수 있는 것이 아니다. Fedora 사용자는 제3자의 보관소를 추가해야만 소프트웨어를 이용할 수 있을 것이다. Terminal에 아래의 명령을 입력하여 제3자 라이브러리를 추가한다.
su -c 'yum localinstall --nogpgcheck http://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-stable.noarch.rpm http://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-stable.noarch.rpm'
아래 패키지가 설치되어 있도록 한다.
- Fedora: gstreamer-plugins-bad, gstreamer-plugins-ugly, gstreamer-ffmpeg, gstreamer-tools
- Ubuntu/Debian: gstreamer0.10-plugins-bad, gstreamer0.10-pluginsugly, gstreamer0.10-plugins-ffmpeg, gstreamer0.10-tools, libgstreamer-plugins-base0.10-dev
GStreamer의 기본 개념 이해하기
GStreamer란 GNOME이 멀티미디어 기능을 지원하기 위해 사용하는 미디어 처리 프레임워크다. 여기에는 미디어 스트림의 열기, 인코딩, 디코딩, 필터링에 사용되는 추상화 레이어(abstraction layer)를 제공하는 플러그인 구조(plugin infrastructure)가 있다. 즉, 특정 멀티미디어 포맷에 대한 플러그인이 있는 한 해당 포맷으로 파일을 열거나 쓰고 미디어를 재생하는 것이 가능하다는 의미다. 아래 그림은 GStreamer API의 아키텍처를 간소화한 모습이다.
그림에서 볼 수 있듯이 애플리케이션은 GStreamer가 제공한 APIs를 사용할 수 있고, 특정 코덱이나 필터의 구현에서도 API를 사용할 수 있다. 애플리케이션은 코덱이나 필터에 대한 세부적인 내용을 알 필요가 없으며, 코덱이나 필터 또한 애플리케이션의 세부 내용을 알 필요 없다. 불행하게도 실제 세계에서는 그림에 표시된 모든 것을 스스로 구현하는 동시 서로 차이가 있는 오디오 및 비디오 재생 애플리케이션들이 존재한다.
GStreamer에는 "elements"(요소)라는 개념이 있다. 이는 미디어 스트림에 수반되는 개체들의 기본적인 부분이다. 스트림이 시작되는 요소, 스트림이 끝나는 요소, 그리고 스트림이 전달되는 동안 스트림이 조작되는 추가적인 요소들도 있다.
데이터는 물로 생각할 수 있다. 요소들이 서로 파이프를 통해 연결된 시스템으로 물이 흐른다. 물은 요소에 의해 시스템으로 들어오는데, 물 갤런(water gallon), 물컵, 호흡기관, 또는 가상으로 산소와 수소 분자로부터 물을 생성하는 반응기까지 모두 이러한 시스템의 예가 된다.
색상이나 냄새가 필터 요소에 의해 변경될 수 있도록 시스템 내에서 물을 조작한다. 심지어 이러한 필터들을 결합할 수도 있다.
그리고 나면 물은 양동이, 물컵, 스프레이, 또는 증발기와 같은 요소를 통해 시스템에서 빠져나간다.
각 요소에는 최소한 하나의 문이 있는데, 문은 소스(source)나 싱크(sink), 혹은 둘 다 해당하기도 한다. "Source"(소스)는 데이터가 흐르기 시작하는 원점을 의미하고, "sink"(싱크)는 데이터가 흘러 들어가는 종점을 의미한다. 이러한 문을 "pad"(패드)라고 부른다. 각 요소는 하나 이상의 패드를 가질 수 있는데, 오디오 스트림과 비디오 스트림을 모두 생성하는 요소를 예로 들 수 있겠다. 이러한 패드는 정적으로 또는 동적으로 생성이 가능하다.
요소를 시각적으로 표시하자면 다음 그림과 같다.
각 요소에는 고유의 상태가 있는데, 상태는 아래 값들 중 하나에 해당한다.
- Null: 요소의 기본 상태.
- Ready: 스트림을 사용할 준비가 되어 있고 흐르기를 기다리는 상태.
- Paused: 스트림은 열려 있는데 흐름이 freeze된 상태.
- Playing: 스트림이 열려 있고 흐르고 있는 상태.
파이프가 물을 시스템으로 전달하듯이 데이터는 버퍼를 이용해 시스템으로 전달된다. 전체적인 시스템은 제어 정보를 이동시키는 이벤트를 이용해 조직된다. 이러한 이벤트들은 요소로 전송되어, 전달된 이벤트에 따라 반응한다.
'이벤트' 혹은 GStreamer 용어를 이용하자면 '메시지'는 버스를 통해 이동한다. 버스는 파이프라인에 의해 생성된다. 그리고 우리는 버스를 이용해 그곳으로 발송된 메시지를 구독할 수 있다.
명령행을 이용해 GStreamer 파이프라인 접근하기
GStreamer는 명령행만을 이용해 파이프라인을 테스트하도록 도와주는 툴을 제공한다. 이는 파이프라인이 올바른지 여부를 빠르게 확인하는 데에 매우 유용하다. 구체적인 예를 들어보자. 스테레오 MP3 파일을 재생하길 원한다고 가정하자. 소스 요소는 MP3 파일 오프너(opener), 즉 filesrc가 되겠고, 스트림은 다음 요소인 mad, 즉 MP3 디코더로 전달한다. 이후 디코딩된 스트림은 raw 오디오 스트림을 가령 모노 채널 등으로 변환하는 audioconvert 요소로 전달된다.
이렇게 새로 수정된 스트림이 audioresample로 전달되면 8KHz의 오디오 스트림으로 변환한다. 그리고 마지막 스트림은 alsasink로 전달되어 스트림을 사운드 카드로 재생시킨다.
실행하기 - 파이프라인 테스트하기
이러한 파이프라인은 명령행과 GStream 툴인 gst-launch를 이용해 구현할 수 있다. 그 방법을 살펴보자.
- Terminal을 열어 아래의 명령을 하나의 행으로 입력하라.
$ gst-launch-0.10 filesrc location=bass.mp3 ! mad ! audioconvert ! audio/x-raw-int,channels=1 ! audioresample ! audio/x-raw-int, rate=8000 ! alsasink
- 오디오가 재생되는지 듣고 화면에 아래와 같은 내용이 출력되는지 확인하라.
Setting pipeline to PAUSED ... Pipeline is PREROLLING ... Pipeline is PREROLLED ... Setting pipeline to PLAYING ... New clock: GstAudioSinkClock Got EOS from element "pipeline0". Execution ended after 9311663370 ns. Setting pipeline to PAUSED ... Setting pipeline to READY ... Setting pipeline to NULL ... Freeing pipeline ...
무슨 일이 일어났는가?
파이프라인은 아래 그림과 같이 시각화할 수 있겠다.
gst-launch 툴은 명령행에 명시된 바와 같이 파이프라인을 구축한다. 우리는 요소들 사이에 느낌표를 이용해 서로를 연결한다.
가장 먼저 filesrc로 파이프라인을 시작하고, 재생하고자 하는 파일명을 입력함으로써 위치의 프로퍼티를 명시한다.
filesrc location=bass.mp3 !
이후 mad 라이브러리를 이용해 스트림을 mad 요소, 즉 MP3 디코더로 전달한다.
mad !
그 다음 스트림을 audioconvert 요소로 전달한다.
audioconvert ! audio/x-raw-int,channels=1 !
여기서 스트림을 채널이 하나인 정수 포맷의 오디오로 변환하기 위해 소스 패드를 명시한다. 그리고 audioresample 요소로 전달한다.
audioresample ! audio/x-raw-int, rate=8000 !
스트림을 8-KHz 오디오 스트림으로 변환하기 위해 소스 패드를 명시한다.
마지막으로 "Advanced Linux Sound Architecture (ALSA)"를 통해 스트림을 사운드 카드로 출력하는 alsasink 요소로 스트림을 전달한다.
alsasink
마지막으로 오디오가 재생되는 것을 듣는다!
실행하기 - 프로그램적으로 오디오 재생하기
명령행 툴을 이용해 GStreamer 요소와 상호작용하는 방법을 익히면 파이프라인 디자인이 작동할 것인지 여부를 확인하는 데에 매우 유용하다. 이를 프로그램적으로 실행해보도록 하겠다.
- audio.js라는 새로운 스크립트를 생성하고 아래 코드로 채워라.
#!/usr/bin/env seed GLib = imports.gi.GLib; Gst = imports.gi.Gst; GObject = imports.gi.GObject; Main = new GType({ parent: GObject.Object.type, name: "Main", init: function() { var pipeline = new Gst.Pipeline({ name: 'pipe' }); var filesrc = Gst.ElementFactory.make ('filesrc', 'source'); var mad = Gst.ElementFactory.make ('mad', 'decoder'); var converter = Gst.ElementFactory.make ('audioconvert', 'converter'); var resampler = Gst.ElementFactory.make ('audioresample', 'resampler'); var alsasink = Gst.ElementFactory.make ('alsasink', 'sink'); pipeline.add (filesrc); pipeline.add (mad); pipeline.add (converter); pipeline.add (resampler); pipeline.add (alsasink); filesrc.location = "bass.mp3"; filesrc.link(mad); mad.link(converter); var caps = Gst.caps_from_string("audio/x-raw-int,channels=1") converter.link_filtered(resampler, caps); caps = Gst.caps_from_string("audio/x-raw-int,rate=8000") resampler.link_filtered(alsasink, caps); this.play = function() { pipeline.set_state (Gst.State.PLAYING); }; } }); Gst.init(Seed.argv); var main = new Main(); main.play(); var context = GLib.main_context_default(); var loop = new GLib.MainLoop.c_new(context); loop.run();
- 아니면 UI와 라이센스 없이 audio.vala 라는 Vala 프로젝트를 생성할 수도 있다. src/audio.vala 파일을 아래 코드로 채워라.
using Gst; using GLib; public class Main : GLib.Object { Pipeline pipeline; public Main () { pipeline = Gst.parse_launch ("filesrc location=bass.mp3 ! mad ! audioconvert ! audio/x-raw-int,channels=1 ! audioresample ! audio/x-raw-int, rate=4000 ! alsasink") as Gst.Pipeline; } public void play() { pipeline.set_state (State.PLAYING); } static int main (string[] args) { Gst.init (ref args); var app = new Main (); app.play(); new MainLoop().run(); return 0; } }
- 아래 코드에서,
PKG_CHECK_MODULES(AUDIO, [gtk+-3.0 ])
configure.ac를 아래와 같이 수정하라.PKG_CHECK_MODULES(AUDIO, [gstreamer-0.10 ])
- 아래 코드에서,
audio_VALAFLAGS = \ --pkg gtk+-3.0
src/Makefile.am 을 아래와 같이 수정하라.audio_VALAFLAGS = \ --pkg gstreamer-0.10
- 프로그램을 실행하는 디렉터리로 bass.mp3 파일을 복사할 것을 잊지 마라.
- 프로그램을 실행하면 앞의 예제에서 재생된 것과 동일한 오디오가 들릴 것이다.
무슨 일이 일어났는가?
Vala와 JavaScript에서 작성된 코드 버전을 같이 살펴보면 둘 사이에 상당한 차이가 있음을 발견할 것이다. 이는 언어의 특성 때문이라기보다는 두 언어가 GStreamer와 상호작용하기 위해 대안적 방법을 사용하기 때문에 생긴 것이다.
JavaScript 코드를 먼저 살펴보자. 아래 코드에서 우리는 관련된 모든 요소들과 파이프라인을 정의한다.
var pipeline = new Gst.Pipeline({ name: 'pipe' });
var filesrc = Gst.ElementFactory.make ('filesrc', 'source');
var mad = Gst.ElementFactory.make ('mad', 'decoder');
var converter = Gst.ElementFactory.make ('audioconvert', 'converter');
var resampler = Gst.ElementFactory.make ('audioresample', 'resampler');
var alsasink = Gst.ElementFactory.make ('alsasink', 'sink');
각 요소에서 원하는 요소의 이름과 요소로 참조하길 원하는 이름을 선언해야 한다. 그 다음으로 접근 용이성을 위해 로컬 변수에 보관한다.
그리고 모든 요소를 파이프라인으로 추가한다.
pipeline.add (filesrc);
pipeline.add (mad);
pipeline.add (converter);
pipeline.add (resampler);
pipeline.add (alsasink);
이 때는 요소들이 추가되긴 했지만 서로 연결은 되지 않은 상태다. 따라서 아래와 같이 filesrc 요소의 location 프로퍼티를 명시한다.
filesrc.location = "bass.mp3";
filesrc.link(mad);
mad.link(converter);
이를 통해 bass.mp3 파일을 열고 싶다는 사실을 요소에게 알리는 셈이다. 또 filesrc 요소를 mad 요소로 연결한다. 그 다음으로는 mad 요소를 converter 요소로 연결함으로써 연결 작업을 계속한다.
아래의 코드는 converter 요소의 기능을 설정하는데, 가령 단일 채널을 이용하는 정수 포맷의 raw 오디오 스트림을 처리하는 기능을 들 수 있겠다.
var caps = Gst.caps_from_string("audio/x-raw-int,channels=1")
converter.link_filtered(resampler, caps);
이러한 기능들을 사용할 수 있다면, link 함수 대신 link_filtered 함수를 이용해 converter 요소를 resampler로 연결할 수 있다.
아래의 코드는 caps 변수를 재사용하고 그 값을 새 값으로 리셋한다.
caps = Gst.caps_from_string("audio/x-raw-int,rate=8000")
resampler.link_filtered(alsasink, caps);
앞의 코드에서 비트 전송률을 8000 Hertz로 설정한다. 이후 caps 변수가 있는 resampler 요소를 alsasink 요소로 연결한다. alsasink 요소는 사운드 카드에게 오디오를 재생시킬 것을 명령할 때 사용된다. 현재로선 더 이상 연결이 필요하지 않으므로 끝마친다.
오디오를 재생해야 할 경우 우리가 해야 할 일은 Gst.State.PLAYING 값으로 파이프라인의 상태를 트리거하는 것이다.
pipeline.set_state (Gst.State.PLAYING);
상태가 설정되면 흐름이 시작되어 사운드 카드에서 끝이 나면서 오디오를 들을 수 있게 된다.
Vala 버전에서는 이 결과를 도출하는 방법이 하나 이상임을 표시하기 위해 코드를 약간만 변경한다. 하지만 그보다 먼저 빌드 구조(build infrastructure)를 살펴보자.
gstreamer-0.10 pkgconfig 빌드 플래그를 우리의 빌드 구조로 포함시키도록 configure.ac를 수정함으로써 C 컴파일러가 헤더 파일을 비롯해 필요한 라이브러리를 어디서 찾을 것인지 인식하고 이해하도록 한다. 이 때 사용할 코드는 다음과 같다.
PKG_CHECK_MODULES(AUDIO, [gstreamer-0.10 ])
그리고 Gst 네임스페이스를 사용하길 원한다는 사실을 Vala 컴파일러가 알 수 있도록 Makefile.am 파일을 수정하는데, 해당 네임스페이스는 gstreamer-0.10 package에서 비롯된다.
audio_VALAFLAGS = \
--pkg gstreamer-0.10
아래와 같이 한 행의 코드만으로 파이프라인을 구성할 수 있다.
pipeline = Gst.parse_launch ("filesrc location=bass.mp3 ! mad ! audioconvert ! audio/x-raw-int,channels=1 ! audioresample ! audio/x-raw-int, rate=4000 ! alsasink") as Gst.Pipeline;
앞의 코드에서는 명령행 코드 변환을 복사하여 Gst.parse_launch 함수로 붙여 넣을 뿐이다. 이 함수는 Gst.Element를 리턴하지만 우리는 캐스팅(casting)을 명시하기 위해 행 끝에 as Gst.Pipeline을 추가함으로써 Gst.Element를 Gst.Pipeline으로 변환해야 한다. 그 다음으로 상태를 설정함으로써 흐름을 시작한다.
pipeline.set_state (State.PLAYING);
두 가지 버전 모두에서 GLib 메인 루프를 사용하여 시스템이 이벤트에 관해 알고 즉시 종료하지 않도록 한다. 그 결과 프로그램을 종료하기 위해서는 Ctrl+C를 눌러야 하는데, 우리는 스트림의 끝을 알리는 이벤트는 처리하지 않기 때문이다.
실행하기 - 이벤트 처리하기
이제 각 이벤트에 적절하게 반응할 수 있도록 스트림으로부터 이벤트를 수신하는 법을 배워보자. 스트림이 끝나면 애플리케이션을 종료해야 한다고 가정해보자. 안타깝게도 이 때는 Vala를 이용할 수 밖에 없는데, Seed가 사용하는 GObject 자가점검 라이브러리의 현재 버전에는 우리의 목적을 달성하는 데 필요한 함수가 포함되어 있지 않기 때문이다. 그러한 이벤트를 처리하기 위해서는 아래 단계를 실행하라.
- 오디오 프로젝트를 사용해 아래의 코드와 같은 모습이 되도록 audio.vala 파일을 수정하라.
public class Main : GLib.Object { Pipeline pipeline; public signal void eos(); bool bus_handler (Bus bus, Message message) { if (message.type == MessageType.EOS) { stdout.printf("End of stream!\n"); eos(); } return true; } public Main () { pipeline = Gst.parse_launch ("filesrc location=bass.mp3 ! mad ! audioconvert ! audio/x-raw-int,channels=1 ! audioresample ! audio/x-raw-int, rate=4000 ! alsasink") as Gst.Pipeline; var bus = pipeline.get_bus (); bus.add_watch(bus_handler); } public void play() { pipeline.set_state (State.PLAYING); } static int main (string[] args) { Gst.init (ref args); var loop = new MainLoop(); var app = new Main (); app.play(); app.eos.connect(() => { loop.quit(); }); loop.run(); return 0; } }
- 프로그램을 빌드하여 실행하라. 프로그램은 아래의 내용을 출력한 후 종료됨을 확인할 수 있을 것이다.
End of stream!
무슨 일이 일어났는가?
우리는 그저 스트림이 끝날 때마다 반응할 수 있길 원할 뿐이다. 따라서 이벤트를 수신할 때 콜백 함수를 호출하는 방법을 고려하고, 이벤트를 트리거하는 방법을 알아내면 되겠다.
아래의 코드는 파이프라인으로부터 GStreamer 버스를 얻는 데 사용할 수 있다.
var bus = pipeline.get_bus ();
bus.add_watch(bus_handler);
앞서 논의하였듯이 버스는 그 안으로 전송되는 모든 메시지를 이동시킨다(carry). 따라서 add_watch 함수를 이용해 버스를 감시해야 한다.
이후 다른 부분들을 준비해야 하는데, 먼저 우리만의 시그널을 설정한다.
public signal void eos();
두 번째는 시그널 핸들러가 되겠다.
bool bus_handler (Bus bus, Message message) {
if (message.type == MessageType.EOS) {
stdout.printf("End of stream!\n");
eos();
}
return true;
}
앞의 코드에서는 우리만의 시그널을 준비하여 GStreamer 메시지를 우리 클래스의 클라이언트에게 이벤트로서 전달한다. 그 이유는 클라이언트가 GStreamer 메시지에 대해 전혀 알지 못할 수도 있기 때문에 GStreamer 이벤트를 우리만의 이벤트로 래핑(wrap)하여 시그널을 간소화하는 것이다.
메시지 핸들러는 꽤 간단한데, 메시지 타입이 "End of Stream(EOS)" 메시지인지 아닌지를 확인한다. 스트림 끝 메시지가 맞다면 텍스트를 출력하고 우리만의 시그널을 발생시키면 된다.
아래 코드는 클라이언트가 우리 시그널로 어떻게 연결되는지를 보여준다.
app.eos.connect(() => {
loop.quit();
});
시그널을 수신하면 메인 루프를 종료하고 우리의 목적이 달성된다.
비디오 미디어 재생하기
이제 비디오를 재생해보자. 하지만 먼저 스트림 디자인을 생각해낼 필요가 있다. 아래 그림에 표시된 디자인이 우리의 요구를 충족시켜줄 것이다.
앞의 그림에서는 파일로부터 스트림이 Ogg 디멀티플렉서에 의해 두 부분으로 나뉨을 확인할 수 있다. 그 결과 소스 스트림은 오디오와 비디오 모두를 포함한다. 비디오 스트림은 theora 디코더와 색상 공간 변환기로 전달된다. 이러한 플러그인은 미디어로부터 비디오 내용을 디코딩하는 데 사용된다. 그리고 나면 ximage 싱크로 전달된다. 이는 디코딩된 자료를 화면에 표시하기 위함이다. 오디오 스트림은 vorbis 디코더로 전달된 후 오디오 변환기와 resampler 요소로 전달된다. vorbis 플러그인은 미디어에서 오디오 내용을 디코딩하는 데 사용된다. 이후 오디오를 사운드카드에서 재생하도록 alsa 싱크로 전달한다.
실행하기 - 비디오 재생하기
Wikipedia에서 받은 Al17_spill.org 파일을 재생할 것이다. 이 파일은 오디오가 있는 OggTheora 비디오 스트림이다. 아래 단계를 따라하여 비디오를 재생해보자.
- 명령행 terminal에 아래 내용을 입력하라.
gst-launch-0.10 filesrc location="Ap17_spill.ogg" ! oggdemux name=demux demux. ! queue ! theoradec ! ffmpegcolorspace ! ximagesink demux. ! queue ! vorbisdec ! audioconvert ! audioresample ! alsasink
- 아래 스크린샷과 같이 오디오와 비디오가 함께 재생되는 작은 창이 표시될 것이다.
무슨 일이 일어났는가?
명령행 코드는 앞서 표시한 디자인을 엄격하게 따른다. 여기서 Ap17_spill.org 파일을 연다.
filesrc location="Ap17_spill.ogg" !
이제 filesrc로 연결된 Ogg 디멀티플렉서를 생성하고, 디멀티플렉서를 demux로 명명한다.
oggdemux name=demux demux. !
여기서 Ogg 디멀티플렉서의 소스를 나눈다.
queue !
첫 번째 branch는 theora 디코더로 연결되어 theora 스트림으로부터 비디오 스트림을 얻는다.
theoradec !
그리고 ffmpecolorspace로 전달하여 화면으로 표시하는 데에 적합한 색상 공간을 얻는다.
ffmpegcolorspace !
마지막으로 비디오 스트림을 ximagesink로 전달하여 화면으로 표시한다.
ximagesink demux. !
demux라고 불리는 요소로 연결된 이 경로가 여기서 끝난다는 것을 나타내기 위해 demux 다음에 마침표를 넣는다.
queue !
여기서는 두 번째 branch에 또 다른 경로를 넣는다.
vorbisdec !
아래의 코드 행에서는 스트림을 이용해 오디오 스트림을 얻는다.
audioconvert ! audioresample ! alsasink
이러한 시퀀스는 오디오 스트림만 재생했던 앞의 예제와 비슷하다.
앞서 제시한 바와 같이 branch 이름이 마침표로 끝나도록 큐의 끝을 표시하는 한 명령행에 오디오나 비디오 스트림 중 무엇을 먼저 넣는지는 중요하지 않다.
시도해보기 - 오디오를 먼저 정의하기
한 번 시도해보는 건 어떨까? 파이프라인에 오디오를 먼저 정의하고 나서 명령행을 이용해 비디오를 정의해보자. 결과에는 시각적으로나 음향적으로 차이가 없을 것이다.
실행하기 - 프로그램적으로 비디오 재생하기
이제 앞에서 제시한 스트림 흐름 디자인을 이용해 프로그램에서 구현하길 원한다고 치자. 프로그램의 UI는 아래에 Play/Stop 버튼이 배치된 매우 간단한 비디오 박스라고 가정하자. 아래 순서를 따라하라.
- video 라는 새로운 Vala 프로젝트를 생성하라. 이번에는 GtkBuilder 기능을 사용해보자.
- video.ui 파일을 편집하여 두 개의 항목이 있는 수직 박스를 넣어라. 상단 항목에는 DrawingArea 위젯을 넣고 하단 항목에는 버튼을 사용하라. DrawingArea 위젯이 확장 가능하도록 확보하라.
- src/Makefile.am 이 아래의 내용을 포함하도록 수정하라.
video_VALAFLAGS = \ --pkg gtk+-3.0 --pkg gstreamer-0.10 --pkg gstreamer-interfaces-0.10 --pkg gdk-x11-3.0
- configure.ac가 아래의 내용을 포함하도록 수정하라.
PKG_CHECK_MODULES(VIDEO, [gtk+-3.0 gstreamer-0.10 gstreamer-interfaces-0.10 gstreamer-plugins-base-0.10 gdk-x11-3.0])
- 본 서적의 코드 번들에서 이용 가능한 video.vala 파일을 포함시켜라. 이 기능에서 주요 부분은 다음과 같다.
public class Main : GLib.Object { const string UI_FILE = "src/video.ui"; public Main () { Builder builder; pipeline = new Pipeline ("video"); src = ElementFactory.make ("filesrc", "filesrc"); demux = ElementFactory.make ("oggdemux", "demux"); queue1 = ElementFactory.make ("queue", "queue1"); queue2 = ElementFactory.make ("queue", "queue2"); theoraDecoder = ElementFactory.make ("theoradec", "theora"); vorbisDecoder = ElementFactory.make ("vorbisdec", "vorbis"); colorConverter = ElementFactory.make ("ffmpegcolorspace", "colorspace"); audioConverter = ElementFactory.make ("audioconvert", "audio"); audioResampler = ElementFactory.make ("audioresample", "resampler"); audioSink = ElementFactory.make ("alsasink", "audiosink"); videoSink = ElementFactory.make ("ximagesink", "videosink"); pipeline.add_many (src, demux, theoraDecoder, vorbisDecoder, queue1, queue2, colorConverter, audioConverter, audioResampler, audioSink, videoSink); src.link (demux); demux.link_many(queue1, queue2); demux.pad_added.connect((element, src_pad) => { var caps = src_pad.get_caps(); var name = caps.get_structure(0).get_name(); Pad sink_pad = null; if (name == "video/x-theora") { ink_pad = queue1.get_pad("sink"); } else if (name == "audio/x-vorbis") { sink_pad = queue2.get_pad("sink"); } else { return; } if (sink_pad != null && sink_pad.is_linked() == false) { src_pad.link (sink_pad); } }); queue1.link_many (theoraDecoder, colorConverter, videoSink); queue2.link_many (vorbisDecoder, audioConverter, audioResampler, audioSink); try { builder = new Builder (); builder.add_from_file (UI_FILE); builder.connect_signals (this); videoArea = builder.get_object ("drawingarea1") as Widget; videoArea.draw.connect(() => { var xoverlay = videoSink as XOverlay; var xid = (ulong)Gdk.X11Window.get_xid(videoArea.get_window()); overlay.set_xwindow_id(xid); return false; }); var window = builder.get_object ("window") as Window; window.show_all (); } catch (Error e) { stderr.printf ("Could not load UI: %s\n", e.message); } var bus = pipeline.get_bus (); bus.add_signal_watch (); bus.message.connect((bus, message) => { if (message.type == Gst.MessageType.EOS) { stop(); pipeline.set_state (State.READY); reopen(); } }); playButton = builder.get_object("button1") as Button; playButton.clicked.connect(() => { if (playing) { stop(); } else { play(); } }); reopen (); stop (); } }
- 프로젝트 폴더에 Ap17_spill.org 파일이 있는지 확인하라.
- 프로그램을 빌드하여 실행하라. 아래 스크린샷처럼 비디오를 재생 및 중지할 수 있고, 프로그램을 종료할 수도 있다.
무슨 일이 일어났는가?
이 연습문제에는 주의해서 살펴보아야 할 부분들이 몇 가지 있다.
첫 번째는 오디오와 비디오 스트림 소스를 큐의 다음 요소 내 싱크로 어떻게 연결하는지가 된다. 스트림이 재생되기 전에는 디멀티플렉서와 큐를 연결할 수 없다. 따라서 우리가 해야 할 일은 요소들을 디멀티플렉서 내에서 pad_added 시그널로 연결하는 일이다. 이번 예제에서는 스트림이 재생될 때마다 오디오 및 비디오 스트림 패드가 생성된다.
demux.pad_added.connect((element, src_pad) => {
var caps = src_pad.get_caps();
var name = caps.get_structure(0).get_name();
pad sink_pad = null;
시그널 핸들러에서는 스트림이 비디오 스트림을 포함하는지(video/x-theora와 비교 확인) 확인한 후 패드를 queue1 요소와 연결해야 한다.
if (name == "video/x-theora") {
sink_pad = queue1.get_pad("sink");
}
반대로 오디오 스트림을 얻으면 패드를 queue2와 연결해야 한다. 그 외의 경우는 아무 것도 실행하지 않는다. 그러면 아래 코드에서 볼 수 있듯이 audiosink와 videosink에 이르기까지 filesrc 요소로부터 모든 것이 연결되어 있을 것이다.
else if (name == "audio/x-vorbis") {
sink_pad = queue2.get_pad("sink");
} else {
return;
}
if (sink_pad != null && sink_pad.is_linked() == false) {
src_pad.link (sink_pad);
}
});
다음으로 중요한 것은 비디오 프레임을 재생하는 방식이다. ximagesink 싱크는 비디오를 표시하는 고유의 창을 갖고 있다. 비디오를 videoArea 위젯으로 부착하기 위해서는 draw 시그널을 연결해야 한다 (아니면 위젯이 준비되었음을 알려주는, 즉 window 객체가 유효함을 알려주는 시그널이라면 어떤 것이든 괜찮다).
우리 핸들러는 videoSink를 XOverlay로 얻게 되는데, 이는 우리가 명시하는 창으로 비디오 프레임을 그리는 데 사용되는 특수 인터페이스다. 그리기를 실행하기 전에 먼저 그리고자 하는 창의 핸들 번호에 해당하는 xid 값을 찾아야 하는데, 이는 아래 코드를 이용하면 될 일이다.
videoArea.draw.connect(() => {
var xoverlay = videoSink as XOverlay;
var xid = (ulong)Gdk.X11Window.get_xid(videoArea.get_window());
xoverlay.set_xwindow_id(xid);
return false;
});
이 창은 videoArea 위젯의 X11 창이다. 먼저 videoArea 위젯에 대해 Gdk.X11Window.get_xid 함수를 호출함으로써 xid 값을 얻는다. xid 값을 얻고 나면 xoverlay에게 프레임을 수신할 때마다 xid 값을 이용해 videoArea 위젯 안에서 렌더링할 것을 알린다.
다음은 스트림과 상호작용하는 방법을 살펴보자. 무언가를 재생할 때는 파이프라인의 상태를 PLAYING으로 설정하면 된다. 일시 정지를 원하면 PAUSE로 설정한다. 그리고 스트림 알림의 끝을 수신하거나 창을 닫을 때는 NULL로 설정하면 된다. NULL로 상태를 설정하고 나서 비디오의 재생을 가능하게 만들기 위해서는 filesrc 요소의 위치를 다시 설정할 필요가 있다.
요약
이번 장에서는 GStreamer를 이용해 오디오 및 비디오 파일을 재생하는 방법을 학습하였다. 또 GStreamer의 기본적인 내용을 논했다. GStreamer가 어떻게 작동하는지, 미디어를 재생하기 위해서는 GStreamer의 요소들을 어떻게 연결해야 하는지에 대해서도 학습했다. 명령행을 이용해 스트림 흐름 디자인을 만들고 나서 추후에 구현할 수 있다. 마지막으로 GTK+ 위젯과 함께 GStreamer를 사용하는 방법을 배웠다.
제 8장, 데이터 다루기에서는 데이터를 다루는 방법을 살펴볼 것이다. 데이터베이스의 데이터뿐만 아니라 다양한 소스로부터 데이터를 다루겠다.