- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF125:GStreamer
Материал из Linuxformat.
- GStreamer Надежный каркас для ваших мультимедиа-приложений
Содержание |
GStreamer: Ваш видеоплейер
- Пусть Linux и не испытывает недостатка в таких настольных приложениях, как проигрыватели мультимедиа – Дмитрий Мусаев все равно покажет вам,
как написать еще один, свой собственный.
Не возникало ли у вас когда-нибудь желания написать свой собственный медиа-плейер? Дело не только в том, чтобы почить на лаврах Totem и Kaffeine – это еще и прекрасный повод познакомиться с мультимедиа-каркасом GStreamer (http://gstreamer.freedesktop.org). Он написан на С и имеет интерфейсы для многих других языков программирования, таких как С++, Python и C#. На данный момент для него существует более 150 модулей расширения (plugin), позволяющих декодировать практически все аудио- и видеоформаты (полный список доступен по адресу http://gstreamer.freedesktop.org/documentation/plugins.html). С помощью этих модулей можно не только просматривать или прослушивать аудио- и видеофайлы, но и перекодировать их; например, вы сможете легко написать скрипт для конвертации фильма в формат, понимаемый вашим сотовым телефоном, если последний вообще умеет воспроизводить видео. Можно получать и отправлять медиа-потоки через сеть – в списке модулей вы найдете реализацию нескольких протоколов. Например, чтобы получить видео с web-камеры, достаточно набрать в терминале команду gst-launch v4l2src! xvimagesink.
Впечатляет? Тогда давайте перейдем от слов к делу.
Немного теории
Общая архитектура каркаса GStreamer представлена на рис. 1. Как можно видеть, он состоит из базового ядра, утилит и подключаемых модулей. Давайте введем некоторые понятия.
Элемент [element] – наиболее важный компонент в GStreamer. Мы будем создавать цепочки связанных между собой элементов и направлять через них поток данных. Элементы соединяются коннекторами [pads]; это не вполне точный перевод, но мне кажется, он удобнее, чем «пады» или «подушки». Коннекторы бывают входными [sink pad] и выходными [source pad]. Элемент может иметь различное количество коннекторов: одни присутствуют всегда, другие создаются в зависимости от типа медиа-данных, которые проходят через элемент. Посмотреть, какие именно коннекторы доступны у данного элемента, можно при помощи утилиты gst-inspect. Выполните команду gst-inspect decodebin, и вы получите много интересной информации.
Pad Templates: SRC template: 'src%d' Availability: Sometimes Capabilities: ANY SINK template: 'sink' Availability: Always Capabilities: ANY
Мы видим, что у элемента есть постоянный входной коннектор “sink”, совместимый с любым типом медиа-данных, и иногда выходные коннекторы “src%d”, количество которых зависит от типа входных данных. Отметим также, что элемент посылает три сигнала (на них мы остановимся позже):
Element Signals: “new-decoded-pad” : void user_function (GstElement* object, GstPad* arg0, gboolean arg1, gpointer user_data); “removed-decoded-pad” : void user_function (GstElement*object, GstPad* arg0, gpointer user_data); “unknown-type” : void user_function (GstElement* object, GstPad* arg0, GstCaps* arg1, gpointer user_data); Контейнер [bin] – это объект для управления набором элемен-
Контейнер [bin] – это объект для управления набором элементов. Контейнер позволяет объединять несколько связанных элементов в один логический. Все, что справедливо для элементов, справедливо и для контейнеров. Например, с помощью контейнеров можно заранее подготовить в программе наборы элементов для кодирования или декодирования различных форматов входных данных и потом, в зависимости от типа последних, использовать тот или иной контейнер.
Конвейер [pipeline] – это специальный подтип контейнера, который позволяет управлять всеми дочерними контейнерами и элементами. Конвейер должен быть контейнером самого верхнего уровня; он присутствует во всех приложения работающих с каркасом.
Элементы GStreamer могут находиться в одном из четырех состояний:
- GST_STATE_NULL Состояние по умолчанию. В этом состоянии элемент освобождает все ресурсы, которые он занимал.
- GST_STATE_READY В этом состоянии элемент размещает глобальные ресурсы. Поток данных закрыт, и позиция в нем выставлена в начало.
- GST_STATE_PAUSED В этом состоянии элемент открывает поток, данные подготавливаются для обработки. Элемент позволяет менять позицию в потоке.
- GST_STATE_PLAYING В этом состоянии запускается обработка данных.
GStreamer является многопоточным каркасом, и чтобы упростить взаимодействие приложения и конвейера, в нем введена простая система передачи сообщений Bus (шина сообщений). Каждый конвейер автоматически создает шину сообщений – приложению остается только назначить обработчики сигналов и реагировать на интересующие его события. Обработчики могут быть синхронными и асинхронными; мы будем использовать оба типа. Асинхронный обработчик сигналов вызывается в контексте главного цикла Gtk-приложения.
GStreamer предоставляет два высокоуровневых контейнера – это Playbin и Decodebin. Они обеспечивают всю рутинную работу по определению типа медиа-данных и их декодированию. Playbin – готовый медиа-плейер, которому нужно указать лишь источник данных и дескриптор окна (что, впрочем, не обязательно – он может создать и свое собственное), куда будет выводиться видео. Но мы будем использовать Decodebin, так как он поддается более тонкой настройке.
Исходя из вышесказанного, давайте составим схему нашего будущего медиа-плейера. Графически она представлена на рис. 2, а в текстовом виде может выглядеть так:
gst-launch filesrc location=bubble_dancer.wmv !decodebin name=decoder decoder. !queue !videoscale !xvimagesink decoder. ! queue !audioconvert !alsasink
Gst-launch – одна из ключевых вспомогательных утилит каркаса. Она позволяет размещать элементы на конвейере, восклицательный знак служит их разделителем: SRCELEMENT.PAD1!SINKELEMENT.PAD1. Если коннекторы не указаны, то перебираются все коннекторы и соединяются подходящие. Свойство name используется для задания имени элемента, что дает нам возможность обратиться к нему. При использовании имени элемента точка в конце обязательна – таков синтаксис команды.
Время кодировать
Перейдем к написанию кода. Я буду использовать C++ и среду разработки Anjuta; при желании, вы можете обойтись простым текстовым редактором. Итак, запускаем Anjuta и создаем новый проект; на вкладке C++ выбираем GTKmm.
Введите имя проекта, например, Playermm, и выберите его местоположение на жестком диске. Готово: мы имеем функцию main() и Glade-форму нашего будущего приложения. Нажимаем F7 и соглашаемся с тем, что нам надо создать проект; после завершения компиляции нажимаем F3. После запуска приложения вы должны увидеть пустое окно с заголовком "Hello world!". Теперь внесем небольшие изменения в main.cc (выделены жирным шрифтом)
int main (int argc, char *argv[]) { Gtk::Main kit(argc, argv); //Загру зить Glade-файл и соз дать его вид жеты: Glib::RefPtr<Gnome::Glade::Xml> refXml; try{ refXml = Gnome::Glade::Xml::create(GLADE_FILE); } catch (const Gnome::Glade::XmlError& ex) { std::cerr << ex.what() << std::endl; return 1; } PlayerWindow* main_win = 0; refXml->get_widget_derived(“main_window”, main_win); if (main_win){ kit.run(*main_win); } delete main_win; return 0; }
Откройте файл playermm.glade двойным щелчком мыши и разместите на форме вертикальный контейнер, состоящий из трех элементов. В самый верхний добавьте меню, в следующий элемент добавьте еще один вертикальный контейнер, состоящий из четырех элементов, и назовите его ‘playerbox’; в самый нижний элемент поместите строку состояния. В первый элемент контейнера playerbox поместите Gtk::DrawingArea – в эту область будет выводиться наше видео. Во второй элемент добавьте метку – Gtk::Label, далее разместите горизонтальную шкалу Gtk::HScale. В самом последнем элементе располагается горизонтальная группа кнопок Gtk::HButtonBox: добавьте туда шесть штук и назовите их btn_play, btn_pause, btn_stop, btn_rewind, btn_forward и btn_open. В результате должен получиться интерфейс, показанный на рис. 4.
Создадим новый класс (Файл > Новый > Класс С++), введем название – PlayerWindow, базовый класс – Gtk::Window. Далее, добавим в заголовочный файл объявления всех виджетов, что мы будем использовать согласно рис. 4 (подробности ищите на прилагаемом DVD).
Теперь, глядя на рис. 2, создадим для каждого его элемента объявление:
Glib::RefPtr<Gst::DecodeBin> m_decode_bin; Glib::RefPtr<Gst::VideoScale> m_scale; Glib::RefPtr<Gst::XvImageSink> m_video_sink; Glib::RefPtr<Gst::AudioConvert> m_conv; Glib::RefPtr<Gst::AlsaSink> m_audio_sink; Glib::RefPtr<Gst::Pipeline> m_pipeline; Glib::RefPtr<Gst::FileSrc> m_src; Glib::RefPtr<Gst::Queue> m_queuev; Glib::RefPtr<Gst::Queue> m_queuea;
Поясним, что означает каждая из этих переменных:
- FileSrc Источник данных, то есть файл.
- DecodeBin Элемент, который будет раскодировать наши медиа-данные. В зависимости от типа данных у него будут создаваться выходы для видео- и аудиопотоков, каждый из которых мы будем передавать в элемент Queue [очередь].
- Queue Элемент, который используется в GStreamer для создания многопоточности; наши аудио и видео будут обрабатываться в разных потоках.
- AudioConvert Конвертирует буфер «сырого» звука [‘raw audio’] между различными возможными форматами. Строго говоря, в нашем случае он не нужен и добавлен «для массовости», чтобы вы могли лучше представить себе структуру типового приложения GStreamer. Нужно помнить, что разные входные коннекторы могут принимать разные форматы, и даже одни и те же коннекторы могут принимать разные форматы на разных машинах. Поэтому лучше добавлять в цепочки для обработки данных конвертирующие элементы, такие как audioconvert и audioresample для звука или ffmpegcolorspace для видео. Об этом, по крайней мере, предупреждает документация.
- VideoScale Изменяет размер изображения. По умолчанию используется билинейный алгоритм, что дает при масштабировании более приятную картинку. В нашем случае элемент также необязательный, поскольку протокол Xv пытается привлечь для масштабирования графическую карту.
- AlsaSink В качестве аудиовыхода используется ALSA (Advanced Linux Sound Architecture).
- XvImageSink Элемент транслирует видеофреймы в нечто, пригодное к выводу на локальный дисплей с использованием видеоконтроллера для преобразования цветов и размера изображения. Также может использоваться для преобразования яркости, контрастности и оттенка цветов. XImageSink для всех этих операций применяет другие элементы, например, VideoScale.
- Pipeline Конвейер, в который помещаются все остальные элементы и который осуществляет управление ими.
Кроме того, в нашем заголовочном файле присутствуют конструктор, деструктор и обработчики сигналов.
Вы, наверное, обратили внимание на использование интеллектуального указателя с подсчетом ссылок – Glib::RefPtr< t_CppObject>. Это стандартная практика управления ресурсами glibmm и gtkmm. Суффикс «mm» в конце имени библиотеки, кстати, говорит о том, что это С++-обертка.
Свет, камера, мотор!
Заголовочный файл готов; приступим к реализации. Конструктор настраивает все виджеты, связывает виджеты с обработчиками сигналов и выставляет кнопки в начальное состояние, то есть блокирует все, кроме кнопки Открыть. Настройку GStreamer вынесем в отдельную функцию, которая будет вызываться из нашего конструктора.
В методе GstreamInit() происходит инициализация GStreamer и настройка всех нужных нам элементов:
void PlayerWindow::GstreamInit() { // инициа лизация GStreamer Gst::init(); //соз даем конвейер m_pipeline = Gst::Pipeline::create(“pipeline”); //шина сообщений Glib::RefPtr<Gst::Bus> bus = m_pipeline->get_bus(); // Разрешить син хронное извлечение сообщений bus->enable_sync_message_emission(); // На значить син хронный обработ чик сообщений bus->signal_sync_message().connect( sigc::mem_fun(*this, &PlayerWindow::on_bus_message_sync)); // На значить асин хронный обработ чик сообщений m_watch_id = bus->add_watch(sigc::mem_fun(*this, &PlayerWindow::on_bus_message) ); //ис точник данных m_src = Gst::FileSrc::create(“source”);
Теперь создадим наш декодер. Выше мы рассматривали вывод команды gst-inspect decodebin и видели, что он посылает три сигнала:
- new-decoded-pad Создан новый коннектор. В этом обработчике мы будем связывать аудио- и видеоцепочки с выходными данными декодера, исходя из типа создаваемого коннектора (см. рис. 2).
- removed-decoded-pad Коннектор удален.
- unknown-type Неизвестный тип медиа-данных.
Из всех этих событий нам нужно обрабатывать только создание новых коннекторов.
m_decode_bin = Gst::DecodeBin::create(“decodebin”); m_decode_bin->signal_new_decoded_pad(). connect(sigc::mem_fun(*this,&PlayerWindow::on_new_decoded_pad));
Следующим шагом создаются оставшиеся элементы:
m_conv = Gst::AudioConvert::create(); m_audio_sink = Gst::AlsaSink::create(); m_scale = Gst::VideoScale::create(); m_video_sink = Gst::XvImageSink::create(“ximagesink”); m_video_sink->set_property(“force-aspect-ratio”, true); m_queuea = Gst::Queue::create(); m_queuev = Gst::Queue::create();
Наконец, мы размещаем все элементы на конвейере и связываем их между собой:
m_pipeline->add(m_src)->add(m_decode_bin)->add(m_queuea)- >add(m_conv)-> add(m_audio_sink)->add(m_queuev)->add(m_scale)- >add(m_video_sink); m_src->link(m_decode_bin); // аудиоветвь m_queuea->link(m_conv)->link(m_audio_sink); // видеоветвь m_queuev->link(m_scale)->link(m_video_sink); m_pipeline->set_state(Gst::STATE_NULL); }
Далее, реализуем обработчик сигналов для декодера. В случае, если речь идет о создании нового коннектора, его текст представлен ниже. В имени вновь созданного коннектора мы ищем строку «video» или «audio», далее получаем входной коннектор соответствующей ветви элементов и связываем их. Наконец, мы проверяем результат связывания, и в случае неудачи сообщаем об этом на консоль программы.
void PlayerWindow::on_new_decoded_pad(const Glib::RefPtr<Gst::Pad>& pad, bool arg1) { if (pad->get_caps()->get_structure(0).get_name().find(“video”) != Glib::ustring::npos) { Glib::RefPtr<Gst::Pad> sinkPad = m_queuev- >get_static_pad(“sink”); // связать только один раз if (!sinkPad->is_linked()) { Gst::PadLinkReturn ret = pad->link(sinkPad); if (ret != Gst::PAD_LINK_OK && ret != Gst::PAD_LINK_WAS_LINKED) { std::cerr << “Невозмож но на значить видеовы ход” << std::endl; } } } //... подключение аудиопотока осу щест в ляется точно так же }
Каждый обработчик сигналов принимает одинаковые последовательности сообщений: первым их получает синхронный обработчик, далее – асинхронный. Рассмотрим синхронный обработчик шины сообщений:
void PlayerWindow::on_bus_message_sync( const Glib::RefPtr<Gst::Message>& message) { // игнорировать все события кроме ‘prepare-xwindow-id’ if(message->get_message_type() != Gst::MESSAGE_ELEMENT && !message->get_structure().has_name(“prepare-xwindow-id”)) return; Glib::RefPtr<Gst::Element> element = Glib::RefPtr<Gst::Element>::cast_dynamic(message->get_source()); Glib::RefPtr< Gst::ElementInterfaced<Gst::XOverlay> > xoverlay = Gst::Interface::cast <Gst::XOverlay>(element); if(xoverlay){ const gulong xWindowId = GDK_WINDOW_XID(m_video_area->get_window()->gobj()); xoverlay->set_xwindow_id(xWindowId); } }
Задача этого обработчика – получить контекст окна, в которое будет выводится видео. Наш асинхронный обработчик, on_bus_message(), обрабатывает только два сигнала: это конец потока данных и ошибка. В любом случае обработчик вызывает метод on_button_stop(), который переводит конвейер в состояние STATE_NULL.
Вот почти и все: осталось только добавить обработчики нажатия кнопок, и приложение можно запускать. Все управление воспроизведением сводится к изменению состояния контейнера посредством шести кнопок. Также в обработчиках кнопок разместим управление таймером. Он нужен нам для обновления прогресса воспроизведения и отслеживания прошедшего времени. Например, обработчик кнопки «Play» может выглядеть так:
void PlayerWindow::on_button_play() { //изменить состояние кнопок m_progress_scale->set_sensitive(); m_play_button->set_sensitive(false); m_pause_button->set_sensitive(); m_stop_button->set_sensitive(); m_rewind_button->set_sensitive(); m_forward_button->set_sensitive(); m_open_button->set_sensitive(false); m_play_button->hide(); m_pause_button->show(); // вызывать функ цию on_timeout ка ж дые 200 мс // для регулярного обнов ления по зиции в потоке m_timeout_connection = Glib::signal_timeout().connect( sigc::mem_fun(*this, &PlayerWindow::on_timeout), 200); // Включить режим воспроизведения m_pipeline->set_state(Gst::STATE_PLAYING); }
Соответственно, постановка на паузу будет выглядеть так:
void PlayerWindow::on_button_pause() { m_play_button->set_sensitive(); m_pause_button->set_sensitive(false); m_pause_button->hide(); m_play_button->show(); // Ос тановить таймер m_timeout_connection.disconnect(); // Пау за m_pipeline->set_state(Gst::STATE_PAUSED); }
Полный текст приложения имеется на прилагающемся к журналу диске. Кроме того, пакеты GStreamer и GStreamermm содержат примеры, которые помогают понять все тонкости использования данного каркаса. Проект активно развивается, расширяется документация (http://gstreamer.freedesktop.org/documentation/), в планах есть интеграция с KDE, что упростит обработку событий; об этом вы можете подробнее прочитать на сайте проекта.
Ну и, прежде чем закончить статью, давайте посмотрим, что у нас получилось! Для запуска приложения скопируйте его в свою рабочую папку, запустите Anjuta, откройте диалог Настроить проект (Сборка > Конфигурация проекта...) отметьте галочку Пересоздать проект и нажмите Выполнить. Теперь Playermm можно запустить, нажав F3.
LXF